diff --git a/configs/ExifTool_config b/configs/ExifTool_config new file mode 100644 index 0000000..1332658 --- /dev/null +++ b/configs/ExifTool_config @@ -0,0 +1,357 @@ + + +#------------------------------------------------------------------------------ +# File: example.config +# +# Description: Example user configuration file for Image::ExifTool +# +# Notes: This example file shows how to define your own shortcuts and +# add new EXIF, IPTC, XMP, PNG, MIE and Composite tags, as well +# as how to specify preferred lenses for the LensID tag, and +# define new file types and default ExifTool option values. +# +# Note that unknown tags may be extracted even if they aren't +# defined, but tags must be defined to be written. Also note +# that it is possible to override an existing tag definition +# with a user-defined tag. +# +# To activate this file, rename it to ".ExifTool_config" and +# place it in your home directory or the exiftool application +# directory. (On Windows and Mac systems this must be done via +# the command line since the GUI's don't allow filenames to begin +# with a dot. Use the "rename" command in Windows or "mv" on the +# Mac.) This causes ExifTool to automatically load the file when +# run. Your home directory is determined by the first defined of +# the following environment variables: +# +# 1. EXIFTOOL_HOME +# 2. HOME +# 3. HOMEDRIVE + HOMEPATH +# 4. (the current directory) +# +# Alternatively, the -config option of the exiftool application +# may be used to load a specific configuration file (note that +# this must be the first option on the command line): +# +# exiftool -config example.config ... +# +# This example file defines the following 16 new tags as well as +# a number of Shortcut and Composite tags: +# +# 1. EXIF:NewEXIFTag +# 2. GPS:GPSPitch +# 3. GPS:GPSRoll +# 4. IPTC:NewIPTCTag +# 5. XMP-xmp:NewXMPxmpTag +# 6. XMP-exif:GPSPitch +# 7. XMP-exif:GPSRoll +# 8. XMP-xxx:NewXMPxxxTag1 +# 9. XMP-xxx:NewXMPxxxTag2 +# 10. XMP-xxx:NewXMPxxxTag3 +# 11. XMP-xxx:NewXMPxxxStruct +# 12. PNG:NewPngTag1 +# 13. PNG:NewPngTag2 +# 14. PNG:NewPngTag3 +# 15. MIE-Meta:NewMieTag1 +# 16. MIE-Test:NewMieTag2 +# +# For detailed information on the definition of tag tables and +# tag information hashes, see lib/Image/ExifTool/README. +#------------------------------------------------------------------------------ + +# Shortcut tags are used when extracting information to simplify +# commonly used commands. They can be used to represent groups +# of tags, or to provide an alias for a tag name. +%Image::ExifTool::UserDefined::Shortcuts = ( + MyShortcut => ['exif:createdate','exposuretime','aperture'], + MyAlias => 'FocalLengthIn35mmFormat', +); + +# Custom definition for albums +%Image::ExifTool::UserDefined::elodie = ( + GROUPS => { 0 => 'XMP', 1 => 'XMP-elodie', 2 => 'Image' }, + NAMESPACE => { 'elodie' => 'https://github.com/jmathai/elodie/' }, + WRITABLE => 'string', + # Example 8. XMP-xxx:NewXMPxxxTag1 + # - replace "NewXMPxxxTag1" with your own tag name (eg. "MyTag") + Album => { Writable => 'lang-alt' }, +); + +# NOTE: All tag names used in the following tables are case sensitive. +# The %Image::ExifTool::UserDefined hash defines new tags to be added +# to existing tables. +%Image::ExifTool::UserDefined = ( + # All EXIF tags are added to the Main table, and WriteGroup is used to + # specify where the tag is written (default is ExifIFD if not specified): + 'Image::ExifTool::Exif::Main' => { + # Example 1. EXIF:NewEXIFTag + 0xd000 => { + Name => 'NewEXIFTag', + Writable => 'int16u', + WriteGroup => 'IFD0', + }, + # add more user-defined EXIF tags here... + }, + # the Geotag feature writes these additional GPS tags if available: + 'Image::ExifTool::GPS::Main' => { + # Example 2. GPS:GPSPitch + 0xd000 => { + Name => 'GPSPitch', + Writable => 'rational64s', + }, + # Example 3. GPS:GPSRoll + 0xd001 => { + Name => 'GPSRoll', + Writable => 'rational64s', + }, + }, + # IPTC tags are added to a specific record type (eg. application record): + # (Note: IPTC tag ID's are limited to the range 0-255) + 'Image::ExifTool::IPTC::ApplicationRecord' => { + # Example 4. IPTC:NewIPTCTag + 160 => { + Name => 'NewIPTCTag', + Format => 'string[0,16]', + }, + # add more user-defined IPTC ApplicationRecord tags here... + }, + # XMP tags may be added to existing namespaces: + 'Image::ExifTool::XMP::xmp' => { + # Example 5. XMP-xmp:NewXMPxmpTag + NewXMPxmpTag => { Groups => { 2 => 'Author' } }, + # add more user-defined XMP-xmp tags here... + }, + # special Geotag tags for XMP-exif: + 'Image::ExifTool::XMP::exif' => { + # Example 6. XMP-exif:GPSPitch + GPSPitch => { Writable => 'rational', Groups => { 2 => 'Location' } }, + # Example 7. XMP-exif:GPSRoll + GPSRoll => { Writable => 'rational', Groups => { 2 => 'Location' } }, + }, + # new XMP namespaces (eg. xxx) must be added to the Main XMP table: + 'Image::ExifTool::XMP::Main' => { + # namespace definition for examples 8 to 11 + xxx => { # <-- must be the same as the NAMESPACE prefix + SubDirectory => { + TagTable => 'Image::ExifTool::UserDefined::xxx', + # (see the definition of this table below) + }, + }, + elodie => { + SubDirectory => { + TagTable => 'Image::ExifTool::UserDefined::elodie', + }, + }, + # add more user-defined XMP namespaces here... + }, + # new PNG tags are added to the PNG::TextualData table: + 'Image::ExifTool::PNG::TextualData' => { + # Example 12. PNG:NewPngTag1 + NewPngTag1 => { }, + # Example 13. PNG:NewPngTag2 + NewPngTag2 => { }, + # Example 14. PNG:NewPngTag3 + NewPngTag3 => { }, + }, + # add a new MIE tag (NewMieTag1) and group (MIE-Test) to MIE-Meta + # (Note: MIE group names must NOT end with a number) + 'Image::ExifTool::MIE::Meta' => { + # Example 15. MIE-Meta:NewMieTag1 + NewMieTag1 => { + Writable => 'rational64u', + Units => [ 'cm', 'in' ], + }, + # new MIE "Test" group for example 16 + Test => { + SubDirectory => { + TagTable => 'Image::ExifTool::UserDefined::MIETest', + DirName => 'MIE-Test', + }, + }, + }, + # Composite tags are added to the Composite table: + 'Image::ExifTool::Composite' => { + # Composite tags are unique: The Require/Desire elements specify + # tags that must/may exist, and the keys of these hashes are used as + # indices in the @val array of the ValueConv expression to access + # the numerical (-n) values of these tags. All Require'd tags must + # exist for the Composite tag to be evaluated. If no Require'd tags + # are specified, then at least one of the Desire'd tags must exist. + # See the Composite table in Image::ExifTool::Exif for more + # examples, and lib/Image/ExifTool/README for all of the details. + BaseName => { + Require => { + 0 => 'FileName', + }, + # remove the extension from FileName + ValueConv => '$val[0] =~ /(.*)\./ ? $1 : $val[0]', + }, + # the next few examples demonstrate simplifications which may be + # used if only one tag is Require'd or Desire'd: + # 1) the Require lookup may be replaced with a simple tag name + # 2) "$val" may be used to represent "$val[0]" in the expression + FileExtension => { + Require => 'FileName', + ValueConv => '$val=~/\.([^.]*)$/; $1', + }, + # override CircleOfConfusion tag to use D/1750 instead of D/1440 + CircleOfConfusion => { + Require => 'ScaleFactor35efl', + Groups => { 2 => 'Camera' }, + ValueConv => 'sqrt(24*24+36*36) / ($val * 1750)', + # an optional PrintConv may be used to format the value + PrintConv => 'sprintf("%.3f mm",$val)', + }, + # generate a description for this file type + FileTypeDescription => { + Require => 'FileType', + ValueConv => 'GetFileType($val,1) || $val', + }, + # calculate physical image size based on resolution + PhysicalImageSize => { + Require => { + 0 => 'ImageWidth', + 1 => 'ImageHeight', + 2 => 'XResolution', + 3 => 'YResolution', + 4 => 'ResolutionUnit', + }, + ValueConv => '$val[0]/$val[2] . " " . $val[1]/$val[3]', + # (the @prt array contains print-formatted values) + PrintConv => 'sprintf("%.1fx%.1f $prt[4]", split(" ",$val))', + }, + # [advanced] select largest JPEG preview image + BigImage => { + Groups => { 2 => 'Preview' }, + Desire => { + 0 => 'JpgFromRaw', + 1 => 'PreviewImage', + 2 => 'OtherImage', + # (DNG and A100 ARW may be have 2 PreviewImage's) + 3 => 'PreviewImage (1)', + }, + # ValueConv may also be a code reference + # Inputs: 0) reference to list of values, 1) ExifTool object + ValueConv => sub { + my $val = shift; + my ($image, $bigImage, $len, $bigLen); + foreach $image (@$val) { + next unless ref $image eq 'SCALAR'; + # check for JPEG image (or "Binary data" if -b not used) + next unless $$image =~ /^(\xff\xd8\xff|Binary data (\d+))/; + $len = $2 || length $$image; # get image length + # save largest image + next if defined $bigLen and $bigLen >= $len; + $bigLen = $len; + $bigImage = $image; + } + return $bigImage; + }, + }, + # **** ADD ADDITIONAL COMPOSITE TAG DEFINITIONS HERE **** + }, +); + +# This is a basic example of the definition for a new XMP namespace. +# This table is referenced through a SubDirectory tag definition +# in the %Image::ExifTool::UserDefined definition above. +# The namespace prefix for these tags is 'xxx', which corresponds to +# an ExifTool family 1 group name of 'XMP-xxx'. +%Image::ExifTool::UserDefined::xxx = ( + GROUPS => { 0 => 'XMP', 1 => 'XMP-xxx', 2 => 'Image' }, + NAMESPACE => { 'xxx' => 'http://ns.myname.com/xxx/1.0/' }, + WRITABLE => 'string', + # Example 8. XMP-xxx:NewXMPxxxTag1 + # - replace "NewXMPxxxTag1" with your own tag name (eg. "MyTag") + NewXMPxxxTag1 => { Writable => 'lang-alt' }, + # Example 9. XMP-xxx:NewXMPxxxTag2 + NewXMPxxxTag2 => { Groups => { 2 => 'Author' } }, + # Example 10. XMP-xxx:NewXMPxxxTag3 + NewXMPxxxTag3 => { List => 'Bag' }, + # Example 11. XMP-xxx:NewXMPxxxStruct + # - example structured XMP tag + NewXMPxxxStruct => { + # the "Struct" entry defines the structure fields + Struct => { + # optional namespace prefix and URI for structure fields + # (required only if different than NAMESPACE above) + NAMESPACE => { 'test' => 'http://x.y.z/test/' }, + # optional structure name (used for warning messages only) + STRUCT_NAME => 'MyStruct', + # optional rdf:type property for the structure + TYPE => 'http://x.y.z/test/xystruct', + # structure fields (very similar to tag definitions) + X => { Writable => 'integer' }, + Y => { Writable => 'integer' }, + # a nested structure... + Things => { + List => 'Bag', + Struct => { + NAMESPACE => { thing => 'http://x.y.z/thing/' }, + What => { }, + Where => { }, + }, + }, + }, + List => 'Seq', # structures may also be elements of a list + }, + # Each field in the structure has an automatically-generated + # corresponding flattened tag with an ID that is the concatenation + # of the original structure tag ID and the field name (after + # capitalizing the first letter of the field name if necessary). + # The Name and/or Description of these flattened tags may be changed + # if desired, but all other tag properties are taken from the + # structure field definition. When this is done, the "Flat" flag + # must also be set in the tag definition. For example: + NewXMPxxxStructX => { Name => 'SomeOtherName', Flat => 1 }, +); + +# Adding a new MIE group requires a few extra definitions +use Image::ExifTool::MIE; +%Image::ExifTool::UserDefined::MIETest = ( + %Image::ExifTool::MIE::tableDefaults, # default MIE table entries + GROUPS => { 0 => 'MIE', 1 => 'MIE-Test', 2 => 'Document' }, + WRITE_GROUP => 'MIE-Test', + # Example 16. MIE-Test:NewMieTag2 + NewMieTag2 => { }, # new user-defined tag in MIE-Test group +); + +# A special 'Lenses' list can be defined to give priority to specific lenses +# in the logic to determine a lens model for the Composite:LensID tag +@Image::ExifTool::UserDefined::Lenses = ( + 'Sigma AF 10-20mm F4-5.6 EX DC', + 'Tokina AF193-2 19-35mm f/3.5-4.5', +); + +# User-defined file types to recognize +%Image::ExifTool::UserDefined::FileTypes = ( + XXX => { # <-- the extension of the new file type (case insensitive) + # BaseType specifies the format upon which this file is based. + # If BaseType is defined, then the file will be fully supported, + # and in this case the Magic pattern should not be defined + BaseType => 'TIFF', + MIMEType => 'image/x-xxx', + Description => 'My XXX file type', + }, + YYY => { + # without BaseType, the file will be recognized but not supported + Magic => '0123abcd', # regular expression to match at start of file + MIMEType => 'application/test', + Description => 'Test imaginary file type', + }, + ZZZ => { + # if neither BaseType nor Magic are defined, the file will be + # recognized by extension only + Description => 'My ZZZ file type', + }, +); + +# Specify default ExifTool option values +# (see the Options function documentation for available options) +%Image::ExifTool::UserDefined::Options = ( + CoordFormat => '%.6f', # change default GPS coordinate format + Duplicates => 1, # make -a default for the exiftool app + GeoMaxHDOP => 4, # ignore GPS fixes with HDOP > 4 +); + +#------------------------------------------------------------------------------ diff --git a/elodie/media/video.py b/elodie/media/video.py index 6479c05..7507a9d 100644 --- a/elodie/media/video.py +++ b/elodie/media/video.py @@ -16,6 +16,7 @@ import shutil import subprocess import time +from elodie import plist_parser from media import Media """ @@ -157,15 +158,8 @@ class Video(Media): if(time is None): return False - source = self.source - exif_metadata = pyexiv2.ImageMetadata(source) - exif_metadata.read() - - exif_metadata['Exif.Photo.DateTimeOriginal'].value = time - exif_metadata['Exif.Image.DateTime'].value = time - - exif_metadata.write() - return True + result = self.__update_using_plist(time=time) + return result """ Set lat/lon for a video @@ -179,7 +173,6 @@ class Video(Media): if(latitude is None or longitude is None): return False - print 'SET LOCATION %s %s' % (latitude, longitude) result = self.__update_using_plist(latitude=latitude, longitude=longitude) return result @@ -202,7 +195,7 @@ class Video(Media): @returns, boolean """ def __update_using_plist(self, **kwargs): - if('latitude' not in kwargs and 'longitude' not in kwargs): + if('latitude' not in kwargs and 'longitude' not in kwargs and 'time' not in kwargs): print 'No lat/lon passed into __create_plist' return False @@ -224,33 +217,53 @@ class Video(Media): print 'Failed to generate plist file' return False - with open(plist_temp.name, 'r') as plist_written: - plist_text = plist_written.read() + plist = plist_parser.Plist(plist_temp.name) + # Depending on the kwargs that were passed in we regex the plist_text before we write it back. + plist_should_be_written = False + if('latitude' in kwargs and 'longitude' in kwargs): + latitude = str(abs(kwargs['latitude'])).lstrip('0') + longitude = kwargs['longitude'] - # Once the plist file has been written we need to open the file to read and update it. - plist_final = None - with open(plist_temp.name, 'w') as plist_written: - # Depending on the kwargs that were passed in we regex the plist_text before we write it back. - if('latitude' in kwargs and 'longitude' in kwargs): - latitude = kwargs['latitude'] - longitude = kwargs['longitude'] + # Add a literal '+' to the lat/lon if it is positive. + # Do this first because we convert longitude to a string below. + lat_sign = '+' if latitude > 0 else '-' + # We need to zeropad the longitude. + # No clue why - ask Apple. + # We set the sign to + or - and then we take the absolute value and fill it. + lon_sign = '+' if longitude > 0 else '-' + longitude_str = '{:9.5f}'.format(abs(longitude)).replace(' ', '0') + lat_lon_str = '%s%s%s%s' % (lat_sign, latitude, lon_sign, longitude_str) - # Add a literal '+' to the lat/lon if it is positive. - # Do this first because we convert longitude to a string below. - lat_sign = '+' if latitude > 0 else '' - # We need to zeropad the longitude. - # No clue why - ask Apple. - # We set the sign to + or - and then we take the absolute value and fill it. - lon_sign = '+' if longitude > 0 else '-' - longitude_str = '{:9.5f}'.format(abs(longitude)).replace(' ', '0') + plist.update_key('common/location', lat_lon_str) + plist_should_be_written = True - plist_updated_text = re.sub('\>([+-])([0-9.]+)([+-])([0-9.]+)', '>%s%s%s%s' % (lat_sign, latitude, lon_sign, longitude_str), plist_text); - plist_final = plist_written.name - plist_written.write(plist_updated_text) + if('time' in kwargs): + # The time formats can be YYYY-mm-dd or YYYY-mm-dd hh:ii:ss + time_parts = str(kwargs['time']).split(' ') + ymd, hms = [None, None] + if(len(time_parts) >= 1): + ymd = [int(x) for x in time_parts[0].split('-')] - # If we've written to the plist file then we proceed - if(plist_final is None): - print 'plist file was not be written to' + if(len(time_parts) == 2): + hms = [int(x) for x in time_parts[1].split(':')] + + if(hms is not None): + d = datetime(ymd[0], ymd[1], ymd[2], hms[0], hms[1], hms[2]) + else: + d = datetime(ymd[0], ymd[1], ymd[2], 12, 00, 00) + + offset = time.strftime("%z", time.gmtime(time.time())) + time_string = d.strftime('%Y-%m-%dT%H:%M:%S{}'.format(offset)) + #2015-10-09T17:11:30-0700 + plist.update_key('common/creationDate', time_string) + plist_should_be_written = True + + + if(plist_should_be_written is True): + plist_final = plist_temp.name + plist.write_file(plist_final) + else: + print 'Nothing to update, plist unchanged' return False # We create a temporary file to save the modified file to. @@ -262,7 +275,7 @@ class Video(Media): # We need to block until the child process completes. # http://stackoverflow.com/a/5631819/1318758 - avmetareadwrite_command = '%s -w %s "%s" "%s"' % (avmetareadwrite, plist_written.name, source, temp_movie) + avmetareadwrite_command = '%s -a %s "%s" "%s"' % (avmetareadwrite, plist_final, source, temp_movie) update_process = subprocess.Popen([avmetareadwrite_command], stdout=subprocess.PIPE, shell=True) streamdata = update_process.communicate()[0] if(update_process.returncode != 0): diff --git a/elodie/plist_parser.py b/elodie/plist_parser.py new file mode 100644 index 0000000..43307ad --- /dev/null +++ b/elodie/plist_parser.py @@ -0,0 +1,27 @@ +""" +Author: Jaisen Mathai +Parse OS X plists. +Wraps standard lib plistlib (https://docs.python.org/3/library/plistlib.html) +""" + +# load modules +from os import path + +import plistlib + +""" +Plist class to parse and interact with a plist file. +""" +class Plist(object): + def __init__(self, source): + if(path.isfile(source) == False): + raise IOError('Could not load plist file %s' % source) + + self.source = source + self.plist = plistlib.readPlist(self.source) + + def update_key(self, key, value): + self.plist[key] = value + + def write_file(self, destination): + plistlib.writePlist(self.plist, destination)