Exif & IPTC Write Access

Registered by Stani on 2009-06-02

Allow to write data to any exif/iptc tag.

Parameters are:
- tag name
- tag value

Blueprint information

Status:
Complete
Approver:
Stani
Priority:
Essential
Drafter:
Stani
Direction:
Approved
Assignee:
Stani
Definition:
Approved
Series goal:
Accepted for 0.2
Implementation:
Implemented
Milestone target:
milestone icon 0.2.1
Started by
Stani on 2009-06-02
Completed by
Stani on 2009-06-15

Related branches

Sprints

Whiteboard

=== modified file 'phatch/actions/copy.py'
--- phatch/actions/copy.py 2009-06-03 15:52:30 +0000
+++ phatch/actions/copy.py 2009-06-04 21:33:43 +0000
@@ -32,6 +32,7 @@
     tags = [_t('file')]
     __doc__ = _t('Copy the image file')
     valid_last = True
+ flush_metadata_before = True

     def interface(self,fields):
         fields[_t('Filename')] = self.FileNameField(choices=self.FILENAMES)

=== modified file 'phatch/actions/gps.py'
--- phatch/actions/gps.py 2009-06-03 16:28:38 +0000
+++ phatch/actions/gps.py 2009-06-05 00:19:29 +0000
@@ -46,17 +46,16 @@
         path = info['path']
         #get time_shift construct timedict if necessary
         if not ('timedict' in cache):
- cache['gps_timeshift'] = self.get_field('Time shift (seconds)',info)
- gpx_file = self.get_field('GPS log (gpx)',info)
+ cache['gps_timeshift'] = self.get_field('Time Shift (seconds)',info)
+ gpx_file = self.get_field('GPS Log (gpx)',info)
             cache['gps_timedict'] = gps.timedict(gpx_file)
         #add geodata
- gps.add_metadata(info['path'],cache['gps_timedict'],cache['gps_timeshift'])
+ photo.metadata.update( gps.get_metadata(
+ info['Exif.Image.DateTime'],
+ cache['gps_timedict'],
+ cache['gps_timeshift']))
         return photo

