# -*- coding: utf-8 -*- #!/usr/bin/env python # # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2009-2011 Rob G. Healey # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # $Id$ # ***************************************************************************** # Python Modules # ***************************************************************************** import os import datetime import calendar import time from PIL import Image # abilty to escape certain characters from output from xml.sax.saxutils import escape as _html_escape from itertools import chain from decimal import Decimal, getcontext getcontext().prec = 6 from fractions import Fraction import subprocess # ----------------------------------------------------------------------------- # GTK modules # ----------------------------------------------------------------------------- import gtk # ----------------------------------------------------------------------------- # GRAMPS modules # ----------------------------------------------------------------------------- import GrampsDisplay from gen.ggettext import gettext as _ from DateHandler import displayer as _dd from DateHandler import parser as _dp from gen.plug import Gramplet from libmetadata import MetadataView, format_datetime from gui.widgets import ValidatableMaskedEntry from Errors import ValidationError from QuestionDialog import QuestionDialog, OptionDialog from gen.lib import Date import gen.mime import Utils from PlaceUtils import conv_lat_lon from gen.db import DbTxn from ListModel import ListModel import pyexiv2 # v0.1 has a different API to v0.2 and above if hasattr(pyexiv2, 'version_info'): OLD_API = False else: # version_info attribute does not exist prior to v0.2.0 OLD_API = True # define the Exiv2 command system_platform = os.sys.platform if system_platform == "win32": EXIV2_FOUND = "exiv2.exe" if Utils.search_for("exiv2.exe") else False else: EXIV2_FOUND = "exiv2" if Utils.search_for("exiv2") else False #------------------------------------------------ # support functions #------------------------------------------------ def _parse_datetime(value): """ Parse date and time and return a datetime object. """ value = value.rstrip() if not value: return None if value.find(u':') >= 0: # Time part present if value.find(u' ') >= 0: # Both date and time part date_text, time_text = value.rsplit(u' ', 1) else: # Time only date_text = u'' time_text = value else: # Date only date_text = value time_text = u'00:00:00' date_part = _dp.parse(date_text) try: time_part = time.strptime(time_text, '%H:%M:%S') except ValueError: time_part = None if (date_part.get_modifier() == Date.MOD_NONE and time_part is not None): return datetime.datetime( date_part.get_year(), date_part.get_month(), date_part.get_day(), time_part.tm_hour, time_part.tm_min, time_part.tm_sec) else: return None # ----------------------------------------------------------------------------- # Constants # ----------------------------------------------------------------------------- # available image types for exiv2 and pyexiv2 _vtypes = [".bmp", ".dng", ".exv", ".jp2", ".jpeg", ".jpg", ".nef", ".pef", ".pgf", ".png", ".psd", ".srw", ".tiff"] _VALIDIMAGEMAP = dict( (index, imgtype) for index, imgtype in enumerate(_vtypes) ) # valid converting types for PIL.Image # there are more image formats that PIL.Image can convert to, # but they are not usable in exiv2/ pyexiv2 _validconvert = [_("<-- Image Types -->"), ".bmp", ".jpg", ".png", ".tiff"] # set up Exif keys for Image Exif metadata keypairs _DATAMAP = { None : "MediaTitle", "Exif.Image.ImageDescription" : "Description", "Exif.Photo.DateTimeOriginal" : "Original", "Exif.Image.DateTime" : "Modified", "Exif.Photo.DateTimeDigitized" : "Digitized", "Exif.Image.Artist" : "Artist", "Exif.Image.Copyright" : "Copyright", "Exif.GPSInfo.GPSLatitudeRef" : "LatitudeRef", "Exif.GPSInfo.GPSLatitude" : "Latitude", "Exif.GPSInfo.GPSLongitudeRef" : "LongitudeRef", "Exif.GPSInfo.GPSLongitude" : "Longitude", "Exif.GPSInfo.GPSAltitudeRef" : "AltitudeRef", "Exif.GPSInfo.GPSAltitude" : "Altitude", "Exif.Photo.DateTimeDigitized" : "Digitized" } _DATAMAP = dict( (key, val) for key, val in _DATAMAP.items()) _DATAMAP.update( (val, key) for key, val in _DATAMAP.items()) # define tooltips for all data entry fields _TOOLTIPS = { # Media Object's Title "MediaTitle" : _("Warning: Changing this entry will update the Media " "object title field in Gramps not Exiv2 metadata."), # Description "Description" : _("Provide a short descripion for this image."), # Artist "Artist" : _("Enter the Artist/ Author of this image. The person's name or " "the company who is responsible for the creation of this image."), # Copyright "Copyright" : _("Enter the copyright information for this image. \n"), # Original Date/ Time "Original" : _("The original date/ time when the image was first created/ taken as in a photograph.\n" "Example: 1830-01-1 09:30:59"), # Last Change/ Modify Date/ Time "Modified" : _("This is the date/ time that the image was last changed/ modified.\n" "Example: 2011-05-24 14:30:00"), # GPS Latitude coordinates "Latitude" : _("Enter the Latitude GPS coordinates for this image,\n" "Example: 43.722965, 43 43 22 N, 38° 38′ 03″ N, 38 38 3"), # GPS Longitude coordinates "Longitude" : _("Enter the Longitude GPS coordinates for this image,\n" "Example: 10.396378, 10 23 46 E, 105° 6′ 6″ W, -105 6 6"), # GPS Altitude (in meters) "Altitude" : _("This is the measurement of Above or Below Sea Level. It is measured in meters." "Example: 200.558, -200.558") } _TOOLTIPS = dict( (key, tooltip) for key, tooltip in _TOOLTIPS.items()) # define tooltips for all buttons # common buttons for all images _BUTTONTIPS = { # Wiki Help button "Help" : _("Displays the Gramps Wiki Help page for 'Edit Image Exif Metadata' " "in your web browser."), # Edit screen button "Edit" : _("This will open up a new window to allow you to edit/ modify " "this image's Exif metadata.\n It will also allow you to be able to " "Save the modified metadata."), # Thumbnail Viewing Window button "Thumbnail" : _("Will produce a Popup window showing a Thumbnail Viewing Area"), # Image Type button "ImageTypes" : _("Select from a drop- down box the image file type that you " "would like to convert your non- Exiv2 compatible media object to."), # Convert to different image type "Convert" : _("If your image is not of an image type that can have " "Exif metadata read/ written to/from, convert it to a type that can?"), # Delete/ Erase/ Wipe Exif metadata button "Delete" : _("WARNING: This will completely erase all Exif metadata " "from this image! Are you sure that you want to do this?")} # ------------------------------------------------------------------------ # # 'Edit Image Exif metadata' Class # # ------------------------------------------------------------------------ class EditExifMetadata(Gramplet): """ Special symbols... degrees symbol = [Ctrl] [Shift] u \00b0 minutes symbol = \2032 seconds symbol = \2033 """ def init(self): """ create variables, and build display """ self.exif_widgets = {} self.dates = {} self.orig_image, self.plugin_image, self.image_path = [False]*3 vbox = self.__build_gui() self.gui.get_container_widget().remove(self.gui.textview) self.gui.get_container_widget().add_with_viewport(vbox) self.dbstate.db.connect('media-update', self.update) self.connect_signal('Media', self.update) def __build_gui(self): """ will display all exif metadata and all buttons. """ main_vbox = gtk.VBox(False, 0) main_vbox.set_border_width(10) # Displays the file name medialabel = gtk.HBox(False, 0) label = self.__create_label("MediaLabel", False, False, False) medialabel.pack_start(label, expand =False) main_vbox.pack_start(medialabel, expand =False, fill =True, padding =0) # Displays mime type information mimetype = gtk.HBox(False, 0) label = self.__create_label("MimeType", False, False, False) mimetype.pack_start(label, expand =False) main_vbox.pack_start(mimetype, expand =False, fill =True, padding =0) # image dimensions imagesize = gtk.HBox(False, 0) label = self.__create_label("ImageSize", False, False, False) imagesize.pack_start(label, expand =False, fill =False, padding =0) main_vbox.pack_start(imagesize, expand =False, fill =True, padding =0) # Displays all plugin messages messagearea = gtk.HBox(False, 0) label = self.__create_label("MessageArea", False, False, False) messagearea.pack_start(label, expand =False) main_vbox.pack_start(messagearea, expand =False, fill =True, padding =0) # Separator line before the buttons main_vbox.pack_start(gtk.HSeparator(), expand =False, fill =False, padding =0) # Thumbnail, ImageType, and Convert buttons new_hbox = gtk.HBox(False, 0) main_vbox.pack_start(new_hbox, expand =False, fill =False, padding =0) new_hbox.show() # Thumbnail button event_box = gtk.EventBox() new_hbox.pack_start(event_box, expand =False, fill =False, padding =0) event_box.show() button = self.__create_button( "Thumbnail", _("Thumbnail"), [self.thumbnail_view], ) event_box.add(button) # Image Types event_box = gtk.EventBox() new_hbox.pack_start(event_box, expand =False, fill =False, padding =0) event_box.show() combo_box = gtk.combo_box_new_text() combo_box.append_text(_validconvert[0]) combo_box.set_active(0) combo_box.set_sensitive(False) event_box.add(combo_box) self.exif_widgets["ImageTypes"] = combo_box combo_box.show() # Convert button event_box = gtk.EventBox() new_hbox.pack_start(event_box, expand =False, fill =False, padding =0) event_box.show() button = self.__create_button( "Convert", False, [self.__convert_dialog], gtk.STOCK_CONVERT) event_box.add(button) # Connect the changed signal to ImageType self.exif_widgets["ImageTypes"].connect("changed", self.changed_cb) # Help, Edit, and Delete buttons new_hbox = gtk.HBox(False, 0) main_vbox.pack_start(new_hbox, expand =False, fill =False, padding =0) new_hbox.show() for (widget, text, callback, icon, is_sensitive) in [ ("Help", False, [self.__help_page], gtk.STOCK_HELP, True), ("Edit", False, [self.display_edit], gtk.STOCK_EDIT, False), ("Delete", False, [self._wipe_dialog], gtk.STOCK_DELETE, False) ]: event_box = gtk.EventBox() new_hbox.pack_start(event_box, expand =False, fill =False, padding =0) event_box.show() button = self.__create_button( widget, text, callback, icon, is_sensitive) event_box.add(button) # add viewing model self.view = MetadataView() main_vbox.pack_start(self.view, expand =False, fill =True, padding =5) # Separator line before the Total main_vbox.pack_start(gtk.HSeparator(), expand =False, fill =True, padding =5) # number of key/ value pairs shown label = self.__create_label("Total", False, False, False) main_vbox.pack_start(label, expand =False, fill =True, padding =5) main_vbox.show_all() return main_vbox def db_changed(self): self.dbstate.db.connect('media-update', self.update) self.connect_signal('Media', self.update) self.update() def active_changed(self, handle): self.update() def main(self): # return false finishes """ get the active media, mime type, and reads the image metadata *** disable all buttons at first, then activate as needed # Help will never be disabled """ db = self.dbstate.db # deactivate all buttons except Help self.deactivate_buttons(["Convert", "Edit", "ImageTypes", "Delete"]) imgtype_format = [] # display all button tooltips only # 1st argument is for Fields, 2nd argument is for Buttons self._setup_widget_tips(fields =False, buttons =True) # clears all labels and display area for widget in ["MediaLabel", "MimeType", "ImageSize", "MessageArea", "Total"]: self.exif_widgets[widget].set_text("") # set Message Ares to Select self.exif_widgets["MessageArea"].set_text(_("Select an image to begin...")) active_handle = self.get_active("Media") if not active_handle: self.set_has_data(False) return # get image from database self.orig_image = db.get_object_from_handle(active_handle) if not self.orig_image: self.set_has_data(False) return # get file path and attempt to find it? self.image_path =Utils.media_path_full(db, self.orig_image.get_path() ) if not os.path.isfile(self.image_path): self.set_has_data(False) return # check image read privileges _readable = os.access(self.image_path, os.R_OK) if not _readable: self.exif_widgets["MessageArea"].set_text(_("Image is NOT readable,\n" "Please choose a different image...")) return ### No longer any reason to return because of errors ### # display file description/ title self.exif_widgets["MediaLabel"].set_text(_html_escape(self.orig_image.get_description())) # Mime type information mime_type = self.orig_image.get_mime_type() self.exif_widgets["MimeType"].set_text(gen.mime.get_description(mime_type)) # check image write privileges _writable = os.access(self.image_path, os.W_OK) if not _writable: self.exif_widgets["MessageArea"].set_text(_("Image is NOT writable,\n" "You will NOT be able to save Exif metadata....")) self.deactivate_buttons(["Edit"]) # get dirpath/ basename, and extension self.basename, self.extension = os.path.splitext(self.image_path) if (mime_type and mime_type.startswith("image/")): if self.extension not in _VALIDIMAGEMAP.values(): # Convert message self.exif_widgets["MessageArea"].set_text(_("Please convert this " "image to an Exiv2- compatible image type...")) imgtype_format = _validconvert self._VCONVERTMAP = dict((index, imgtype) for index, imgtype in enumerate(imgtype_format)) for index in xrange(1, len(imgtype_format) ): self.exif_widgets["ImageTypes"].append_text(imgtype_format[index]) self.exif_widgets["ImageTypes"].set_active(0) self.activate_buttons(["ImageTypes"]) else: # creates, and reads the plugin image instance self.plugin_image = self.setup_image(self.image_path) # activate Edit button self.activate_buttons(["Edit"]) # get image width and height if not OLD_API: self.exif_widgets["ImageSize"].show() width, height = self.plugin_image.dimensions self.exif_widgets["ImageSize"].set_text(_("Image " "Size : %04d x %04d pixels") % (width, height) ) # check for thumbnails has_thumb = self.__check4thumbnails() if has_thumb: self.activate_buttons(["Thumbnail"]) # display all Exif tags for this image, # XmpTag and IptcTag has been purposefully excluded self.__display_exif_tags(self.image_path) def __check4thumbnails(self): """ check for thumbnails and activate Thumbnail button if found? """ if OLD_API: # prior to pyexiv2-0.2.0 try: ttype, tdata = self.plugin_image.getThumbnailData() except (IOError, OSError): return False else: # pyexiv2-0.2.0 and above try: previews = self.plugin_image.previews except (IOError, OSError): return False return True def __display_exif_tags(self, full_path): """ Display the exif tags. """ self.exif_widgets["MessageArea"].set_text(_("Displaying Exif metadata...")) # display exif tags in the treeview has_data = self.view.display_exif_tags(full_path) # update set_has_data functionality self.set_has_data(has_data) if has_data: self.activate_buttons(["Delete"]) def changed_cb(self, ext_value =None): """ will show the Convert Button once an Image Type has been selected, and if image extension is not an Exiv2- compatible image? """ # get convert image type and check it from ImageTypes drop- down ext_value = self.exif_widgets["ImageTypes"].get_active() if ext_value >= 1: # if Convert button is not active, set it to active? # so that the user may convert this image? if not self.exif_widgets["Convert"].get_sensitive(): self.activate_buttons(["Convert"]) # connect clicked signal to Convert button self.exif_widgets["Convert"].connect("clicked", self.__convert_dialog) def _setup_widget_tips(self, fields =None, buttons =None): """ set up widget tooltips * data fields * buttons """ # if True, setup tooltips for all Data Entry Fields if fields: for widget, tooltip in _TOOLTIPS.items(): self.exif_widgets[widget].set_tooltip_text(tooltip) # if True, setup tooltips for all Buttons if buttons: for widget, tooltip in _BUTTONTIPS.items(): self.exif_widgets[widget].set_tooltip_text(tooltip) def setup_image(self, full_path): """ This will: * create the plugin image instance if needed, * setup the tooltips for the data fields, * setup the tooltips for the buttons, """ if OLD_API: # prior to pyexiv2-0.2.0 metadata = pyexiv2.Image(full_path) try: metadata.readMetadata() except (IOError, OSError): self.set_has_data(False) metadata = False else: # pyexiv2-0.2.0 and above metadata = pyexiv2.ImageMetadata(full_path) try: metadata.read() except (IOError, OSError): self.set_has_data(False) metadata = False return metadata def update_has_data(self): active_handle = self.get_active('Media') active = self.dbstate.db.get_object_from_handle(active_handle) self.set_has_data(self.get_has_data(active)) def get_has_data(self, media): """ Return True if the gramplet has data, else return False. """ if media is None: return False full_path = Utils.media_path_full(self.dbstate.db, media.get_path()) return self.view.get_has_data(full_path) def __create_button(self, pos, text, callback =[], icon =False, sensitive =False): """ creates and returns a button for display """ if (icon and not text): button = gtk.Button(stock =icon) else: button = gtk.Button(text) if callback is not []: for call_ in callback: button.connect("clicked", call_) # attach a addon widget to the button for later manipulation self.exif_widgets[pos] = button if not sensitive: button.set_sensitive(False) button.show() return button def __create_label(self, widget, text, width, height, wrap =True): """ creates a label for this addon. """ label = gtk.Label() if text: label.set_text(text) label.set_alignment(0.0, 0.0) if wrap: label.set_line_wrap(True) if (width and height): label.set_size_request(width, height) if widget: self.exif_widgets[widget] = label label.show() return label def __create_event_entry(self, pos, width, height, length_, type_, cb_list): """ handles the creation of an event_box and entry containers and returns them... """ evt_box = gtk.EventBox() if (width and height): evt_box.set_size_request(width, height) self.exif_widgets[pos + "Box"] = evt_box evt_box.show() if type_ == "Validate": entry = ValidatableMaskedEntry() if cb_list: for call_ in cb_list: entry.connect('validate', call_, pos) elif type_ == "Entry": entry = gtk.Entry(max = length_) if cb_list: for call_ in cb_list: entry.connect('validate', call_) evt_box.add(entry) self.exif_widgets[pos] = entry entry.show() return evt_box def thumbnail_view(self, object): """ will allow a display area for a thumbnail pop-up window. """ tip = _("Click Close to close this Thumbnail View Area.") self.tbarea = gtk.Window(gtk.WINDOW_TOPLEVEL) self.tbarea.tooltip = tip self.tbarea.set_title(_("Thumbnail View Area")) pbloader, width, height = self.__get_thumbnail_data() if pbloader: self.tbarea.set_default_size((width + 40), (height + 40)) self.tbarea.set_border_width(10) self.tbarea.connect('destroy', lambda w: self.tbarea.destroy() ) new_vbox = self.build_thumbnail_gui(pbloader, width, height) self.tbarea.add(new_vbox) self.tbarea.show() else: self.deactivate_buttons(["Thumbnail"]) lambda w: self.tbarea.destroy() def __get_thumbnail_data(self): """ returns the thumbnail width and height from the active media object if there is any? """ pbloader, width, height = [False]*3 if OLD_API: # prior to pyexiv2-0.2.0 try: ttype, tdata = self.plugin_image.getThumbnailData() width, height = tdata.dimensions # Create a GTK pixbuf loader to read the thumbnail data pbloader = gtk.gdk.PixbufLoader() pbloader.write(tdata) except (IOError, OSError): return pbloader, width, height else: # pyexiv2-0.2.0 and above try: previews = self.plugin_image.previews if not previews: return pbloader, width, height # Get the largest preview available preview = previews[-1] width, height = preview.dimensions except (IOError, OSError): return pbloader, width, height # Create a GTK pixbuf loader to read the thumbnail data pbloader = gtk.gdk.PixbufLoader() pbloader.write(preview.data) return pbloader, width, height def build_thumbnail_gui(self, pbloader, width, height): """ builds the thumbnail viewing area. """ main_vbox = gtk.VBox() main_vbox.set_size_request((width - 30), (height - 30)) hbox = gtk.HBox(False, 0) main_vbox.pack_start(hbox, expand =False, fill =False, padding =5) hbox.show() # Get the resulting pixbuf and build an image to be displayed pixbuf = pbloader.get_pixbuf() pbloader.close() imgwidget = gtk.Image() imgwidget.set_from_pixbuf(pixbuf) hbox.pack_start(imgwidget, expand = False, fill =True, padding =0) imgwidget.show() main_vbox.show_all() return main_vbox def __convert_dialog(self, object): """ Handles the Convert question Dialog """ # Convert and delete original file or just convert OptionDialog(_("Edit Image Exif Metadata"), _("WARNING: You are about to convert this " "image into a .jpeg image. Are you sure that you want to do this?"), _("Convert and Delete"), self.__convert_delete, _("Convert"), self.__convert_only) self.update() return def __convert_copy(self, full_path =None): """ Will attempt to convert an image to jpeg if it is not? """ if full_path is None: full_path = self.image_path # get image filepath and its filename filepath, basename = os.path.split(self.basename) # get extension selected for converting this image ext_type = self.exif_widgets["ImageTypes"].get_active() if ext_type == 0: return False basename += self._VCONVERTMAP[ext_type] # new file name and dirpath dest_file = os.path.join(filepath, basename) # open source image file im = Image.open(full_path) im.save(dest_file) # pyexiv2 source image file if OLD_API: # prior to pyexiv2-0.2.0 src_meta = pyexiv2.Image(full_path) src_meta.readMetadata() else: src_meta = pyexiv2.ImageMetadata(full_path) src_meta.read() # check to see if source image file has any Exif metadata? if _get_exif_keypairs(src_meta): if OLD_API: # prior to pyexiv2-0.2.0 # Identify the destination image file dest_meta = pyexiv2.Image(dest_file) dest_meta.readMetadata() # copy source metadata to destination file src_meta.copy(dest_meta, comment =False) # writes all Exif Metadata to image even if the fields are all empty so as to remove the value self.write_metadata(dest_meta) else: # pyexiv2-0.2.0 and above # Identify the destination image file dest_meta = pyexiv2.ImageMetadata(dest_file) dest_meta.read() # copy source metadata to destination file src_meta.copy(dest_meta, comment =False) # writes all Exif Metadata to image even if the fields are all empty so as to remove the value self.write_metadata(dest_meta) return dest_file def __convert_delete(self, full_path =None): """ will convert an image file and delete original non-jpeg image. """ if full_path is None: full_path = self.image_path # Convert image and copy over it's Exif metadata (if any?) newfilepath = self.__convert_copy(full_path) if newfilepath: # delete original file from this computer and set new filepath try: os.remove(full_path) delete_results = True except (IOError, OSError): delete_results = False if delete_results: # check for new destination and if source image file is removed? if (os.path.isfile(newfilepath) and not os.path.isfile(full_path) ): self.__update_media_path(newfilepath) # notify user about the convert, delete, and new filepath self.exif_widgets["MessageArea"].set_text(_("Your image has been " "converted and the original file has been deleted, and " "the full path has been updated!")) else: self.exif_widgets["MessageArea"].set_text(_("There has been an error, " "Please check your source and destination file paths...")) else: self.exif_widgets["MessageArea"].set_text(_("There was an error in " "deleting the original file. You will need to delete it yourself!")) def __convert_only(self, full_path =None): """ This will only convert the file and update the media object path. """ if full_path is None: full_path = self.image_path # the convert was sucessful, then update media path? newfilepath = self.__convert_copy(full_path) if newfilepath: # update the media object path self.__update_media_path(newfilepath) else: self.exif_widgets["MessageArea"].set_text(_("There was an error " "in converting your image file.")) def __update_media_path(self, newfilepath =None): """ update the media object's media path. """ if newfilepath: db = self.dbstate.db # begin database tranaction to save media object new path with DbTxn(_("Media Path Update"), db) as trans: self.orig_image.set_path(newfilepath) db.commit_media_object(self.orig_image, trans) db.request_rebuild() else: self.exif_widgets["MessageArea"].set_text(_("There has been an " "error in updating the image file's path!")) def __help_page(self, addonwiki =None): """ will bring up a Wiki help page. """ addonwiki = 'Edit Image Exif Metadata' GrampsDisplay.help(webpage =addonwiki) def activate_buttons(self, buttonlist): """ Enable/ activate the buttons that are in buttonlist """ for widget in buttonlist: self.exif_widgets[widget].set_sensitive(True) def deactivate_buttons(self, buttonlist): """ disable/ de-activate buttons in buttonlist *** if All, then disable ALL buttons in the current display """ if buttonlist == ["All"]: buttonlist = [(buttonname) for buttonname in _BUTTONTIPS.keys() if buttonname is not "Help"] for widget in buttonlist: self.exif_widgets[widget].set_sensitive(False) def display_edit(self, object): """ creates the editing area fields. """ tip = _("Click the close button when you are finished modifying this " "image's Exif metadata.") main_scr_width = self.uistate.screen_width() # on a screen of 1024 x 768, width = 614, height will always remain at 600 for netbooks # with a screen height of 600 maximum... width_ = int(main_scr_width * 0.60) edtarea = gtk.Window(gtk.WINDOW_TOPLEVEL) edtarea.tooltip = tip edtarea.set_title( self.orig_image.get_description()) edtarea.set_size_request((width_ + 45), 550) edtarea.set_border_width(10) width_ -= 10 # width = 604 edtarea.connect("destroy", lambda w: edtarea.destroy()) # create a new scrolled window. scrollwindow = gtk.ScrolledWindow() scrollwindow.set_size_request(width_, 600) scrollwindow.set_border_width(10) width_ -= 10 # width = 594 # will show scrollbars only when necessary scrollwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) edtarea.add(scrollwindow) scrollwindow.show() vbox = self.__build_edit_gui(width_) scrollwindow.add_with_viewport(vbox) vbox.show_all() edtarea.show() # display all fields and button tooltips... # need to add Save, Clear, and Close over here... _BUTTONTIPS.update( (key, tip) for key, tip in { # Add the Save button... "Save" : _("Saves a copy of the data fields into the image's Exif metadata."), # Re- display the data fields button... "Copy" : _("Re -display the data fields that were cleared from the Edit Area."), # Add the Clear button... "Clear" : _("This button will clear all of the data fields shown here."), # Add the Close button... "Close" : _("Closes this popup Edit window.\n" "WARNING: This action will NOT Save any changes/ modification made to this " "image's Exif metadata.") }.items() ) # True, True -- all data fields and button tooltips will be displayed... self._setup_widget_tips(fields =True, buttons = True) # display all data fields and their values... self.edit_area(_get_exif_keypairs(self.plugin_image)) def __build_edit_gui(self, width_): """ creates the content for the edit window... """ main_vbox = gtk.VBox() main_vbox.set_border_width(10) width_ -= 10 # width = 584 # 520 is the normal height of this vertical box... main_vbox.set_size_request(width_, 500) # Notification Area for the Edit Area window... label = self.__create_label("EditMessage", False, width =(width_ - 62), height =25) main_vbox.pack_start(label, expand = False, fill =True, padding =0) # Media Title Frame... width_ -= 10 # 574 on a screen width of 1024 title_frame = gtk.Frame(_("Media Object Title")) title_frame.set_size_request(width_, 60) # width = 574 main_vbox.pack_start(title_frame, expand =False, fill =True, padding =10) title_frame.show() new_vbox = gtk.VBox(False, 0) title_frame.add(new_vbox) new_vbox.show() for widget, text in [ ("MediaTitle", _("media Title: ")) ]: new_hbox = gtk.HBox(False, 0) new_vbox.pack_start(new_hbox, expand =False, fill =False, padding =5) new_hbox.show() label = self.__create_label(False, text, width =90, height =25) new_hbox.pack_start(label, expand =False, fill =False, padding =0) event_box = self.__create_event_entry(widget, 464, 30, 100, "Entry", []) new_hbox.pack_start(event_box, expand =False, fill =False, padding =0) # create the data fields... # ***Description, Artist, and Copyright gen_frame = gtk.Frame(_("General Data")) gen_frame.set_size_request(width_, 155) # width = 574 main_vbox.pack_start(gen_frame, expand =False, fill =True, padding =10) gen_frame.show() new_vbox = gtk.VBox(False, 0) gen_frame.add(new_vbox) new_vbox.show() for widget, text in [ ("Description", _("Description: ")), ("Artist", _("Artist: ")), ("Copyright", _("Copyright: ")) ]: new_hbox = gtk.HBox(False, 0) new_vbox.pack_start(new_hbox, expand =False, fill =False, padding =5) new_hbox.show() label = self.__create_label(False, text, width =90, height =25) new_hbox.pack_start(label, expand =False, fill =False, padding =0) event_box = self.__create_event_entry(widget, 464, 30, 100, "Entry", []) new_hbox.pack_start(event_box, expand =False, fill =False, padding =0) # iso format: Year, Month, Day spinners... datetime_frame = gtk.Frame(_("Date/ Time")) datetime_frame.set_size_request(width_, 90) # width = 574 main_vbox.pack_start(datetime_frame, expand =False, fill =False, padding =0) datetime_frame.show() new_vbox = gtk.VBox(False, 0) datetime_frame.add(new_vbox) new_vbox.show() new_hbox = gtk.HBox(False, 0) new_vbox.pack_start(new_hbox, expand =False, fill =False, padding =0) new_hbox.show() for widget, text in [ ("Original", _("Original: ")), ("Modified", _("Modified: ")) ]: vbox2 = gtk.VBox(False, 0) new_hbox.pack_start(vbox2, expand =False, fill =False, padding =5) vbox2.show() label = self.__create_label(widget, text, width =90, height =25) vbox2.pack_start(label, expand =False, fill =False, padding =0) label.show() # each box width = 157 event_box = self.__create_event_entry(widget, 272, 30, 0, "Validate", [self.validate_datetime]) vbox2.pack_start(event_box, expand =False, fill =False, padding =0) self.dates[widget] = None # GPS coordinates... latlong_frame = gtk.Frame(_("Latitude/ Longitude/ Altitude GPS coordinates")) latlong_frame.set_size_request(width_, 80) # width = 574 main_vbox.pack_start(latlong_frame, expand =False, fill =False, padding =0) latlong_frame.show() new_vbox = gtk.VBox(False, 0) latlong_frame.add(new_vbox) new_vbox.show() new_hbox = gtk.HBox(False, 0) new_vbox.pack_start(new_hbox, expand =False, fill =False, padding =0) new_hbox.show() for widget, text in [ ("Latitude", _("Latitude :") ), ("Longitude", _("Longitude :") ), ("Altitude", _("Altitude :") ) ]: vbox2 = gtk.VBox(False, 0) new_hbox.pack_start(vbox2, expand =False, fill =False, padding =5) vbox2.show() label = self.__create_label(widget, text, width =90, height =25) vbox2.pack_start(label, expand =False, fill =False, padding =0) label.show() event_box = self.__create_event_entry(widget, 178, 30, 0, "Validate", [self.validate_coordinate]) vbox2.pack_start(event_box, expand =False, fill =False, padding =0) # Help, Save, Clear, Copy, and Close buttons... new_hbox = gtk.HBox(False, 0) main_vbox.pack_start(new_hbox, expand =False, fill =True, padding =5) new_hbox.show() for (widget, text, callback, icon, is_sensitive) in [ ("Help", False, [self.__help_page], gtk.STOCK_HELP, True), ("Save", False, [self.save_metadata, self.update], gtk.STOCK_SAVE, True), ("Clear", False, [self.clear_metadata], gtk.STOCK_CLEAR, True), ("Copy", False, [self.__display_exif_tags], gtk.STOCK_COPY, True), ("Close", False, [lambda w: self.edtarea.destroy()], gtk.STOCK_CLOSE, True) ]: event_box = gtk.EventBox() event_box.set_size_request(112, 30) new_hbox.pack_start(event_box, expand =False, fill =True, padding =1) event_box.show() event_box.add(self.__create_button( widget, text, callback, icon, is_sensitive) ) return main_vbox def set_datetime(self, widget, field): """ Parse date and time from text entry """ value = _parse_datetime(unicode(widget.get_text())) if value is not None: self.dates[field] = "%04d:%02d:%02d %02d:%02d:%02d" % ( value.year, value.month, value.day, value.hour, value.minute, value.second) else: self.dates[field] = None def validate_datetime(self, widget, data, field): """ Validate current date and time in text entry """ if self.dates[field] is None: return ValidationError(_('Bad Date/Time')) def validate_coordinate(self, widget, data, field): """ Validate current latitude or longitude in text entry """ # validate the Latitude field... if field == "Latitude" and not conv_lat_lon(data, "0", "ISO-D"): return ValidationError(_(u"Invalid latitude (syntax: 18\u00b09'") + _('48.21"S, -18.2412 or -18:9:48.21)')) # validate the Longitude field... if field == "Longitude" and not conv_lat_lon("0", data, "ISO-D"): return ValidationError(_(u"Invalid longitude (syntax: 18\u00b09'") + _('48.21"E, -18.2412 or -18:9:48.21)')) def _wipe_dialog(self, object): """ Handles the Delete Dialog... """ QuestionDialog(_("Edit Image Exif Metadata"), _("WARNING! You are about to completely " "delete the Exif metadata from this image?"), gtk.STOCK_DELETE, self.strip_metadata) self.update() def clear_metadata(self, object): """ clears all data fields to nothing """ for widget in _TOOLTIPS.keys(): self.exif_widgets[widget].set_text("") def edit_area(self, mediadatatags): """ displays the image Exif metadata in the Edit Area... """ if mediadatatags: mediadatatags = [key for key in mediadatatags if key in _DATAMAP] for key in mediadatatags: widget = _DATAMAP[key] tag_value = _get_value(self.plugin_image, key) if widget in ["Description", "Artist", "Copyright"]: if tag_value: self.exif_widgets[widget].set_text(tag_value) # Original Date... elif widget == "Original": use_date = format_datetime(tag_value) if use_date: self.exif_widgets[widget].set_text(use_date) # Last Modified date elif widget == "Modified": use_date = format_datetime(tag_value) if use_date: self.exif_widgets["Modified"].set_text(use_date) # set Modified Datetime to non-editable... self.exif_widgets[widget].set_editable(False) # LatitudeRef, Latitude, LongitudeRef, Longitude... elif widget == "Latitude": latitude, longitude = tag_value, _get_value(self.plugin_image, _DATAMAP["Longitude"]) # if latitude and longitude exist, display them? if (latitude and longitude): # split latitude metadata into (degrees, minutes, and seconds) latdeg, latmin, latsec = rational_to_dms(latitude) # split longitude metadata into degrees, minutes, and seconds longdeg, longmin, longsec = rational_to_dms(longitude) # check to see if we have valid GPS coordinates? latfail = any(coords == False for coords in [latdeg, latmin, latsec]) longfail = any(coords == False for coords in [longdeg, longmin, longsec]) if (not latfail and not longfail): # Latitude Direction Reference latref = _get_value(self.plugin_image, _DATAMAP["LatitudeRef"] ) # Longitude Direction Reference longref = _get_value(self.plugin_image, _DATAMAP["LongitudeRef"] ) # set display for Latitude GPS coordinates latitude = """%s° %s′ %s″ %s""" % (latdeg, latmin, latsec, latref) self.exif_widgets["Latitude"].set_text(latitude) # set display for Longitude GPS coordinates longitude = """%s° %s′ %s″ %s""" % (longdeg, longmin, longsec, longref) self.exif_widgets["Longitude"].set_text(longitude) self.exif_widgets["Latitude"].validate() self.exif_widgets["Longitude"].validate() elif widget == "Altitude": altitude = tag_value altref = _get_value(self.plugin_image, _DATAMAP["AltitudeRef"]) if (altitude and altref): altitude = convert_value(altitude) if altitude: if altref == "1": altitude = "-" + altitude self.exif_widgets[widget].set_text(altitude) # no Exif metadata, but there is a media object date available else: mediaobj_date = self.orig_image.get_date_object() if mediaobj_date: self.exif_widgets["Original"].set_text(_dd.display(mediaobj_date)) # Media Object Title... self.media_title = self.orig_image.get_description() self.exif_widgets["MediaTitle"].set_text(self.media_title) def write_metadata(self, plugininstance): """ writes the Exif metadata to the image. OLD_API -- prior to pyexiv2-0.2.0 -- pyexiv2-0.2.0 and above... """ if OLD_API: plugininstance.writeMetadata() else: plugininstance.write() def convert_format(self, latitude, longitude, format): """ Convert GPS coordinates into a specified format. """ if (not latitude and not longitude): return [False]*2 latitude, longitude = conv_lat_lon( unicode(latitude), unicode(longitude), format) return latitude, longitude def convert2dms(self, latitude =None, longitude =None): """ will convert a decimal GPS coordinates into degrees, minutes, seconds for display only """ if (not latitude or not longitude): return [False]*2 latitude, longitude = self.convert_format(latitude, longitude, "DEG-:") return latitude, longitude def save_metadata(self, object): """ gets the information from the plugin data fields and sets the key = widgetvaluee image metadata """ # set up default variables... db = self.dbstate.db valid = 0 latref, longref, altref = [False]*3 # get all data field values... mediatitle = self.exif_widgets["MediaTitle"].get_text() description = self.exif_widgets["Description"].get_text() artist = self.exif_widgets["Artist"].get_text() copyright = self.exif_widgets["Copyright"].get_text() # special variables have been set up for the dates... original = self.exif_widgets["Original"].get_text() if original: self.set_datetime(self.exif_widgets["Original"], "Original") # update dynamically set Modified date... modified = datetime.datetime.now() latitude = self.exif_widgets["Latitude"].get_text() longitude = self.exif_widgets["Longitude"].get_text() altitude = self.exif_widgets["Altitude"].get_text() widgets = ["MediaTitle", "Description", "Artist", "Copyright", "Original", "Modified", "Latitude", "Longitude", "Altitude"] values = [mediatitle, description, artist, copyright, original, modified, latitude, longitude, altitude] namevalues = list(zip(widgets, values)) namevalues = [(w, v) for w, v in namevalues if v] if namevalues: for widgetname, widgetvalue in namevalues: key = _DATAMAP[widgetname] # Media Object's Title... # this will only affect the Media object from wthin the database... if widgetname == "MediaTitle": if (self.media_title and self.media_title is not mediatitle): with DbTxn(_("Media Title Update"), db) as trans: self.orig_image.set_description(mediatitle) db.commit_media_object(self.orig_image, trans) db.request_rebuild() # original date of image... elif widgetname == "Original": objdate_ = False if original: mediaobj_date = self.orig_image.get_date_object() if mediaobj_date.is_empty(): objdate_ = Date() if objdate_: original = _parse_datetime(original) if original: try: objdate_.set_yr_mon_day(original.year, original.month, original.day) except ValueError: objdate_ = False if objdate_: with DbTxn(_("Media Object Date Created"), db) as trans: self.orig_image.set_date_object(objdate_) db.commit_media_object(self.orig_image, trans) db.request_rebuild() # Latitude Reference, Latitude, Longitude Reference, and Longitude... # if equal to None, then convert failed? elif widgetname == "Latitude": latitude = self.exif_widgets["Latitude"].get_text() longitude = self.exif_widgets["Longitude"].get_text() if (latitude and longitude): latitude, longitude = self.convert2dms(latitude, longitude) if (latitude and longitude): latref = 'N' if "-" in latitude: latref = "S" latitude = latitude.replace("-", "") longref = 'E' if "-" in longitude: longref = "W" longitude = longitude.replace("-", "") # convert Latitude/ Longitude into pyexiv2.Rational()... latitude = coords_to_rational(latitude) longitude = coords_to_rational(longitude) # Altitude Reference, and Altitude... elif widgetname == "Altitude": altref = '0' if "-" in widgetvalue: widgetvalue = widgetvalue.replace("-", "") altref = "1" # convert altitude to pyexiv2.Rational for saving... altitude = altitude2rational(widgetvalue) # get all values for fields to be saved... # except for MediaTitle which is handled above... widgets = ["Description", "Artist", "Copyright", "Original", "Modified", "Latitude", "Longitude", "Altitude"] values = [description, artist, copyright, original, modified, latitude, longitude, altitude] namevalues = list(zip(widgets, values)) namevalues = [(w, v) for w, v in namevalues if v] if namevalues: for widgetname, widgetvalue in namevalues: key = _DATAMAP[widgetname] valid = _set_value(self.plugin_image, key, widgetvalue) # save all References for (Latitude, Longitude, and Altitude)... widgets = ["LatitudeRef", "LongitudeRef", "AltitudeRef"] values = [latref, longref, altref] namevalues = list(zip(widgets, values)) namevalues = [(w, v) for w, v in namevalues if v] if namevalues: for widgetname, widgetvalue in namevalues: key = _DATAMAP[widgetname] valid = _set_value(self.plugin_image, key, widgetvalue) # if valid is in [1, 2, 3], then we write to image? # see _set_value() for further information... if valid in xrange(1, 4): # Update dynamically created Modified date... modified = datetime.datetime.now() self.exif_widgets["Modified"].set_text(format_datetime(modified)) # set Edit Message to Saved... self.exif_widgets["EditMessage"].set_text(_("Saving Exif metadata to this image...")) # writes/ saves only the fields that have values... self.write_metadata(self.plugin_image) # update the display... self.update() def strip_metadata(self, mediadatatags =None): """ Will completely and irrevocably erase all Exif metadata from this image. """ # make sure the image has Exif metadata... mediadatatags = _get_exif_keypairs(self.plugin_image) if mediadatatags: if EXIV2_FOUND: # use exiv2 to delete the Exif metadata... try: erase = subprocess.check_call( [EXIV2_FOUND, "delete", self.image_path] ) erase_results = str(erase) except subprocess.CalledProcessError: erase_results = False else: # use pyexiv2 to delete Exif metadata... for key in mediadatatags: del self.plugin_image[key] erase_results = True if erase_results: # write wiped metadata to image... self.write_metadata(self.plugin_image) for widget in ["MediaLabel", "MimeType", "ImageSize", "MessageArea", "Total"]: self.exif_widgets[widget].set_text("") self.exif_widgets["MessageArea"].set_text(_("All Exif metadata " "has been deleted from this image...")) self.update() else: self.exif_widgets["MessageArea"].set_text(_("There was an error " "in stripping the Exif metadata from this image...")) def close_window(self, widgetWindow): """ closes the window title by widgetWindow. """ lambda w: widgetWindow.destroy() def string_to_rational(coordinate): """ convert string to rational variable for GPS """ if '.' in coordinate: value1, value2 = coordinate.split('.') return pyexiv2.Rational(int(float(value1 + value2)), 10**len(value2)) else: return pyexiv2.Rational(int(coordinate), 1) def coords_to_rational(coordinates): """ returns the rational equivalent for (degrees, minutes, seconds)... """ return [string_to_rational(coordinate) for coordinate in coordinates.split(":")] def altitude2rational(meters_): """ convert Altitude to pyexiv2.Rational """ return [string_to_rational(meters_)] def convert_value(value): """ will take a value from the coordinates and return its value """ if isinstance(value, (Fraction, pyexiv2.Rational)): return str((Decimal(value.numerator) / Decimal(value.denominator))) return value def rational_to_dms(coordinates): """ takes a rational set of coordinates and returns (degrees, minutes, seconds) [Fraction(40, 1), Fraction(0, 1), Fraction(1079, 20)] """ # coordinates look like: # [Rational(38, 1), Rational(38, 1), Rational(150, 50)] # or [Fraction(38, 1), Fraction(38, 1), Fraction(318, 100)] return [convert_value(coordinate) for coordinate in coordinates] def _get_exif_keypairs(plugin_image): """ Will be used to retrieve and update the Exif metadata from the image. """ if plugin_image: return [key for key in (plugin_image.exifKeys() if OLD_API else plugin_image.exif_keys) ] else: return False #------------------------------------------------ # Exiv2 support functions # * gets from and sets to the image... # * it will still need to be saved... #------------------------------------------------ def _get_value(plugininstance, exiv2_key): """ gets the value from the Exif Key, and returns it... @param: key -- image metadata key """ exiv_value = '' try: if OLD_API: exiv_value = plugininstance[exiv2_key] else: exiv_value = plugininstance[exiv2_key].value except (KeyError, ValueError, AttributeError): pass return exiv_value def _set_value(plugininstance, key, widgetvalue_): """ sets the value for the metadata keys """ if not plugininstance: return False valid = 0 try: if OLD_API: plugininstance[key] = widgetvalue_ valid = 1 else: plugininstance[key].value = widgetvalue_ valid = 2 except KeyError: plugininstance[key] = pyexiv2.ExifTag(key, widgetvalue_) valid = 3 except (ValueError, AttributeError): valid = 4 return valid