1741 lines
64 KiB
Python
1741 lines
64 KiB
Python
# -*- coding: utf-8 -*-
|
||
#!/usr/bin/python
|
||
#
|
||
# Gramps - a GTK+/GNOME based genealogy program
|
||
#
|
||
# Copyright (C) 2009-2011 Rob G. Healey <robhealey1@gmail.com>
|
||
#
|
||
# 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
|
||
from datetime import datetime
|
||
import calendar
|
||
import time
|
||
|
||
# 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 = 4
|
||
from fractions import Fraction
|
||
|
||
import subprocess
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# GTK modules
|
||
# -----------------------------------------------------------------------------
|
||
import gtk
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# GRAMPS modules
|
||
# -----------------------------------------------------------------------------
|
||
import GrampsDisplay
|
||
from QuestionDialog import WarningDialog, QuestionDialog, OptionDialog
|
||
|
||
from gen.ggettext import gettext as _
|
||
|
||
from gen.plug import Gramplet
|
||
from DateHandler import displayer as _dd
|
||
from DateHandler import parser as _dp
|
||
from gen.lib.date import Date
|
||
from gui.widgets import ValidatableMaskedEntry
|
||
from Errors import ValidationError
|
||
|
||
import gen.lib
|
||
import gen.mime
|
||
import Utils
|
||
from PlaceUtils import conv_lat_lon
|
||
from ListModel import ListModel
|
||
|
||
#####################################################################
|
||
# Check for pyexiv2 library...
|
||
#####################################################################
|
||
# pyexiv2 download page (C) Olivier Tilloy
|
||
_DOWNLOAD_LINK = "http://tilloy.net/dev/pyexiv2/download.html"
|
||
|
||
# make sure the pyexiv2 library is installed and at least a minimum version
|
||
software_version = False
|
||
Min_VERSION = (0, 1, 3)
|
||
Min_VERSION_str = "pyexiv2-%d.%d.%d" % Min_VERSION
|
||
Pref_VERSION_str = "pyexiv2-%d.%d.%d" % (0, 3, 0)
|
||
|
||
try:
|
||
import pyexiv2
|
||
software_version = pyexiv2.version_info
|
||
|
||
except ImportError, msg:
|
||
WarningDialog(_("You need to install, %s or greater, for this addon to work...\n"
|
||
"I would recommend installing, %s, and it may be downloaded from here: \n%s") % (
|
||
Min_VERSION_str, Pref_VERSION_str, _DOWNLOAD_LINK), str(msg))
|
||
raise Exception(_("Failed to load 'Edit Image Exif Metadata'..."))
|
||
|
||
# This only happends if the user has pyexiv2-0.1.3 installed on their computer...
|
||
except AttributeError:
|
||
pass
|
||
|
||
# v0.1 has a different API to v0.2 and above...
|
||
if hasattr(pyexiv2, 'version_info'):
|
||
LesserVersion = False
|
||
else:
|
||
# version_info attribute does not exist prior to v0.2.0
|
||
LesserVersion = True
|
||
|
||
# the library is either not installed or does not meet minimum required version for this addon....
|
||
if (software_version and (software_version < Min_VERSION)):
|
||
msg = _("The minimum required version for pyexiv2 must be %s \n"
|
||
"or greater. Or you do not have the python library installed yet. "
|
||
"You may download it from here: %s\n\n I recommend getting, %s") % (
|
||
Min_VERSION_str, _DOWNLOAD_LINK, Pref_VERSION_str)
|
||
WarningDialog(msg)
|
||
raise Exception(msg)
|
||
|
||
# *******************************************************************
|
||
# Determine if we have access to outside Programs installed on this computer,
|
||
# which will extend the functionality of this addon?
|
||
#
|
||
# The programs are ImageMagick, and jhead
|
||
# * ImageMagick -- Convert and Delete all Exif metadata...
|
||
# * jhead -- re-initialize a jpeg, and other features...
|
||
# * del/ rm -- delete old files from the convert command...
|
||
#********************************************************************
|
||
# Windows 32bit systems
|
||
system_platform = os.sys.platform
|
||
if system_platform == "win32":
|
||
_MAGICK_FOUND = "convert.exe" if Utils.search_for("convert.exe") else False
|
||
_JHEAD_FOUND = "jhead.exe" if Utils.search_for("jhead.exe") else False
|
||
_DEL_FOUND = "del.exe" if Utils.search_for("del.exe") else False
|
||
|
||
elif system_platform == "linux2":
|
||
_MAGICK_FOUND = "convert" if Utils.search_for("convert") else False
|
||
_JHEAD_FOUND = "jhead" if Utils.search_for("jhead") else False
|
||
_DEL_FOUND = "rm" if Utils.search_for("rm") else False
|
||
|
||
else:
|
||
_MAGICK_FOUND = "convert" if Utils.search_for("convert") else False
|
||
_JHEAD_FOUND = "jhead" if Utils.search_for("jhead") else False
|
||
_DEL_FOUND = "del" if Utils.search_for("del") else False
|
||
|
||
# if external programs are not found, let the user know about the missing functionality?
|
||
if not _MAGICK_FOUND:
|
||
print(_("ImageMagick's convert program was not found on this computer.\n"
|
||
"You may download it from here: %s...") % (
|
||
"http://www.imagemagick.org/script/index.php"))
|
||
|
||
if not _JHEAD_FOUND:
|
||
print(_("Jhead program was not found on this computer.\n"
|
||
"You may download it from: %s...") % (
|
||
"http://www.sentex.net/~mwandel/jhead/"))
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Constants
|
||
# -----------------------------------------------------------------------------
|
||
# available image types for exiv2 and pyexiv2
|
||
_vtypes = [".jpeg", ".jpg", ".jfif", ".exv", ".tiff", ".dng", ".nef", ".pef", ".pgf", ".png", ".psd", ".jp2"]
|
||
|
||
# set up Exif keys for Image.exif_keys
|
||
_DATAMAP = {
|
||
"Xmp.xmp.Label" : "ExifLabel",
|
||
"Exif.Image.ImageDescription" : "Description",
|
||
"Exif.Image.DateTime" : "Modified",
|
||
"Exif.Image.Artist" : "Artist",
|
||
"Exif.Image.Copyright" : "Copyright",
|
||
"Exif.Photo.DateTimeOriginal" : "Original",
|
||
"Exif.Photo.DateTimeDigitized" : "Digitized",
|
||
"Xmp.xmp.ModifyDate" : "ModifyDate",
|
||
"Exif.GPSInfo.GPSTimeStamp" : "gpsTimeStamp",
|
||
"Exif.GPSInfo.GPSLatitudeRef" : "LatitudeRef",
|
||
"Exif.GPSInfo.GPSLatitude" : "Latitude",
|
||
"Exif.GPSInfo.GPSLongitudeRef" : "LongitudeRef",
|
||
"Exif.GPSInfo.GPSLongitude" : "Longitude",
|
||
"Exif.GPSInfo.GPSAltitudeRef" : "AltitudeRef",
|
||
"Exif.GPSInfo.GPSAltitude" : "Altitude"}
|
||
_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...
|
||
_DATATIPS = {
|
||
|
||
# Exif Label/ Title...
|
||
"ExifLabel" : _("This is equivalent to the Title field in the media object editor."),
|
||
|
||
# Description...
|
||
"Description" : _("Provide a short descripion for this image."),
|
||
|
||
# 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"),
|
||
|
||
# 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"),
|
||
|
||
# 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"),
|
||
|
||
# Date/ Time (received from the GPS Satellites)...
|
||
"gpsTimeStamp" : _("The time that the GPS Latitude/ Longitude was received from the GPS Satellites.") }
|
||
_DATATIPS = dict( (key, tooltip) for key, tooltip in _DATATIPS.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.") }
|
||
|
||
if (_MAGICK_FOUND or _JHEAD_FOUND):
|
||
_BUTTONTIPS.update( {
|
||
# Thumbnail Viewing Window button...
|
||
"Thumbnail" : _("Will produce a Popup window showing a Thumbnail Viewing Area"),
|
||
|
||
# Convert to .Jpeg button...
|
||
"Convert" : _("If your image is not a .jpg image, convert it to a .jpg image?"),
|
||
|
||
# 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?") } )
|
||
|
||
# ------------------------------------------------------------------------
|
||
# Gramplet class
|
||
# ------------------------------------------------------------------------
|
||
class EditExifMetadata(Gramplet):
|
||
def init(self):
|
||
|
||
self.exif_widgets = {}
|
||
self.dates = {}
|
||
|
||
self.orig_image = False
|
||
self.image_path = False
|
||
self.plugin_image = False
|
||
|
||
vbox = self.build_gui()
|
||
self.connect_signal("Media", self.update)
|
||
|
||
self.gui.get_container_widget().remove(self.gui.textview)
|
||
self.gui.get_container_widget().add_with_viewport(vbox)
|
||
|
||
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)
|
||
label = self.__create_label("MediaLabel", False, False, False)
|
||
medialabel.pack_start(label, expand =False)
|
||
main_vbox.pack_start(medialabel, expand =False, fill =False, padding =2)
|
||
label.show()
|
||
|
||
# Displays mime type information...
|
||
mimetype = gtk.HBox(False)
|
||
label = self.__create_label("MimeType", False, False, False)
|
||
mimetype.pack_start(label, expand =False)
|
||
main_vbox.pack_start(mimetype, expand =False, fill =False, padding =2)
|
||
label.show()
|
||
|
||
# image dimensions...
|
||
imagesize = gtk.HBox(False)
|
||
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 =5)
|
||
label.hide()
|
||
|
||
# Displays all plugin messages...
|
||
messagearea = gtk.HBox(False)
|
||
label = self.__create_label("MessageArea", False, False, False)
|
||
messagearea.pack_start(label, expand =False)
|
||
main_vbox.pack_start(messagearea, expand =False, fill =False, padding =2)
|
||
label.show()
|
||
|
||
# Separator line before the buttons...
|
||
main_vbox.pack_start(gtk.HSeparator(), expand =False, fill =False, padding =2)
|
||
|
||
# Thumbnail View, and Convert horizontal box
|
||
thc_box = gtk.HButtonBox()
|
||
thc_box.set_layout(gtk.BUTTONBOX_START)
|
||
main_vbox.pack_start(thc_box, expand =False, fill =False, padding =5)
|
||
|
||
# Thumbnail View button...
|
||
thc_box.add( self.__create_button(
|
||
"Thumbnail", _("Thumbnail"), [self.thumbnail_view]) )
|
||
|
||
# is ImageMagick installed?
|
||
if _MAGICK_FOUND:
|
||
|
||
# Convert button...
|
||
thc_box.add( self.__create_button(
|
||
"Convert", False, [self.__convert_dialog], gtk.STOCK_CONVERT) )
|
||
|
||
# Help, Edit, and Delete horizontal box
|
||
hed_box = gtk.HButtonBox()
|
||
hed_box.set_layout(gtk.BUTTONBOX_START)
|
||
main_vbox.pack_start(hed_box, expand =False, fill =False, padding =5)
|
||
|
||
# Help button...
|
||
hed_box.add( self.__create_button(
|
||
"Help", False, [self.__help_page], gtk.STOCK_HELP, True) )
|
||
|
||
# Edit button...
|
||
hed_box.add( self.__create_button(
|
||
"Edit", False, [self.display_edit_window], gtk.STOCK_EDIT) )
|
||
|
||
if _MAGICK_FOUND:
|
||
# Delete All Metadata button...
|
||
hed_box.add(self.__create_button(
|
||
"Delete", False, [self.__wipe_dialog], gtk.STOCK_DELETE) )
|
||
|
||
# greyed- shaded lines display area...
|
||
new_vbox = self.build_shaded_display()
|
||
main_vbox.pack_start(new_vbox, expand =False, fill =False, padding =10)
|
||
|
||
# number of key/value pairs shown...
|
||
label = self.__create_label("Total", False, False, False)
|
||
main_vbox.pack_start(label, expand =False, fill =False, padding =10)
|
||
label.show()
|
||
|
||
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 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
|
||
|
||
# clears all labels and display area...
|
||
for widgetName in ["MediaLabel", "MimeType", "ImageSize", "MessageArea"]:
|
||
self.exif_widgets[widgetName].set_text("")
|
||
self.model.clear()
|
||
|
||
# set Message Ares to Select...
|
||
self.exif_widgets["MessageArea"].set_text(_("Select an image to view it's Exif metadata..."))
|
||
|
||
active_handle = self.get_active("Media")
|
||
if not active_handle:
|
||
self.set_has_data(False)
|
||
return
|
||
|
||
self.orig_image = db.get_object_from_handle(active_handle)
|
||
self.image_path = Utils.media_path_full(db, self.orig_image.get_path() )
|
||
basename, self.extension = os.path.splitext(self.image_path)
|
||
if (not self.orig_image or not os.path.isfile(self.image_path)):
|
||
self.exif_widgets["MessageArea"].set_text(_("Image is either missing or deleted,\n"
|
||
"Please choose a different image..."))
|
||
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
|
||
|
||
# Activate the Clear and Edit buttons...
|
||
self.activate_buttons(["Edit"])
|
||
|
||
# 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...."))
|
||
|
||
# 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) )
|
||
|
||
# display all button tooltips only...
|
||
# 1st argument is for Fields, 2nd argument is for Buttons...
|
||
self._setup_widget_tips([False, True])
|
||
|
||
# determine if it is a mime image object?
|
||
if mime_type:
|
||
if mime_type.startswith("image"):
|
||
|
||
# Checks to make sure that ImageMagick is installed on this computer and
|
||
# the image is NOT a (".jpeg", ".jfif", or ".jpg") image...
|
||
# This allows you to Convert the nonjpeg image to a jpeg file...
|
||
if (_MAGICK_FOUND and self.extension not in [".jpeg", ".jpg", ".jfif"] ):
|
||
self.activate_buttons(["Convert"])
|
||
|
||
# creates, and reads the plugin image instance...
|
||
self.plugin_image = self.setup_image(self.image_path)
|
||
|
||
# Check for Thumbnails...
|
||
previews = self.plugin_image.previews
|
||
if (len(previews) > 0):
|
||
self.activate_buttons(["Thumbnail"])
|
||
|
||
# display all exif metadata...
|
||
mediadatatags = _get_exif_keypairs(self.plugin_image)
|
||
if mediadatatags:
|
||
self.display_metadata(mediadatatags)
|
||
|
||
else:
|
||
# set Message Area to None...
|
||
self.exif_widgets["MessageArea"].set_text(_("No Exif metadata for this image..."))
|
||
|
||
# has mime, but not an image...
|
||
else:
|
||
self.exif_widgets["MessageArea"].set_text(_("Please choose a different image..."))
|
||
return
|
||
|
||
# has no mime...
|
||
else:
|
||
self.exif_widgets["MessageArea"].set_text(_("Please choose a different image..."))
|
||
return
|
||
|
||
def _setup_widget_tips(self, _ttypes):
|
||
"""
|
||
set up widget tooltips...
|
||
* data fields
|
||
* buttons
|
||
"""
|
||
fields, buttons = _ttypes
|
||
|
||
# if True, setup tooltips for all Data Entry Fields...
|
||
if fields:
|
||
for widget, tooltip in _DATATIPS.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 LesserVersion: # prior to pyexiv2-0.2.0
|
||
metadata = pyexiv2.Image(full_path)
|
||
try:
|
||
metadata.readMetadata()
|
||
except (IOError, OSError):
|
||
self.set_has_data(False)
|
||
return
|
||
|
||
else: # pyexiv2-0.2.0 and above
|
||
metadata = pyexiv2.ImageMetadata(full_path)
|
||
try:
|
||
metadata.read()
|
||
except (IOError, OSError):
|
||
self.set_has_data(False)
|
||
return
|
||
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.
|
||
"""
|
||
db = self.dbstate.db
|
||
|
||
if media is None:
|
||
return False
|
||
|
||
full_path = Utils.media_path_full(db, media.get_path() )
|
||
if not os.path.isfile(full_path):
|
||
return False
|
||
|
||
if LesserVersion: # prior to pyexiv2-0.2.0
|
||
metadata = pyexiv2.Image(full_path)
|
||
try:
|
||
metadata.readMetadata()
|
||
except (IOError, OSError):
|
||
return False
|
||
|
||
else: # pyexiv2-0.2.0 and above
|
||
metadata = pyexiv2.ImageMetadata(full_path)
|
||
try:
|
||
metadata.read()
|
||
except (IOError, OSError):
|
||
return False
|
||
|
||
# update image Exif metadata...
|
||
MediaDataTags = _get_exif_keypairs(self.plugin_image)
|
||
if MediaDataTags:
|
||
return True
|
||
|
||
return False
|
||
|
||
def display_metadata(self, object):
|
||
"""
|
||
displays all of the image's Exif metadata in a grey-shaded tree view...
|
||
"""
|
||
|
||
# get All Exif metadata...
|
||
metadatatags = _get_exif_keypairs(self.plugin_image)
|
||
if not metadatatags:
|
||
self.exif_widgets["MessageArea"].set_text(_("No Exif metadata to display..."))
|
||
return
|
||
|
||
# set Message Area to Display...
|
||
self.exif_widgets["MessageArea"].set_text(_("Displaying all Exif metadata keypairs..."))
|
||
|
||
if not LesserVersion: # pyexiv2-0.2.0 and above...
|
||
|
||
# image Dimensions...
|
||
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))
|
||
|
||
# Activate Delete button if ImageMagick or jhead is found?...
|
||
if (_MAGICK_FOUND or _JHEAD_FOUND):
|
||
self.activate_buttons(["Delete"])
|
||
|
||
for KeyTag in metadatatags:
|
||
if LesserVersion: # prior to pyexiv2-0.2.0
|
||
label = metadata.tagDetails(KeyTag)[0]
|
||
|
||
# if KeyTag is one of the dates, display as the user wants it in preferences
|
||
if KeyTag in [_DATAMAP["Modified"], _DATAMAP["Original"], _DATAMAP["Digitized"] ]:
|
||
human_value = _format_datetime(self.plugin_image[KeyTag])
|
||
else:
|
||
human_value = self.plugin_image.interpretedExifValue(KeyTag)
|
||
|
||
else: # pyexiv2-0.2.0 and above
|
||
tag = self.plugin_image[KeyTag]
|
||
|
||
# if KeyTag is one of the dates, display as the user wants it in preferences
|
||
if KeyTag in [_DATAMAP["Modified"], _DATAMAP["Original"], _DATAMAP["Digitized"] ]:
|
||
_value = self._get_value(KeyTag)
|
||
if _value:
|
||
label = tag.label
|
||
human_value = _format_datetime(_value)
|
||
else:
|
||
human_value = False
|
||
elif ("Xmp" in KeyTag or "Iptc" in KeyTag):
|
||
label = self.plugin_image[KeyTag]
|
||
human_value = self._get_value(KeyTag)
|
||
|
||
else:
|
||
label = tag.label
|
||
human_value = tag.human_value
|
||
|
||
if human_value:
|
||
|
||
# add to model for display...
|
||
self.model.add((label, human_value))
|
||
|
||
self.set_has_data(self.model.count > 0)
|
||
self.exif_widgets["Total"].set_text(_("Number of Key/ Value pairs : %04d") % self.model.count)
|
||
|
||
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 manipulation...
|
||
self.exif_widgets[pos] = button
|
||
|
||
if not sensitive:
|
||
button.set_sensitive(False)
|
||
|
||
return button
|
||
|
||
def __create_label(self, widget, text, width, height, wrap =True):
|
||
"""
|
||
creates a label for this addon.
|
||
"""
|
||
|
||
label = gtk.Label()
|
||
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 text:
|
||
label.set_text(text)
|
||
|
||
if widget:
|
||
self.exif_widgets[widget] = label
|
||
|
||
return label
|
||
|
||
def build_shaded_display(self):
|
||
"""
|
||
Build the GUI interface.
|
||
"""
|
||
|
||
top = gtk.TreeView()
|
||
titles = [(_('Key'), 1, 180),
|
||
(_('Value'), 2, 310)]
|
||
self.model = ListModel(top, titles)
|
||
|
||
return top
|
||
|
||
def thumbnail_view(self, object):
|
||
"""
|
||
will allow a display area for a thumbnail pop-up window.
|
||
"""
|
||
|
||
dirpath, filename = os.path.split(self.image_path)
|
||
|
||
if LesserVersion: # prior to pyexiv2-0.2.0
|
||
try:
|
||
ttype, tdata = self.plugin_image.getThumbnailData()
|
||
width, height = tdata.dimensions
|
||
|
||
except (IOError, OSError):
|
||
print(_('Error: %s does not contain an EXIF thumbnail.') % filename)
|
||
self.close_window(self.tbarea)
|
||
|
||
# Create a GTK pixbuf loader to read the thumbnail data
|
||
pbloader = gtk.gdk.PixbufLoader()
|
||
pbloader.write(tdata)
|
||
|
||
else: # pyexiv2-0.2.0 and above
|
||
try:
|
||
previews = self.plugin_image.previews
|
||
if not previews:
|
||
print(_('Error: %s does not contain an EXIF thumbnail.') % filename)
|
||
self.close_window(self.tbarea)
|
||
|
||
except (IOError, OSError):
|
||
print(_('Error: %s does not contain an EXIF thumbnail.') % filename)
|
||
self.close_window(self.tbarea)
|
||
|
||
# Get the largest preview available...
|
||
preview = previews[-1]
|
||
width, height = preview.dimensions
|
||
|
||
# Create a GTK pixbuf loader to read the thumbnail data
|
||
pbloader = gtk.gdk.PixbufLoader()
|
||
pbloader.write(preview.data)
|
||
|
||
tip = _("Click Close to close this Thumbnail Viewing Area.")
|
||
|
||
self.tbarea = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
||
self.tbarea.tooltip = tip
|
||
self.tbarea.set_title(_("Thumbnail Viewing Area"))
|
||
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()
|
||
|
||
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, obj):
|
||
"""
|
||
Handles the Convert question Dialog...
|
||
"""
|
||
|
||
# if ImageMagick and delete has been found on this computer?
|
||
if (_MAGICK_FOUND and _DEL_FOUND):
|
||
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 original"), self.convertdelete,
|
||
_("Convert"), self.convert2Jpeg)
|
||
|
||
# is ImageMagick is installed?
|
||
elif _MAGICK_FOUND:
|
||
|
||
QuestionDialog(_("Edit Image Exif Metadata"), _("Convert this image to a .jpeg image?"),
|
||
_("Convert"), self.convert2Jpeg)
|
||
|
||
def convertdelete(self, object):
|
||
"""
|
||
will convert2Jpeg and delete original non-jpeg image.
|
||
"""
|
||
|
||
self.convert2Jpeg()
|
||
|
||
if system_platform == "linux2":
|
||
delete = subprocess.check_call( [_DEL_FOUND, "-rf", self.image_path] )
|
||
else:
|
||
delete = subprocess.check_call( [_DEL_FOUND, "-y", self.image_path] )
|
||
delete_result = str(delete)
|
||
|
||
if delete_result:
|
||
self.exif_widgets["MessageArea"].set_text(_("Image has been converted to a .jpg image,\n"
|
||
"and original image has been deleted!"))
|
||
|
||
def convert2Jpeg(self):
|
||
"""
|
||
Will attempt to convert an image to jpeg if it is not?
|
||
"""
|
||
|
||
filepath, basename = os.path.split(self.image_path)
|
||
basename, oldext = os.path.splitext(self.image_path)
|
||
newextension = ".jpeg"
|
||
|
||
convert = subprocess.check_call(["convert", self.image_path,
|
||
os.path.join(filepath, basename + newextension) ] )
|
||
if str(convert):
|
||
|
||
# set Message Area to Convert...
|
||
self.exif_widgets["MessageArea"].set_text(_("Converting image,\n"
|
||
"You will need to delete the original image file..."))
|
||
|
||
self.deactivate_buttons(["Convert"])
|
||
|
||
def __help_page(self, object):
|
||
"""
|
||
will bring up a Wiki help page.
|
||
"""
|
||
|
||
GrampsDisplay.help(webpage = "Edit Image Exif Metadata")
|
||
|
||
def activate_buttons(self, ButtonList):
|
||
"""
|
||
Enable/ activate the buttons that are in ButtonList
|
||
"""
|
||
|
||
for ButtonName in ButtonList:
|
||
self.exif_widgets[ButtonName].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"]:
|
||
for widget, tooltip in _BUTTONTIPS.items():
|
||
if widget is not "Help":
|
||
self.exif_widgets[widget].set_sensitive(False)
|
||
|
||
else:
|
||
for widgetName in ButtonList:
|
||
self.exif_widgets[widgetName].set_sensitive(False)
|
||
|
||
def active_buttons(self, obj):
|
||
"""
|
||
will handle the toggle action of the Edit button.
|
||
|
||
If there is no Exif metadata, then the data fields are connected to the
|
||
'changed' signal to be able to activate the Edit button once data has been entered
|
||
into the data fields...
|
||
|
||
Activate these buttons once info has been entered into the data fields...
|
||
"""
|
||
|
||
if not self.exif_widgets["Edit"].get_sensitive():
|
||
self.activate_buttons(["Edit"])
|
||
|
||
# set Message Area to Entering Data...
|
||
self.exif_widgets["MessageArea"].set_text(_("Entering data..."))
|
||
|
||
if _MAGICK_FOUND:
|
||
if not self.exif_widgets["Delete"].get_sensitive():
|
||
self.activate_buttons(["Delete"])
|
||
|
||
# if Save is in the list of button tooltips, then check it too?
|
||
if "Save" in _BUTTONTIPS.keys():
|
||
if not self.exif_widgets["Save"].get_sensitive():
|
||
self.activate_buttons(["Save"])
|
||
|
||
def display_edit_window(self, object):
|
||
"""
|
||
creates the editing area fields.
|
||
"""
|
||
|
||
tip = _("Click the close button when you are finished modifying this image's Exif metadata.")
|
||
|
||
self.edtarea = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
||
self.edtarea.tooltip = tip
|
||
self.edtarea.set_title( self.orig_image.get_description() )
|
||
self.edtarea.set_default_size(570, 642)
|
||
self.edtarea.set_border_width(10)
|
||
self.edtarea.connect("destroy", lambda w: self.edtarea.destroy() )
|
||
|
||
# create a new scrolled window.
|
||
scrollwindow = gtk.ScrolledWindow()
|
||
scrollwindow.set_border_width(10)
|
||
scrollwindow.set_size_request(510, 650)
|
||
scrollwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
||
|
||
# The dialog window is created with a vbox packed into it.
|
||
self.edtarea.add(scrollwindow)
|
||
scrollwindow.show()
|
||
|
||
vbox = self.build_edit_gui()
|
||
scrollwindow.add_with_viewport(vbox)
|
||
self.edtarea.show()
|
||
|
||
# display all fields and button tooltips...
|
||
# need to add Save 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."),
|
||
|
||
# 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."),
|
||
|
||
# Clear button...
|
||
"Clear" : _("This button will clear all of the data fields shown here."),
|
||
|
||
# Re- display the data fields button...
|
||
"Display" : _("Re -display the data fields that were cleared from the Edit Area."),
|
||
|
||
# Convert 2 Decimal button...
|
||
"Decimal" : _("Convert GPS Latitude/ Longitude Coordinates to Decimal representation."),
|
||
|
||
# Convert 2 Degrees, Minutes, Seconds button...
|
||
"DMS" : _("Convert GPS Latitude/ Longitude Coordinates to (Degrees, Minutes, Seconds) Representation.")
|
||
}.items() )
|
||
self._setup_widget_tips([True, True])
|
||
|
||
# display all data fields and their values...
|
||
self.EditArea(self.plugin_image)
|
||
|
||
def build_edit_gui(self):
|
||
"""
|
||
will build the edit screen ...
|
||
"""
|
||
|
||
main_vbox = gtk.VBox()
|
||
main_vbox.set_border_width(10)
|
||
main_vbox.set_size_request(500, 640)
|
||
|
||
label = self.__create_label("Edit:Message", False, False, False)
|
||
main_vbox.pack_start(label, expand =False, fill =False, padding =5)
|
||
label.show()
|
||
|
||
# create the data fields...
|
||
# ***Label/ Title, Description, Artist, and Copyright
|
||
gen_frame = gtk.Frame(_("General Data"))
|
||
gen_frame.set_size_request(490, 200)
|
||
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 [
|
||
("ExifLabel", _("Exif Label :") ),
|
||
("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)
|
||
label.show()
|
||
|
||
event_box = gtk.EventBox()
|
||
event_box.set_size_request(390, 30)
|
||
new_hbox.pack_start(event_box, expand =False, fill =False, padding =0)
|
||
event_box.show()
|
||
|
||
entry = gtk.Entry(max =100)
|
||
event_box.add(entry)
|
||
self.exif_widgets[widget] = entry
|
||
entry.show()
|
||
|
||
# iso format: Year, Month, Day spinners...
|
||
datetime_frame = gtk.Frame(_("Date/ Time"))
|
||
datetime_frame.set_size_request(490, 110)
|
||
main_vbox.pack_start(datetime_frame, expand =False, fill =False, padding =0)
|
||
datetime_frame.show()
|
||
|
||
new_vbox = gtk.VBox(False, 0)
|
||
new_vbox.set_border_width(5)
|
||
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 Date/ Time :") ),
|
||
("Modified", _("Last Changed :") ) ]:
|
||
|
||
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 =100, height = 25)
|
||
vbox2.pack_start(label, expand =False, fill =False, padding =0)
|
||
label.show()
|
||
|
||
event_box = gtk.EventBox()
|
||
event_box.set_size_request(225, 40)
|
||
vbox2.pack_start(event_box, expand =False, fill =False, padding =0)
|
||
event_box.show()
|
||
|
||
entry = ValidatableMaskedEntry()
|
||
entry.connect('validate', self.validate, widget)
|
||
entry.connect('content-changed', self.set_datetime, widget)
|
||
event_box.add(entry)
|
||
self.exif_widgets[widget] = entry
|
||
entry.show()
|
||
|
||
self.dates[widget] = None
|
||
|
||
# if there is text in the modified Date/ Time field, disable editing...
|
||
if self.exif_widgets["Modified"].get_text():
|
||
self.exif_widgets["Modified"].set_editable(False)
|
||
|
||
# GPS Coordinates...
|
||
latlong_frame = gtk.Frame(_("Latitude/ Longitude/ Altitude GPS Coordinates"))
|
||
latlong_frame.set_size_request(490, 210)
|
||
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 :") ) ]:
|
||
|
||
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 =230, height =25)
|
||
vbox2.pack_start(label, expand =False, fill =False, padding =0)
|
||
label.show()
|
||
|
||
event_box = gtk.EventBox()
|
||
event_box.set_size_request(230, 40)
|
||
vbox2.pack_start(event_box, expand =False, fill =False, padding =0)
|
||
event_box.show()
|
||
|
||
entry = gtk.Entry(max =50)
|
||
event_box.add(entry)
|
||
self.exif_widgets[widget] = entry
|
||
entry.show()
|
||
|
||
new_hbox = gtk.HBox(False, 0)
|
||
new_vbox.pack_start(new_hbox, expand =False, fill =False, padding =0)
|
||
new_hbox.show()
|
||
|
||
# AAltitude and GPS TimeStamp...
|
||
for widget, text in [
|
||
("Altitude", _("Altitude (in meters) :") ),
|
||
("gpsTimeStamp", _("GPS TimeStamp :") ) ]:
|
||
|
||
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 =230, height =25)
|
||
vbox2.pack_start(label, expand =False, fill =False, padding =0)
|
||
label.show()
|
||
|
||
event_box = gtk.EventBox()
|
||
event_box.set_size_request(230, 40)
|
||
vbox2.pack_start(event_box, expand =False, fill =False, padding =0)
|
||
event_box.show()
|
||
|
||
entry = gtk.Entry(max =50)
|
||
event_box.add(entry)
|
||
self.exif_widgets[widget] = entry
|
||
entry.show()
|
||
|
||
# add an empty row for spacing...
|
||
new_hbox = gtk.HBox(False, 0)
|
||
new_vbox.pack_start(new_hbox, expand =False, fill =False, padding =10)
|
||
new_hbox.show()
|
||
|
||
new_hbox = gtk.HBox(False, 0)
|
||
new_vbox.pack_start(new_hbox, expand =False, fill =False, padding =0)
|
||
new_hbox.show()
|
||
|
||
label = self.__create_label(
|
||
False, _("Convert GPS :"), width =100, height =25)
|
||
new_hbox.pack_start(label, expand =False, fill =False, padding =5)
|
||
label.show()
|
||
|
||
# Convert2decimal and DMS buttons...
|
||
for widget, text, callback in [
|
||
("Decimal", _("Decimal"), [self.convert2decimal] ),
|
||
("DMS", _("Deg., Mins., Secs."), [self.convert2dms] ) ]:
|
||
|
||
event_box = gtk.EventBox()
|
||
event_box.set_size_request(180, 40)
|
||
new_hbox.pack_end(event_box, expand =False, fill =False, padding =2)
|
||
event_box.show()
|
||
|
||
button = self.__create_button(
|
||
widget, text, callback, False, True)
|
||
event_box.add(button)
|
||
button.show()
|
||
|
||
# Help, Save, Clear, and Close horizontal box
|
||
hscdc_box = gtk.HButtonBox()
|
||
hscdc_box.set_layout(gtk.BUTTONBOX_START)
|
||
main_vbox.pack_start(hscdc_box, expand =False, fill =False, padding =10)
|
||
hscdc_box.show()
|
||
|
||
# Help button...
|
||
hscdc_box.add(self.__create_button(
|
||
"Help", False, [self.__help_page], gtk.STOCK_HELP, True) )
|
||
|
||
# Save button...
|
||
hscdc_box.add(self.__create_button("Save", False, [self.save_metadata, self.update,
|
||
self.display_metadata], gtk.STOCK_SAVE, True) )
|
||
|
||
# Clear button...
|
||
hscdc_box.add(self.__create_button(
|
||
"Clear", False, [self.clear_metadata], gtk.STOCK_CLEAR, True) )
|
||
|
||
# Re -display the edit area button...
|
||
hscdc_box.add(self.__create_button(
|
||
"Display", _("Display"), [self.EditArea], False, True) )
|
||
|
||
# Close button...
|
||
hscdc_box.add(self.__create_button(
|
||
"Close", False, [lambda w: self.edtarea.destroy()], gtk.STOCK_CLOSE, True) )
|
||
|
||
# disable all data fields if not one of the available exiv2 image types?
|
||
if not any(exiv2type == self.extension for exiv2type in _vtypes):
|
||
for widget in _DATATIPS.keys():
|
||
self.exif_widgets[widget].set_editable(False)
|
||
self.edtarea.destroy()
|
||
return
|
||
|
||
main_vbox.show_all()
|
||
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(self, widget, data, field):
|
||
"""
|
||
Validate current date and time in text entry
|
||
"""
|
||
if self.dates[field] is None:
|
||
return ValidationError(_('Bad Date/Time'))
|
||
|
||
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?"), _("Delete"),
|
||
self.strip_metadata)
|
||
|
||
def _get_value(self, KeyTag):
|
||
"""
|
||
gets the value from the Exif Key, and returns it...
|
||
|
||
@param: KeyTag -- image metadata key
|
||
"""
|
||
|
||
if LesserVersion:
|
||
KeyValue = self.plugin_image[KeyTag]
|
||
|
||
else:
|
||
try:
|
||
valu_ = self.plugin_image.__getitem__(KeyTag)
|
||
KeyValue = valu_.value
|
||
|
||
except (KeyError, ValueError, AttributeError):
|
||
KeyValue = False
|
||
|
||
return KeyValue
|
||
|
||
def clear_metadata(self, object):
|
||
"""
|
||
clears all data fields to nothing
|
||
"""
|
||
|
||
for widget in _DATATIPS.keys():
|
||
self.exif_widgets[widget].set_text("")
|
||
|
||
def EditArea(self, object):
|
||
"""
|
||
displays the image Exif metadata in the Edit Area...
|
||
"""
|
||
|
||
MediaDataTags = _get_exif_keypairs(self.plugin_image)
|
||
if MediaDataTags:
|
||
MediaDataTags = [KeyTag for KeyTag in MediaDataTags if KeyTag in _DATAMAP]
|
||
|
||
for KeyTag in MediaDataTags:
|
||
widgetName = _DATAMAP[KeyTag]
|
||
|
||
tagValue = self._get_value(KeyTag)
|
||
if tagValue:
|
||
|
||
if widgetName in ["ExifLabel", "Description", "Artist", "Copyright"]:
|
||
self.exif_widgets[widgetName].set_text(tagValue)
|
||
|
||
# Last Changed/ Modified...
|
||
elif widgetName in ["Modified", "Original"]:
|
||
use_date = _format_datetime(tagValue)
|
||
if use_date:
|
||
self.exif_widgets[widgetName].set_text(use_date)
|
||
|
||
# LatitudeRef, Latitude, LongitudeRef, Longitude...
|
||
elif widgetName == "Latitude":
|
||
|
||
latitude, longitude = tagValue, self._get_value(_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 = self._get_value(_DATAMAP["LatitudeRef"] )
|
||
|
||
# Longitude Direction Reference
|
||
LongRef = self._get_value(_DATAMAP["LongitudeRef"] )
|
||
|
||
# set display for Latitude GPS Coordinates
|
||
self.exif_widgets["Latitude"].set_text(
|
||
"""%s° %s′ %s″ %s""" % (latdeg, latmin, latsec, LatRef) )
|
||
|
||
# set display for Longitude GPS Coordinates
|
||
self.exif_widgets["Longitude"].set_text(
|
||
"""%s° %s′ %s″ %s""" % (longdeg, longmin, longsec, LongRef) )
|
||
|
||
elif widgetName == "Altitude":
|
||
altitude = tagValue
|
||
AltitudeRef = self._get_value(_DATAMAP["AltitudeRef"])
|
||
|
||
if (altitude and AltitudeRef):
|
||
altitude = convert_value(altitude)
|
||
if altitude:
|
||
if AltitudeRef == "1":
|
||
altitude = "-" + altitude
|
||
self.exif_widgets[widgetName].set_text(altitude)
|
||
|
||
elif widgetName == "gpsTimeStamp":
|
||
hour, minutes, seconds = rational_to_dms(tagValue)
|
||
hour, minutes, seconds = int(hour), int(minutes), int(seconds)
|
||
self.exif_widgets[widgetName].set_text("%02d:%02d:%02d" % (
|
||
hour, minutes, seconds) )
|
||
|
||
else:
|
||
# set Edit Message Area to None...
|
||
self.exif_widgets["Edit:Message"].set_text(_("There is NO Exif metadata for this image."))
|
||
|
||
for widget in _DATATIPS.keys():
|
||
|
||
# once the user types in that field,
|
||
# the Edit, Clear, and Delete buttons will become active...
|
||
self.exif_widgets[widget].connect("changed", self.active_buttons)
|
||
|
||
def _set_value(self, KeyTag, KeyValue):
|
||
"""
|
||
sets the value for the metadata KeyTags
|
||
"""
|
||
|
||
tagClass = KeyTag[0:4]
|
||
|
||
if LesserVersion:
|
||
self.plugin_image[KeyTag] = KeyValue
|
||
|
||
else:
|
||
try:
|
||
self.plugin_image.__setitem__(KeyTag, KeyValue)
|
||
except KeyError:
|
||
if tagClass == "Exif":
|
||
self.plugin_image[KeyTag] = pyexiv2.ExifTag(KeyTag, KeyValue)
|
||
elif tagClass == "Xmp.":
|
||
self.plugin_image[KeyTag] = pyexiv2.XmpTag(KeyTag, KeyValue)
|
||
elif tagClass == "Iptc":
|
||
self.plugin_image[KeyTag] = IptcTag(KeyTag, KeyValue)
|
||
except (ValueError, AttributeError):
|
||
pass
|
||
|
||
def write_metadata(self, plugininstance):
|
||
"""
|
||
writes the Exif metadata to the image.
|
||
|
||
LesserVersion -- prior to pyexiv2-0.2.0
|
||
-- pyexiv2-0.2.0 and above...
|
||
"""
|
||
if LesserVersion:
|
||
plugininstance.writeMetadata()
|
||
|
||
else:
|
||
plugininstance.write()
|
||
|
||
def close_window(self, widgetWindow):
|
||
"""
|
||
closes the window title by widgetWindow.
|
||
"""
|
||
|
||
lambda w: widgetWindow.destroy()
|
||
|
||
# -------------------------------------------------------------------
|
||
# GPS Coordinates functions
|
||
# -------------------------------------------------------------------
|
||
def addsymbols2gps(self, latitude =False, longitude =False):
|
||
"""
|
||
converts a degrees, minutes, seconds representation of Latitude/ Longitude
|
||
without their symbols to having them...
|
||
|
||
@param: latitude -- Latitude GPS Coordinates
|
||
@param: longitude -- Longitude GPS Coordinates
|
||
"""
|
||
LatitudeRef, LongitudeRef = "N", "E"
|
||
|
||
# check to see if Latitude/ Longitude exits?
|
||
if (latitude and longitude):
|
||
|
||
if (latitude.count(".") == 1 and longitude.count(".") == 1):
|
||
self.convert2dms(self.plugin_image)
|
||
|
||
# get Latitude/ Longitude after converting it...
|
||
latitude = self.exif_widgets["Latitude"].get_text()
|
||
longitude = self.exif_widgets["Longitude"].get_text()
|
||
|
||
# add DMS symbols if necessary?
|
||
# the conversion to decimal format, require the DMS symbols
|
||
elif ( (latitude.count("°") == 0 and longitude.count("°") == 0) and
|
||
(latitude.count("′") == 0 and longitude.count("′") == 0) and
|
||
(latitude.count('″') == 0 and longitude.count('″') == 0) ):
|
||
|
||
# is there a direction element here?
|
||
if (latitude.count("N") == 1 or latitude.count("S") == 1):
|
||
latdeg, latmin, latsec, LatitudeRef = latitude.split(" ", 3)
|
||
else:
|
||
atitudeRef = "N"
|
||
latdeg, latmin, latsec = latitude.split(" ", 2)
|
||
if latdeg[0] == "-":
|
||
latdeg = latdeg.replace("-", "")
|
||
LatitudeRef = "S"
|
||
|
||
# is there a direction element here?
|
||
if (longitude.count("E") == 1 or longitude.count("W") == 1):
|
||
longdeg, longmin, longsec, LongitudeRef = longitude.split(" ", 3)
|
||
else:
|
||
ongitudeRef = "E"
|
||
longdeg, longmin, longsec = longitude.split(" ", 2)
|
||
if longdeg[0] == "-":
|
||
longdeg = longdeg.replace("-", "")
|
||
LongitudeRef = "W"
|
||
|
||
latitude = """%s° %s′ %s″ %s""" % (latdeg, latmin, latsec, LatitudeRef)
|
||
longitude = """%s° %s′ %s″ %s""" % (longdeg, longmin, longsec, LongitudeRef)
|
||
|
||
return latitude, longitude
|
||
|
||
def convert2decimal(self, object):
|
||
"""
|
||
will convert a decimal GPS Coordinates into decimal format.
|
||
|
||
@param: latitude -- GPS Latitude Coordinates from data field...
|
||
@param: longitude -- GPS Longitude Coordinates from data field...
|
||
"""
|
||
|
||
# get Latitude/ Longitude from the data fields
|
||
latitude = self.exif_widgets["Latitude"].get_text()
|
||
longitude = self.exif_widgets["Longitude"].get_text()
|
||
|
||
# if latitude and longitude exist?
|
||
if (latitude and longitude):
|
||
|
||
# is Latitude/ Longitude are in DMS format?
|
||
if (latitude.count(" ") >= 2 and longitude.count(" ") >= 2):
|
||
|
||
# add DMS symbols if necessary?
|
||
# the conversion to decimal format, require the DMS symbols
|
||
if ( (latitude.count("°") == 0 and longitude.count("°") == 0) and
|
||
(latitude.count("′") == 0 and longitude.count("′") == 0) and
|
||
(latitude.count('″') == 0 and longitude.count('″') == 0) ):
|
||
|
||
latitude, longitude = self.addsymbols2gps(latitude, longitude)
|
||
|
||
# convert degrees, minutes, seconds w/ symbols to an 8 point decimal
|
||
latitude, longitude = conv_lat_lon( unicode(latitude),
|
||
unicode(longitude), "D.D8")
|
||
|
||
self.exif_widgets["Latitude"].set_text(latitude)
|
||
self.exif_widgets["Longitude"].set_text(longitude)
|
||
|
||
def convert2dms(self, object):
|
||
"""
|
||
will convert a decimal GPS Coordinates into degrees, minutes, seconds
|
||
for display only
|
||
"""
|
||
|
||
latitude = self.exif_widgets["Latitude"].get_text()
|
||
longitude = self.exif_widgets["Longitude"].get_text()
|
||
|
||
# if Latitude/ Longitude exists?
|
||
if (latitude and longitude):
|
||
|
||
# if coordinates are in decimal format?
|
||
if (latitude.count(".") == 1 and longitude.count(".") == 1):
|
||
|
||
# convert latitude and longitude to a DMS with separator of ":"
|
||
latitude, longitude = conv_lat_lon(latitude, longitude, "DEG-:")
|
||
|
||
# remove negative symbol if there is one?
|
||
LatitudeRef = "N"
|
||
if latitude[0] == "-":
|
||
latitude = latitude.replace("-", "")
|
||
LatitudeRef = "S"
|
||
latdeg, latmin, latsec = latitude.split(":", 2)
|
||
|
||
# remove negative symbol if there is one?
|
||
LongitudeRef = "E"
|
||
if longitude[0] == "-":
|
||
longitude = longitude.replace("-", "")
|
||
LongitudeRef = "W"
|
||
longdeg, longmin, longsec = longitude.split(":", 2)
|
||
|
||
latitude = """%s° %s′ %s″ %s""" % (latdeg, latmin, latsec, LatitudeRef)
|
||
self.exif_widgets["Latitude"].set_text(latitude)
|
||
|
||
longitude = """%s° %s′ %s″ %s""" % (longdeg, longmin, longsec, LongitudeRef)
|
||
self.exif_widgets["Longitude"].set_text(longitude)
|
||
|
||
return latitude, longitude
|
||
|
||
def save_metadata(self, object):
|
||
"""
|
||
gets the information from the plugin data fields
|
||
and sets the KeyTag = keyvalue image metadata
|
||
"""
|
||
|
||
for widgetName in _DATATIPS.keys():
|
||
|
||
widgetValue = self.exif_widgets[widgetName].get_text()
|
||
|
||
# Exif Label, Description, Artist, Copyright...
|
||
if widgetName in ["ExifLabel", "Description", "Artist", "Copyright"]:
|
||
self._set_value(_DATAMAP[widgetName], widgetValue)
|
||
|
||
# Modify Date/ Time...
|
||
elif widgetName == "Modified":
|
||
modified = datetime.now()
|
||
self._set_value(_DATAMAP[widgetName], modified)
|
||
self.exif_widgets[widgetName].set_text(_format_datetime(modified) )
|
||
|
||
# Original Date/ Time...
|
||
elif widgetName == "Original":
|
||
original = self.dates["Original"]
|
||
if original is not None:
|
||
self._set_value(_DATAMAP[widgetName], original)
|
||
|
||
# Latitude/ Longitude...
|
||
elif widgetName == "Latitude":
|
||
latitude = self.exif_widgets["Latitude"].get_text()
|
||
longitude = self.exif_widgets["Longitude"].get_text()
|
||
|
||
# check to see if Latitude/ Longitude exists?
|
||
if (latitude and longitude):
|
||
|
||
# complete some error checking to prevent crashes...
|
||
# if "?" character exist, remove it?
|
||
if "?" in latitude:
|
||
latitude = latitude.replace("?", "")
|
||
if "?" in longitude:
|
||
longitude = longitude.replace("?", "")
|
||
|
||
# if "," character exists, remove it?
|
||
if "," in latitude:
|
||
latitude = latitude.replace(",", "")
|
||
if "," in longitude:
|
||
longitude = longitude.replace(",", "")
|
||
|
||
# if it is in decimal format, convert it to DMS?
|
||
# if not, then do nothing?
|
||
self.convert2dms(self.plugin_image)
|
||
|
||
# get Latitude/ Longitude from the data fields
|
||
latitude = self.exif_widgets["Latitude"].get_text()
|
||
longitude = self.exif_widgets["Longitude"].get_text()
|
||
|
||
# will add (degrees, minutes, seconds) symbols if needed?
|
||
# if not, do nothing...
|
||
latitude, longitude = self.addsymbols2gps(latitude, longitude)
|
||
|
||
# set up display
|
||
self.exif_widgets["Latitude"].set_text(latitude)
|
||
self.exif_widgets["Longitude"].set_text(longitude)
|
||
|
||
LatitudeRef = " N"
|
||
if "S" in latitude:
|
||
LatitudeRef = " S"
|
||
latitude = latitude.replace(LatitudeRef, "")
|
||
LatitudeRef = LatitudeRef.replace(" ", "")
|
||
|
||
LongitudeRef = " E"
|
||
if "W" in longitude:
|
||
LongitudeRef = " W"
|
||
longitude = longitude.replace(LongitudeRef, "")
|
||
LongitudeRef = LongitudeRef.replace(" ", "")
|
||
|
||
# remove symbols for saving Latitude/ Longitude GPS Coordinates
|
||
latitude, longitude = _removesymbols4saving(latitude, longitude)
|
||
|
||
# convert (degrees, minutes, seconds) to Rational for saving
|
||
self._set_value(_DATAMAP["LatitudeRef"], LatitudeRef)
|
||
self._set_value(_DATAMAP["Latitude"], coords_to_rational(latitude) )
|
||
|
||
# convert (degrees, minutes, seconds) to Rational for saving
|
||
self._set_value(_DATAMAP["LongitudeRef"], LongitudeRef)
|
||
self._set_value(_DATAMAP["Longitude"], coords_to_rational(longitude) )
|
||
|
||
# Altitude, and Altitude Reference...
|
||
elif widgetName == "Altitude":
|
||
altitude = widgetValue
|
||
|
||
AltitudeRef = "0"
|
||
if altitude:
|
||
if altitude[0] == "-":
|
||
altitude = altitude.replace("-", "")
|
||
AltitudeRef = "1"
|
||
|
||
self._set_value(_DATAMAP["AltitudeRef"], AltitudeRef)
|
||
self._set_value(_DATAMAP["Altitude"], coords_to_rational(altitude) )
|
||
|
||
# gpsTimeStamp...
|
||
elif widgetName == "gpsTimeStamp":
|
||
if widgetValue:
|
||
widgetValue = coords_to_rational(widgetValue)
|
||
self._set_value(_DATAMAP[widgetName], widgetValue)
|
||
|
||
# set Message Area to Saved...
|
||
self.exif_widgets["Edit:Message"].set_text(_("Saving Exif metadata to image..."))
|
||
|
||
# writes all Exif Metadata to image even if the fields are all empty...
|
||
self.write_metadata(self.plugin_image)
|
||
|
||
def strip_metadata(self):
|
||
"""
|
||
Will completely and irrevocably erase all Exif metadata from this image.
|
||
"""
|
||
|
||
# update the image Exif metadata...
|
||
MediaDataTags = _get_exif_keypairs(self.plugin_image)
|
||
if not MediaDataTags:
|
||
return
|
||
|
||
if _MAGICK_FOUND:
|
||
erase = subprocess.check_call( ["convert", self.image_path, "-strip", self.image_path] )
|
||
erase_results = str(erase)
|
||
|
||
elif (_JHEAD_FOUND and self.extension in [".jpeg", ".jfif", ".jpg"]):
|
||
erase = subprocess.check_call( ["jhead", "-purejpeg", self.image_path] )
|
||
|
||
else:
|
||
if MediaDataTags:
|
||
for KeyTag in MediaDataTags:
|
||
del self.plugin_image[KeyTag]
|
||
erase_results = True
|
||
|
||
# write wiped metadata to image...
|
||
self.write_metadata(self.plugin_image)
|
||
|
||
if erase_results:
|
||
|
||
# set Message Area for deleting...
|
||
self.exif_widgets["MessageArea"].set_text(_("Deleting all Exif metadata..."))
|
||
|
||
# Clear the Edit Areas
|
||
self.model.clear()
|
||
|
||
# set Message Area to Delete...
|
||
self.exif_widgets["MessageArea"].set_text(_("All Exif metadata has been "
|
||
"deleted from this image..."))
|
||
|
||
self.update()
|
||
|
||
def update_spinners(self, syear, smonth, day, hour, minutes, seconds):
|
||
"""
|
||
update Date/ Time spinners.
|
||
"""
|
||
|
||
for widget, value in {
|
||
"Year" : syear,
|
||
"Month" : smonth,
|
||
"Day" : day,
|
||
"Hour" : hour,
|
||
"Minutes" : minutes,
|
||
"Seconds" : seconds}.items():
|
||
|
||
# make sure that the amount of days for that year and month is not > than the number of days selected...
|
||
if widget == "Day":
|
||
numdays = [0] + [calendar.monthrange(year, month)[1] for year in [syear] for month in range(1, 13) ]
|
||
if value > numdays[smonth]:
|
||
value = numdays[smonth]
|
||
|
||
# set the date/ time SpinButttons
|
||
self.exif_widgets[widget].set_value(value)
|
||
|
||
def _get_exif_keypairs(plugin_image):
|
||
"""
|
||
Will be used to retrieve and update the Exif metadata from the image.
|
||
"""
|
||
|
||
if not plugin_image:
|
||
return False
|
||
|
||
MediaDataTags = [KeyTag for KeyTag in (plugin_image.exifKeys() if LesserVersion
|
||
else chain( plugin_image.exif_keys,
|
||
plugin_image.xmp_keys,
|
||
plugin_image.iptc_keys) ) ]
|
||
return MediaDataTags
|
||
|
||
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 Latitude/ Longitude, gpsTimeStamp, and Altitude...
|
||
"""
|
||
|
||
# Latitude/ Longitude...
|
||
if " " in Coordinates:
|
||
Coordinates = [string_to_rational(coordinate) for coordinate in Coordinates.split(" ")]
|
||
|
||
# gpsTimeStamp...
|
||
elif ":" in Coordinates:
|
||
Coordinates = [string_to_rational(coordinate) for coordinate in Coordinates.split(":")]
|
||
|
||
# Altitude...
|
||
else:
|
||
Coordinates = [string_to_rational(Coordinates)]
|
||
|
||
return Coordinates
|
||
|
||
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) ) )
|
||
|
||
def _removesymbols4saving(latitude, longitude):
|
||
"""
|
||
will recieve a DMS with symbols and return it without them
|
||
|
||
@param: latitude -- Latitude GPS Coordinates
|
||
@param: longitude -- GPS Longitude Coordinates
|
||
"""
|
||
|
||
# check to see if latitude/ longitude exist?
|
||
if (latitude and longitude):
|
||
|
||
# remove degrees symbol if it exist?
|
||
latitude = latitude.replace("°", "")
|
||
longitude = longitude.replace("°", "")
|
||
|
||
# remove minutes symbol if it exist?
|
||
latitude = latitude.replace("′", "")
|
||
longitude = longitude.replace("′", "")
|
||
|
||
# remove seconds symbol if it exist?
|
||
latitude = latitude.replace('″', "")
|
||
longitude = longitude.replace('″', "")
|
||
|
||
return latitude, longitude
|
||
|
||
def rational_to_dms(coords):
|
||
"""
|
||
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)]
|
||
if isinstance(coords, list):
|
||
|
||
if len(coords) == 3:
|
||
return [convert_value(coordinate) for coordinate in coords]
|
||
|
||
elif isinstance(coords, (Fraction, pyexiv2.Rational) ):
|
||
if len(coords) == 1:
|
||
return convert_value(coords)
|
||
|
||
return [False]*3
|
||
|
||
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(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
|
||
|
||
def _format_datetime(value):
|
||
"""
|
||
Convert a python datetime object into a string for display, using the
|
||
standard Gramps date format.
|
||
"""
|
||
if type(value) != datetime:
|
||
return ''
|
||
date_part = gen.lib.Date()
|
||
date_part.set_yr_mon_day(value.year, value.month, value.day)
|
||
date_str = _dd.display(date_part)
|
||
time_str = _('%(hr)02d:%(min)02d:%(sec)02d') % {'hr': value.hour,
|
||
'min': value.minute,
|
||
'sec': value.second}
|
||
return _('%(date)s %(time)s') % {'date': date_str, 'time': time_str}
|