- def apply_pil(self,image):
- """Just pass the image unaltered."""
- return image
-
     def is_done(self,photo):
         """Method used for resuming when a batch was interrupted.
         For metadata there is no way to know if this image has been done

=== modified file 'phatch/actions/rename.py'
--- phatch/actions/rename.py 2009-06-03 15:52:30 +0000
+++ phatch/actions/rename.py 2009-06-04 21:33:29 +0000
@@ -32,6 +32,7 @@
     tags = [_t('file')]
     __doc__ = _t('Rename the image file')
     valid_last = True
+ flush_metadata_before = True

     def interface(self,fields):
         fields[_t('Filename')] = self.FileNameField(choices=self.FILENAMES)

=== modified file 'phatch/actions/save.py'
--- phatch/actions/save.py 2009-06-03 15:52:30 +0000
+++ phatch/actions/save.py 2009-06-04 21:31:33 +0000
@@ -46,6 +46,7 @@
     tags = [_t('default')]
     __doc__ = _t('Save an image')
     valid_last = True
+ flush_metadata_before = True

     def interface(self,fields):
         fields[_t('Filename')] = self.FileNameField(choices=self.FILENAMES)

=== added file 'phatch/actions/write_tag.py'
--- phatch/actions/write_tag.py 1970-01-01 00:00:00 +0000
+++ phatch/actions/write_tag.py 2009-06-05 00:06:08 +0000
@@ -0,0 +1,109 @@
+# Phatch - Photo Batch Processor
+# Copyright (C) 2007-2008 www.stani.be
+#
+# 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 3 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, see http://www.gnu.org/licenses/
+#
+# Phatch recommends SPE (http://pythonide.stani.be) for editing python files.
+
+# Embedded icon is designed by Igor Kekeljevic (http://www.admiror-ns.co.yu).
+
+import os
+from core import ct, models
+from core.translation import _t
+#from core.lib import gps
+
+class Action(models.Action):
+ """Defined variables: <filename> <type> <folder> <width> <height>"""
+
+ label = _t('Write Tag')
+ author = 'Stani'
+ email = '<email address hidden>'
+ version = '0.1'
+ tags = [_t('metadata')]
+ __doc__ = _t('Write new value to a tag')
+ cache = True
+ valid_last = True
+
+ def interface(self,fields):
+ fields[_t('Tag (Exif, Iptc)')]= self.ExifItpcField('Exif.Image.Software',choices=self.EXIF_IPTC)
+ fields[_t('Value')] = self.CharField('Phatch')
+
+ def apply(self,photo,setting,cache):
+ info = photo.get_info()
+ tag = self.get_field('Tag (Exif, Iptc)',info)
+ value = self.get_field('Value',info)
+ if value.strip() == '':
+ value = None
+ photo.metadata[tag] = value
+ return photo
+
+ def is_done(self,photo):
+ """Method used for resuming when a batch was interrupted.
+ For metadata there is no way to know if this image has been done
+ already, so return False by default."""
+ return False
+
+ def is_overwrite_existing_images_forced(self):
+ """Always force overwrite as we want to store the tags
+ in existing images."""
+ return True
+
+ #FIXME: replace this icon with another one (Nadia?)
+ icon = \
+'x\xda\x01\x9c\x04c\xfb\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x000\x00\
+\x00\x000\x08\x06\x00\x00\x00W\x02\xf9\x87\x00\x00\x00\x04sBIT\x08\x08\x08\
+\x08|\x08d\x88\x00\x00\x04SIDATh\x81\xed\x98]\x88[E\x14\xc7\xffg\x92\xbd\xa6\
+\xba\xban\xebWA\x10\xbb+T\xd6\xc4$c6\xc4me\x1f\xb4\x94\n\x16\x8a[\n\x8dE|\
+\xab/U\x10\x14\x8a\x88_\xef\xf5\xebE\xc4\x17\x1fDj\xb5\x8a\x08\xa2\xa0\xed\
+\x83KI\xc6,\xa9y\x10\xb2\xac\xae\xa8\x0f]vMP6\x9b\xee\xbd\xc7\x87&\xf5\xe6\
+\xee\xdc\xe4N>\xd8\x97\xfc\xdef\xce\x993\xff\xff\xbd3\x97\xb9\x03\x0c\x192d\
+\xc8vB\xdb-\x00\x00\xe2\xf1\xf8}\xe1p8\xc6\xcc\x02\x00\x98\x99\x00\xc4\x88h\
+\r\xc0\xb2+uY)u\xc9=v\xdb\r$\x93\xc9,\x11}\x08`$H>\x11\x9d\xc9\xe7\xf3\xcf7\
+\xdbb`\xca\x02"\x84x\x16\x01\xc5\x03\x003\x9f\x9c\x9a\x9a\xb2\xae\x8f\xefe\
+\xf2L&\xb33\x95J\xdd\xd5K\rf\xfe\xd9p\xc8/\xa5R\xa9\xdel\x84\xbb\x99TJy\x00\
+\xc0\x99z\xbd~\x7f\xa3}\x85\x88^\xcb\xe7\xf3\xef\x9a\xd6\xb2,\xebT\xbd^_\x01\
+0\xd9\xe8\x8a\x02\xd8\xebI;\x0b\x00D\xb4f\xdb\xf6\xab\xee\x80\xf1\x1eh\x88\
+\xff\n\xfa\xd7\xfe\xb2R\xea\r\xd3\x9a\x9e\xfaQ\x00EW\xd7e\xa5T\xcc/\xdfh\t\
+\xcd\xce\xce\x86\x01\xbc\x0f\xff5\xfb\x8a\x94\xd2\xfb\xf4\x06\x8a\x91\x81j\
+\xb5z\x04\xc0=mR\xc2\x00N\xf5\xa4\xc8\x10#\x03Dt2@Z6\x1e\x8f\xdf\xda\xa5\x1e\
+c\x02\x1bH\xa5RS\x00f\x03\xa4\x8e\x86B\xa1\x13]+2$\xb0\x01\xc7q\x9e6\xa8\xfb\
+\x8c\xb9\x94\xee\x08d`nn.\x04\xe0\xb8&t\x15\x8dO\x9c\x87\x07\x93\xc9\xa4\xef\
+\x97\xa3\x9f\x042\xb0\xb4\xb4t\x00\xc0nM\xe8{\x00\x1f\xe8\xc6\x10\xd1S=\xe8\
+\nL \x03\x8e\xe3\xf8\xad\xe9\xb3\x95J\xe5\x02\x80\x8a&v\xbc\xf1\xe6\x06JG\
+\x03\xe9t\xfa\x16":\xac\tm\x028_.\x977\x00|\xa3\x89\xef^\\\\|\xacW\x81\x9d\
+\xe8h`ss\xf3(\x80\x1d\xde~f\xfeA)\xb5\xd2h~\xa1\x1bKD\x03\xff\x1au4\xd0f-_\
+\xdf\xbc\xb6m\x7f\x8dk\x1b\xda\xcb\xe1\x99\x99\x99\x9b\xbb\xd4\x16\x88\xb6\
+\x06\xa6\xa7\xa7\xefe\xe6\xfd\x9a\x90\xcd\xcc\x9f7\x1b\x0b\x0b\x0b\x7f\x13\
+\xd1EM\xde\x8d\xeb\xeb\xebO\xf6*\xb2\x1dm\r\xd8\xb6\x9d\x85\xfe\xc0w\xb1P(\\\
+qw0\xf3\x97\xba\x1a\x83^F\xed\x0c\x10\x00\xbf\xc9?\xf5v8\x8e\xa3\xdd\x07\x00\
+\x1eI$\x12\xed\xceO=\xe1k \x95Je\xf0\xff\x19\xdd\x8d#\x84\xf8\xcc\xdbY(\x14~\
+\x03\xb0\xa0\x9b#\x14\ne\xbb\x97\xd8\x1e\xdf\x1f\x1a\xdb\xb6O\x10i\x7f\x17\
+\xd6\x1c\xc7yNJ\xa9\x8b\xb1\xb6\x939\x0b\xe0\xcd\xae\x14v@k`rr\xf2\x06":\xea\
+3f\x17\x80\x17\r\xe7\xd9+\xa5L{o\x14\xfa\x81v\t\x8d\x8d\x8d=\x01`\xbc\x9f\
+\x13\r\xeah\xa150\x88\xc9\x98\xf9\x98\xfb6\xa1_l1\x10\x8b\xc5\xee`\xe6\x83\
+\xfd\x9e\x08\xc0\xaeH$r\xa8\xdfE\xb7\x18\x18\x19\x199\x06\x83{\x1a\x13\x1a\
+\x9b\xb9\xafl\xd9\xc4\xcc|\xc4\xe7\xeb\xf3\'\x80\x8f\x82\x14e\xe6\xfdD\xf4\
+\xb0\xb7\x9f\x88\x0e\xc5b\xb1\x9b\x8a\xc5\xe2\xbf\xc6J}h1\x90H$n\'\xa2}>\xb9\
+\x1f+\xa5^\nRTJ\xf98\xae]\xbdx\xd9aY\xd6A\x00\xe7\xccd\xfa\xd3\xb2\x84\x88(\
+\r\xc0\xef\x0c\x7f>h\xd1J\xa5\xf2\x1d\x80\x7ft1f~(\xb0\xba\x00x\xf7\xc0m>y\
+\xbf*\xa5~\x0cZ\xb4\\.o\xb8\x0f{n\x88\xe8\xce\xa0u\x82\xd0\xb2\x84\x88\xa8\
+\xa8Kb\xe6\xb7\x008&\x85\x89\xe8=\x00[\x0e\x83\xba\xbbP)\xe5H\xbd^\x1f\x05\
+\x00\xc7q\xc6\x85hy\xae\xe3\xd1ht\xbc\x11\xbbZ*\x95Z\xdelK\xa6R\xea\'f>\r`\
+\xc3%\xe4\x93\x89\x89\x89wL\xc47j]\x02p\x1a\xae\xff\x04f>W\xab\xd5Z\xeeO\xa5\
+\x94\xfb\x00,[\x96\xb5jY\xd6\xaa\x10\xe2\x82\xa7\xd4\xdd\xcdX$\x12\xa9J)\xdf\
+v\x07\xb5\x9f\x1b)\xe5\x18\x11\xed!\xa2\x95\\.\xf7\xbb\xa9x7\x99Lfg\xadV\xdb\
+#\x84\xf8C)\xf5\x977\x9eL&\xbf%\xa2GMj\n!\x1e\xc8\xe5r%\xc0\xe7,\xa4\x94\xaa\
+\x00(t\xa5\xd8\xc3\xfc\xfc\xfc*\x80\xd56b\xaa\xcc\xda3\xa0\x1f\x0c\xa0\xdalt\
+u\xbd\xdeO\x84\x10/\xd8\xb6=\n@\xa2\xf3/n\x85\x88^\xefuU\x0c\x192dH\xff\xf8\
+\x0f\xb7\xe6I\r\x8d&\xf6#\x00\x00\x00\x00IEND\xaeB`\x82\xd4M$\xd4'

=== modified file 'phatch/core/api.py'
--- phatch/core/api.py 2009-06-03 15:52:30 +0000
+++ phatch/core/api.py 2009-06-04 22:15:16 +0000
@@ -274,15 +274,18 @@
     result['abort'] = False
     return photo, result

+def flush_log(photo):
+ if photo.log:
+ log_error(photo.log,image_file,action,label='Warning')
+ photo.log = ''
+
 def apply_action(action,photo,setting,cache,image_file,result):
     try:
         photo = action.apply(photo,setting,cache)
         result['skip'] = False
         result['abort'] = False
         #log non fatal errors/warnings
- if photo.log:
- log_error(photo.log,image_file,action,label='Warning')
- photo.log = ''
+ flush_log(photo)
         return photo, result
     except Exception, details:
         folder, image = os.path.split(ensure_unicode(image_file))
@@ -377,6 +380,9 @@

         #do the actions
         for action_index, action in enumerate(actions):
+ if action.flush_metadata_before:
+ photo.flush_metadata()
+ flush_log(photo)
             #update progress
             progress_result = {}
             send.progress_update_index(progress_result,image_index,action_index)
@@ -390,6 +396,8 @@
             if result['abort']: return
             elif result['skip']:
                 break
+ photo.flush_metadata()
+ flush_log(photo)
         del photo, progress_result, action_index, action
     send.progress_close()

=== modified file 'phatch/core/lib/_pyexiv2.py'
--- phatch/core/lib/_pyexiv2.py 2009-05-22 02:52:50 +0000
+++ phatch/core/lib/_pyexiv2.py 2009-06-04 23:37:43 +0000
@@ -80,6 +80,19 @@
     target.writeMetadata()
     return '\n'.join(warnings)

+def flush(path,metadata):
+ image = pyexiv2.Image(path)
+ image.readMetadata()
+ warnings = []
+ for tag, value in metadata.items():
+ try:
+ image[tag] = value
+ except Exception, message:
+ warnings.append(message)
+ #save metadata (this might rise an exception)
+ image.writeMetadata()
+ return '\n'.join(warnings)
+
 def test():
     import Image
     IMAGE = 'IMGA3230.JPG'

=== modified file 'phatch/core/lib/formField.py'
--- phatch/core/lib/formField.py 2009-06-04 19:57:32 +0000
+++ phatch/core/lib/formField.py 2009-06-05 00:26:28 +0000
@@ -28,9 +28,12 @@

 #gui independent (core.lib)
 from odict import odict as Fields
+from unicoding import ensure_unicode

 NO_FIELDS = Fields()
 _t = unicode
+USE_INSPECTOR = _('Use the Image Inspector to list all the variables.')
+
 #---image
 ALIGN_HORIZONTAL = [_t('left'),_t('center'),_t('right')]
 ALIGN_VERTICAL = [_t('top'),_t('middle'),_t('bottom')]
@@ -231,6 +234,18 @@
         FILENAME,
         '<%s>'%_t('path'),
     ]
+ EXIF_IPTC = ['Exif.Image.Artist','Exif.Image.Copyright',
+ 'Exif.Image.Description', 'Exif.Image.DateTime',
+ 'Exif.Image.Orientation', 'Exif.Image.Software',
+ 'Exif.Photo.UserComment', 'Exif.Photo.WhiteBalance',
+ 'Iptc.Application2.Byline','Iptc.Application2.BylineTitle',
+ 'Iptc.Application2.Caption','Iptc.Application2.CaptionWriter',
+ 'Iptc.Application2.Category', 'Iptc.Application2.City',
+ 'Iptc.Application2.Copyright', 'Iptc.Application2.CountryName',
+ 'Iptc.Application2.DateCreated', 'Iptc.Application2.Keywords',
+ 'Iptc.Application2.ObjectName',
+ 'Iptc.Application2.ProvinceState', 'Iptc.Application2.Writer']
+

     def __init__(self,**options):
         """For the possible options see the source code."""
@@ -356,6 +371,12 @@

     def __str__(self):
         return self._message
+
+ def __unicode__(self):
+ if self.details:
+ return '%s\n%s'%(self._message, self.details)
+ else:
+ return self._message

 #---field mixins
 class PilConstantMixin:
@@ -430,7 +451,7 @@
                 raise ValidationError(self.description,
                 "%s: %s '%s' %s."%(_(label),_("the variable"),
                     variable,_("doesn't exist")),
- _('Use the Image Inspector to list all the variables.'))
+ USE_INSPECTOR)

     def to_python(self,x,label):
         return x
@@ -438,6 +459,10 @@
     def to_string(self,x):
         return unicode(x)

+ def fix_string(self,x):
+ """For the ui (see 'write tag' action)"""
+ return x
+
     def get_as_string(self):
         """For GUI: Translation, but no interpolation here"""
         return self.value_as_string
@@ -669,11 +694,11 @@
     def __init__(self,value,**keyw):
         super(ImageTypeField,self).__init__(value,IMAGE_EXTENSIONS,**keyw)

- def set_as_string(self,x):
+ def fix_string(self,x):
         #ignore translation
         if x and x[0]=='.':
             x = x[1:]
- super(ImageTypeField,self).set_as_string(x)
+ return super(ImageTypeField,self).fix_string(x)

 class ImageReadTypeField(ChoiceField):
     def __init__(self,value,**keyw):
@@ -790,6 +815,21 @@
         super(TiffCompressionField,self).__init__(value,
             TIFF_COMPRESSIONS,**keyw)

+class ExifItpcField(CharField):
+ allow_empty = 'False'
+
+ def fix_string(self,x):
+ #ignore translation
+ if x and x[0]=='<' and x[-1]=='>':
+ x = x[1:-1]
+ return super(ExifItpcField,self).fix_string(x)
+
+ def to_python(self,x,label):
+ if not(x[:5] in ('Exif.','Iptc.')):
+ raise ValidationError(self.description,
+ _('Tag should start with "Exif." or "Iptc."'),
+ USE_INSPECTOR)
+ return super(ExifItpcField,self).to_python(x,label)

 class ColorField(Field):
     pass

=== modified file 'phatch/core/models.py'
--- phatch/core/models.py 2009-06-03 15:52:30 +0000
+++ phatch/core/models.py 2009-06-04 22:49:36 +0000
@@ -30,12 +30,9 @@
 def init():
     pass

-def pil(image):
- return image
-
 class Action(Form):
     all_layers = False
- pil = staticmethod(pil)
+ pil = None
     author = 'Stani'
     cache = False
     dpi = new('dpi')
@@ -45,6 +42,7 @@
     tags = []
     update_size = False
     valid_last = False
+ flush_metadata_before = False
     __doc__ = 'Action base class.'

     def values(self,info,pixel_fields={}):
@@ -62,7 +60,9 @@
             return photo.update_size()
         return photo

- def apply_pil(self,image):
+ def apply_pil(self,image):
+ if pil is None:
+ return image
         return self.pil(image,**self.values(extract_info_pil(image)))

     def rename(self,values,old,new):

=== modified file 'phatch/core/pil.py'
--- phatch/core/pil.py 2009-06-04 19:57:32 +0000
+++ phatch/core/pil.py 2009-06-05 00:08:16 +0000
@@ -38,6 +38,7 @@
 except:
     pyexiv2 = None
     exif = False
+WWW_PYEXIV2 = 'http://tilloy.net/dev/pyexiv2/'

 try:
     from unicoding import ensure_unicode
@@ -342,6 +343,20 @@
         self.layers = {name:layer}
         self.log = ''
         self.init_info(layer,filename,image_index,save_metadata,folder)
+ self.metadata = {}
+
+ def flush_metadata(self):
+ #is there something to be flushed?
+ if not self.metadata:
+ return
+ #throw an error if pyexiv2 is not installed
+ if not exif:
+ raise ImportError(_('pyexiv2 needs to be installed')\
+ +' (%s)'%WWW_PYEXIV2)
+ self.log += exif.flush(self.info['path'],self.metadata)
+ #as metadata has changed, use new source
+ self._exif_source = self.info['path']
+ self.metadata = {}

     def get_filename(self,folder,filename,typ):
         self.info[_t('new')+DOT+_t('width')], \

=== modified file 'phatch/pyWx/lib/imageInspector.py'
--- phatch/pyWx/lib/imageInspector.py 2009-05-29 17:38:53 +0000
+++ phatch/pyWx/lib/imageInspector.py 2009-06-05 00:27:53 +0000
@@ -34,6 +34,7 @@
 SELECT = _('Select')
 ALL = _('All')
 TAGS = [SELECT, ALL,'Pil']
+
 if pyexiv2:
     TAGS.extend(['Exif','Iptc'])
 TAGS.extend(['EXIF','Pexif','Zexif'])

=== modified file 'phatch/pyWx/lib/treeEdit.py'
--- phatch/pyWx/lib/treeEdit.py 2009-02-16 21:55:26 +0000
+++ phatch/pyWx/lib/treeEdit.py 2009-06-04 23:28:40 +0000
@@ -211,10 +211,11 @@
                         not self.is_form_enabled(item))

     def set_form_field_value(self,item, value_as_string):
- label, old = self.GetPyData(item)
+ label, old = self.GetPyData(item)
+ form = self.get_form(item,label)
+ field = form._get_field(label)
+ value_as_string = field.fix_string(value_as_string)
         if value_as_string.strip() and value_as_string != old:
- form = self.get_form(item,label)
- field = form._get_field(label)
             #test-validate the user input (see formField.Field.get)
             try:
                 if isinstance(field,formField.FileSizeField):

(?)

Work Items

Dependency tree

* Blueprints in grey have been implemented.

This blueprint contains Public information 
Everyone can see this information.