def update_iptcdata(filepath, new_caption, new_keywords, new_datetime, new_rating, new_gps, new_rectangles, new_persons, image_width=-1, image_height=-1, hierarchical_subject=None, preserve_time=True, title=""): """Updates the caption and keywords of an image file.""" # Some cameras write into Description, so we wipe it out to not cause # conflicts with Caption-Abstract. command = [EXIFTOOL, '-F', '-m', '-Description='] if preserve_time: command.append("-P") # marcelb: added title support if title: command.append('-Headline=%s' % (title, )) tmp = _write_caption_file(new_caption, command) if new_datetime: try: command.append('-DateTimeOriginal="%s"' % (new_datetime.strftime("%Y:%m:%d %H:%M:%S"))) except ValueError, ex: su.perr("Cannot update timestamp for %s: %s" % (filepath, str(ex)))
def generate(self, options): """makes sure all files exist in other album, and generates if necessary.""" try: source_file = su.resolve_alias(self.photo.image_path) do_export = self._check_need_to_export(source_file, options) # if we use links, we update the IPTC data in the original file do_iptc = (options.iptc == 1 and do_export) or options.iptc == 2 if do_iptc and options.link: if self.check_iptc_data(source_file, options, file_updated=do_export): do_export = True exists = True # True if the file exists or was updated. if do_export: exists = imageutils.copy_or_link_file(source_file, self.export_file, options.dryrun, options.link, self.size, options) else: _logger.debug(u'%s up to date.', self.export_file) # if we copy, we update the IPTC data in the copied file if exists and do_iptc and not options.link: self.check_iptc_data(self.export_file, options, file_updated=do_export) if (options.originals and self.photo.originalpath and not self.photo.rotation_is_only_edit): self._generate_original(options) except (OSError, MacOS.Error) as ose: su.perr(u"Failed to export %s to %s: %s" % (self.photo.image_path, self.export_file, ose))
def _get_iptc_cache(image_file): """Gets the _IptcCache for the folder of image_file.""" folder = os.path.split(image_file)[0] if _get_iptc_cache.cache and _get_iptc_cache.cache.folder == folder: return _get_iptc_cache.cache # We have no cached data, or there are for the wrong folder. _get_iptc_cache.cache = None save_path = os.path.join(folder, CACHE_NAME) if os.path.exists(save_path): cache_file = None try: cache_file = open(save_path) _get_iptc_cache.cache = cPickle.load(cache_file) except Exception, ex: su.perr(u"Could not read Phoshare IPTC cache data from %s: %s." % ( save_path, unicode(ex))) if cache_file: cache_file.close() # Make sure the folder matches. if _get_iptc_cache.cache and _get_iptc_cache.cache.folder != folder: _get_iptc_cache.cache = None try: if _get_iptc_cache.cache and _get_iptc_cache.cache.cache_version != CACHE_VERSION: _get_iptc_cache.cache = None except AttributeError: su.perr(u"Found non-cache data in %s, ignoring" % (folder)) _get_iptc_cache.cache = None if _get_iptc_cache.cache: _get_iptc_cache.cache.clear_expired()
def _get_iptc_cache(image_file): """Gets the _IptcCache for the folder of image_file.""" folder = os.path.split(image_file)[0] if _get_iptc_cache.cache and _get_iptc_cache.cache.folder == folder: return _get_iptc_cache.cache # We have no cached data, or there are for the wrong folder. _get_iptc_cache.cache = None save_path = os.path.join(folder, CACHE_NAME) if os.path.exists(save_path): cache_file = None try: cache_file = open(save_path) _get_iptc_cache.cache = cPickle.load(cache_file) except Exception, ex: su.perr(u"Could not read Phoshare IPTC cache data from %s: %s." % (save_path, unicode(ex))) if cache_file: cache_file.close() # Make sure the folder matches. if _get_iptc_cache.cache and _get_iptc_cache.cache.folder != folder: _get_iptc_cache.cache = None try: if _get_iptc_cache.cache and _get_iptc_cache.cache.cache_version != CACHE_VERSION: _get_iptc_cache.cache = None except AttributeError: su.perr(u"Found non-cache data in %s, ignoring" % (folder)) _get_iptc_cache.cache = None if _get_iptc_cache.cache: _get_iptc_cache.cache.clear_expired()
def generate(self, options): """makes sure all files exist in other album, and generates if necessary.""" source_file = self.photo.image_path try: do_export = self._check_need_to_export(source_file, options) # if we use links, we update the IPTC data in the original file do_iptc = (options.iptc == 1 and do_export) or options.iptc == 2 if do_iptc and options.link: if self.check_iptc_data(source_file, options): do_export = True exists = True # True if the file exists or was updated. if do_export: exists = imageutils.copy_or_link_file( source_file, self.export_file, options.dryrun, options.link, options.size, options.update) else: _logger.debug(u'%s up to date.', self.export_file) # if we copy, we update the IPTC data in the copied file if exists and do_iptc and not options.link: self.check_iptc_data(self.export_file, options) if (options.originals and self.photo.originalpath and not self.photo.rotation_is_only_edit): self._generate_original(options) except (OSError, MacOS.Error) as ose: su.perr("Failed to export %s: %s" % (source_file, ose))
def _parse_datetime_original(xml_desc, iptc_data, image_file): """Parses the DateTimeOriginal data from exiftool output.""" for xml_element in xml_desc.getElementsByTagName( 'ExifIFD:DateTimeOriginal'): if not xml_element.firstChild: continue try: date_time_original = time.strptime( xml_element.firstChild.nodeValue, '%Y:%m:%d %H:%M:%S') iptc_data.date_time_original = datetime.datetime( date_time_original.tm_year, date_time_original.tm_mon, date_time_original.tm_mday, date_time_original.tm_hour, date_time_original.tm_min, date_time_original.tm_sec) except ValueError, _ve: su.perr('Exiftool returned an invalid date %s for %s - ignoring.' % (xml_element.firstChild.nodeValue, image_file))
def update_iptcdata(filepath, new_caption, new_keywords, new_datetime, new_rating, new_gps, new_rectangles, new_persons, image_width=-1, image_height=-1, hierarchical_subject=None, preserve_time=True): """Updates the caption and keywords of an image file.""" # Some cameras write into Description, so we wipe it out to not cause # conflicts with Caption-Abstract. command = [EXIFTOOL, '-F', '-m', '-Description='] if preserve_time: command.append("-P") tmp = _write_caption_file(new_caption, command) if new_datetime: try: command.append('-DateTimeOriginal="%s"' % ( new_datetime.strftime("%Y:%m:%d %H:%M:%S"))) except ValueError, ex: su.perr("Cannot update timestamp for %s: %s" % (filepath, str(ex)))
def _parse_datetime_original(xml_desc, iptc_data, image_file): """Parses the DateTimeOriginal data from exiftool output.""" for xml_element in xml_desc.getElementsByTagName('ExifIFD:DateTimeOriginal'): if not xml_element.firstChild: continue try: date_time_original = time.strptime( xml_element.firstChild.nodeValue, '%Y:%m:%d %H:%M:%S') iptc_data.date_time_original = datetime.datetime( date_time_original.tm_year, date_time_original.tm_mon, date_time_original.tm_mday, date_time_original.tm_hour, date_time_original.tm_min, date_time_original.tm_sec) except ValueError, _ve: su.perr('Exiftool returned an invalid date %s for %s - ignoring.' % ( xml_element.firstChild.nodeValue, image_file))
def get_iptc_data(image_file): """get caption, keywords, datetime, rating, and GPS info all in one operation.""" output = su.execandcombine( (EXIFTOOL, "-X", "-m", "-q", "-q", '-c', '%.6f', "-Keywords", "-Caption-Abstract", "-DateTimeOriginal", "-Rating", "-GPSLatitude", "-Subject", "-GPSLongitude", "-RegionRectangle", "-RegionPersonDisplayName", image_file)) keywords = [] caption = None date_time_original = None rating = 0 gps = None region_names = [] region_rectangles = [] if output: try: gps_latitude = None gps_longitude = None xml_data = minidom.parseString(output) for xml_desc in xml_data.getElementsByTagName('rdf:Description'): _get_xml_nodevalues(xml_desc, 'IPTC:Keywords', keywords) # Keywords can also be stored as Subject in the XMP directory _get_xml_nodevalues(xml_desc, 'XMP:Subject', keywords) for xml_caption in xml_data.getElementsByTagName( 'IPTC:Caption-Abstract'): caption = xml_caption.firstChild.nodeValue for xml_element in xml_data.getElementsByTagName( 'ExifIFD:DateTimeOriginal'): if not xml_element.firstChild: continue try: date_time_original = time.strptime( xml_element.firstChild.nodeValue, '%Y:%m:%d %H:%M:%S') date_time_original = datetime.datetime( date_time_original.tm_year, date_time_original.tm_mon, date_time_original.tm_mday, date_time_original.tm_hour, date_time_original.tm_min, date_time_original.tm_sec) except ValueError, _ve: su.perr('Exiftool returned an invalid ' 'date %s for %s - ignoring.' % ( xml_element.firstChild.nodeValue, image_file)) for xml_element in xml_data.getElementsByTagName( 'XMP-xmp:Rating'): rating = int(xml_element.firstChild.nodeValue) for xml_element in xml_data.getElementsByTagName( 'Composite:GPSLatitude'): gps_latitude = xml_element.firstChild.nodeValue for xml_element in xml_data.getElementsByTagName( 'Composite:GPSLongitude'): gps_longitude = xml_element.firstChild.nodeValue string_rectangles = [] _get_xml_nodevalues(xml_desc, 'XMP-MP:RegionRectangle', string_rectangles) for string_rectangle in string_rectangles: rectangle = [] for c in string_rectangle.split(','): rectangle.append(float(c)) region_rectangles.append(rectangle) _get_xml_nodevalues(xml_desc, 'XMP-MP:RegionPersonDisplayName', region_names) xml_data.unlink() if gps_latitude and gps_longitude: gps = imageutils.GpsLocation().from_composite(gps_latitude, gps_longitude) except parsers.expat.ExpatError, ex: su.perr('Could not parse exiftool output %s: %s' % ( output, ex))
def run_phoshare(cmd_args): """main routine for phoshare.""" parser = get_option_parser() (options, args) = parser.parse_args(cmd_args) if len(args) != 0: parser.error("Found some unrecognized arguments on the command line.") if options.version: print '%s %s' % (phoshare.phoshare_version.PHOSHARE_VERSION, phoshare.phoshare_version.PHOSHARE_BUILD) return 1 if options.iptc > 0 and not exiftool.check_exif_tool(): print >> sys.stderr, ("Exiftool is needed for the --itpc or --iptcall" + " options.") return 1 if options.size and options.link: parser.error("Cannot use --size and --link together.") if not options.iphoto: parser.error("Need to specify the iPhoto library with the --iphoto " "option.") if options.export or options.picasaweb or options.checkalbumsize: if not (options.albums or options.events or options.smarts or options.facealbums): parser.error("Need to specify at least one event, album, or smart " "album for exporting, using the -e, -a, or -s " "options.") else: parser.error("No action specified. Use --export to export from your " "iPhoto library.") if options.picasaweb: if options.picasapassword: google_password = options.picasapassword else: google_password = getpass.getpass('Google password for %s: ' % options.picasaweb) if options.ratings: options.ratings = [int(r) for r in options.ratings.split(",")] if options.reverse: if not options.dryrun: su.pout(u"Turning on dryrun mode because of --reverse option.") options.dryrun = True logging_handler = logging.StreamHandler() logging_handler.setLevel(logging.DEBUG if options.verbose else logging.INFO) _logger.addHandler(logging_handler) album_xml_file = iphotodata.get_album_xmlfile( su.expand_home_folder(options.iphoto)) if options.omitdatabasefile: album_sql_file="" else: album_sql_file = iphotodata.get_album_sqlfile( su.expand_home_folder(options.iphoto)) data = iphotodata.get_iphoto_data(album_xml_file, album_sql_file, ratings=options.ratings, verbose=options.verbose, aperture=options.aperture) if options.originals and options.export: data.load_aperture_originals() options.aperture = data.aperture and not data.aperture_data options.foldertemplate = unicode(options.foldertemplate) options.nametemplate = unicode(options.nametemplate) options.captiontemplate = unicode(options.captiontemplate) if options.checkalbumsize: data.checkalbumsizes(int(options.checkalbumsize)) if options.export: album = ExportLibrary(su.expand_home_folder(options.export)) export_iphoto(album, data, options.exclude, options) if options.picasaweb: try: import phoshare.picasaweb as picasaweb albums = picasaweb.PicasaAlbums(options.picasaweb, google_password) export_iphoto(albums, data, options.exclude, options) except ImportError: su.perr('Sorry, this version of Phoshare does not support uploading to PicasaWeb.')
def _get_iptc_data_exiftool(image_file): """Returns IptcData for an image file using exiftool.""" args = [EXIFTOOL, "-X", "-m", "-q", "-q", '-c', '%.6f', "-Keywords", "-Caption-Abstract", "-ImageDescription", "-DateTimeOriginal", "-Rating", "-GPSLatitude", "-Subject", "-GPSLongitude", "-RegionName", "-RegionType", "-RegionAreaX", "-RegionAreaY", "-RegionAreaW", "-RegionAreaH", "-ImageWidth", "-ImageHeight", "-HierarchicalSubject", "-AlreadyApplied", image_file ] output = su.execandcombine(args) if not output: return None iptc_data = None try: xml_data = minidom.parseString(output) for xml_desc in xml_data.getElementsByTagName('rdf:Description'): iptc_data = IptcData() iptc_data.rating = 0 iptc_data.image_file = xml_desc.getAttribute("rdf:about") iptc_data.keywords = [] _get_xml_nodevalues(xml_desc, 'IPTC:Keywords', iptc_data.keywords) _get_xml_nodevalues(xml_desc, 'XMP-lr:HierarchicalSubject', iptc_data.hierarchical_subject) # Keywords can also be stored as Subject in the XMP directory _get_xml_nodevalues(xml_desc, 'XMP:Subject', iptc_data.keywords) for xml_caption in xml_desc.getElementsByTagName('IPTC:Caption-Abstract'): if xml_caption.firstChild: iptc_data.caption = xml_caption.firstChild.nodeValue if not iptc_data.caption: for xml_caption in xml_desc.getElementsByTagName('IFD0:ImageDescription'): if xml_caption.firstChild: iptc_data.caption = xml_caption.firstChild.nodeValue _parse_datetime_original(xml_desc, iptc_data, image_file) for xml_element in xml_desc.getElementsByTagName('XMP-xmp:Rating'): if xml_element.firstChild: iptc_data.rating = int(xml_element.firstChild.nodeValue) _parse_gps(xml_desc, iptc_data) for xml_element in xml_desc.getElementsByTagName('File:ImageWidth'): if xml_element.firstChild: iptc_data.image_width = int(xml_element.firstChild.nodeValue) for xml_element in xml_desc.getElementsByTagName('File:ImageHeight'): if xml_element.firstChild: iptc_data.image_height = int(xml_element.firstChild.nodeValue) for xml_element in xml_desc.getElementsByTagName('XMP-crs:AlreadyApplied'): if xml_element.firstChild: iptc_data.already_applied = xml_element.firstChild.nodeValue #string_rectangles = [] #_get_xml_nodevalues(xml_desc, 'XMP-MP:RegionRectangle', string_rectangles) #for string_rectangle in string_rectangles: # rectangle = [] # for c in string_rectangle.split(','): # rectangle.append(float(c)) # iptc_data.region_rectangles.append(rectangle) #_get_xml_nodevalues(xml_desc, 'XMP-MP:RegionPersonDisplayName', # iptc_data.region_names) # Handle Region tags _parse_regions(xml_desc, iptc_data) break xml_data.unlink() except parsers.expat.ExpatError, ex: su.perr('Could not parse exiftool output %s: %s' % (output, ex))
def _get_iptc_data_exiftool(image_file): """Returns IptcData for an image file using exiftool.""" args = [ EXIFTOOL, "-X", "-m", "-q", "-q", '-c', '%.6f', "-Keywords", "-Caption-Abstract", "-ImageDescription", "-DateTimeOriginal", "-Rating", "-GPSLatitude", "-Subject", "-GPSLongitude", "-RegionName", "-RegionType", "-RegionAreaX", "-RegionAreaY", "-RegionAreaW", "-RegionAreaH", "-ImageWidth", "-ImageHeight", "-HierarchicalSubject", "-AlreadyApplied", image_file ] output = su.execandcombine(args) if not output: return None iptc_data = None try: xml_data = minidom.parseString(output) for xml_desc in xml_data.getElementsByTagName('rdf:Description'): iptc_data = IptcData() iptc_data.rating = 0 iptc_data.image_file = xml_desc.getAttribute("rdf:about") iptc_data.keywords = [] _get_xml_nodevalues(xml_desc, 'IPTC:Keywords', iptc_data.keywords) _get_xml_nodevalues(xml_desc, 'XMP-lr:HierarchicalSubject', iptc_data.hierarchical_subject) # Keywords can also be stored as Subject in the XMP directory _get_xml_nodevalues(xml_desc, 'XMP:Subject', iptc_data.keywords) for xml_caption in xml_desc.getElementsByTagName( 'IPTC:Caption-Abstract'): if xml_caption.firstChild: iptc_data.caption = xml_caption.firstChild.nodeValue if not iptc_data.caption: for xml_caption in xml_desc.getElementsByTagName( 'IFD0:ImageDescription'): if xml_caption.firstChild: iptc_data.caption = xml_caption.firstChild.nodeValue _parse_datetime_original(xml_desc, iptc_data, image_file) for xml_element in xml_desc.getElementsByTagName('XMP-xmp:Rating'): if xml_element.firstChild: iptc_data.rating = int(xml_element.firstChild.nodeValue) _parse_gps(xml_desc, iptc_data) for xml_element in xml_desc.getElementsByTagName( 'File:ImageWidth'): if xml_element.firstChild: iptc_data.image_width = int( xml_element.firstChild.nodeValue) for xml_element in xml_desc.getElementsByTagName( 'File:ImageHeight'): if xml_element.firstChild: iptc_data.image_height = int( xml_element.firstChild.nodeValue) for xml_element in xml_desc.getElementsByTagName( 'XMP-crs:AlreadyApplied'): if xml_element.firstChild: iptc_data.already_applied = xml_element.firstChild.nodeValue #string_rectangles = [] #_get_xml_nodevalues(xml_desc, 'XMP-MP:RegionRectangle', string_rectangles) #for string_rectangle in string_rectangles: # rectangle = [] # for c in string_rectangle.split(','): # rectangle.append(float(c)) # iptc_data.region_rectangles.append(rectangle) #_get_xml_nodevalues(xml_desc, 'XMP-MP:RegionPersonDisplayName', # iptc_data.region_names) # Handle Region tags _parse_regions(xml_desc, iptc_data) break xml_data.unlink() except parsers.expat.ExpatError, ex: su.perr('Could not parse exiftool output %s: %s' % (output, ex))
def run_phoshare(cmd_args): """main routine for phoshare.""" parser = get_option_parser() (options, args) = parser.parse_args(cmd_args) if len(args) != 0: parser.error("Found some unrecognized arguments on the command line.") if options.version: print '%s %s' % (phoshare.phoshare_version.PHOSHARE_VERSION, phoshare.phoshare_version.PHOSHARE_BUILD) return 1 if options.iptc > 0 and not exiftool.check_exif_tool(): print >> sys.stderr, ("Exiftool is needed for the --itpc or --iptcall" + " options.") return 1 if options.size and options.link: parser.error("Cannot use --size and --link together.") if not options.iphoto: parser.error("Need to specify the iPhoto library with the --iphoto " "option.") if options.export or options.picasaweb or options.checkalbumsize: if not (options.albums or options.events or options.smarts or options.facealbums): parser.error("Need to specify at least one event, album, or smart " "album for exporting, using the -e, -a, or -s " "options.") else: parser.error("No action specified. Use --export to export from your " "iPhoto library.") if options.picasaweb: if options.picasapassword: google_password = options.picasapassword else: google_password = getpass.getpass('Google password for %s: ' % options.picasaweb) if options.ratings: options.ratings = [int(r) for r in options.ratings.split(",")] if options.reverse: if not options.dryrun: su.pout(u"Turning on dryrun mode because of --reverse option.") options.dryrun = True logging_handler = logging.StreamHandler() logging_handler.setLevel(logging.DEBUG if options.verbose else logging.INFO) _logger.addHandler(logging_handler) album_xml_file = iphotodata.get_album_xmlfile( su.expand_home_folder(options.iphoto)) album_sql_file = iphotodata.get_album_sqlfile( su.expand_home_folder(options.iphoto)) data = iphotodata.get_iphoto_data(album_xml_file, album_sql_file, ratings=options.ratings, verbose=options.verbose, aperture=options.aperture) if options.originals and options.export: data.load_aperture_originals() options.aperture = data.aperture and not data.aperture_data options.foldertemplate = unicode(options.foldertemplate) options.nametemplate = unicode(options.nametemplate) options.captiontemplate = unicode(options.captiontemplate) if options.checkalbumsize: data.checkalbumsizes(int(options.checkalbumsize)) if options.export: album = ExportLibrary(su.expand_home_folder(options.export)) export_iphoto(album, data, options.exclude, options) if options.picasaweb: try: import phoshare.picasaweb as picasaweb albums = picasaweb.PicasaAlbums(options.picasaweb, google_password) export_iphoto(albums, data, options.exclude, options) except ImportError: su.perr('Sorry, this version of Phoshare does not support uploading to PicasaWeb.')
def update_iptcdata(filepath, new_caption, new_keywords, new_datetime, new_rating, new_gps, new_rectangles, new_persons): """Updates the caption and keywords of an image file.""" # Some cameras write into ImageDescription, so we wipe it out to not cause # conflicts with Caption-Abstract. We also wipe out the XMP Subject and # Description tags (we use Keywords and Caption-Abstract). command = [ EXIFTOOL, '-F', '-m', '-P', '-ImageDescription=', '-Subject=', '-Description=' ] tmp = None if not new_caption is None: if not new_caption: command.append('-Caption-Abstract=') else: tmpfd, tmp = tempfile.mkstemp(dir="/var/tmp") os.close(tmpfd) file1 = open(tmp, "w") print >> file1, new_caption.encode("utf-8") file1.close() command.append('-Caption-Abstract<=%s' % (tmp)) if new_datetime: command.append('-DateTimeOriginal="%s"' % (new_datetime.strftime("%Y:%m:%d %H:%M:%S"))) if new_keywords: for keyword in new_keywords: command.append(u'-keywords=%s' % (keyword)) elif new_keywords != None: command.append('-keywords=') if new_rating >= 0: command.append('-Rating=%d' % (new_rating)) if new_gps: command.append('-c') command.append('%.6f') command.append('-GPSLatitude="%f"' % (abs(new_gps.latitude))) command.append('-GPSLatitudeRef=' + new_gps.latitude_ref()) command.append('-GPSLongitude="%f"' % (abs(new_gps.longitude))) command.append('-GPSLongitudeRef=' + new_gps.longitude_ref()) if new_persons: for person in new_persons: command.append(u'-RegionPersonDisplayName=%s' % (person)) elif new_persons != None: command.append('-RegionPersonDisplayName=') if new_rectangles: for rectangle in new_rectangles: command.append('-RegionRectangle=%s' % (','.join(str(c) for c in rectangle))) elif new_rectangles != None: command.append('-RegionRectangle=') command.append("-iptc:CodedCharacterSet=ESC % G") command.append(filepath) result = su.fsdec(su.execandcombine(command)) if tmp: os.remove(tmp) if result.find("1 image files updated") != -1: if result != "1 image files updated": su.pout(result) # wipe out the back file created by exiftool backup_file = filepath + "_original" if os.path.exists(backup_file): os.remove(backup_file) return True else: su.perr("Failed to update IPTC data in image %s: %s" % (filepath, result)) return False
command.append("-iptc:CodedCharacterSet=ESC % G") command.append(filepath) result = su.fsdec(su.execandcombine(command)) if tmp: os.remove(tmp) if result.find("1 image files updated") != -1: if result != "1 image files updated": su.pout(result) # wipe out the back file created by exiftool backup_file = filepath + "_original" if os.path.exists(backup_file): os.remove(backup_file) return True else: su.perr("Failed to update IPTC data in image %s: %s" % (filepath, result)) return False def _write_caption_file(new_caption, command): """If new_caption is set, write it into a tempory file, add a parameter to command, and return the file handle.""" if new_caption is None: return None if not new_caption: command.append('-Caption-Abstract=') command.append('-ImageDescription=') return None tmpfd, tmp = tempfile.mkstemp(dir="/var/tmp") os.close(tmpfd) file1 = open(tmp, "w")
command.append("-iptc:CodedCharacterSet=ESC % G") command.append(filepath) result = su.fsdec(su.execandcombine(command)) if tmp: os.remove(tmp) if result.find("1 image files updated") != -1: if result != "1 image files updated": su.pout(result) # wipe out the back file created by exiftool backup_file = filepath + "_original" if os.path.exists(backup_file): os.remove(backup_file) return True else: su.perr("Failed to update IPTC data in image %s: %s" % ( filepath, result)) return False def _write_caption_file(new_caption, command): """If new_caption is set, write it into a tempory file, add a parameter to command, and return the file handle.""" if new_caption is None: return None if not new_caption: command.append('-Caption-Abstract=') command.append('-ImageDescription=') return None tmpfd, tmp = tempfile.mkstemp(dir="/var/tmp") os.close(tmpfd) file1 = open(tmp, "w") file1.write(new_caption.encode("utf-8"))
def update_iptcdata(filepath, new_caption, new_keywords, new_datetime, new_rating, new_gps, new_rectangles, new_persons): """Updates the caption and keywords of an image file.""" # Some cameras write into ImageDescription, so we wipe it out to not cause # conflicts with Caption-Abstract. We also wipe out the XMP Subject and # Description tags (we use Keywords and Caption-Abstract). command = [EXIFTOOL, '-F', '-m', '-P', '-ImageDescription=', '-Subject=', '-Description='] tmp = None if not new_caption is None: if not new_caption: command.append('-Caption-Abstract=') else: tmpfd, tmp = tempfile.mkstemp(dir="/var/tmp") os.close(tmpfd) file1 = open(tmp, "w") print >> file1, new_caption.encode("utf-8") file1.close() command.append('-Caption-Abstract<=%s' % (tmp)) if new_datetime: command.append('-DateTimeOriginal="%s"' % ( new_datetime.strftime("%Y:%m:%d %H:%M:%S"))) if new_keywords: for keyword in new_keywords: command.append(u'-keywords=%s' % (keyword)) elif new_keywords != None: command.append('-keywords=') if new_rating >= 0: command.append('-Rating=%d' % (new_rating)) if new_gps: command.append('-c') command.append('%.6f') command.append('-GPSLatitude="%f"' % (abs(new_gps.latitude))) command.append('-GPSLatitudeRef=' + new_gps.latitude_ref()) command.append('-GPSLongitude="%f"' % (abs(new_gps.longitude))) command.append('-GPSLongitudeRef=' + new_gps.longitude_ref()) if new_persons: for person in new_persons: command.append(u'-RegionPersonDisplayName=%s' % (person)) elif new_persons != None: command.append('-RegionPersonDisplayName=') if new_rectangles: for rectangle in new_rectangles: command.append('-RegionRectangle=%s' % ( ','.join(str(c) for c in rectangle))) elif new_rectangles != None: command.append('-RegionRectangle=') command.append("-iptc:CodedCharacterSet=ESC % G") command.append(filepath) result = su.fsdec(su.execandcombine(command)) if tmp: os.remove(tmp) if result.find("1 image files updated") != -1: if result != "1 image files updated": su.pout(result) # wipe out the back file created by exiftool backup_file = filepath + "_original" if os.path.exists(backup_file): os.remove(backup_file) return True else: su.perr("Failed to update IPTC data in image %s: %s" % ( filepath, result)) return False
def get_iptc_data(image_file): """get caption, keywords, datetime, rating, and GPS info all in one operation.""" output = su.execandcombine( (EXIFTOOL, "-X", "-m", "-q", "-q", '-c', '%.6f', "-Keywords", "-Caption-Abstract", "-DateTimeOriginal", "-Rating", "-GPSLatitude", "-Subject", "-GPSLongitude", "-RegionRectangle", "-RegionPersonDisplayName", image_file)) keywords = [] caption = None date_time_original = None rating = 0 gps = None region_names = [] region_rectangles = [] if output: try: gps_latitude = None gps_longitude = None xml_data = minidom.parseString(output) for xml_desc in xml_data.getElementsByTagName('rdf:Description'): _get_xml_nodevalues(xml_desc, 'IPTC:Keywords', keywords) # Keywords can also be stored as Subject in the XMP directory _get_xml_nodevalues(xml_desc, 'XMP:Subject', keywords) for xml_caption in xml_data.getElementsByTagName( 'IPTC:Caption-Abstract'): caption = xml_caption.firstChild.nodeValue for xml_element in xml_data.getElementsByTagName( 'ExifIFD:DateTimeOriginal'): if not xml_element.firstChild: continue try: date_time_original = time.strptime( xml_element.firstChild.nodeValue, '%Y:%m:%d %H:%M:%S') date_time_original = datetime.datetime( date_time_original.tm_year, date_time_original.tm_mon, date_time_original.tm_mday, date_time_original.tm_hour, date_time_original.tm_min, date_time_original.tm_sec) except ValueError, _ve: su.perr('Exiftool returned an invalid ' 'date %s for %s - ignoring.' % (xml_element.firstChild.nodeValue, image_file)) for xml_element in xml_data.getElementsByTagName( 'XMP-xmp:Rating'): rating = int(xml_element.firstChild.nodeValue) for xml_element in xml_data.getElementsByTagName( 'Composite:GPSLatitude'): gps_latitude = xml_element.firstChild.nodeValue for xml_element in xml_data.getElementsByTagName( 'Composite:GPSLongitude'): gps_longitude = xml_element.firstChild.nodeValue string_rectangles = [] _get_xml_nodevalues(xml_desc, 'XMP-MP:RegionRectangle', string_rectangles) for string_rectangle in string_rectangles: rectangle = [] for c in string_rectangle.split(','): rectangle.append(float(c)) region_rectangles.append(rectangle) _get_xml_nodevalues(xml_desc, 'XMP-MP:RegionPersonDisplayName', region_names) xml_data.unlink() if gps_latitude and gps_longitude: gps = imageutils.GpsLocation().from_composite( gps_latitude, gps_longitude) except parsers.expat.ExpatError, ex: su.perr('Could not parse exiftool output %s: %s' % (output, ex))