Ajout du produit, sur la base du dépôt luxia r1390 :
authorBenoît Pin <benoit.pin@gmail.com>
Fri, 22 Oct 2010 16:12:20 +0000 (18:12 +0200)
committerBenoît Pin <benoit.pin@gmail.com>
Fri, 22 Oct 2010 16:12:20 +0000 (18:12 +0200)
URL : http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk
Racine du dépôt : http://svn.luxia.fr/svn/labo
UUID du dépôt : 7eb47c9a-6e02-46bb-968b-2b2bf1974b8d
Révision : 1390
Type de nœud : répertoire
Tâche programmée : normale
Auteur de la dernière modification : pin
Révision de la dernière modification : 1371
Date de la dernière modification: 2009-09-10 19:58:28 +0200 (Jeu 10 sep 2009)

59 files changed:
Photo.py [new file with mode: 0755]
TODO.txt [new file with mode: 0644]
TileSupport.py [new file with mode: 0644]
__init__.py [new file with mode: 0755]
blobbases.py [new file with mode: 0755]
cache.py [new file with mode: 0755]
dependencies.txt [new file with mode: 0755]
dtml/File_icon.gif [new file with mode: 0644]
dtml/Image_icon.gif [new file with mode: 0644]
dtml/addPhotoForm.dtml [new file with mode: 0755]
dtml/fileEdit.dtml [new file with mode: 0644]
dtml/imageAdd.dtml [new file with mode: 0644]
dtml/imageEdit.dtml [new file with mode: 0644]
dtml/imageView.dtml [new file with mode: 0644]
dtml/photoEdit.dtml [new file with mode: 0755]
dtml/photoView.dtml [new file with mode: 0755]
dtml/photo_icon.gif [new file with mode: 0644]
dtml/testMenu.dtml [new file with mode: 0755]
exif.py [new file with mode: 0755]
license.txt [new file with mode: 0755]
metadata.py [new file with mode: 0755]
migration/__init__.py [new file with mode: 0644]
migration/from2to3.py [new file with mode: 0644]
migration/toblob.py [new file with mode: 0755]
ppm.py [new file with mode: 0755]
readme.txt [new file with mode: 0755]
standards/__init__.py [new file with mode: 0755]
standards/bridges/__init__.py [new file with mode: 0755]
standards/bridges/_bridges.py [new file with mode: 0755]
standards/bridges/xmp_exif.csv [new file with mode: 0644]
standards/exif/0thIFDExifPrivateTags.csv [new file with mode: 0644]
standards/exif/0thIFDGPSInfoTags.csv [new file with mode: 0644]
standards/exif/0thIFDInteroperabilityTag.csv [new file with mode: 0644]
standards/exif/0thIFDTIFFTags.csv [new file with mode: 0644]
standards/exif/1stIFDTIFFTag.csv [new file with mode: 0644]
standards/exif/__init__.py [new file with mode: 0755]
standards/exif/_exif_tags.py [new file with mode: 0755]
standards/exif/gpsA.csv [new file with mode: 0644]
standards/exif/hand_added.csv [new file with mode: 0644]
standards/exif/ifdA.csv [new file with mode: 0644]
standards/exif/ifdB.csv [new file with mode: 0644]
standards/exif/ifdC.csv [new file with mode: 0644]
standards/exif/ifdD.csv [new file with mode: 0644]
standards/exif/ifdE.csv [new file with mode: 0644]
standards/exif/ifdF.csv [new file with mode: 0644]
standards/exif/ifdG.csv [new file with mode: 0644]
standards/exif/ifdH.csv [new file with mode: 0644]
standards/exif/tiffA.csv [new file with mode: 0644]
standards/exif/tiffB.csv [new file with mode: 0644]
standards/exif/tiffC.csv [new file with mode: 0644]
standards/exif/tiffD.csv [new file with mode: 0644]
standards/xmp/__init__.py [new file with mode: 0755]
standards/xmp/_namespaces.py [new file with mode: 0755]
standards/xmp/accessors.csv [new file with mode: 0644]
version.txt [new file with mode: 0755]
xmp.py [new file with mode: 0755]
xmp_jpeg.py [new file with mode: 0755]
xmputils.py [new file with mode: 0755]
xslt/xmp_merge_descriptions.xsl [new file with mode: 0644]

diff --git a/Photo.py b/Photo.py
new file mode 100755 (executable)
index 0000000..2161eaf
--- /dev/null
+++ b/Photo.py
@@ -0,0 +1,400 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright (C) 2004-2007  Benoît PIN <benoit.pin@ensmp.fr>                         #
+#                                                                                     #
+#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+""" Photo zope object
+
+$Id: Photo.py 1281 2009-08-13 10:44:40Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/Photo.py $
+"""
+
+from Globals import InitializeClass, DTMLFile
+from AccessControl import ClassSecurityInfo
+from AccessControl.Permissions import manage_properties, view
+from metadata import Metadata
+from TileSupport import TileSupport
+from xmputils import TIFF_ORIENTATIONS
+from BTrees.OOBTree import OOBTree
+from cache import memoizedmethod
+
+from blobbases import Image, cookId, getImageInfo
+import PIL.Image
+import string
+from math import floor
+from types import StringType
+from logging import getLogger
+console = getLogger('Photo.Photo')
+
+
+
+def _strSize(size) :
+       return str(size[0]) + '_' + str(size[1])
+       
+def getNewSize(fullSize, maxNewSize) :
+       fullWidth, fullHeight =  fullSize
+       maxWidth, maxHeight = maxNewSize
+       
+       widthRatio = float(maxWidth) / fullWidth
+       if int(fullHeight * widthRatio) > maxWidth :
+               heightRatio = float(maxHeight) / fullHeight
+               return (int(fullWidth * heightRatio) , maxHeight)
+       else :
+               return (maxWidth, int(fullHeight * widthRatio))
+
+
+
+
+
+
+class Photo(Image, TileSupport, Metadata):
+       "Photo éditable en ligne"
+
+       meta_type = 'Photo'
+       
+       security = ClassSecurityInfo()
+               
+       manage_editForm = DTMLFile('dtml/photoEdit',globals(),
+                                                          Kind='Photo', kind='photo')
+       manage_editForm._setName('manage_editForm')
+       manage = manage_main = manage_editForm
+       view_image_or_file = DTMLFile('dtml/photoView',globals())
+       
+       manage_options=(
+                {'label':'Edit', 'action':'manage_main',
+                'help':('OFSP','Image_Edit.stx')},
+                {'label':'View', 'action':'view_image_or_file',
+                'help':('OFSP','Image_View.stx')},) + Image.manage_options[2:]
+
+
+       filters = ['NEAREST', 'BILINEAR', 'BICUBIC', 'ANTIALIAS']
+
+       _properties = Image._properties[:2] + (
+               {'id' : 'height',                               'type' : 'int',         'mode' : 'w'},
+               {'id' : 'width',                                'type' : 'int',         'mode' : 'w'},
+               {'id' : 'auto_update_thumb',    'type' : 'boolean', 'mode' : 'w'},
+               {'id' : 'tiles_available',              'type' : 'int',         'mode' : 'r'},
+               {'id' : 'thumb_height',                 'type' : 'int',         'mode' : 'w'},
+               {'id' : 'thumb_width',                  'type' : 'int',         'mode' : 'w'},
+               {'id' : 'prop_filter',
+                'label' : 'Filter',
+                'type' : 'selection',
+                'select_variable' : 'filters',
+                'mode' : 'w'},
+               )
+               
+               
+       security.declareProtected(manage_properties, 'manage_editProperties')
+       def manage_editProperties(self, REQUEST=None, no_refresh = 0, **kw):
+               "Save Changes and update the thumbnail"
+               Image.manage_changeProperties(self, REQUEST, **kw)
+               
+               if hasattr(self, 'thumbnail') and self.auto_update_thumb == 1 and no_refresh == 0 :
+                       self.makeThumbnail()
+                               
+               if REQUEST:
+                       message="Saved changes."
+                       return self.manage_propertiesForm(self,REQUEST,
+                                                                                         manage_tabs_message=message)
+
+
+       def __init__(self, id, title, file, content_type='', precondition='', **kw) :
+               # 0 means: tiles are not generated
+               # 1 means: tiles are all generated
+               # 2 means: tiling is not available is this photo (deliberated choice of the owner)
+               # -1 means: no data tiles cannot be generated
+               self.tiles_available = 0
+               super(Photo, self).__init__(id, title, file, content_type='', precondition='')
+
+               self.auto_update_thumb = kw.get('auto_update_thumb', 1)
+               self.thumb_height = kw.get('thumb_height', 180)
+               self.thumb_width = kw.get('thumb_width', 120)
+               self.prop_filter = kw.get('prop_filter', 'ANTIALIAS')
+
+               defaultBlankThumbnail = kw.get('defaultBlankThumbnail', None)
+               if defaultBlankThumbnail :
+                       blankThumbnail = Image('thumbnail', '',
+                                                                  getattr(defaultBlankThumbnail, '_data', getattr(defaultBlankThumbnail, 'data', None)))
+                       self.thumbnail = blankThumbnail
+               
+               self._methodResultsCache = OOBTree()
+               TileSupport.__init__(self)
+               
+       def update_data(self, file, content_type=None) :
+               super(Photo, self).update_data(file, content_type)
+               
+               if self.content_type != 'image/jpeg' and self.size :
+                       raw = self.open('r')
+                       im = PIL.Image.open(raw)
+                       self.content_type = 'image/%s' % im.format.lower()
+                       self.width, self.height = im.size
+                       
+                       if im.mode not in ('L', 'RGB'):
+                               im = im.convert('RGB')
+
+                       jpeg_image = Image('jpeg_image', '', '', content_type='image/jpeg')
+                       out = jpeg_image.open('w')
+                       im.save(out, 'JPEG', quality=90)
+                       jpeg_image.updateFormat(out.tell(), im.size, 'image/jpeg')
+                       out.close()
+                       self.jpeg_image = jpeg_image
+                       
+               self._methodResultsCache = OOBTree()
+               self._v__methodResultsCache = OOBTree()
+               
+               self._tiles = OOBTree()
+               if self.tiles_available in [1, -1]:
+                       self.tiles_available = 0
+               
+               if hasattr(self, 'thumbnail') and self.auto_update_thumb == 1 :
+                       self.makeThumbnail()
+               
+               
+       
+       def _getJpegBlob(self) :
+               if self.size :
+                       if self.content_type == 'image/jpeg' :
+                               return self.bdata
+                       else :
+                               return self.jpeg_image.bdata
+               else :
+                       return None
+               
+       security.declareProtected(view, 'getJpegImage')
+       def getJpegImage(self, REQUEST, RESPONSE) :
+               """ return JPEG formated image """
+               if self.content_type == 'image/jpeg' :
+                       return self.index_html(REQUEST, RESPONSE)
+               elif self.jpeg_image :
+                       return  self.jpeg_image.index_html(REQUEST, RESPONSE)
+       
+       security.declareProtected(view, 'tiffOrientation')
+       @memoizedmethod()
+       def tiffOrientation(self) :
+               tiffOrientation = self.getXmpValue('tiff:Orientation')
+               if tiffOrientation :
+                       return int(tiffOrientation)
+               else :
+                       # TODO : falling back to legacy Exif metadata
+                       return 1
+       
+       def _rotateOrFlip(self, im) :
+               orientation = self.tiffOrientation()
+               rotation, flip = TIFF_ORIENTATIONS[orientation]
+               if rotation :
+                       im = im.rotate(-rotation)
+               if flip :
+                       im = im.transpose(PIL.Image.FLIP_LEFT_RIGHT)
+               return im
+       
+       @memoizedmethod('size', 'keepAspectRatio')
+       def _getResizedImage(self, size, keepAspectRatio) :
+               """ returns a resized version of the raw image.
+               """
+
+               fullSizeFile = self._getJpegBlob().open('r')
+               fullSizeImage = PIL.Image.open(fullSizeFile)
+               if fullSizeImage.mode not in ('L', 'RGB'):
+                       fullSizeImage.convert('RGB')
+               fullSize = fullSizeImage.size
+
+               if (keepAspectRatio) :
+                       newSize = getNewSize(fullSize, size)
+               else :
+                       newSize = size
+               
+               fullSizeImage.thumbnail(newSize, PIL.Image.ANTIALIAS)
+               fullSizeImage = self._rotateOrFlip(fullSizeImage)
+               
+               for hook in self._getAfterResizingHooks() :
+                       hook(self, fullSizeImage)
+               
+               
+               resizedImage = Image(self.getId() + _strSize(size), 'resized copy of %s' % self.getId(), '')
+               out = resizedImage.open('w')
+               fullSizeImage.save(out, "JPEG", quality=90)
+               resizedImage.updateFormat(out.tell(), fullSizeImage.size, 'image/jpeg')
+               out.close()
+               return resizedImage
+       
+       def _getAfterResizingHooks(self) :
+               """ returns a list of hook scripts that are executed
+                       after the image is resized.
+               """
+               return []
+               
+               
+       security.declarePrivate('makeThumbnail')
+       def makeThumbnail(self) :
+               "make a thumbnail from jpeg data"
+               b = self._getJpegBlob()
+               if b is not None :
+                       # récupération des propriétés de redimentionnement
+                       thumb_size = []
+                       if int(self.width) >= int(self.height) :
+                               thumb_size.append(self.thumb_height)
+                               thumb_size.append(self.thumb_width)
+                       else :
+                               thumb_size.append(self.thumb_width)
+                               thumb_size.append(self.thumb_height)
+                       thumb_size = tuple(thumb_size)
+
+                       if thumb_size[0] <= 1 or thumb_size[1] <= 1 :
+                               thumb_size = (180, 180)
+                       thumb_filter = getattr(PIL.Image, self.prop_filter, PIL.Image.ANTIALIAS)
+                       
+                       # create a thumbnail image file
+                       original_file = b.open('r')
+                       image = PIL.Image.open(original_file)
+                       if image.mode not in ('L', 'RGB'):
+                               image = image.convert('RGB')
+                       
+                       image.thumbnail(thumb_size, thumb_filter)
+                       image = self._rotateOrFlip(image)
+                       
+                       thumbnail = Image('thumbnail', 'Thumbail', '', 'image/jpeg')
+                       out = thumbnail.open('w')
+                       image.save(out, "JPEG", quality=90)
+                       thumbnail.updateFormat(out.tell(), image.size, 'image/jpeg')
+                       out.close()
+                       original_file.close()
+                       self.thumbnail = thumbnail
+                       return True
+               else :
+                       return False
+
+       security.declareProtected(view, 'getThumbnail')
+       def getThumbnail(self, REQUEST, RESPONSE) :
+               "Return the thumbnail image and create it before if it does not exist yet."
+               if not hasattr(self, 'thumbnail') :
+                       self.makeThumbnail()
+               return self.thumbnail.index_html(REQUEST=REQUEST, RESPONSE=RESPONSE)
+       
+       security.declareProtected(view, 'getThumbnailSize')
+       def getThumbnailSize(self) :
+               """ return thumbnail size dict
+               """
+               if not hasattr(self, 'thumbnail') :
+                       if not self.width :
+                               return {'height' : 0, 'width' : 0}
+                       else :
+                               thumbMaxFrame = []
+                               if int(self.width) >= int(self.height) :
+                                       thumbMaxFrame.append(self.thumb_height)
+                                       thumbMaxFrame.append(self.thumb_width)
+                               else :
+                                       thumbMaxFrame.append(self.thumb_width)
+                                       thumbMaxFrame.append(self.thumb_height)
+                               thumbMaxFrame = tuple(thumbMaxFrame)
+
+                               if thumbMaxFrame[0] <= 1 or thumbMaxFrame[1] <= 1 :
+                                       thumbMaxFrame = (180, 180)
+                               
+                               th = self.height * thumbMaxFrame[0] / float(self.width)
+                               # resizing round limit is not 0.5 but seems to be strictly up to 0.75
+                               # TODO check algorithms
+                               if th > floor(th) + 0.75 :
+                                       th = int(floor(th)) + 1
+                               else :
+                                       th = int(floor(th))
+
+                               if th <= thumbMaxFrame[1] :
+                                       thumbSize = (thumbMaxFrame[0], th)
+                               else :
+                                       tw = self.width * thumbMaxFrame[1] / float(self.height)
+                                       if tw > floor(tw) + 0.75 :
+                                               tw = int(floor(tw)) + 1
+                                       else :
+                                               tw = int(floor(tw))
+                                       thumbSize = (tw, thumbMaxFrame[1])
+                               
+                               if self.tiffOrientation() <= 4 :
+                                       return {'width':thumbSize[0], 'height' : thumbSize[1]}
+                               else :
+                                       return {'width':thumbSize[1], 'height' : thumbSize[0]}
+                                       
+               else :
+                       return {'height' : self.thumbnail.height, 'width' :self.thumbnail.width}
+               
+
+       security.declareProtected(view, 'getResizedImageSize')
+       def getResizedImageSize(self, REQUEST=None, size=(), keepAspectRatio=True, asXml=False) :
+               """ return the reel image size the after resizing """
+               if not size :
+                       size = REQUEST.SESSION.get('preferedImageSize', (600, 600))
+               elif type(size) == StringType :
+                       size = tuple([int(n) for n in size.split('_')])
+               
+               resizedImage = self._getResizedImage(size, keepAspectRatio)
+               size = (resizedImage.width, resizedImage.height)
+                       
+               if asXml :
+                       REQUEST.RESPONSE.setHeader('content-type', 'text/xml; charset=utf-8')
+                       return '<size><width>%d</width><height>%d</height></size>' % size
+               else :
+                       return size
+               
+       
+       security.declareProtected(view, 'getResizedImage')
+       def getResizedImage(self, REQUEST, RESPONSE, size=(), keepAspectRatio=True) :
+               """
+               Return a volatile resized image.
+               The 'preferedImageSize' tuple (width, height) is looked up into SESSION data.
+               Default size is 600 x 600 px
+               """
+               if not size :
+                       size = REQUEST.SESSION.get('preferedImageSize', (600, 600))
+               elif type(size) == StringType :
+                       size = size.split('_')
+                       if len(size) == 1 :
+                               i = int(size[0])
+                               size = (i, i)
+                               keepAspectRatio = True
+                       else :
+                               size = tuple([int(n) for n in size])
+               
+               return self._getResizedImage(size, keepAspectRatio).index_html(REQUEST=REQUEST, RESPONSE=RESPONSE)
+               
+                                               
+InitializeClass(Photo)
+
+
+# Factories
+def addPhoto(dispatcher, id, file='', title='',
+                        precondition='', content_type='', REQUEST=None, **kw) :
+       """
+       Add a new Photo object.
+       Creates a new Photo object 'id' with the contents of 'file'.
+       """
+       id=str(id)
+       title=str(title)
+       content_type=str(content_type)
+       precondition=str(precondition)
+
+       id, title = cookId(id, title, file)
+       parentContainer = dispatcher.Destination()
+
+       parentContainer._setObject(id, Photo(id,title,file,content_type, precondition, **kw))
+
+       if REQUEST is not None:
+               try:    url=dispatcher.DestinationURL()
+               except: url=REQUEST['URL1']
+               REQUEST.RESPONSE.redirect('%s/manage_main' % url)
+       return id
+
+# creation form
+addPhotoForm = DTMLFile('dtml/addPhotoForm', globals())
diff --git a/TODO.txt b/TODO.txt
new file mode 100644 (file)
index 0000000..3795e3a
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1,8 @@
+Migration vers nouvelle version
+
+Attributs à virer :
+— _variants
+— _metadata
+
+Attributs à ajouter
+— self._methodResultsCache = OOBTree()
\ No newline at end of file
diff --git a/TileSupport.py b/TileSupport.py
new file mode 100644 (file)
index 0000000..905b0f4
--- /dev/null
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright (C) 2004-2007  Benoît PIN <benoit.pin@ensmp.fr>                         #
+#                                                                                     #
+#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+""" Tile support module
+
+$Id: TileSupport.py 1371 2009-09-10 17:58:28Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/TileSupport.py $
+"""
+
+from AccessControl import ClassSecurityInfo
+from AccessControl import Unauthorized
+from AccessControl import getSecurityManager
+from AccessControl.Permissions import view, change_images_and_files
+from PIL import Image as PILImage
+from math import ceil
+from blobbases import Image
+from xmputils import TIFF_ORIENTATIONS
+from cache import memoizedmethod
+from BTrees.OOBTree import OOBTree
+from BTrees.IOBTree import IOBTree
+from ppm import PPMFile
+from threading import Lock
+from subprocess import Popen, PIPE
+from tempfile import TemporaryFile
+
+JPEG_ROTATE = 'jpegtran -rotate %d'
+JPEG_FLIP = 'jpegtran -flip horizontal'
+
+def runOnce(lock):
+       """ Decorator. exit if already running """
+
+       def wrapper(f):
+               def method(*args, **kw):
+                       if not lock.locked() :
+                               lock.acquire()
+                               try:
+                                       return f(*args, **kw)
+                               finally:
+                                       lock.release()
+                       else :
+                               return False
+               return method
+       return wrapper
+
+
+
+class TileSupport :
+       """ Mixin class to generate tiles from image """
+       
+       security = ClassSecurityInfo()
+       tileSize = 256
+       tileGenerationLock = Lock()
+       
+       def __init__(self) :
+               self._tiles = OOBTree()
+       
+       security.declarePrivate('makeTilesAt')
+       @runOnce(tileGenerationLock)
+       def makeTilesAt(self, zoom):
+               """generates tiles at zoom level"""
+               
+               if self._tiles.has_key(zoom) :
+                       return True
+               
+               assert zoom <= 1, "zoom arg must be <= 1 found: %s" % zoom
+
+               ppm = self._getPPM()
+               if zoom < 1 :
+                       ppm = ppm.resize(ratio=zoom)
+               
+               self._makeTilesAt(zoom, ppm)
+               return True
+       
+       def _getPPM(self) :
+               bf = self._getJpegBlob()
+               f = bf.open('r')
+               
+               orientation = self.tiffOrientation()
+               rotation, flip = TIFF_ORIENTATIONS[orientation]
+               
+               if rotation and flip :
+                       tf = TemporaryFile(mode='w+')
+                       pRot = Popen(JPEG_ROTATE % rotation
+                                       , stdin=f
+                                       , stdout=PIPE
+                                       , shell=True)
+                       pFlip = Popen(JPEG_FLIP
+                                               , stdin=pRot.stdout
+                                               , stdout=tf
+                                               , shell=True)
+                       pFlip.wait()
+                       f.close()
+                       tf.seek(0)
+                       f = tf
+
+               elif rotation :
+                       tf = TemporaryFile(mode='w+')
+                       pRot = Popen(JPEG_ROTATE % rotation
+                                               , stdin=f
+                                               , stdout=tf
+                                               , shell=True)
+                       pRot.wait()
+                       f.close()
+                       tf.seek(0)
+                       f = tf
+
+               elif flip :
+                       tf = TemporaryFile(mode='w+')
+                       pFlip = Popen(JPEG_FLIP
+                                               , stdin=f
+                                               , stdout=tf
+                                               , shell=True)
+                       pFlip.wait()
+                       f.close()
+                       tf.seek(0)
+                       f = tf
+
+               ppm = PPMFile(f, tileSize=self.tileSize)
+               f.close()
+               return ppm
+       
+       def _makeTilesAt(self, zoom, ppm):
+               hooks = self._getAfterTilingHooks()             
+               self._tiles[zoom] = IOBTree()
+               bgColor = getattr(self, 'tiles_background_color', '#fff')
+               
+               for x in xrange(ppm.tilesX) :
+                       self._tiles[zoom][x] = IOBTree()
+                       for y in xrange(ppm.tilesY) :
+                               tile = ppm.getTile(x, y)
+                               for hook in hooks :
+                                       hook(self, tile)
+                               
+                               # fill with solid color
+                               if min(tile.size) < self.tileSize :
+                                       blankTile = PILImage.new('RGB', (self.tileSize, self.tileSize), bgColor)
+                                       box = (0,0) + tile.size
+                                       blankTile.paste(tile, box)
+                                       tile = blankTile
+                               
+                               zImg = Image('tile', 'tile', '', content_type='image/jpeg')
+                               out = zImg.open('w')
+                               tile.save(out, 'JPEG', quality=90)
+                               zImg.updateFormat(out.tell(), tile.size, 'image/jpeg')
+                               out.close()
+
+                               self._tiles[zoom][x][y] = zImg
+               
+       def _getAfterTilingHooks(self) :
+               return []
+       
+       
+       security.declareProtected(view, 'getAvailableZooms')
+       def getAvailableZooms(self):
+               zooms = list(self._tiles.keys())
+               zooms.sort()
+               return zooms
+       
+       security.declareProtected(view, 'getTile')
+       def getTile(self, REQUEST, RESPONSE, zoom=1, x=0, y=0):
+               """ publishes tile
+               """
+               zoom, x, y = float(zoom), int(x), int(y)
+               if not self._tiles.has_key(zoom) :
+                       sm = getSecurityManager()
+                       if not sm.checkPermission(change_images_and_files, self) :
+                               raise Unauthorized("Tiling arbitrary zoom unauthorized")
+                       if self.makeTilesAt(zoom) :
+                               tile = self._tiles[zoom][x][y]
+                               return tile.index_html(REQUEST=REQUEST, RESPONSE=RESPONSE)
+               else :
+                       tile = self._tiles[zoom][x][y]
+                       return tile.index_html(REQUEST=REQUEST, RESPONSE=RESPONSE)
+
diff --git a/__init__.py b/__init__.py
new file mode 100755 (executable)
index 0000000..2b1e375
--- /dev/null
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright (C) 2004-2008  Beno\94t PIN <benoit.pin@ensmp.fr>                         #
+#                                                                                     #
+#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+""" Product for photos manipulation.
+       main features:
+       - keep origninal uploaded data
+       - automatic convertion to jpeg (if needed) from supported PIL image's formats
+       - resizing support
+       - metadata extraction
+       
+       experimental features:
+       - full psd support with Adobe photoshop (windows only, drived by pythoncom)
+       - tile support (to display a grid of tiles with a javascript web interface)
+
+$Id: __init__.py 949 2009-04-30 14:42:24Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/__init__.py $
+"""
+
+from AccessControl.Permissions import add_documents_images_and_files
+import xmp_jpeg
+
+from Photo import Photo, addPhotoForm, addPhoto
+import blobbases
+
+def initialize(registrar) :
+       registrar.registerClass(
+               Photo,
+               constructors = (addPhotoForm, addPhoto),
+               icon = 'dtml/photo_icon.gif'
+               )
+       
+       registrar.registerClass(
+               blobbases.File,
+               permission = add_documents_images_and_files,
+               constructors = (('blobFileAdd', blobbases.manage_addFileForm), blobbases.manage_addFile),
+               icon='dtml/File_icon.gif'
+               )
+
+       registrar.registerClass(
+               blobbases.Image,
+               permission = add_documents_images_and_files,
+               constructors = (('blobImageAdd', blobbases.manage_addImageForm), blobbases.manage_addImage),
+               icon='dtml/Image_icon.gif'
+               )
diff --git a/blobbases.py b/blobbases.py
new file mode 100755 (executable)
index 0000000..9d2fb6f
--- /dev/null
@@ -0,0 +1,812 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+# This module is based on OFS.Image originaly copyrighted as:
+#
+# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Image object
+
+$Id: blobbases.py 949 2009-04-30 14:42:24Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/blobbases.py $
+"""
+
+import struct
+from warnings import warn
+from zope.contenttype import guess_content_type
+from Globals import DTMLFile
+from Globals import InitializeClass
+from OFS.PropertyManager import PropertyManager
+from AccessControl import ClassSecurityInfo
+from AccessControl.Role import RoleManager
+from AccessControl.Permissions import change_images_and_files
+from AccessControl.Permissions import view_management_screens
+from AccessControl.Permissions import view as View
+from AccessControl.Permissions import ftp_access
+from AccessControl.Permissions import delete_objects
+from webdav.common import rfc1123_date
+from webdav.Lockable import ResourceLockedError
+from webdav.WriteLockInterface import WriteLockInterface
+from OFS.SimpleItem import Item_w__name__
+from cStringIO import StringIO
+from Globals import Persistent
+from Acquisition import Implicit
+from DateTime import DateTime
+from OFS.Cache import Cacheable
+from mimetools import choose_boundary
+from ZPublisher import HTTPRangeSupport
+from ZPublisher.HTTPRequest import FileUpload
+from ZPublisher.Iterators import filestream_iterator
+from zExceptions import Redirect
+from cgi import escape
+import transaction
+from ZODB.blob import Blob
+
+CHUNK_SIZE = 1 << 16
+
+manage_addFileForm=DTMLFile('dtml/imageAdd', globals(),Kind='File',kind='file')
+def manage_addFile(self,id,file='',title='',precondition='', content_type='',
+                                  REQUEST=None):
+       """Add a new File object.
+
+       Creates a new File object 'id' with the contents of 'file'"""
+
+       id=str(id)
+       title=str(title)
+       content_type=str(content_type)
+       precondition=str(precondition)
+
+       id, title = cookId(id, title, file)
+
+       self=self.this()
+       self._setObject(id, File(id,title,file,content_type, precondition))
+
+       if REQUEST is not None:
+               REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
+
+
+class File(Persistent, Implicit, PropertyManager,
+                  RoleManager, Item_w__name__, Cacheable):
+       """A File object is a content object for arbitrary files."""
+
+       __implements__ = (WriteLockInterface, HTTPRangeSupport.HTTPRangeInterface)
+       meta_type='Blob File'
+
+       security = ClassSecurityInfo()
+       security.declareObjectProtected(View)
+
+       precondition=''
+       size=None
+
+       manage_editForm  =DTMLFile('dtml/fileEdit',globals(),
+                                                          Kind='File',kind='file')
+       manage_editForm._setName('manage_editForm')
+
+       security.declareProtected(view_management_screens, 'manage')
+       security.declareProtected(view_management_screens, 'manage_main')
+       manage=manage_main=manage_editForm
+       manage_uploadForm=manage_editForm
+
+       manage_options=(
+               (
+               {'label':'Edit', 'action':'manage_main',
+                'help':('OFSP','File_Edit.stx')},
+               {'label':'View', 'action':'',
+                'help':('OFSP','File_View.stx')},
+               )
+               + PropertyManager.manage_options
+               + RoleManager.manage_options
+               + Item_w__name__.manage_options
+               + Cacheable.manage_options
+               )
+
+       _properties=({'id':'title', 'type': 'string'},
+                                {'id':'content_type', 'type':'string'},
+                                )
+
+       def __init__(self, id, title, file, content_type='', precondition=''):
+               self.__name__=id
+               self.title=title
+               self.precondition=precondition
+               self.uploaded_filename = cookId('', '', file)[0]
+               self.bdata = Blob()
+
+               content_type=self._get_content_type(file, id, content_type)
+               self.update_data(file, content_type)
+       
+       security.declarePrivate('save')
+       def save(self, file):
+               bf = self.bdata.open('w')
+               bf.write(file.read())
+               self.size = bf.tell()
+               bf.close()
+       
+       security.declarePrivate('open')
+       def open(self, mode='r'):
+               bf = self.bdata.open(mode)
+               return bf
+       
+       security.declarePrivate('updateSize')
+       def updateSize(self, size=None):
+               if size is None :
+                       bf = self.open('r')
+                       bf.seek(0,2)
+                       self.size = bf.tell()
+                       bf.close()
+               else :
+                       self.size = size
+
+       def _getLegacyData(self) :
+               warn("Accessing 'data' attribute may be inefficient with "
+                        "this blob based file. You should refactor your product "
+                        "by accessing data like: "
+                        "f = self.open('r') "
+                        "data = f.read()",
+                       DeprecationWarning, stacklevel=2)
+               f = self.open()
+               data = f.read()
+               f.close()
+               return data
+       
+       def _setLegacyData(self, data) :
+               warn("Accessing 'data' attribute may be inefficient with "
+                        "this blob based file. You should refactor your product "
+                        "by accessing data like: "
+                        "f = self.save(data)",
+                       DeprecationWarning, stacklevel=2)
+               if isinstance(data, str) :
+                       sio = StringIO()
+                       sio.write(data)
+                       sio.seek(0)
+                       data = sio
+               self.save(data)
+               
+       data = property(_getLegacyData, _setLegacyData,
+                                       "Data Legacy attribute to ensure compatibility "
+                                       "with derived classes that access data by this way.")
+
+       def id(self):
+               return self.__name__
+
+       def _if_modified_since_request_handler(self, REQUEST, RESPONSE):
+               # HTTP If-Modified-Since header handling: return True if
+               # we can handle this request by returning a 304 response
+               header=REQUEST.get_header('If-Modified-Since', None)
+               if header is not None:
+                       header=header.split( ';')[0]
+                       # Some proxies seem to send invalid date strings for this
+                       # header. If the date string is not valid, we ignore it
+                       # rather than raise an error to be generally consistent
+                       # with common servers such as Apache (which can usually
+                       # understand the screwy date string as a lucky side effect
+                       # of the way they parse it).
+                       # This happens to be what RFC2616 tells us to do in the face of an
+                       # invalid date.
+                       try:    mod_since=long(DateTime(header).timeTime())
+                       except: mod_since=None
+                       if mod_since is not None:
+                               if self._p_mtime:
+                                       last_mod = long(self._p_mtime)
+                               else:
+                                       last_mod = long(0)
+                               if last_mod > 0 and last_mod <= mod_since:
+                                       RESPONSE.setHeader('Last-Modified',
+                                                                          rfc1123_date(self._p_mtime))
+                                       RESPONSE.setHeader('Content-Type', self.content_type)
+                                       RESPONSE.setHeader('Accept-Ranges', 'bytes')
+                                       RESPONSE.setStatus(304)
+                                       return True
+
+       def _range_request_handler(self, REQUEST, RESPONSE):
+               # HTTP Range header handling: return True if we've served a range
+               # chunk out of our data.
+               range = REQUEST.get_header('Range', None)
+               request_range = REQUEST.get_header('Request-Range', None)
+               if request_range is not None:
+                       # Netscape 2 through 4 and MSIE 3 implement a draft version
+                       # Later on, we need to serve a different mime-type as well.
+                       range = request_range
+               if_range = REQUEST.get_header('If-Range', None)
+               if range is not None:
+                       ranges = HTTPRangeSupport.parseRange(range)
+
+                       if if_range is not None:
+                               # Only send ranges if the data isn't modified, otherwise send
+                               # the whole object. Support both ETags and Last-Modified dates!
+                               if len(if_range) > 1 and if_range[:2] == 'ts':
+                                       # ETag:
+                                       if if_range != self.http__etag():
+                                               # Modified, so send a normal response. We delete
+                                               # the ranges, which causes us to skip to the 200
+                                               # response.
+                                               ranges = None
+                               else:
+                                       # Date
+                                       date = if_range.split( ';')[0]
+                                       try: mod_since=long(DateTime(date).timeTime())
+                                       except: mod_since=None
+                                       if mod_since is not None:
+                                               if self._p_mtime:
+                                                       last_mod = long(self._p_mtime)
+                                               else:
+                                                       last_mod = long(0)
+                                               if last_mod > mod_since:
+                                                       # Modified, so send a normal response. We delete
+                                                       # the ranges, which causes us to skip to the 200
+                                                       # response.
+                                                       ranges = None
+
+                       if ranges:
+                               # Search for satisfiable ranges.
+                               satisfiable = 0
+                               for start, end in ranges:
+                                       if start < self.size:
+                                               satisfiable = 1
+                                               break
+
+                               if not satisfiable:
+                                       RESPONSE.setHeader('Content-Range',
+                                               'bytes */%d' % self.size)
+                                       RESPONSE.setHeader('Accept-Ranges', 'bytes')
+                                       RESPONSE.setHeader('Last-Modified',
+                                               rfc1123_date(self._p_mtime))
+                                       RESPONSE.setHeader('Content-Type', self.content_type)
+                                       RESPONSE.setHeader('Content-Length', self.size)
+                                       RESPONSE.setStatus(416)
+                                       return True
+
+                               ranges = HTTPRangeSupport.expandRanges(ranges, self.size)
+
+                               if len(ranges) == 1:
+                                       # Easy case, set extra header and return partial set.
+                                       start, end = ranges[0]
+                                       size = end - start
+
+                                       RESPONSE.setHeader('Last-Modified',
+                                               rfc1123_date(self._p_mtime))
+                                       RESPONSE.setHeader('Content-Type', self.content_type)
+                                       RESPONSE.setHeader('Content-Length', size)
+                                       RESPONSE.setHeader('Accept-Ranges', 'bytes')
+                                       RESPONSE.setHeader('Content-Range',
+                                               'bytes %d-%d/%d' % (start, end - 1, self.size))
+                                       RESPONSE.setStatus(206) # Partial content
+
+                                       bf = self.open('r')
+                                       bf.seek(start)
+                                       RESPONSE.write(bf.read(size))
+                                       bf.close()
+                                       return True
+
+                               else:
+                                       boundary = choose_boundary()
+
+                                       # Calculate the content length
+                                       size = (8 + len(boundary) + # End marker length
+                                               len(ranges) * (                 # Constant lenght per set
+                                                       49 + len(boundary) + len(self.content_type) +
+                                                       len('%d' % self.size)))
+                                       for start, end in ranges:
+                                               # Variable length per set
+                                               size = (size + len('%d%d' % (start, end - 1)) +
+                                                       end - start)
+
+
+                                       # Some clients implement an earlier draft of the spec, they
+                                       # will only accept x-byteranges.
+                                       draftprefix = (request_range is not None) and 'x-' or ''
+
+                                       RESPONSE.setHeader('Content-Length', size)
+                                       RESPONSE.setHeader('Accept-Ranges', 'bytes')
+                                       RESPONSE.setHeader('Last-Modified',
+                                               rfc1123_date(self._p_mtime))
+                                       RESPONSE.setHeader('Content-Type',
+                                               'multipart/%sbyteranges; boundary=%s' % (
+                                                       draftprefix, boundary))
+                                       RESPONSE.setStatus(206) # Partial content
+
+                                       bf = self.open('r')
+
+                                       for start, end in ranges:
+                                               RESPONSE.write('\r\n--%s\r\n' % boundary)
+                                               RESPONSE.write('Content-Type: %s\r\n' %
+                                                       self.content_type)
+                                               RESPONSE.write(
+                                                       'Content-Range: bytes %d-%d/%d\r\n\r\n' % (
+                                                               start, end - 1, self.size))
+
+                                               
+                                               size = end - start
+                                               bf.seek(start)
+                                               RESPONSE.write(bf.read(size))
+                                       
+                                       bf.close()
+
+                                       RESPONSE.write('\r\n--%s--\r\n' % boundary)
+                                       return True
+
+       security.declareProtected(View, 'index_html')
+       def index_html(self, REQUEST, RESPONSE):
+               """
+               The default view of the contents of a File or Image.
+
+               Returns the contents of the file or image.      Also, sets the
+               Content-Type HTTP header to the objects content type.
+               """
+
+               if self._if_modified_since_request_handler(REQUEST, RESPONSE):
+                       # we were able to handle this by returning a 304
+                       # unfortunately, because the HTTP cache manager uses the cache
+                       # API, and because 304 responses are required to carry the Expires
+                       # header for HTTP/1.1, we need to call ZCacheable_set here.
+                       # This is nonsensical for caches other than the HTTP cache manager
+                       # unfortunately.
+                       self.ZCacheable_set(None)
+                       return ''
+
+               if self.precondition and hasattr(self, str(self.precondition)):
+                       # Grab whatever precondition was defined and then
+                       # execute it.  The precondition will raise an exception
+                       # if something violates its terms.
+                       c=getattr(self, str(self.precondition))
+                       if hasattr(c,'isDocTemp') and c.isDocTemp:
+                               c(REQUEST['PARENTS'][1],REQUEST)
+                       else:
+                               c()
+
+               if self._range_request_handler(REQUEST, RESPONSE):
+                       # we served a chunk of content in response to a range request.
+                       return ''
+
+               RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime))
+               RESPONSE.setHeader('Content-Type', self.content_type)
+               RESPONSE.setHeader('Content-Length', self.size)
+               RESPONSE.setHeader('Accept-Ranges', 'bytes')
+
+               if self.ZCacheable_isCachingEnabled():
+                       result = self.ZCacheable_get(default=None)
+                       if result is not None:
+                               # We will always get None from RAMCacheManager and HTTP
+                               # Accelerated Cache Manager but we will get
+                               # something implementing the IStreamIterator interface
+                               # from a "FileCacheManager"
+                               return result
+
+               self.ZCacheable_set(None)
+
+               bf = self.open('r')
+               chunk = bf.read(CHUNK_SIZE)
+               while chunk :
+                       RESPONSE.write(chunk)
+                       chunk = bf.read(CHUNK_SIZE)
+               bf.close()
+               return ''               
+
+       security.declareProtected(View, 'view_image_or_file')
+       def view_image_or_file(self, URL1):
+               """
+               The default view of the contents of the File or Image.
+               """
+               raise Redirect, URL1
+
+       security.declareProtected(View, 'PrincipiaSearchSource')
+       def PrincipiaSearchSource(self):
+               """ Allow file objects to be searched.
+               """
+               if self.content_type.startswith('text/'):
+                       bf = self.open('r')
+                       data = bf.read()
+                       bf.close()
+                       return data
+               return ''
+
+       security.declarePrivate('update_data')
+       def update_data(self, file, content_type=None):
+               if isinstance(file, unicode):
+                       raise TypeError('Data can only be str or file-like.      '
+                                                       'Unicode objects are expressly forbidden.')
+               elif isinstance(file, str) :
+                       sio = StringIO()
+                       sio.write(file)
+                       sio.seek(0)
+                       file = sio
+
+               if content_type is not None: self.content_type=content_type
+               self.save(file)
+               self.ZCacheable_invalidate()
+               self.ZCacheable_set(None)
+               self.http__refreshEtag()
+
+       security.declareProtected(change_images_and_files, 'manage_edit')
+       def manage_edit(self, title, content_type, precondition='',
+                                       filedata=None, REQUEST=None):
+               """
+               Changes the title and content type attributes of the File or Image.
+               """
+               if self.wl_isLocked():
+                       raise ResourceLockedError, "File is locked via WebDAV"
+
+               self.title=str(title)
+               self.content_type=str(content_type)
+               if precondition: self.precondition=str(precondition)
+               elif self.precondition: del self.precondition
+               if filedata is not None:
+                       self.update_data(filedata, content_type)
+               else:
+                       self.ZCacheable_invalidate()
+               if REQUEST:
+                       message="Saved changes."
+                       return self.manage_main(self,REQUEST,manage_tabs_message=message)
+
+       security.declareProtected(change_images_and_files, 'manage_upload')
+       def manage_upload(self,file='',REQUEST=None):
+               """
+               Replaces the current contents of the File or Image object with file.
+
+               The file or images contents are replaced with the contents of 'file'.
+               """
+               if self.wl_isLocked():
+                       raise ResourceLockedError, "File is locked via WebDAV"
+
+               content_type=self._get_content_type(file, self.__name__,
+                                                                                       'application/octet-stream')
+               self.update_data(file, content_type)
+
+               if REQUEST:
+                       message="Saved changes."
+                       return self.manage_main(self,REQUEST,manage_tabs_message=message)
+
+       def _get_content_type(self, file, id, content_type=None):
+               headers=getattr(file, 'headers', None)
+               if headers and headers.has_key('content-type'):
+                       content_type=headers['content-type']
+               else:
+                       name = getattr(file, 'filename', self.uploaded_filename) or id
+                       content_type, enc=guess_content_type(name, '', content_type)
+               return content_type
+
+       security.declareProtected(delete_objects, 'DELETE')
+
+       security.declareProtected(change_images_and_files, 'PUT')
+       def PUT(self, REQUEST, RESPONSE):
+               """Handle HTTP PUT requests"""
+               self.dav__init(REQUEST, RESPONSE)
+               self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1)
+               type=REQUEST.get_header('content-type', None)
+
+               file=REQUEST['BODYFILE']
+
+               content_type = self._get_content_type(file, self.__name__,
+                                                                                         type or self.content_type)
+               self.update_data(file, content_type)
+
+               RESPONSE.setStatus(204)
+               return RESPONSE
+
+       security.declareProtected(View, 'get_size')
+       def get_size(self):
+               """Get the size of a file or image.
+
+               Returns the size of the file or image.
+               """
+               size=self.size
+               if size is None :
+                       bf = self.open('r')
+                       bf.seek(0,2)
+                       self.size = size = bf.tell()
+                       bf.close()
+               return size
+
+       # deprecated; use get_size!
+       getSize=get_size
+
+       security.declareProtected(View, 'getContentType')
+       def getContentType(self):
+               """Get the content type of a file or image.
+
+               Returns the content type (MIME type) of a file or image.
+               """
+               return self.content_type
+
+
+       def __str__(self): return str(self.data)
+       def __len__(self): return 1
+
+       security.declareProtected(ftp_access, 'manage_FTPstat')
+       security.declareProtected(ftp_access, 'manage_FTPlist')
+
+       security.declareProtected(ftp_access, 'manage_FTPget')
+       def manage_FTPget(self):
+               """Return body for ftp."""
+               RESPONSE = self.REQUEST.RESPONSE
+
+               if self.ZCacheable_isCachingEnabled():
+                       result = self.ZCacheable_get(default=None)
+                       if result is not None:
+                               # We will always get None from RAMCacheManager but we will get
+                               # something implementing the IStreamIterator interface
+                               # from FileCacheManager.
+                               # the content-length is required here by HTTPResponse, even
+                               # though FTP doesn't use it.
+                               RESPONSE.setHeader('Content-Length', self.size)
+                               return result
+
+               bf = self.open('r')
+               data = bf.read()
+               bf.close()
+               RESPONSE.setBase(None)
+               return data
+
+manage_addImageForm=DTMLFile('dtml/imageAdd',globals(),
+                                                        Kind='Image',kind='image')
+def manage_addImage(self, id, file, title='', precondition='', content_type='',
+                                       REQUEST=None):
+       """
+       Add a new Image object.
+
+       Creates a new Image object 'id' with the contents of 'file'.
+       """
+
+       id=str(id)
+       title=str(title)
+       content_type=str(content_type)
+       precondition=str(precondition)
+
+       id, title = cookId(id, title, file)
+
+       self=self.this()
+       self._setObject(id, Image(id,title,file,content_type, precondition))
+
+       if REQUEST is not None:
+               try:    url=self.DestinationURL()
+               except: url=REQUEST['URL1']
+               REQUEST.RESPONSE.redirect('%s/manage_main' % url)
+       return id
+
+
+def getImageInfo(file):
+       height = -1
+       width = -1
+       content_type = ''
+
+       # handle GIFs
+       data = file.read(24)
+       size = len(data)
+       if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):
+               # Check to see if content_type is correct
+               content_type = 'image/gif'
+               w, h = struct.unpack("<HH", data[6:10])
+               width = int(w)
+               height = int(h)
+
+       # See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
+       # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
+       # and finally the 4-byte width, height
+       elif ((size >= 24) and (data[:8] == '\211PNG\r\n\032\n')
+                 and (data[12:16] == 'IHDR')):
+               content_type = 'image/png'
+               w, h = struct.unpack(">LL", data[16:24])
+               width = int(w)
+               height = int(h)
+
+       # Maybe this is for an older PNG version.
+       elif (size >= 16) and (data[:8] == '\211PNG\r\n\032\n'):
+               # Check to see if we have the right content type
+               content_type = 'image/png'
+               w, h = struct.unpack(">LL", data[8:16])
+               width = int(w)
+               height = int(h)
+
+       # handle JPEGs
+       elif (size >= 2) and (data[:2] == '\377\330'):
+               content_type = 'image/jpeg'
+               jpeg = file
+               jpeg.seek(0)
+               jpeg.read(2)
+               b = jpeg.read(1)
+               try:
+                       while (b and ord(b) != 0xDA):
+                               while (ord(b) != 0xFF): b = jpeg.read(1)
+                               while (ord(b) == 0xFF): b = jpeg.read(1)
+                               if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
+                                       jpeg.read(3)
+                                       h, w = struct.unpack(">HH", jpeg.read(4))
+                                       break
+                               else:
+                                       jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2)
+                               b = jpeg.read(1)
+                       width = int(w)
+                       height = int(h)
+               except: pass
+
+       return content_type, width, height
+
+
+class Image(File):
+       """Image objects can be GIF, PNG or JPEG and have the same methods
+       as File objects.  Images also have a string representation that
+       renders an HTML 'IMG' tag.
+       """
+       __implements__ = (WriteLockInterface,)
+       meta_type='Blob Image'
+
+       security = ClassSecurityInfo()
+       security.declareObjectProtected(View)
+
+       alt=''
+       height=''
+       width=''
+
+       # FIXME: Redundant, already in base class
+       security.declareProtected(change_images_and_files, 'manage_edit')
+       security.declareProtected(change_images_and_files, 'manage_upload')
+       security.declareProtected(change_images_and_files, 'PUT')
+       security.declareProtected(View, 'index_html')
+       security.declareProtected(View, 'get_size')
+       security.declareProtected(View, 'getContentType')
+       security.declareProtected(ftp_access, 'manage_FTPstat')
+       security.declareProtected(ftp_access, 'manage_FTPlist')
+       security.declareProtected(ftp_access, 'manage_FTPget')
+       security.declareProtected(delete_objects, 'DELETE')
+
+       _properties=({'id':'title', 'type': 'string'},
+                                {'id':'alt', 'type':'string'},
+                                {'id':'content_type', 'type':'string','mode':'w'},
+                                {'id':'height', 'type':'string'},
+                                {'id':'width', 'type':'string'},
+                                )
+
+       manage_options=(
+               ({'label':'Edit', 'action':'manage_main',
+                'help':('OFSP','Image_Edit.stx')},
+                {'label':'View', 'action':'view_image_or_file',
+                'help':('OFSP','Image_View.stx')},)
+               + PropertyManager.manage_options
+               + RoleManager.manage_options
+               + Item_w__name__.manage_options
+               + Cacheable.manage_options
+               )
+
+       manage_editForm  =DTMLFile('dtml/imageEdit',globals(),
+                                                          Kind='Image',kind='image')
+       manage_editForm._setName('manage_editForm')
+
+       security.declareProtected(View, 'view_image_or_file')
+       view_image_or_file =DTMLFile('dtml/imageView',globals())
+
+       security.declareProtected(view_management_screens, 'manage')
+       security.declareProtected(view_management_screens, 'manage_main')
+       manage=manage_main=manage_editForm
+       manage_uploadForm=manage_editForm
+       
+       security.declarePrivate('update_data')
+       def update_data(self, file, content_type=None):
+               super(Image, self).update_data(file, content_type)
+               self.updateFormat(size=self.size, content_type=content_type)
+               
+       security.declarePrivate('updateFormat')
+       def updateFormat(self, size=None, dimensions=None, content_type=None):
+               self.updateSize(size=size)
+
+               if dimensions is None or content_type is None :
+                       bf = self.open('r')
+                       ct, width, height = getImageInfo(bf)
+                       bf.close()
+                       if ct:
+                               content_type = ct
+                       if width >= 0 and height >= 0:
+                               self.width = width
+                               self.height = height
+
+                       # Now we should have the correct content type, or still None
+                       if content_type is not None: self.content_type = content_type
+               else :
+                       self.width, self.height = dimensions
+                       self.content_type = content_type
+
+       def __str__(self):
+               return self.tag()
+
+       security.declareProtected(View, 'tag')
+       def tag(self, height=None, width=None, alt=None,
+                       scale=0, xscale=0, yscale=0, css_class=None, title=None, **args):
+               """
+               Generate an HTML IMG tag for this image, with customization.
+               Arguments to self.tag() can be any valid attributes of an IMG tag.
+               'src' will always be an absolute pathname, to prevent redundant
+               downloading of images. Defaults are applied intelligently for
+               'height', 'width', and 'alt'. If specified, the 'scale', 'xscale',
+               and 'yscale' keyword arguments will be used to automatically adjust
+               the output height and width values of the image tag.
+
+               Since 'class' is a Python reserved word, it cannot be passed in
+               directly in keyword arguments which is a problem if you are
+               trying to use 'tag()' to include a CSS class. The tag() method
+               will accept a 'css_class' argument that will be converted to
+               'class' in the output tag to work around this.
+               """
+               if height is None: height=self.height
+               if width is None:  width=self.width
+
+               # Auto-scaling support
+               xdelta = xscale or scale
+               ydelta = yscale or scale
+
+               if xdelta and width:
+                       width =  str(int(round(int(width) * xdelta)))
+               if ydelta and height:
+                       height = str(int(round(int(height) * ydelta)))
+
+               result='<img src="%s"' % (self.absolute_url())
+
+               if alt is None:
+                       alt=getattr(self, 'alt', '')
+               result = '%s alt="%s"' % (result, escape(alt, 1))
+
+               if title is None:
+                       title=getattr(self, 'title', '')
+               result = '%s title="%s"' % (result, escape(title, 1))
+
+               if height:
+                       result = '%s height="%s"' % (result, height)
+
+               if width:
+                       result = '%s width="%s"' % (result, width)
+
+               # Omitting 'border' attribute (Collector #1557)
+#               if not 'border' in [ x.lower() for x in  args.keys()]:
+#                       result = '%s border="0"' % result
+
+               if css_class is not None:
+                       result = '%s class="%s"' % (result, css_class)
+
+               for key in args.keys():
+                       value = args.get(key)
+                       if value:
+                               result = '%s %s="%s"' % (result, key, value)
+
+               return '%s />' % result
+
+
+def cookId(id, title, file):
+       if not id and hasattr(file,'filename'):
+               filename=file.filename
+               title=title or filename
+               id=filename[max(filename.rfind('/'),
+                                               filename.rfind('\\'),
+                                               filename.rfind(':'),
+                                               )+1:]
+       return id, title
+
+#class Pdata(Persistent, Implicit):
+#      # Wrapper for possibly large data
+#
+#      next=None
+#
+#      def __init__(self, data):
+#              self.data=data
+#
+#      def __getslice__(self, i, j):
+#              return self.data[i:j]
+#
+#      def __len__(self):
+#              data = str(self)
+#              return len(data)
+#
+#      def __str__(self):
+#              next=self.next
+#              if next is None: return self.data
+#
+#              r=[self.data]
+#              while next is not None:
+#                      self=next
+#                      r.append(self.data)
+#                      next=self.next
+#
+#              return ''.join(r)
diff --git a/cache.py b/cache.py
new file mode 100755 (executable)
index 0000000..ad2a5ac
--- /dev/null
+++ b/cache.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright (C) 2008  Benoît PIN <benoit.pin@ensmp.fr>                              #
+#                                                                                     #
+#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+""" Memoization utils
+
+$Id: cache.py 400 2008-07-11 10:31:26Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/cache.py $
+"""
+
+import inspect
+from BTrees.OOBTree import OOBTree
+
+def memoizedmethod(*indexes, **options) :
+       """ Used as decorator, this function stores result
+               of method m inside self._methodResultsCache or
+               self._v__methodResultsCache if volatile.
+               This decorator may be used inside a class which provides
+               a mapping object named _methodResultsCache and / or
+               _v__methodResultsCache.
+               
+               example :
+               
+               1 - simple metdhod memoization
+               
+               @memoizedmethod()
+               def methodWithNoParameters(self): pass
+               
+               2 - indexed memoisation:
+               Parameters names are passed to memoizedmethod are
+               evaluated to construct an indexed cache.
+               Names must be a subset of the memoized method signature.
+               
+               @memoizedmethod('arg1', 'arg2')
+               def methodWithParameters(self, arg1, arg2=None): pass
+       """
+       volatile = options.get('volatile', False)
+       cacheVarName = '_methodResultsCache'
+       if volatile==True :
+               cacheVarName = '_v_%s' % cacheVarName
+       
+       def makeMemoizedMethod(m) :
+               methodname = m.__name__
+               
+               if not indexes :
+                       def memoizedMethod(self) :
+                               if not hasattr(self, cacheVarName) :
+                                       setattr(self, cacheVarName, OOBTree())
+                               cache = getattr(self, cacheVarName)
+                               if cache.has_key(methodname) :
+                                       return cache[methodname]
+                               else :
+                                       res = m(self)
+                                       cache[methodname] = res
+                                       return res
+
+                       memoizedMethod.__name__ = methodname
+                       memoizedMethod.__doc__ = m.__doc__
+                       return memoizedMethod
+               
+               else :
+                       args, varargs, varkw, defaults = inspect.getargspec(m)
+                       args = list(args)
+                       if defaults is None :
+                               defaults = []
+                       mandatoryargs = args[1:-len(defaults)]
+                       optargs = args[-len(defaults):]
+                       defaultValues = dict(zip([name for name in args[-len(defaults):]], [val for val in defaults]))
+                       
+                       indexPositions = []
+                       for index in indexes :
+                               try :
+                                       indexPositions.append((index, args.index(index)))
+                               except ValueError :
+                                       raise ValueError("%r argument is not in signature of %r" % (index, methodname))
+                       
+                       if indexPositions :
+                               indexPositions.sort(lambda a, b : cmp(a[1], b[1]))
+                       
+                       indexPositions = tuple(indexPositions)
+                               
+                       
+                       def memoizedMethod(self, *args, **kw) :
+                               # test if m if called by ZPublished
+                               if len(args) < len(mandatoryargs) and hasattr(self, 'REQUEST') :
+                                       assert not kw
+                                       args = list(args)
+                                       get = lambda name : self.REQUEST[name]
+                                       for name in mandatoryargs :
+                                               try :
+                                                       args.append(get(name))
+                                               except KeyError :
+                                                       exactOrAtLeast = defaults and 'exactly' or 'at least'
+                                                       raise TypeError('%(methodname)s takes %(exactOrAtLeast)s %(mandatoryArgsLength)d argument (%(givenArgsLength)s given)' % \
+                                                                                       { 'methodname': methodname
+                                                                                       , 'exactOrAtLeast': exactOrAtLeast
+                                                                                       , 'mandatoryArgsLength': len(mandatoryargs)
+                                                                                       , 'givenArgsLength': len(args)})
+                                       
+                                       for name in optargs :
+                                               get = self.REQUEST.get
+                                               args.append(get(name, defaultValues[name]))
+
+                                       args = tuple(args)
+                               
+                               if not hasattr(self, cacheVarName) :
+                                       setattr(self, cacheVarName, OOBTree())
+                               cache = getattr(self, cacheVarName)
+                               if not cache.has_key(methodname) :
+                                       cache[methodname] = OOBTree()
+                               
+                               cache = cache[methodname]
+                               index = aggregateIndex(indexPositions, args)
+                               
+                               if cache.has_key(index) :
+                                       return cache[index]
+                               else :
+                                       res = m(self, *args, **kw)
+                                       cache[index] = res
+                                       return res
+
+                       memoizedMethod.__name__ = methodname
+                       memoizedMethod.__doc__ = m.__doc__
+                       return memoizedMethod
+
+       return makeMemoizedMethod
+
+def aggregateIndex(indexPositions, args):
+       '''
+       Returns the index to be used when looking for or inserting
+       a cache entry.
+       view_name is a string.
+       local_keys is a mapping or None.
+       '''
+       
+       agg_index = []
+       
+       for name, pos in indexPositions :
+               val = args[pos-1]
+               agg_index.append((name, str(val)))
+       
+       return tuple(agg_index) 
diff --git a/dependencies.txt b/dependencies.txt
new file mode 100755 (executable)
index 0000000..ede10ef
--- /dev/null
@@ -0,0 +1,2 @@
+PIL - Python Imaging Library - 1.1.4 or later
+http://www.pythonware.com/products/pil/
diff --git a/dtml/File_icon.gif b/dtml/File_icon.gif
new file mode 100644 (file)
index 0000000..f0eb5bf
Binary files /dev/null and b/dtml/File_icon.gif differ
diff --git a/dtml/Image_icon.gif b/dtml/Image_icon.gif
new file mode 100644 (file)
index 0000000..bf11d02
Binary files /dev/null and b/dtml/Image_icon.gif differ
diff --git a/dtml/addPhotoForm.dtml b/dtml/addPhotoForm.dtml
new file mode 100755 (executable)
index 0000000..ecdfbdf
--- /dev/null
@@ -0,0 +1,58 @@
+<dtml-var manage_page_header>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Add Photo')">
+
+<p class="form-help">
+Select a file to upload from your local computer by clicking the
+<em>Browse</em> button.
+</p>
+
+<form action="addPhoto" method="post"
+      enctype="multipart/form-data">
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    File
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="file" name="file" size="25" value="" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    </td>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" 
+     value=" Add " /> 
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+<dtml-var manage_page_footer>
+
diff --git a/dtml/fileEdit.dtml b/dtml/fileEdit.dtml
new file mode 100644 (file)
index 0000000..feb7e7e
--- /dev/null
@@ -0,0 +1,142 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+
+<p class="form-help">
+You can update the data for this file object using the form below. 
+Select a data file from your local computer by clicking the <em>browse</em> 
+button and click <em>upload</em> to update the contents of the
+file. You may also edit the file content directly if the content is a 
+text type and small enough to be edited in a text area.
+</p>
+
+<form action="&dtml-URL1;" method="post" enctype="multipart/form-data">
+<table cellpadding="2" cellspacing="0" width="100%" border="0">
+<tr>
+  <td align="left" valign="top">
+  <div class="form-optional">
+  Title
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <input type="text" name="title" size="40" value="<dtml-if 
+   title>&dtml-title;</dtml-if>">
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-label">
+  Content Type
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <input type="text" name="content_type:required" size="40" value="<dtml-if 
+   content_type>&dtml-content_type;</dtml-if>">
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-optional">
+  Precondition
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <input type="text" name="precondition" size="40" value="<dtml-if 
+   precondition>&dtml-precondition;</dtml-if>">
+  </td>
+</tr>
+
+<dtml-let ct=getContentType>
+  <dtml-if "(ct.startswith('text') or ct.endswith('javascript')) 
+            and this().get_size() < 65536">
+  <tr>
+    <td align="left" valign="top" colspan="2">
+    <div style="width: 100%;">
+    <dtml-let cols="REQUEST.get('dtpref_cols', '100%')"
+              rows="REQUEST.get('dtpref_rows', '20')">
+    <dtml-if "cols[-1]=='%'">
+    <textarea name="filedata:text" wrap="off" style="width: &dtml-cols;;"
+    <dtml-else>
+    <textarea name="filedata:text" wrap="off" cols="&dtml-cols;"
+    </dtml-if>
+              rows="&dtml-rows;"><dtml-var __str__ html_quote></textarea>
+    </dtml-let>
+    </div>
+    </td>
+  </tr>
+  <dtml-else>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Last Modified
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <div class="form-text">
+    <dtml-var bobobase_modification_time fmt="%Y-%m-%d %H:%M">
+    </div>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    File Size
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <div class="form-text">
+    <dtml-var size thousands_commas> bytes
+    </div>
+    </td>
+  </tr>
+  </dtml-if>
+</dtml-let>
+
+<tr>
+  <td></td>
+  <td align="left" valign="top">
+  <div class="form-element">
+  <dtml-if wl_isLocked>
+   <em>Locked by WebDAV</em>
+  <dtml-else>
+   <input class="form-element" type="submit" name="manage_edit:method" 
+    value="Save Changes">
+  </dtml-if>
+  </div>
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <br />
+  <div class="form-label">
+  File Data
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <br />
+  <input type="file" name="file" size="25" />
+  </td>
+</tr>
+
+<tr>
+  <td></td>
+  <td align="left" valign="top">
+  <div class="form-element">
+  <dtml-if wl_isLocked>
+   <em>Locked by WebDAV</em>
+  <dtml-else>
+   <input class="form-element" type="submit" name="manage_upload:method" 
+    value="Upload">
+  </dtml-if>
+  </div>
+  </td>
+</tr>
+
+</table>
+</form>
+
+<dtml-var manage_page_footer>
+
diff --git a/dtml/imageAdd.dtml b/dtml/imageAdd.dtml
new file mode 100644 (file)
index 0000000..a773d54
--- /dev/null
@@ -0,0 +1,61 @@
+<dtml-var manage_page_header>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Add %s' % Kind,
+           help_product='OFSP',
+           help_topic='File_Add.stx'
+          )">
+
+<p class="form-help">
+Select a file to upload from your local computer by clicking the
+<em>Browse</em> button.
+</p>
+
+<form action="manage_add&dtml-Kind;" method="post"
+      enctype="multipart/form-data">
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    File
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="file" name="file" size="25" value="" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    </td>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" 
+     value=" Add " /> 
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+<dtml-var manage_page_footer>
+
diff --git a/dtml/imageEdit.dtml b/dtml/imageEdit.dtml
new file mode 100644 (file)
index 0000000..6268783
--- /dev/null
@@ -0,0 +1,131 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+
+<p class="form-help">
+You can update the data for this &dtml-kind; using the form below. 
+Select a data file from your local computer by clicking the <em>browse</em> 
+button and click <em>upload</em> to update the contents of the &dtml-kind;.
+</p>
+
+<form action="&dtml-URL1;/manage_edit" method="post"
+  enctype="multipart/form-data">
+<table cellpadding="2" cellspacing="0" width="100%" border="0">
+<tr>
+  <td align="left" valign="top">
+  <div class="form-optional">
+  Title
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <input type="text" name="title" size="40" value="<dtml-if 
+   title>&dtml-title;</dtml-if>">
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-label">
+  Content Type
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <input type="text" name="content_type:required" size="40" value="<dtml-if 
+   content_type>&dtml-content_type;</dtml-if>">
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-label">
+  Preview
+  </div>
+  </td>
+  <td align="left" valign="top">
+    <dtml-if "_.same_type(height, 1) and height and height > 250">
+    <dtml-var "tag(scale=250.0 / height)">
+    <dtml-elif "_.same_type(height, 's') and height and _.int(height) > 250">
+    <dtml-var "tag(scale=250.0 / _.int(height))">
+    <dtml-else>
+    <dtml-var tag>
+    </dtml-if>
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-label">
+  Last Modified
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <div class="form-text">
+  <dtml-var bobobase_modification_time fmt="%Y-%m-%d %H:%M">
+  </div>
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-label">
+  File Size
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <div class="form-text">
+  <dtml-var size thousands_commas> bytes
+  </div>
+  </td>
+</tr>
+
+<tr>
+  <td></td>
+  <td align="left" valign="top">
+  <div class="form-element">
+  <dtml-if wl_isLocked>
+   <em>Locked by WebDAV</em>
+  <dtml-else>
+   <input class="form-element" type="submit" name="submit"
+    value="Save Changes">
+  </dtml-if>
+  </div>
+  </td>
+</tr>
+
+</table>
+</form>
+
+<form action="&dtml-URL1;/manage_upload" method="post"
+  enctype="multipart/form-data">
+<table cellpadding="2" cellspacing="0" width="100%" border="0">
+<tr>
+  <td align="left" valign="top">
+  <br />
+  <div class="form-label">
+  File Data
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <br />
+  <input type="file" name="file" size="25" />
+  </td>
+</tr>
+
+<tr>
+  <td></td>
+  <td align="left" valign="top">
+  <div class="form-element">
+  <dtml-if wl_isLocked>
+   <em>Locked by WebDAV</em>
+  <dtml-else>
+   <input class="form-element" type="submit" name="submit"
+    value="Upload">
+  </dtml-if>
+  </div>
+  </td>
+</tr>
+
+</table>
+</form>
+
+<dtml-var manage_page_footer>
diff --git a/dtml/imageView.dtml b/dtml/imageView.dtml
new file mode 100644 (file)
index 0000000..5ec12d3
--- /dev/null
@@ -0,0 +1,9 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<p>
+ <dtml-var tag>
+</p>
+
+<dtml-var manage_page_footer>
+
diff --git a/dtml/photoEdit.dtml b/dtml/photoEdit.dtml
new file mode 100755 (executable)
index 0000000..e15a679
--- /dev/null
@@ -0,0 +1,126 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+
+<p class="form-help">
+You can update the data for this <dtml-var kind> using the form below. 
+Select a data file from your local computer by clicking the <em>browse</em> 
+button and click <em>upload</em> to update the contents of the <dtml-var 
+kind>.
+</p>
+
+<form action="<dtml-var URL1>/manage_edit" method="post"
+  enctype="multipart/form-data">
+<table cellpadding="2" cellspacing="0" width="100%" border="0">
+<tr>
+  <td align="left" valign="top">
+  <div class="form-optional">
+  Title
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <input type="text" name="title" size="40" value="<dtml-if 
+   title><dtml-var title html_quote></dtml-if>">
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-label">
+  Content Type
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <input type="text" name="content_type:required" size="40" value="<dtml-if 
+   content_type><dtml-var content_type html_quote></dtml-if>">
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-label">
+  Preview
+  </div>
+  </td>
+  <td align="left" valign="top">
+    <img src="getThumbnail" alt="preview of <dtml-var title_or_id>" />
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-label">
+  Last Modified
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <div class="form-text">
+  <dtml-var bobobase_modification_time fmt="%Y-%m-%d %H:%M">
+  </div>
+  </td>
+</tr>
+
+<tr>
+  <td align="left" valign="top">
+  <div class="form-label">
+  File Size
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <div class="form-text">
+  <dtml-var size thousands_commas> bytes
+  </div>
+  </td>
+</tr>
+
+<tr>
+  <td></td>
+  <td align="left" valign="top">
+  <div class="form-element">
+  <dtml-if wl_isLocked>
+   <em>Locked by WebDAV</em>
+  <dtml-else>
+   <input class="form-element" type="submit" name="submit"
+    value="Save Changes">
+  </dtml-if>
+  </div>
+  </td>
+</tr>
+
+</table>
+</form>
+
+<form action="<dtml-var URL1>/manage_upload" method="post"
+  enctype="multipart/form-data">
+<table cellpadding="2" cellspacing="0" width="100%" border="0">
+<tr>
+  <td align="left" valign="top">
+  <br />
+  <div class="form-label">
+  File Data
+  </div>
+  </td>
+  <td align="left" valign="top">
+  <br />
+  <input type="file" name="file" size="25" />
+  </td>
+</tr>
+
+<tr>
+  <td></td>
+  <td align="left" valign="top">
+  <div class="form-element">
+  <dtml-if wl_isLocked>
+   <em>Locked by WebDAV</em>
+  <dtml-else>
+   <input class="form-element" type="submit" name="submit"
+    value="Upload">
+  </dtml-if>
+  </div>
+  </td>
+</tr>
+
+</table>
+</form>
+
+<dtml-var manage_page_footer>
diff --git a/dtml/photoView.dtml b/dtml/photoView.dtml
new file mode 100755 (executable)
index 0000000..8f70420
--- /dev/null
@@ -0,0 +1,9 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<p>
+  <img src="getJpegImage">
+</p>
+
+<dtml-var manage_page_footer>
+
diff --git a/dtml/photo_icon.gif b/dtml/photo_icon.gif
new file mode 100644 (file)
index 0000000..bde5168
Binary files /dev/null and b/dtml/photo_icon.gif differ
diff --git a/dtml/testMenu.dtml b/dtml/testMenu.dtml
new file mode 100755 (executable)
index 0000000..7050197
--- /dev/null
@@ -0,0 +1,93 @@
+<dtml-var manage_page_header>
+
+<style type="text/css">
+<!--
+
+td {
+  font-family: Verdana, Helvetica, sans-serif; 
+  font-size: 10pt; 
+  color: #333333;
+}
+
+-->
+</style>
+
+<table width="100%" cellspacing="0" border="0">
+<tr bgcolor="#000000">
+  <td valign="top" nowrap>
+  <dtml-if icon>
+  <a href="manage_workspace" target="manage_main"><img 
+   src="&dtml-BASEPATH1;/&dtml-icon;" border="0"
+   title="Click to open this item" alt="&dtml-meta_type;" /></a> 
+  </dtml-if>
+  <strong>
+  <a href="manage_workspace" target="manage_main">
+  <font color="#ffffff">
+  <dtml-if expr="URLPATH1==BASEPATH1">
+  Root Folder
+  <dtml-else>
+  <dtml-var id>
+  </dtml-if>
+  </font>
+  </a>
+  </strong>
+  </td>
+</tr>
+</table>
+<dtml-tree cmf nowrap=1>
+<dtml-if icon>
+<a href="<dtml-var tree-item-url fmt=url-quote>/manage_workspace" 
+ target="manage_main"><img src="&dtml-BASEPATH1;/&dtml-icon;" border="0"
+ title="Click to open this item" alt="&dtml-meta_type;" /></a> 
+</dtml-if>
+<a href="<dtml-var tree-item-url fmt=url-quote>/manage_workspace" 
+ target="manage_main">&dtml-id;</a>
+</dtml-tree>
+<table cellspacing="0">
+<tr>
+  <td width="16"></td>
+  <td valign="top" nowrap>
+  <strong>
+  <a href="manage_copyright" target="manage_main">
+  &copy; Zope Corporation
+  </a>
+  </strong>
+  </td>
+</tr>
+<tr>
+  <td width="16"></td>
+  <td valign="top" nowrap>
+  <strong>
+  <a href="manage_menu">Refresh</a>
+  </strong>
+  </td>
+</tr>
+</table>
+
+<dtml-unless expr="REQUEST.get('zmi_top_frame', '1')">
+<table width="100%" bgcolor="#6699cc">
+<tr>
+<td valign="top" align="center">
+<div class="form-element">
+<form action="<dtml-var BASEPATH1>/" method="POST" target="manage_main">
+<span class="std-text">Logged in as <strong><dtml-var 
+ AUTHENTICATED_USER></strong></span> &nbsp;&nbsp;
+<br />
+<select class="form-element" name=":action" onChange="window.parent.manage_main.location.href='&dtml-BASEPATH1;/'+this.options[this.selectedIndex].value">
+<option value="zope_quick_start">Zope Quick Start</option>
+<dtml-if "AUTHENTICATED_USER.getUserName() != 'Anonymous User'">
+<option value="manage_zmi_prefs">Set Preferences</option>
+<option value="manage_zmi_logout">Logout</option>
+</dtml-if>
+</select> 
+<input class="form-element" type="submit" name="submit" value=" Go " />
+&nbsp;
+</form>
+</div>
+</td>
+</tr>
+</table>
+</dtml-unless>
+
+<dtml-var manage_page_footer>
+
diff --git a/exif.py b/exif.py
new file mode 100755 (executable)
index 0000000..2050dbe
--- /dev/null
+++ b/exif.py
@@ -0,0 +1,374 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright © 2008  Benoît PIN <benoit.pin@ensmp.fr>                                #
+#                                                                                     #
+#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+""" Exif version 2.2 read/write module.
+
+$Id: exif.py 360 2008-02-21 09:17:32Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/exif.py $
+"""
+
+TYPES_SIZES = {
+         1: 1          # BYTE An 8-bit unsigned integer.,
+       , 2: 1          # ASCII An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.,
+       , 3: 2          # SHORT A 16-bit (2-byte) unsigned integer,
+       , 4: 4          # LONG A 32-bit (4-byte) unsigned integer,
+       , 5: 8          # RATIONAL Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator.,
+       , 7: 1          # UNDEFINED An 8-bit byte that can take any value depending on the field definition,
+       , 9: 4          # SLONG A 32-bit (4-byte) signed integer (2's complement notation),
+       , 10 : 8        # SRATIONAL Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
+}
+
+# tags for parsing metadata
+Exif_IFD_POINTER = 0x8769
+GPS_INFO_IFD_POINTER = 0x8825
+INTEROPERABILITY_IFD_POINTER = 0xA005
+
+# tags to get thumbnail
+COMPRESSION_SCHEME = 0x103
+COMPRESSION_SCHEME_TYPES = {1:'image/bmp', 6:'image/jpeg'}
+OFFSET_TO_JPEG_SOI = 0x201
+BYTES_OF_JPEG_DATA = 0x202
+STRIPOFFSETS = 0x111
+STRIPBYTECOUNTS = 0x117
+
+# constants for writing
+INTEROPERABILITY_FIELD_LENGTH = 12
+POINTER_TAGS = { Exif_IFD_POINTER:True
+                          , GPS_INFO_IFD_POINTER:True
+                          , INTEROPERABILITY_IFD_POINTER:True}
+
+
+class Exif(dict) :
+       
+       def __init__(self, f) :
+               # File Headers are 8 bytes as defined in the TIFF standard.
+               self.f = f
+               
+               byteOrder = f.read(2)
+               self.byteOrder = byteOrder
+               
+               if byteOrder == 'MM' :
+                       r16 = self.r16 = lambda:ib16(f.read(2))
+                       r32 = self.r32 = lambda:ib32(f.read(4))
+               elif byteOrder == 'II' :
+                       r16 = self.r16 = lambda:il16(f.read(2))
+                       r32 = self.r32 = lambda:il32(f.read(4))
+               else :
+                       raise ValueError, "Unkwnown byte order: %r" % byteOrder
+               
+               assert r16() == 0x002A, "Incorrect exif header"
+               
+               self.tagReaders = {
+                         1:  lambda c : [ord(f.read(1)) for i in xrange(c)]
+                       , 2:  lambda c : f.read(c)
+                       , 3:  lambda c : [r16() for i in xrange(c)]
+                       , 4:  lambda c : [r32() for i in xrange(c)]
+                       , 5:  lambda c : [(r32(), r32()) for i in xrange(c)]
+                       , 7:  lambda c : f.read(c)
+                       , 9:  lambda c : [r32() for i in xrange(c)]
+                       , 10: lambda c : [(r32(), r32()) for i in xrange(c)]
+               }
+               
+               self.tagInfos = {}
+               self.mergedTagInfos = {}
+               self.gpsTagInfos = {}
+               
+               ifd0Offset = r32()
+               
+               ifd1Offset = self._loadTagsInfo(ifd0Offset, 'IFD0')
+               others = [(lambda:self[Exif_IFD_POINTER], 'Exif'),
+                                 (lambda:self.get(GPS_INFO_IFD_POINTER), 'GPS'),
+                                 (lambda:self.get(INTEROPERABILITY_IFD_POINTER), 'Interoperability'),
+                                 (lambda:ifd1Offset, 'IFD1')]
+               
+               self.ifdnames = ['IFD0']
+               
+               for startfunc, ifdname in others :
+                       start = startfunc()
+                       if start :
+                               ret = self._loadTagsInfo(start, ifdname)
+                               assert ret == 0
+                               self.ifdnames.append(ifdname)
+               
+               
+       def _loadTagsInfo(self, start, ifdname) :
+               r16, r32 = self.r16, self.r32
+               
+               self.f.seek(start)
+               
+               numberOfFields = r16()
+               ifdInfos = self.tagInfos[ifdname] = {}
+               
+               for i in xrange(numberOfFields) :
+                       #  12 bytes of the field Interoperability
+                       tag = r16()
+                       typ = r16()
+                       count = r32()
+
+                       ts = TYPES_SIZES[typ]
+                       size = ts * count
+                       
+                       # In cases where the value fits in 4 bytes,
+                       # the value itself is recorded.
+                       # If the value is smaller than 4 bytes, the value is
+                       # stored in the 4-byte area starting from the left.
+                       if size <= 4 :
+                               offsetIsValue = True
+                               offset = self.tagReaders[typ](count)
+                               if count == 1:
+                                       offset = offset[0]
+                               noise = self.f.read(4 - size)
+                       else :
+                               offsetIsValue = False
+                               offset = r32()
+                       
+                       ifdInfos[tag] = (typ, count, offset, offsetIsValue)
+               
+               if ifdname == 'GPS' :
+                       self.gpsTagInfos.update(ifdInfos)
+               else :
+                       self.mergedTagInfos.update(ifdInfos)
+
+               # return nexf ifd offset
+               return r32()
+       
+       def getThumbnail(self) :
+               if hasattr(self, 'ifd1Offset') :
+                       comp = self[COMPRESSION_SCHEME]
+                       if comp == 6 :
+                               # TODO : handle uncompressed thumbnails
+                               mime = COMPRESSION_SCHEME_TYPES.get(comp, 'unknown')
+                               start = self[OFFSET_TO_JPEG_SOI]
+                               count = self[BYTES_OF_JPEG_DATA]
+                               f = self.f
+                               f.seek(start)
+                               data = f.read(count)
+                               return data, mime
+                       else :
+                               return None
+               else :
+                       return None
+       
+               
+       
+       #
+       # dict interface
+       #
+       def keys(self) :
+               return self.mergedTagInfos.keys()
+       
+       def has_key(self, key) :
+               return self.mergedTagInfos.has_key(key)
+       
+       __contains__ = has_key # necessary ?
+       
+       def __getitem__(self, key) :
+               typ, count, offset, offsetIsValue = self.mergedTagInfos[key]
+               if offsetIsValue :
+                       return offset
+               else :
+                       self.f.seek(offset)
+                       value = self.tagReaders[typ](count)
+                       if count == 1:
+                               return value[0]
+                       else :
+                               return value
+       
+       def get(self, key) :
+               if self.has_key(key):
+                       return self[key]
+               else :
+                       return None
+       
+       def getIFDNames(self) :
+               return self.ifdnames
+               
+               
+       def getIFDTags(self, name) :
+               tags = [tag for tag in self.tagInfos[name].keys()]
+               tags.sort()
+               return tags
+               
+
+       def save(self, out) :
+               byteOrder = self.byteOrder
+               
+               if byteOrder == 'MM' :
+                       w16 = self.w16 = lambda i : out.write(ob16(i))
+                       w32 = self.w32 = lambda i : out.write(ob32(i))
+               elif byteOrder == 'II' :
+                       w16 = self.w16 = lambda i : out.write(ol16(i))
+                       w32 = self.w32 = lambda i : out.write(ol32(i))
+               
+               tagWriters = {
+                         1:  lambda l : [out.write(chr(i)) for i in l]
+                       , 2:  lambda l : out.write(l)
+                       , 3:  lambda l : [w16(i) for i in l]
+                       , 4:  lambda l : [w32(i) for i in l]
+                       , 5:  lambda l : [(w32(i[0]), w32(i[1])) for i in l]
+                       , 7:  lambda l : out.write(l)
+                       , 9:  lambda l : [w32(i) for i in l]
+                       , 10: lambda l : [(w32(i[0]), w32(i[1])) for i in l]
+               }
+               
+               
+               # tiff header
+               out.write(self.byteOrder)
+               w16(0x002A)
+               tags = self.keys()
+               r32(8) # offset of IFD0
+               ifdStarts = {}
+               pointerTags = []
+               isPtrTag = POINTER_TAGS.has_key
+               
+               for ifdname in self.getIFDName() :
+                       ifdInfos = self.tagInfos[name]
+                       tags = ifdInfos.keys()
+                       tags.sort()
+                       
+                       ifdStarts[ifdname] = out.tell()
+                       
+                       tiffOffset = ifdStarts[ifdname] + INTEROPERABILITY_FIELD_LENGTH * len(tags) + 4
+                       moreThan4bytesValuesTags = []
+                       
+                       for tag, info in ifdInfos.items() :
+                               if isPtrTag(tag) :
+                                       pointerTags.append((tag, out.tell()))
+                               typ, count, offset, offsetIsValue = info
+
+                               w16(tag)
+                               w16(typ)
+                               w32(count)
+                               
+                               ts = TYPES_SIZES[typ]
+                               size = ts * count
+
+                               if size <= 4 :
+                                       if count == 1 : offset = [offset]                                       
+                                       tagWriters[typ](offset)
+                                       
+                                       # padding
+                                       for i in range(4 - size) : out.write('\0')
+                               else :
+                                       w32(tiffOffset)
+                                       tiffOffset += size
+                                       moreThan4bytesValuesTags.append(tag)
+                       
+                       for tag in moreThan4bytesValuesTags :
+                               typ, count, offset, offsetIsValue = ifdInfos[tag]
+                               self.f.seek(offset)
+                               size = TYPES_SIZES[typ] * count
+                               out.write(self.f.read(size))
+                       
+                       # write place-holder for next ifd offset (updated later)
+                       r32(0)
+                       
+
+def ib16(c):
+       return ord(c[1]) + (ord(c[0])<<8)
+def ob16(i) :
+       return chr(i >> 8 & 255) + chr(i & 255)
+       
+def ib32(c):
+       return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24)
+def ob32(c):
+       return chr(i >> 24 & 0xff) + chr(i >> 16 & 0xff) + chr(i >> 8 & 0xff) + chr(i & 0xff)
+
+
+def il16(c):
+       return ord(c[0]) + (ord(c[1])<<8)
+def ol16(i):
+       return chr(i&255) + chr(i>>8&255)
+       
+def il32(c):
+       return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24)
+def ol32(i):
+       return chr(i&255) + chr(i>>8&255) + chr(i>>16&255) + chr(i>>24&255)
+
+
+
+
+def testRead(*paths) :
+       from PIL.Image import open as imgopen
+       from standards.exif import TAGS
+       from cStringIO import StringIO
+       
+       import os
+       paths = list(paths)
+       paths.extend(['testimages/%s'%name for name in os.listdir('testimages') \
+                                       if name.endswith('.jpg') and \
+                                          not name.endswith('_thumb.jpg')])
+       
+       for path in paths :
+               print '------------'
+               print path
+               print '------------'
+               im = imgopen(path)
+               applist = im.applist
+               exifBlock = [a[1] for a in applist if a[0] == 'APP1' and a[1].startswith("Exif\x00\x00")][0]
+               exif = exifBlock[6:]
+               sio = StringIO(exif)
+
+               e = Exif(sio)
+               for name in e.getIFDNames() :
+                       print '%s: ' %name
+                       for tag in e.getIFDTags(name) :
+                               print hex(tag), TAGS.get(tag), e[tag]
+                       print
+               
+               thumb = e.getThumbnail()
+               if thumb is not None :
+                       data, mime = thumb
+                       out = open('%s_thumb.jpg' % path[:-4], 'w')
+                       out.write(data)
+                       out.close()
+
+def testWrite(*paths) :
+       from PIL.Image import open as imgopen
+       from standards.exif import TAGS
+       from cStringIO import StringIO
+       
+#      import os
+#      paths = list(paths)
+#      paths.extend(['testimages/%s'%name for name in os.listdir('testimages') \
+#                                      if name.endswith('.jpg') and \
+#                                         not name.endswith('_thumb.jpg')])
+       
+       for path in paths :
+               print '------------'
+               print path
+               print '------------'
+               im = imgopen(path)
+               applist = im.applist
+               exifBlock = [a[1] for a in applist if a[0] == 'APP1' and a[1].startswith("Exif\x00\x00")][0]
+               exif = exifBlock[6:]
+               from cStringIO import StringIO
+               sio = StringIO(exif)
+
+               e = Exif(sio)
+               
+               out = StringIO()
+               e.save(out)
+               out.seek(0)
+               print '%r' % out.read()
+               
+
+if __name__ == '__main__' :
+       testRead('testMM.jpg', 'testII.jpg')
+       #testWrite('testMM.jpg', 'testII.jpg')
diff --git a/license.txt b/license.txt
new file mode 100755 (executable)
index 0000000..4025d75
--- /dev/null
@@ -0,0 +1,345 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/metadata.py b/metadata.py
new file mode 100755 (executable)
index 0000000..96a44aa
--- /dev/null
@@ -0,0 +1,337 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright © 2004-2008  Benoît PIN <benoit.pin@ensmp.fr>                           #
+#                                                                                     #
+#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+""" Photo metadata read / write module
+
+$Id: metadata.py 1272 2009-08-11 08:57:35Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/metadata.py $
+"""
+
+from AccessControl import ClassSecurityInfo
+from Globals import InitializeClass
+from AccessControl.Permissions import view
+from ZODB.interfaces import BlobError
+from ZODB.utils import cp
+from OFS.Image import File
+from xmp import XMP
+from logging import getLogger
+from cache import memoizedmethod
+from libxml2 import parseDoc
+from standards.xmp import accessors as xmpAccessors
+import xmputils
+from types import TupleType
+from subprocess import Popen, PIPE
+from Products.PortalTransforms.libtransforms.utils import bin_search, \
+                                                                                                                 MissingBinary
+
+XPATH_EMPTY_TAGS = "//node()[name()!='' and not(node()) and not(@*)]"
+console = getLogger('Photo.metadata')
+
+try :
+       XMPDUMP = 'xmpdump'
+       XMPLOAD = 'xmpload'
+       bin_search(XMPDUMP)
+       bin_search(XMPLOAD)
+       xmpIO_OK = True
+except MissingBinary :
+       xmpIO_OK = False
+       console.warn("xmpdump or xmpload not available.")
+
+class Metadata :
+       """ Photo metadata read / write mixin """
+       
+       security = ClassSecurityInfo()
+       
+       
+       #
+       # reading api
+       #
+
+       security.declarePrivate('getXMP')
+       if xmpIO_OK :
+               @memoizedmethod()
+               def getXMP(self):
+                       """returns xmp metadata packet with xmpdump call
+                       """
+                       if self.size :
+                               blob_file_path = self.bdata._current_filename()
+                               dumpcmd = '%s %s' % (XMPDUMP, blob_file_path)
+                               p = Popen(dumpcmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=True)
+                               xmp, err = p.communicate()
+                               if err :
+                                       raise SystemError, err
+                               return xmp
+
+       else :
+               @memoizedmethod()
+               def getXMP(self):
+                       """returns xmp metadata packet with XMP object
+                       """
+                       xmp = None
+                       if self.size :
+                               try :
+                                       bf = self.open('r')
+                                       x = XMP(bf, content_type=self.content_type)
+                                       xmp = x.getXMP()
+                               except NotImplementedError :
+                                       pass
+               
+                       return xmp
+               
+       security.declareProtected(view, 'getXmpFile')
+       def getXmpFile(self, REQUEST):
+               """returns the xmp packet over http.
+               """
+               xmp = self.getXMP()
+               if xmp is not None :
+                       return File('xmp', 'xmp', xmp, content_type='text/xml').index_html(REQUEST, REQUEST.RESPONSE)
+               else :
+                       return None
+
+       security.declarePrivate('getXmpBag')
+       def getXmpBag(self, name, root, index=None) :
+               index = self.getXmpPathIndex()
+               if index :
+                       path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
+                       node = index.get(path)
+               
+                       if node :
+                               values = xmputils.getBagValues(node.element)
+                               return values
+               return tuple()
+       
+       security.declarePrivate('getXmpSeq')
+       def getXmpSeq(self, name, root) :
+               index = self.getXmpPathIndex()
+               if index :
+                       path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
+                       node = index.get(path)
+               
+                       if node :
+                               values = xmputils.getSeqValues(node.element)
+                               return values
+               return tuple()
+
+       security.declarePrivate('getXmpAlt')
+       def getXmpAlt(self, name, root) :
+               index = self.getXmpPathIndex()
+               if index :
+                       path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
+                       node = index.get(path)
+
+                       if node :
+                               firstLi = node['rdf:Alt/rdf:li']
+                               assert firstLi.unique, "More than one rdf:Alt (localisation not yet supported)"
+                               return firstLi.element.content
+               return ''
+
+       security.declarePrivate('getXmpProp')
+       def getXmpProp(self, name, root):
+               index = self.getXmpPathIndex()
+               if index :
+                       path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
+                       node = index.get(path)
+                       if node :
+                               return node.element.content
+               return '' 
+               
+               
+       security.declarePrivate('getXmpPathIndex')
+       @memoizedmethod(volatile=True)
+       def getXmpPathIndex(self):
+               xmp = self.getXMP()
+               if xmp :
+                       d = parseDoc(xmp)
+                       index = xmputils.getPathIndex(d)
+                       return index
+                       
+       security.declarePrivate('getXmpValue')
+       def getXmpValue(self, name):
+               """ returns pythonic version of xmp property """
+               info = xmpAccessors[name]
+               root = info['root']
+               rdfType = info['rdfType'].capitalize()
+               methName = 'getXmp%s' % rdfType
+               meth = getattr(self.aq_base, methName)
+               return meth(name, root)
+       
+       
+       security.declareProtected(view, 'getXmpField')
+       def getXmpField(self, name):
+               """ returns data formated for a html form field """
+               editableValue = self.getXmpValue(name)
+               if type(editableValue) == TupleType :
+                       editableValue = ', '.join(editableValue)
+               return {'id' : name.replace(':', '_'),
+                               'value' : editableValue}
+       
+       
+       #
+       # writing api
+       #
+
+       security.declarePrivate('setXMP')
+       if xmpIO_OK :
+               def setXMP(self, xmp):
+                       """setXMP with xmpload call
+                       """
+                       if self.size :
+                               blob = self.bdata
+                               if blob.readers :
+                                       raise BlobError("Already opened for reading.")
+                       
+                               if blob._p_blob_uncommitted is None:
+                                       filename = blob._create_uncommitted_file()
+                                       uncommitted = file(filename, 'w')
+                                       cp(file(blob._p_blob_committed, 'rb'), uncommitted)
+                                       uncommitted.close()
+                               else :
+                                       filename = blob._p_blob_uncommitted
+                       
+                               loadcmd = '%s %s' % (XMPLOAD, filename)
+                               p = Popen(loadcmd, stdin=PIPE, stderr=PIPE, shell=True)
+                               p.stdin.write(xmp)
+                               p.stdin.close()
+                               p.wait()
+                               err = p.stderr.read()
+                               if err :
+                                       raise SystemError, err
+                       
+                               f = file(filename)
+                               f.seek(0,2)
+                               self.updateSize(size=f.tell())
+                               f.close()
+                               self.bdata._p_changed = True
+                       
+                       
+                               # purge caches
+                               try : del self._methodResultsCache['getXMP']
+                               except KeyError : pass 
+                       
+                               for name in ('getXmpPathIndex',) :
+                                       try :
+                                               del self._v__methodResultsCache[name]
+                                       except (AttributeError, KeyError):
+                                               continue
+
+                               self.ZCacheable_invalidate()
+                               self.ZCacheable_set(None)
+                               self.http__refreshEtag()
+       
+       else :
+               def setXMP(self, xmp):
+                       """setXMP with XMP object
+                       """
+                       if self.size :
+                               bf = self.open('r+')
+                               x = XMP(bf, content_type=self.content_type)
+                               x.setXMP(xmp)
+                               x.save()
+                               self.updateSize(size=bf.tell())
+
+                               # don't call update_data
+                               self.ZCacheable_invalidate()
+                               self.ZCacheable_set(None)
+                               self.http__refreshEtag()
+
+                               # purge caches
+                               try : del self._methodResultsCache['getXMP']
+                               except KeyError : pass
+                               for name in ('getXmpPathIndex', ) :
+                                       try :
+                                               del self._v__methodResultsCache[name]
+                                       except (AttributeError, KeyError):
+                                               continue
+               
+               
+                       
+       security.declarePrivate('setXmpField')
+       def setXmpFields(self, **kw):
+               xmp = self.getXMP()
+               if xmp :
+                       doc = parseDoc(xmp)
+               else :
+                       doc = xmputils.createEmptyXmpDoc()
+               
+               index = xmputils.getPathIndex(doc)
+               
+               pathPrefix = 'rdf:RDF/rdf:Description'
+               preferedNsDeclaration = 'rdf:RDF/rdf:Description'
+
+               for id, value in kw.items() :
+                       name = id.replace('_', ':')
+                       info = xmpAccessors.get(name)
+                       if not info : continue
+                       root = info['root']
+                       rdfType = info['rdfType']
+                       path = '/'.join([p for p in [pathPrefix, root, name] if p])
+
+                       Metadata._setXmpField(index
+                                                               , path
+                                                               , rdfType
+                                                               , name
+                                                               , value
+                                                               , preferedNsDeclaration)
+               
+               # clean empty tags without attributes
+               context = doc.xpathNewContext()
+               nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
+               while nodeset :
+                       for n in nodeset :
+                               n.unlinkNode()
+                               n.freeNode()
+                       nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
+               
+               
+               
+               xmp = doc.serialize('utf-8')
+               # remove <?xml version="1.0" encoding="utf-8"?> header
+               xmp = xmp.split('?>', 1)[1].lstrip('\n')
+               self.setXMP(xmp)
+
+       @staticmethod
+       def _setXmpField(index, path, rdfType, name, value, preferedNsDeclaration) :
+               if rdfType in ('Bag', 'Seq') :
+                       value = value.replace(';', ',')
+                       value = value.split(',')
+                       value = [item.strip() for item in value]
+                       value = filter(None, value)
+
+               if value :
+                       # edit
+                       xmpPropIndex = index.getOrCreate(path
+                                                                       , rdfType
+                                                                       , preferedNsDeclaration)
+                       if rdfType == 'prop' :
+                               xmpPropIndex.element.setContent(value)
+                       else :
+                               #rdfPrefix = index.getDocumentNs()['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
+                               func = getattr(xmputils, 'createRDF%s' % rdfType)
+                               newNode = func(name, value, index)
+                               oldNode = xmpPropIndex.element
+                               oldNode.replaceNode(newNode)
+               else :
+                       # delete
+                       xmpPropIndex = index.get(path)
+                       if xmpPropIndex is not None :
+                               xmpPropIndex.element.unlinkNode()
+                               xmpPropIndex.element.freeNode()
+               
+
+InitializeClass(Metadata)
diff --git a/migration/__init__.py b/migration/__init__.py
new file mode 100644 (file)
index 0000000..4287ca8
--- /dev/null
@@ -0,0 +1 @@
+#
\ No newline at end of file
diff --git a/migration/from2to3.py b/migration/from2to3.py
new file mode 100644 (file)
index 0000000..13882ca
--- /dev/null
@@ -0,0 +1,30 @@
+from BTrees.OOBTree import OOBTree
+from BTrees.IOBTree import IOBTree
+
+def migrate(p) :
+       if hasattr(p, '_variants') :
+               delattr(p, '_variants')
+       
+       if not hasattr(p, 'tiles_available') :
+               p.tiles_available = 0
+       
+       
+       if hasattr(p, '_methodResultsCache') and p._methodResultsCache.has_key('_getTile'):
+               p._tiles = OOBTree()
+               for args, value in p._methodResultsCache['_getTile'].items() :
+                       args = dict(args)
+                       zoom = float(args['zoom'])
+                       x = int(args['x'])
+                       y = int(args['y'])
+
+                       if not p._tiles.has_key(zoom) :
+                               p._tiles[zoom] = IOBTree()
+                       if not p._tiles[zoom].has_key(x) :
+                               p._tiles[zoom][x] = IOBTree()
+                       
+                       p._tiles[zoom][x][y] = value
+               del p._methodResultsCache['_getTile']
+               
+       elif not hasattr(p, '_tiles'):
+               p._tiles = OOBTree()
+               p.tiles_available = 0
diff --git a/migration/toblob.py b/migration/toblob.py
new file mode 100755 (executable)
index 0000000..e1fff2e
--- /dev/null
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+"""
+$Id: toblob.py 909 2009-04-20 13:38:47Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/migration/toblob.py $
+Script de migration du stockage du fichier depuis l'attribut 'data'
+vers l'attribut de type blob 'bdata'.
+IMPORTANT :
+les lignes 144 à 147 de blobbases.py doivent être commentéés
+avant exécution.
+
+147 | #                data = property(_getLegacyData, _setLegacyData,
+148 | #                                                "Data Legacy attribute to ensure compatibility "
+149 | #                                                "with derived classes that access data by this way.")
+
+"""
+
+from ZODB.blob import Blob
+
+def migrate(self) :
+       if hasattr(self.aq_base, 'data') :
+               data = str(self.data)
+               self.bdata = Blob()
+               bf = self.bdata.open('w')
+               bf.write(data)
+               bf.close()
+               delattr(self, 'data')
+               return True
+       else :
+               assert hasattr(self.aq_base, 'bdata')
+               return False
+       
\ No newline at end of file
diff --git a/ppm.py b/ppm.py
new file mode 100755 (executable)
index 0000000..55a824b
--- /dev/null
+++ b/ppm.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+####################################################
+# Copyright © 2009 Luxia SAS. All rights reserved. #
+#                                                  #
+# Contributors:                                    #
+#  - Benoît Pin <pinbe@luxia.fr>                   #
+####################################################
+""" PPM File support module
+
+$Id: ppm.py 1276 2009-08-11 16:38:02Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/ppm.py $
+"""
+
+from subprocess import Popen, PIPE
+from tempfile import TemporaryFile
+import os
+from math import ceil
+from PIL.Image import open as imgopen
+from PIL.Image import fromstring
+from PIL.Image import ANTIALIAS
+from cStringIO import StringIO
+
+DGJPEG = 'djpeg'
+RESIZING_TILE_SIZE = 1024
+
+class PPMFile(object) :
+       
+       def __init__(self, f, tileSize=256, isRaw=False) :
+               # convert jpeg -> ppm with djpeg
+               if not isRaw :
+                       # print 'djpeg'
+                       self.fp = TemporaryFile(mode='w+')
+                       p = Popen(DGJPEG, stdin=f, stdout=self.fp, stderr=PIPE, shell=True)
+                       p.wait()
+                       err = p.stderr.read()
+                       if err :
+                               raise SystemError, err
+               else :
+                       self.fp = f
+               
+               # get image specs with PIL
+               self.fp.seek(0)
+               im = imgopen(self.fp)
+               decoder, region, offset, parameters = im.tile[0]
+               x, y, width, height = region
+               del im
+               assert decoder == 'raw'
+               mode = parameters[0]
+               assert mode in ('RGB', 'L'), "Unsupported mode %s" % mode
+
+               if mode == 'RGB' :
+                       sampleSize = 3
+               elif mode == 'L' :
+                       sampleSize = 1
+               
+               self.width = width
+               self.height = height
+               self.offset = offset
+               self.mode = parameters[0]
+               self.sampleSize = sampleSize
+               self._setTileSize(tileSize)
+       
+       def _setTileSize(self, tileSize) :
+               self.tileSize = tileSize
+               self.tilesX = int(ceil(float(self.width) / self.tileSize))
+               self.tilesY = int(ceil(float(self.height) / self.tileSize))
+       
+       def getTile(self, xt, yt) :
+               f = self.fp
+               ss = self.sampleSize
+               x = xt * self.tileSize
+               y = yt * self.tileSize
+               start = (self.width * y + x) * ss + self.offset
+               
+               tw = th = self.tileSize
+
+               bw = self.width - x
+               if bw < self.tileSize  :
+                       tw = bw
+               bh = self.height - y
+               if bh < self.tileSize :
+                       th = bh
+               
+               assert tw > 0 and th > 0, "Tile requested out of image."
+
+               size = (tw, th)
+               tll = tw * ss
+               jump = (self.width - tw) * ss
+               
+               f.seek(start)
+               data = StringIO()
+               
+               for line in xrange(size[1]) :
+                       data.write(f.read(tll))
+                       f.seek(jump, 1)
+               
+               data.seek(0)
+               im = fromstring(self.mode, size, data.read())
+               return im
+       
+       def getTileSequence(self):
+               seq = []
+               for y in xrange(self.tilesY) :
+                       for x in xrange(self.tilesX) :
+                               seq.append((x, y))
+               return seq
+       
+       def resize(self, ratio=None, maxLength=None) :
+               if ratio and maxLength :
+                       raise AttributeError("'ratio' and 'size' are mutually exclusive.")
+               if maxLength :
+                       maxFullLength = max(self.width, self.height)
+                       ratio = float(maxLength) / maxFullLength
+
+               tileSizeBak = self.tileSize
+               
+               self._setTileSize(RESIZING_TILE_SIZE)
+               
+               width = height = 0
+               # cumul des arrondis
+               width = int(round(self.tileSize * ratio)) * (self.tilesX -1)
+               width += int(round((self.width - self.tileSize * (self.tilesX -1)) * ratio))
+               
+               height = int(round(self.tileSize * ratio)) * (self.tilesY -1)
+               height += int(round((self.height - self.tileSize * (self.tilesY -1)) * ratio))
+               
+               magic = self.mode == 'RGB' and 6 or 5
+               head = 'P%d %d %d 255\n' % (magic, width, height)
+               offset = len(head)
+
+               out = TemporaryFile(mode='w+')
+               out.write(head)
+
+               ss = self.sampleSize
+               rTll = int(round(self.tileSize * ratio))
+               
+               for x, y in self.getTileSequence() :
+                       # print 'resize', (x,y)
+                       tile = self.getTile(x,y)
+                       tileSize = tile.size
+                       size = map(lambda l : int(round(l * ratio)), tileSize)
+                       
+                       if size[0] and size[1] :
+                               resized = tile.resize(size, ANTIALIAS)
+                               data = resized.tostring()
+                               
+                               start = (y * width + x) * ss * rTll + offset
+                               jump = (width - size[0]) * ss
+                               
+                               out.seek(start)
+                               tll = size[0] * ss
+                               
+                               # écriture dans le bon ordre (c'est quand même plus agréable à l'œil)
+                               for l in xrange(size[1]) :
+                                       lineData = data[l*tll:(l+1)*tll]
+                                       out.write(lineData)
+                                       out.seek(jump, 1)
+               
+               out.seek(0,2)
+               length = out.tell()
+               assert length - len(head) == width * height * ss, (length - len(head), width * height * ss)
+               out.seek(0)
+
+               self._setTileSize(tileSizeBak)
+               return PPMFile(out, tileSize=tileSizeBak, isRaw=True)
+       
+       def getImage(self) :
+               self.fp.seek(0)
+               return imgopen(self.fp)
+       
+       def __del__(self) :
+               self.fp.close()
+
+
+if __name__ == '__main__' :
+       f = open('/Users/pinbe/Desktop/Chauve_souris.jpg')
+       try :
+               ppm = PPMFile(f, tileSize=256)
+               rppm = ppm.resize(maxLength=800)
+               im = rppm.getImage()
+               im.show()
+               for x, y in ppm.getTileSequence() :
+                       im = ppm.getTile(x, y)
+                       im.save('testoutput/%d_%d.jpg' % (x, y), 'JPEG', quality=90)
+       finally :
+               f.close()
diff --git a/readme.txt b/readme.txt
new file mode 100755 (executable)
index 0000000..80b2183
--- /dev/null
@@ -0,0 +1,22 @@
+Photo product extends the default Zope Image product and add image resizing support.
+
+Thumbnail Support
+
+ You can get a thumbnail copy of a Photo instance by
+adding '/getThumbnail' in the request url.
+This thumbnail returned is a persistent standard 
+Zope Image instance. You can set thumbnail's creation
+parameters (height, width, filter and auto refreshing)
+into the Photo's property sheet.
+
+Volatile resizing support
+
+ You can also get a volatile copy of the Photo instance
+by adding '/getResizedImage' in the request url. The 
+default size is (800, 600) px. There is two ways to customize this size :
+
+- pass an optional parameter size = (width, height) to 'getResizedImage'
+
+- set the session variable named 'preferedImageSize' with a tuple (width, height)
+
+note : the request var name must be 'SESSION' (default value of Session Data Manager).
\ No newline at end of file
diff --git a/standards/__init__.py b/standards/__init__.py
new file mode 100755 (executable)
index 0000000..4287ca8
--- /dev/null
@@ -0,0 +1 @@
+#
\ No newline at end of file
diff --git a/standards/bridges/__init__.py b/standards/bridges/__init__.py
new file mode 100755 (executable)
index 0000000..3c499ec
--- /dev/null
@@ -0,0 +1 @@
+from _bridges import xmp2exif, exif2xmp
\ No newline at end of file
diff --git a/standards/bridges/_bridges.py b/standards/bridges/_bridges.py
new file mode 100755 (executable)
index 0000000..b637a82
--- /dev/null
@@ -0,0 +1,16 @@
+from os.path import join
+from Globals import package_home
+home = package_home(globals())
+
+f = file(join(home, 'xmp_exif.csv'))
+lines = f.readlines()
+f.close()
+xmp2exif = {}
+exif2xmp = {}
+
+for l in [l for l in lines if not l.startswith('#')] :
+       fields = [f.strip() for f in l.split(';')]
+       assert len(fields) == 2, "%s malformed at line: '%s')" % (path, l)
+       xmpName, exifTag = fields
+       xmp2exif[xmpName] = exifTag
+       exif2xmp[exifTag] = xmpName
diff --git a/standards/bridges/xmp_exif.csv b/standards/bridges/xmp_exif.csv
new file mode 100644 (file)
index 0000000..316c98f
--- /dev/null
@@ -0,0 +1,104 @@
+#xmp field; exif tag
+tiff:ImageWidth;0x100
+tiff:ImageLength;0x101
+tiff:BitsPerSample;0x102
+tiff:Compression;0x103
+tiff:PhotometricInterpretation;0x106
+tiff:Orientation;0x112
+tiff:SamplesPerPixel;0x115
+tiff:PlanarConfiguration;0x11C
+tiff:YCbCrSubSampling;0x212
+tiff:YCbCrPositioning;0x213
+tiff:XResolution;0x11A
+tiff:YResolution;0x11B
+tiff:ResolutionUnit;0x128
+tiff:TransferFunction;0x12D
+tiff:WhitePoint;0x13E
+tiff:PrimaryChromaticities;0x13F
+tiff:YCbCrCoefficients;0x211
+tiff:ReferenceBlackWhite;0x214
+tiff:DateTime;0x9290
+tiff:ImageDescription;0x10E
+tiff:Make;0x10F
+tiff:Model;0x110
+tiff:Software;0x131
+tiff:Artist;0x13B
+tiff:Copyright;0x8298
+exif:ExifVersion;0x9000
+exif:FlashpixVersion;0xA000
+exif:ColorSpace;0xA001
+exif:ComponentsConfiguration;0x9101
+exif:CompressedBitsPerPixel;0x9102
+exif:PixelXDimension;0xA002
+exif:PixelYDimension;0xA003
+exif:UserComment;0x9286
+exif:RelatedSoundFile;0xA004
+exif:DateTimeOriginal;0x9291
+exif:DateTimeDigitized;0x9292
+exif:ExposureTime;0x829A
+exif:FNumber;0x829D
+exif:ExposureProgram;0x8822
+exif:SpectralSensitivity;0x8824
+exif:ISOSpeedRatings;0x8827
+exif:OECF;0x8828
+exif:ShutterSpeedValue;0x9201
+exif:ApertureValue;0x9202
+exif:BrightnessValue;0x9203
+exif:ExposureBiasValue;0x9204
+exif:MaxApertureValue;0x9205
+exif:SubjectDistance;0x9206
+exif:MeteringMode;0x9207
+exif:LightSource;0x9208
+exif:Flash;0x9209
+exif:FocalLength;0x920A
+exif:SubjectArea;0x9214
+exif:FlashEnergy;0xA20B
+exif:SpatialFrequencyResponse;0xA20C
+exif:FocalPlaneXResolution;0xA20E
+exif:FocalPlaneYResolution;0xA20F
+exif:FocalPlaneResolutionUnit;0xA210
+exif:SubjectLocation;0xA214
+exif:ExposureIndex;0xA215
+exif:SensingMethod;0xA217
+exif:FileSource;0xA300
+exif:SceneType;0xA301
+exif:CFAPattern;0xA302
+exif:CustomRendered;0xA401
+exif:ExposureMode;0xA402
+exif:WhiteBalance;0xA403
+exif:DigitalZoomRatio;0xA404
+exif:FocalLengthIn35mmFilm;0xA405
+exif:SceneCaptureType;0xA406
+exif:GainControl;0xA407
+exif:Contrast;0xA408
+exif:Saturation;0xA409
+exif:Sharpness;0xA40A
+exif:DeviceSettingDescription;0xA40B
+exif:SubjectDistanceRange;0xA40C
+exif:ImageUniqueID;0xA420
+exif:GPSVersionID;0x00
+exif:GPSLatitude;0x01
+exif:GPSLongitude;0x03
+exif:GPSAltitudeRef;0x5
+exif:GPSAltitude;0x06
+exif:GPSTimeStamp;0x1D
+exif:GPSSatellites;0x08
+exif:GPSStatus;0x09
+exif:GPSMeasureMode;0x0A
+exif:GPSDOP;0x0B
+exif:GPSSpeedRef;0x0C
+exif:GPSSpeed;0x0D
+exif:GPSTrackRef;0x0E
+exif:GPSTrack;0x0F
+exif:GPSImgDirectionRef;0x10
+exif:GPSImgDirection;0x11
+exif:GPSMapDatum;0x12
+exif:GPSDestLatitude;0x13
+exif:GPSDestLongitude;0x15
+exif:GPSDestBearingRef;0x17
+exif:GPSDestBearing;0x18
+exif:GPSDestDistanceRef;0x19
+exif:GPSDestDistance;0x1A
+exif:GPSProcessingMethod;0x1B
+exif:GPSAreaInformation;0x1C
+exif:GPSDifferential;0x1E
diff --git a/standards/exif/0thIFDExifPrivateTags.csv b/standards/exif/0thIFDExifPrivateTags.csv
new file mode 100644 (file)
index 0000000..57c939a
--- /dev/null
@@ -0,0 +1,60 @@
+# Table 15 Tag Support Levels (2) - 0th IFD Exif Private Tags
+# Tag Name Field Name  ;  Tag ID  ; Uncompressed  ; Compressed ; 
+# Dec  ; Hex  ; Chunky  ; Planar  ; YCC  ; 
+Exposure time ExposureTime  ; 33434  ; 829A  ; R  ; R  ; R  ; R  ; 
+F number FNumber  ; 33437  ; 829D  ; O  ; O  ; O  ; O  ; 
+Exposure program ExposureProgram  ; 34850  ; 8822  ; O  ; O  ; O  ; O  ; 
+Spectral sensitivity SpectralSensitivity  ; 34852  ; 8824  ; O  ; O  ; O  ; O  ; 
+ISO speed ratings ISOSpeedRatings  ; 34855  ; 8827  ; O  ; O  ; O  ; O  ; 
+Optoelectric coefficient OECF  ; 34856  ; 8828  ; O  ; O  ; O  ; O  ; 
+Exif Version ExifVersion  ; 36864  ; 9000  ; M  ; M  ; M  ; M  ; 
+Date and time original image was generated DateTimeOriginal  ; 36867  ; 9003  ; O  ; O  ; O  ; O  ; 
+Date and time image was made digital data DateTimeDigitized  ; 36868  ; 9004  ; O  ; O  ; O  ; O  ; 
+Meaning of each component ComponentsConfiguration  ; 37121  ; 9101  ; N  ; N  ; N  ; M  ; 
+Image compression mode CompressedBitsPerPixel  ; 37122  ; 9102  ; N  ; N  ; N  ; O  ; 
+Shutter speed ShutterSpeedValue  ; 37377  ; 9201  ; O  ; O  ; O  ; O  ; 
+Aperture ApertureValue  ; 37378  ; 9202  ; O  ; O  ; O  ; O  ; 
+Brightness BrightnessValue  ; 37379  ; 9203  ; O  ; O  ; O  ; O  ; 
+Exposure bias ExposureBiasValue  ; 37380  ; 9204  ; O  ; O  ; O  ; O  ; 
+Maximum lens aperture MaxApertureValue  ; 37381  ; 9205  ; O  ; O  ; O  ; O  ; 
+Subject distance SubjectDistance ;  37382  ; 9206  ; O  ; O  ; O  ; O  ; 
+Metering mode MeteringMode  ; 37383  ; 9207  ; O  ; O  ; O  ; O  ; 
+Light source LightSource  ; 37384  ; 9208  ; O  ; O  ; O  ; O  ; 
+Flash Flash  ; 37385  ; 9209  ; R  ; R  ; R  ; R  ; 
+Lens focal length FocalLength  ; 37386  ; 920A  ; O  ; O  ; O  ; O  ; 
+Subject area SubjectArea  ; 37396  ; 9214  ; O  ; O  ; O  ; O  ; 
+Manufacturer notes MakerNote  ; 37500  ; 927C  ; O  ; O  ; O  ; O  ; 
+User comments UserComment  ; 37510  ; 9286  ; O  ; O  ; O  ; O  ; 
+DateTime subseconds SubSecTime  ; 37520  ; 9290  ; O  ; O  ; O  ; O  ; 
+DateTimeOriginal subseconds SubSecTimeOriginal  ; 37521  ; 9291  ; O  ; O  ; O  ; O  ; 
+DateTimeDigitized subseconds SubSecTimeDigitized  ; 37522  ; 9292  ; O  ; O  ; O  ; O  ; 
+Supported Flashpix version FlashpixVersion  ; 40960  ; A000  ; M  ; M  ; M  ; M  ; 
+Color space information ColorSpace  ; 40961  ; A001  ; M  ; M  ; M  ; M  ; 
+Valid image width PixelXDimension  ; 40962  ; A002  ; N  ; N  ; N  ; M  ; 
+Valid image height PixelYDimension  ; 40963  ; A003  ; N  ; N  ; N  ; M  ; 
+Related audio file RelatedSoundFile  ; 40964  ; A004  ; O  ; O  ; O  ; O  ; 
+Interoperability tag Interoperability IFD Pointer  ; 40965  ; A005  ; N  ; N  ; N  ; O  ; 
+Flash energy FlashEnergy  ; 41483  ; A20B  ; O  ; O  ; O  ; O  ; 
+Spatial frequency response SpatialFrequencyResponse  ; 41484  ; A20C  ; O  ; O  ; O  ; O  ; 
+Focal plane X resolution FocalPlaneXResolution  ; 41486  ; A20E  ; O  ; O  ; O  ; O  ; 
+Focal plane Y resolution FocalPlaneYResolution  ; 41487  ; A20F  ; O  ; O  ; O  ; O  ; 
+Focal plane resolution unit FocalPlaneResolutionUnit  ; 41488  ; A210  ; O  ; O  ; O  ; O  ; 
+Subject location SubjectLocation  ; 41492  ; A214  ; O  ; O  ; O  ; O  ; 
+Exposure index ExposureIndex  ; 41493  ; A215  ; O  ; O  ; O  ; O  ; 
+Sensing method SensingMethod  ; 41495  ; A217  ; O  ; O  ; O  ; O  ; 
+File source FileSource  ; 41728  ; A300  ; O  ; O  ; O  ; O  ; 
+Scene type SceneType  ; 41729  ; A301  ; O  ; O  ; O  ; O  ; 
+CFA pattern CFAPattern  ; 41730  ; A302  ; O  ; O  ; O  ; O  ; 
+Custom image processing CustomRendered  ; 41985  ; A401  ; O  ; O  ; O  ; O  ; 
+Exposure mode ExposureMode  ; 41986  ; A402  ; R  ; R  ; R  ; R  ; 
+White balance WhiteBalance  ; 41987  ; A403  ; R  ; R  ; R  ; R  ; 
+Digital zoom ratio DigitalZoomRatio ;  41988  ; A404  ; O  ; O  ; O  ; O  ; 
+Focal length in 35 mm film FocalLengthIn35mmFilm  ; 41989  ; A405  ; O  ; O  ; O  ; O  ; 
+Scene capture type SceneCaptureType  ; 41990  ; A406  ; R  ; R  ; R  ; R  ; 
+Gain control GainControl  ; 41991  ; A407  ; O  ; O  ; O  ; O  ; 
+Contrast Contrast  ; 41992  ; A408  ; O  ; O  ; O  ; O  ; 
+Saturation Saturation  ; 41993  ; A409  ; O  ; O  ; O  ; O  ; 
+Sharpness Sharpness  ; 41994  ; A40A  ; O  ; O  ; O  ; O  ; 
+Device settings description DeviceSettingDescription  ; 41995  ; A40B  ; O  ; O  ; O  ; O  ; 
+Subject distance range SubjectDistanceRange  ; 41996  ; A40C  ; O  ; O  ; O  ; O  ; 
+Unique image ID ImageUniqueID  ; 42016  ; A420  ; O  ; O  ; O  ; O  ; 
diff --git a/standards/exif/0thIFDGPSInfoTags.csv b/standards/exif/0thIFDGPSInfoTags.csv
new file mode 100644 (file)
index 0000000..909cf17
--- /dev/null
@@ -0,0 +1,34 @@
+# Table 16 Tag Support Levels (3) - 0th IFD GPS Info Tags
+# Tag Name Field Name  ;  Tag ID  ; Uncompressed  ; Comp-r essed ; 
+# Dec  ; Hex  ; Chunky  ; Planar  ; YCC  ; 
+GPS tag version GPSVersionID  ; 0  ; 0  ; O  ; O  ; O  ; O  ; 
+North or South Latitude GPSLatitudeRef ;  1  ; 1  ; O  ; O  ; O  ; O  ; 
+Latitude GPSLatitude  ; 2  ; 2  ; O  ; O  ; O  ; O  ; 
+East or West Longitude GPSLongitudeRef  ; 3  ; 3  ; O  ; O  ; O  ; O  ; 
+Longitude GPSLongitude  ; 4  ; 4  ; O  ; O  ; O  ; O  ; 
+Altitude reference GPSAltitudeRef ;  5  ; 5  ; O  ; O  ; O  ; O  ; 
+Altitude GPSAltitude  ; 6  ; 6  ; O  ; O  ; O  ; O  ; 
+GPS time (atomic clock) GPSTimeStamp ;  7  ; 7  ; O  ; O  ; O  ; O  ; 
+GPS satellites used for measurement GPSSatellites  ; 8  ; 8  ; O  ; O  ; O  ; O  ; 
+GPS receiver status GPSStatus  ; 9  ; 9  ; O  ; O  ; O  ; O  ; 
+GPS measurement mode GPSMeasureMode  ; 10  ; A  ; O  ; O  ; O  ; O  ; 
+Measurement precision GPSDOP ;  11  ; B  ; O  ; O  ; O  ; O  ; 
+Speed unit GPSSpeedRef  ; 12  ; C  ; O  ; O  ; O  ; O  ; 
+Speed of GPS receiver GPSSpeed  ; 13  ; D  ; O  ; O  ; O  ; O  ; 
+Reference for direction of movement GPSTrackRef  ; 14  ; E  ; O  ; O  ; O  ; O  ; 
+Direction of movement GPSTrack ;  15  ; F  ; O  ; O  ; O  ; O  ; 
+Reference for direction of image GPSImgDirectionRef  ; 16  ; 10  ; O  ; O  ; O  ; O  ; 
+Direction of image GPSImgDirection  ; 17  ; 11  ; O  ; O  ; O  ; O  ; 
+Geodetic survey data used GPSMapDatum ;  18  ; 12  ; O  ; O  ; O  ; O  ; 
+Reference for latitude of destination GPSDestLatitudeRef  ; 19  ; 13  ; O  ; O  ; O  ; O  ; 
+Latitude of destination GPSDestLatitude  ; 20  ; 14  ; O  ; O  ; O  ; O  ; 
+Reference for longitude of destination GPSDestLongitudeRef  ; 21  ; 15  ; O  ; O  ; O  ; O  ; 
+Longitude of destination GPSDestLongitude  ; 22  ; 16  ; O  ; O  ; O  ; O  ; 
+Reference for bearing of destination GPSDestBearingRef  ; 23  ; 17  ; O  ; O  ; O  ; O  ; 
+Bearing of destination GPSDestBearing ;  24  ; 18  ; O  ; O  ; O  ; O  ; 
+Reference for distance to destination GPSDestDistanceRef  ; 25  ; 19  ; O  ; O  ; O  ; O  ; 
+Distance to destination GPSDestDistance  ; 26  ; 1A  ; O  ; O  ; O  ; O  ; 
+Name of GPS processing method GPSProcessingMethod  ; 27  ; 1B  ; O  ; O  ; O  ; O  ; 
+Name of GPS area GPSAreaInformation  ; 28  ; 1C  ; O  ; O  ; O  ; O  ; 
+GPS date GPSDateStamp  ; 29  ; 1D  ; O  ; O  ; O  ; O  ; 
+GPS differential correction GPSDifferential  ; 30  ; 1E  ; O  ; O  ; O  ; O  ; 
diff --git a/standards/exif/0thIFDInteroperabilityTag.csv b/standards/exif/0thIFDInteroperabilityTag.csv
new file mode 100644 (file)
index 0000000..85c63eb
Binary files /dev/null and b/standards/exif/0thIFDInteroperabilityTag.csv differ
diff --git a/standards/exif/0thIFDTIFFTags.csv b/standards/exif/0thIFDTIFFTags.csv
new file mode 100644 (file)
index 0000000..6635dfa
--- /dev/null
@@ -0,0 +1,35 @@
+# Table 14 Tag Support Levels (1) - 0th IFD TIFF Tags
+# Tag Name Field Name  ;  Tag ID  ; Uncompressed  ; Compresse d ; 
+# Dec  ; Hex  ; Chunky  ; Planar  ; YCC  ; 
+Image width ImageWidth  ; 256  ; 100  ; M  ; M  ; M  ; J  ; 
+Image height ImageLength  ; 257  ; 101  ; M  ; M  ; M  ; J  ; 
+Number of bits per component BitsPerSample  ; 258  ; 102  ; M  ; M  ; M  ; J  ; 
+Compression scheme Compression  ; 259  ; 103  ; M  ; M  ; M  ; J  ; 
+Pixel composition PhotometricInterpretation  ; 262  ; 106  ; M  ; M  ; M  ; N  ; 
+Image title ImageDescription  ; 270  ; 10E  ; R  ; R  ; R  ; R  ; 
+Manufacturer of image input equipment Make  ; 271  ; 10F  ; R  ; R  ; R  ; R  ; 
+Model of image input equipment Model  ; 272  ; 110  ; R  ; R  ; R  ; R  ; 
+Image data location StripOffsets  ; 273  ; 111  ; M  ; M  ; M  ; N  ; 
+Orientation of image Orientation  ; 274  ; 112  ; R  ; R  ; R  ; R  ; 
+Number of components SamplesPerPixel  ; 277  ; 115  ; M  ; M  ; M  ; J  ; 
+Number of rows per strip RowsPerStrip  ; 278  ; 116  ; M  ; M  ; M  ; N  ; 
+Bytes per compressed strip StripByteCounts  ; 279  ; 117  ; M  ; M  ; M  ; N  ; 
+Image resolution in width direction XResolution  ; 282  ; 11A  ; M  ; M  ; M  ; M  ; 
+Image resolution in height direction YResolution  ; 283  ; 11B  ; M  ; M  ; M  ; M  ; 
+Image data arrangement PlanarConfiguration  ; 284  ; 11C  ; O  ; M  ; O  ; J  ; 
+Unit of X and Y resolution ResolutionUnit  ; 296  ; 128  ; M  ; M  ; M  ; M  ; 
+Transfer function TransferFunction  ; 301  ; 12D  ; R  ; R  ; R  ; R  ; 
+Software used Software  ; 305  ; 131  ; O  ; O  ; O  ; O  ; 
+File change date and time DateTime  ; 306  ; 132  ; R  ; R  ; R  ; R  ; 
+Person who created the image Artist  ; 315  ; 13B  ; O  ; O  ; O  ; O  ; 
+White point chromaticity WhitePoint  ; 318  ; 13E  ; O  ; O  ; O  ; O  ; 
+Chromaticities of primaries PrimaryChromaticities  ; 319  ; 13F  ; O  ; O  ; O  ; O  ; 
+Offset to JPEG SOI JPEGInterchangeFormat  ; 513  ; 201  ; N  ; N  ; N  ; N  ; 
+Bytes of JPEG data JPEGInterchangeFormatLength  ; 514  ; 202  ; N  ; N  ; N  ; N  ; 
+Color space transformation matrix coefficients YCbCrCoefficients  ; 529  ; 211  ; N  ; N  ; O  ; O  ; 
+Subsampling ratio of Y to C YCbCrSubSampling  ; 530  ; 212  ; N  ; N  ; M  ; J  ; 
+Y and C positioning YCbCrPositioning  ; 531  ; 213  ; N  ; N  ; M  ; M  ; 
+Pair of black and white reference values ReferenceBlackWhite  ; 532  ; 214  ; O  ; O  ; O  ; O  ; 
+Copyright holder Copyright  ; 33432  ; 8298  ; O  ; O  ; O  ; O  ; 
+Exif tag Exif IFD Pointer  ; 34665  ; 8769  ; M  ; M  ; M  ; M  ; 
+GPS tag GPSInfo IFD Pointer  ; 34853  ; 8825  ; O  ; O  ; O  ; O  ; 
diff --git a/standards/exif/1stIFDTIFFTag.csv b/standards/exif/1stIFDTIFFTag.csv
new file mode 100644 (file)
index 0000000..7bdd80b
--- /dev/null
@@ -0,0 +1,35 @@
+# Table 18 Tag Support Levels (5) - 1st IFD TIFF Tag
+# Tag Name Field Name  ;  Tag ID  ; Uncompressed  ; Comp-ressed ; 
+# Dec  ; Hex  ; Chunky  ; Planar  ; YCC  ; 
+Image width ImageWidth  ; 256  ; 100  ; M  ; M  ; M  ; J  ; 
+Image height ImageLength  ; 257  ; 101  ; M  ; M  ; M  ; J  ; 
+Number of bits per component BitsPerSample  ; 258  ; 102  ; M  ; M  ; M  ; J  ; 
+Compression scheme Compression  ; 259  ; 103  ; M  ; M  ; M  ; M  ; 
+Pixel composition PhotometricInterpretation  ; 262  ; 106  ; M  ; M  ; M  ; J  ; 
+Image title ImageDescription  ; 270  ; 10E ;  O  ; O  ; O  ; O  ; 
+Manufacturer of image input equipment Make  ; 271  ; 10F ;  O  ; O  ; O  ; O  ; 
+Model of image input equipment Model  ; 272  ; 110 ;  O  ; O  ; O  ; O  ; 
+Image data location StripOffsets  ; 273  ; 111  ; M  ; M  ; M  ; N  ; 
+Orientation of image Orientation  ; 274  ; 112 ;  O  ; O  ; O  ; O  ; 
+Number of components SamplesPerPixel  ; 277  ; 115  ; M  ; M  ; M  ; J  ; 
+Number of rows per strip RowsPerStrip  ; 278  ; 116  ; M  ; M  ; M  ; N  ; 
+Bytes per compressed strip StripByteCounts  ; 279  ; 117  ; M  ; M  ; M  ; N  ; 
+Image resolution in width direction XResolution  ; 282  ; 11A  ; M  ; M  ; M  ; M  ; 
+Image resolution in height direction YResolution  ; 283  ; 11B  ; M  ; M  ; M  ; M  ; 
+Image data arrangement PlanarConfiguration  ; 284  ; 11C  ; O  ; M  ; O  ; J  ; 
+Unit of X and Y resolution ResolutionUnit  ; 296  ; 128  ; M  ; M  ; M  ; M  ; 
+Transfer function TransferFunction  ; 301  ; 12D ;  O  ; O  ; O  ; O  ; 
+Software used Software  ; 305  ; 131  ; O  ; O  ; O  ; O  ; 
+File change date and time DateTime  ; 306  ; 132  ; O  ; O  ; O  ; O  ; 
+Person who created the image Artist  ; 315  ; 13B ;  O  ; O  ; O  ; O  ; 
+White point chromaticity WhitePoint  ; 318  ; 13E ;  O  ; O  ; O  ; O  ; 
+Chromaticities of primaries PrimaryChromaticities  ; 319  ; 13F ;  O  ; O  ; O  ; O  ; 
+Offset to JPEG SOI JPEGInterchangeFormat  ; 513  ; 201  ; N  ; N  ; N  ; M  ; 
+Bytes of JPEG data JPEGInterchangeFormatLength  ; 514  ; 202  ; N  ; N  ; N  ; M  ; 
+Color space transformation matrix coefficients YCbCrCoefficients  ; 529  ; 211  ; N  ; N  ; O  ; O  ; 
+Subsampling ratio of Y to C YCbCrSubSampling  ; 530  ; 212  ; N  ; N  ; M  ; J  ; 
+Y and C positioning YCbCrPositioning  ; 531  ; 213  ; N  ; N  ; O  ; O  ; 
+Pair of black and white reference values ReferenceBlackWhite  ; 532  ; 214  ; O  ; O  ; O  ; O  ; 
+Copyright holder Copyright  ; 33432  ; 8298 ;  O  ; O  ; O  ; O  ; 
+Exif tag Exif IFD Pointer  ; 34665  ; 8769 ;  O  ; O  ; O  ; O  ; 
+GPS tag GPSInfo IFD Pointer  ; 34853  ; 8825 ;  O  ; O  ; O  ; O  ; 
diff --git a/standards/exif/__init__.py b/standards/exif/__init__.py
new file mode 100755 (executable)
index 0000000..6d00259
--- /dev/null
@@ -0,0 +1 @@
+from _exif_tags import TAGS, TAG_TYPES
diff --git a/standards/exif/_exif_tags.py b/standards/exif/_exif_tags.py
new file mode 100755 (executable)
index 0000000..afb6836
--- /dev/null
@@ -0,0 +1,43 @@
+""" Exif tags based on JEITA CP-3451 Exif Version 2.2 specification tables.
+
+$Id: _exif_tags.py 360 2008-02-21 09:17:32Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/standards/exif/_exif_tags.py $
+"""
+from os.path import join
+from Globals import package_home
+home = package_home(globals())
+
+files = [
+#        'gpsA.csv'
+         'ifdA.csv'
+       , 'ifdB.csv'
+       , 'ifdC.csv'
+       , 'ifdD.csv'
+       , 'ifdE.csv'
+       , 'ifdF.csv'
+       , 'ifdG.csv'
+       , 'ifdH.csv'
+       , 'tiffA.csv'
+       , 'tiffB.csv'
+       , 'tiffC.csv'
+       , 'tiffD.csv'
+       , 'hand_added.csv'
+]
+
+
+TAGS = {}
+TAG_TYPES = {}
+
+for name in files :
+       f = file(join(home, name))
+       lines = f.readlines()
+       f.close()
+       for l in [l for l in lines if not l.startswith('#')] :
+               fields = [f.strip() for f in l.split(';')]
+               assert len(fields) == 7, "%s malformed at line: '%s')" % (path, l)
+               tagName, fieldName, tagIdDec, noise, typ, count, noise = fields
+               tagId = int(tagIdDec)
+               if TAGS.has_key(tagId) :
+                       raise ValueError, "%x tag is already defined" % tagId
+               TAGS[tagId] = (fieldName, tagName)
+               TAG_TYPES[tagId] = (typ, count)
diff --git a/standards/exif/gpsA.csv b/standards/exif/gpsA.csv
new file mode 100644 (file)
index 0000000..55122f0
--- /dev/null
@@ -0,0 +1,31 @@
+GPS tag version  ; GPSVersionID  ; 0  ; 0  ; BYTE  ; 4  ; 
+North or South Latitude  ; GPSLatitudeRef  ; 1  ; 1  ; ASCII  ; 2  ; 
+Latitude  ; GPSLatitude  ; 2  ; 2  ; RATIONAL  ; 3  ; 
+East or West Longitude  ; GPSLongitudeRef  ; 3  ; 3  ; ASCII  ; 2  ; 
+Longitude  ; GPSLongitude  ; 4  ; 4  ; RATIONAL  ; 3  ; 
+Altitude reference  ; GPSAltitudeRef  ; 5  ; 5  ; BYTE  ; 1  ; 
+Altitude  ; GPSAltitude  ; 6  ; 6  ; RATIONAL  ; 1  ; 
+GPS time (atomic clock)  ; GPSTimeStamp  ; 7  ; 7  ; RATIONAL  ; 3  ; 
+GPS satellites used for measurement  ; GPSSatellites  ; 8  ; 8  ; ASCII  ; Any  ; 
+GPS receiver status  ; GPSStatus  ; 9  ; 9  ; ASCII  ; 2  ; 
+GPS measurement mode  ; GPSMeasureMode  ; 10  ; A  ; ASCII  ; 2  ; 
+Measurement precision  ; GPSDOP  ; 11  ; B  ; RATIONAL  ; 1  ; 
+Speed unit  ; GPSSpeedRef  ; 12  ; C  ; ASCII  ; 2  ; 
+Speed of GPS receiver  ; GPSSpeed  ; 13  ; D  ; RATIONAL  ; 1  ; 
+Reference for direction of movement  ; GPSTrackRef  ; 14  ; E  ; ASCII  ; 2  ; 
+Direction of movement  ; GPSTrack  ; 15  ; F  ; RATIONAL  ; 1  ; 
+Reference for direction of image  ; GPSImgDirectionRef  ; 16  ; 10  ; ASCII  ; 2  ; 
+Direction of image  ; GPSImgDirection  ; 17  ; 11  ; RATIONAL  ; 1  ; 
+Geodetic survey data used  ; GPSMapDatum  ; 18  ; 12  ; ASCII  ; Any  ; 
+Reference for latitude of destination  ; GPSDestLatitudeRef  ; 19  ; 13  ; ASCII  ; 2  ; 
+Latitude of destination  ; GPSDestLatitude  ; 20  ; 14  ; RATIONAL  ; 3  ; 
+Reference for longitude of destination  ; GPSDestLongitudeRef  ; 21  ; 15  ; ASCII  ; 2  ; 
+Longitude of destination  ; GPSDestLongitude  ; 22  ; 16  ; RATIONAL  ; 3  ; 
+Reference for bearing of destination  ; GPSDestBearingRef  ; 23  ; 17  ; ASCII  ; 2  ; 
+Bearing of destination  ; GPSDestBearing  ; 24  ; 18  ; RATIONAL  ; 1  ; 
+Reference for distance to destination  ; GPSDestDistanceRef  ; 25  ; 19  ; ASCII  ; 2  ; 
+Distance to destination  ; GPSDestDistance  ; 26  ; 1A  ; RATIONAL  ; 1  ; 
+Name of GPS processing method  ; GPSProcessingMethod  ; 27  ; 1B  ; UNDEFINED  ; Any  ; 
+Name of GPS area  ; GPSAreaInformation  ; 28  ; 1C  ; UNDEFINED  ; Any  ; 
+GPS date  ; GPSDateStamp  ; 29  ; 1D  ; ASCII  ; 11  ; 
+GPS differential correction  ; GPSDifferential  ; 30  ; 1E  ; SHORT  ; 1  ; 
diff --git a/standards/exif/hand_added.csv b/standards/exif/hand_added.csv
new file mode 100644 (file)
index 0000000..e4b9ef0
--- /dev/null
@@ -0,0 +1,6 @@
+Exif tag  ; Exif IFD Pointer  ; 34665  ; 8769  ; LONG  ; 1  ; 
+Interoperability tag ; Interoperability IFD Pointer ; 40965 ; A005 ; LONG ; 1 ; 
+Related Image Width ; RelatedImageWidth ; 4097 ; 1001 ; SHORT ; 1 ; 
+Related Image Height ; RelatedImageHeight ; 4098 ; 1002 ; SHORT ; 1 ; 
+Interoperability Identification ; InteroperabilityIndex ; 1 ; 1 ; ASCII ; Any ; 
+Interoperability Version ; InteroperabilityVersion ; 2 ; 2 ; UNDEFINED ; Any ; 
diff --git a/standards/exif/ifdA.csv b/standards/exif/ifdA.csv
new file mode 100644 (file)
index 0000000..3f510f8
--- /dev/null
@@ -0,0 +1,2 @@
+Exif version ; ExifVersion  ; 36864  ; 9000  ; UNDEFINED  ; 4  ; 
+Supported Flashpix version ; FlashpixVersion  ; 40960  ; A000  ; UNDEFINED  ; 4  ; 
diff --git a/standards/exif/ifdB.csv b/standards/exif/ifdB.csv
new file mode 100644 (file)
index 0000000..7c0f76f
--- /dev/null
@@ -0,0 +1 @@
+Color space information ; ColorSpace  ; 40961  ; A001  ; SHORT  ; 1  ; 
diff --git a/standards/exif/ifdC.csv b/standards/exif/ifdC.csv
new file mode 100644 (file)
index 0000000..93235e8
--- /dev/null
@@ -0,0 +1,4 @@
+ Meaning of each component  ; ComponentsConfiguration  ; 37121  ; 9101  ; UNDEFINED  ; 4  ; 
+ Image compression mode  ; CompressedBitsPerPixel  ; 37122  ; 9102  ; RATIONAL  ; 1  ; 
+ Valid image width  ; PixelXDimension  ; 40962  ; A002  ; SHORT or LONG  ; 1  ; 
+ Valid image height  ; PixelYDimension  ; 40963  ; A003  ; SHORT or LONG  ; 1  ; 
diff --git a/standards/exif/ifdD.csv b/standards/exif/ifdD.csv
new file mode 100644 (file)
index 0000000..44ddd41
--- /dev/null
@@ -0,0 +1,2 @@
+Manufacturer notes ; MakerNote  ; 37500  ; 927C  ; UNDEFINED  ; Any ; 
+User comments ; UserComment  ; 37510  ; 9286  ; UNDEFINED  ; Any  ; 
diff --git a/standards/exif/ifdE.csv b/standards/exif/ifdE.csv
new file mode 100644 (file)
index 0000000..afb3cee
--- /dev/null
@@ -0,0 +1 @@
+Related audio file ; RelatedSoundFile  ; 40964  ; A004  ; ASCII  ; 13  ;
diff --git a/standards/exif/ifdF.csv b/standards/exif/ifdF.csv
new file mode 100644 (file)
index 0000000..8d32732
--- /dev/null
@@ -0,0 +1,5 @@
+ Date and time of original data generation  ;  DateTimeOriginal  ; 36867  ; 9003  ; ASCII  ; 20  ; 
+ Date and time of digital data generation  ;  DateTimeDigitized  ; 36868  ; 9004  ; ASCII  ; 20  ; 
+  DateTime subseconds  ; SubSecTime  ; 37520  ; 9290  ; ASCII  ; Any ; 
+  DateTimeOriginal subseconds  ; SubSecTimeOriginal  ; 37521  ; 9291  ; ASCII  ; Any ; 
+  DateTimeDigitized subseconds  ; SubSecTimeDigitized  ; 37522  ; 9292  ; ASCII  ; Any  ; 
diff --git a/standards/exif/ifdG.csv b/standards/exif/ifdG.csv
new file mode 100644 (file)
index 0000000..fca9336
--- /dev/null
@@ -0,0 +1,40 @@
+Exposure time  ; ExposureTime  ; 33434  ; 829A  ; RATIONAL  ; 1  ; 
+F number  ; FNumber  ; 33437  ; 829D  ; RATIONAL  ; 1  ; 
+Exposure program  ; ExposureProgram  ; 34850  ; 8822  ; SHORT  ; 1  ; 
+Spectral sensitivity  ; SpectralSensitivity  ; 34852  ; 8824  ; ASCII  ; Any  ; 
+ISO speed rating  ; ISOSpeedRatings  ; 34855  ; 8827  ; SHORT  ; Any  ; 
+Optoelectric conversion factor  ; OECF  ; 34856  ; 8828  ; UNDEFINED  ; Any  ; 
+Shutter speed  ; ShutterSpeedValue  ; 37377  ; 9201  ; SRATIONAL  ; 1  ; 
+Aperture  ; ApertureValue  ; 37378  ; 9202  ; RATIONAL  ; 1  ; 
+Brightness  ; BrightnessValue  ; 37379  ; 9203  ; SRATIONAL  ; 1  ; 
+Exposure bias  ; ExposureBiasValue  ; 37380  ; 9204  ; SRATIONAL  ; 1  ; 
+Maximum lens aperture  ; MaxApertureValue  ; 37381  ; 9205  ; RATIONAL  ; 1  ; 
+Subject distance  ; SubjectDistance ;  37382  ; 9206  ; RATIONAL  ; 1  ; 
+Metering mode  ; MeteringMode  ; 37383  ; 9207  ; SHORT  ; 1  ; 
+Light source  ; LightSource  ; 37384  ; 9208  ; SHORT  ; 1  ; 
+Flash  ; Flash  ; 37385  ; 9209  ; SHORT  ; 1  ; 
+Lens focal length  ; FocalLength  ; 37386  ; 920A  ; RATIONAL  ; 1  ; 
+Subject area  ; SubjectArea  ; 37396  ; 9214  ; SHORT  ; 2 or 3 or 4  ; 
+Flash energy  ; FlashEnergy  ; 41483  ; A20B  ; RATIONAL  ; 1  ; 
+Spatial frequency response  ; SpatialFrequencyResponse  ; 41484  ; A20C  ; UNDEFINED  ; Any  ; 
+Focal plane X resolution  ; FocalPlaneXResolution  ; 41486  ; A20E  ; RATIONAL  ; 1  ; 
+Focal plane Y resolution  ; FocalPlaneYResolution  ; 41487  ; A20F  ; RATIONAL  ; 1  ; 
+Focal plane resolution unit  ; FocalPlaneResolutionUnit  ; 41488  ; A210  ; SHORT  ; 1  ; 
+Subject location  ; SubjectLocation  ; 41492  ; A214  ; SHORT  ; 2  ; 
+Exposure index  ; ExposureIndex  ; 41493  ; A215  ; RATIONAL  ; 1  ; 
+Sensing method  ; SensingMethod  ; 41495  ; A217  ; SHORT  ; 1  ; 
+File source  ; FileSource  ; 41728  ; A300  ; UNDEFINED  ; 1  ; 
+Scene type  ; SceneType  ; 41729  ; A301  ; UNDEFINED  ; 1  ; 
+CFA pattern  ; CFAPattern  ; 41730  ; A302  ; UNDEFINED  ; Any  ; 
+Custom image processing  ; CustomRendered  ; 41985  ; A401  ; SHORT  ; 1  ; 
+Exposure mode  ; ExposureMode  ; 41986  ; A402  ; SHORT  ; 1  ; 
+White balance  ; WhiteBalance  ; 41987  ; A403  ; SHORT  ; 1  ; 
+Digital zoom ratio  ; DigitalZoomRatio ;  41988  ; A404  ; RATIONAL  ; 1  ; 
+Focal length in 35 mm film  ; FocalLengthIn35mmFilm  ; 41989  ; A405  ; SHORT  ; 1  ; 
+Scene capture type  ; SceneCaptureType  ; 41990  ; A406  ; SHORT  ; 1  ; 
+Gain control  ; GainControl  ; 41991  ; A407  ; RATIONAL  ; 1  ; 
+Contrast  ; Contrast  ; 41992  ; A408  ; SHORT  ; 1  ; 
+Saturation  ; Saturation  ; 41993  ; A409  ; SHORT  ; 1  ; 
+Sharpness  ; Sharpness  ; 41994  ; A40A  ; SHORT  ; 1  ; 
+Device settings description  ; DeviceSettingDescription  ; 41995  ; A40B  ; UNDEFINED  ; Any  ; 
+Subject distance range  ; SubjectDistanceRange  ; 41996  ; A40C  ; SHORT  ; 1  ; 
diff --git a/standards/exif/ifdH.csv b/standards/exif/ifdH.csv
new file mode 100644 (file)
index 0000000..20ade7c
--- /dev/null
@@ -0,0 +1 @@
+Unique image ID ; ImageUniqueID ;  42016  ; A420  ; ASCII  ; 33  ; 
diff --git a/standards/exif/tiffA.csv b/standards/exif/tiffA.csv
new file mode 100644 (file)
index 0000000..2ca0ac7
--- /dev/null
@@ -0,0 +1,14 @@
+# Tags relating to image data structure
+Image width  ; ImageWidth  ; 256  ; 100  ; SHORT or LONG  ; 1  ; 
+Image height  ; ImageLength  ; 257  ; 101  ; SHORT or LONG  ; 1  ; 
+Number of bits per component  ; BitsPerSample  ; 258  ; 102  ; SHORT  ; 3  ; 
+Compression scheme  ; Compression  ; 259  ; 103  ; SHORT  ; 1  ; 
+Pixel composition  ; PhotometricInterpretation  ; 262  ; 106  ; SHORT  ; 1  ; 
+Orientation of image  ; Orientation  ; 274  ; 112  ; SHORT  ; 1  ; 
+Number of components  ; SamplesPerPixel  ; 277  ; 115  ; SHORT  ; 1  ; 
+Image data arrangement  ; PlanarConfiguration  ; 284  ; 11C  ; SHORT  ; 1  ; 
+Subsampling ratio of Y to C  ; YCbCrSubSampling  ; 530  ; 212  ; SHORT  ; 2  ; 
+Y and C positioning  ; YCbCrPositioning  ; 531  ; 213  ; SHORT  ; 1  ; 
+Image resolution in width direction  ; XResolution  ; 282  ; 11A  ; RATIONAL  ; 1  ; 
+Image resolution in height direction  ;  YResolution  ; 283  ; 11B  ; RATIONAL  ; 1  ; 
+Unit of X and Y resolution  ; ResolutionUnit  ; 296  ; 128  ; SHORT  ; 1  ; 
diff --git a/standards/exif/tiffB.csv b/standards/exif/tiffB.csv
new file mode 100644 (file)
index 0000000..bdfd08a
--- /dev/null
@@ -0,0 +1,6 @@
+# Tags relating to recording offset
+Image data location  ; StripOffsets  ; 273  ; 111  ; SHORT or LONG  ; *S  ; 
+Number of rows per strip  ; RowsPerStrip  ; 278  ; 116  ; SHORT or LONG  ; 1  ; 
+Bytes per compressed strip  ; StripByteCounts  ; 279  ; 117  ; SHORT or LONG  ; *S  ; 
+Offset to JPEG SOI  ; JPEGInterchangeFormat  ; 513  ; 201  ; LONG  ; 1  ; 
+Bytes of JPEG data  ; JPEGInterchangeFormatLength  ; 514  ; 202  ; LONG  ; 1  ; 
\ No newline at end of file
diff --git a/standards/exif/tiffC.csv b/standards/exif/tiffC.csv
new file mode 100644 (file)
index 0000000..a141392
--- /dev/null
@@ -0,0 +1,6 @@
+# Tags relating to image data characteristics
+Transfer function  ; TransferFunction  ; 301  ; 12D  ; SHORT  ; 3 * 256  ; 
+White point chromaticity  ; WhitePoint  ; 318  ; 13E  ; RATIONAL  ; 2  ; 
+Chromaticities of primaries  ; PrimaryChromaticities  ; 319  ; 13F  ; RATIONAL  ; 6  ; 
+Color space transformation matrix coefficients  ;  YCbCrCoefficients  ; 529  ; 211  ; RATIONAL  ; 3  ; 
+Pair of black and white reference values ;  ReferenceBlackWhite  ; 532  ; 214  ; RATIONAL  ; 6  ; 
diff --git a/standards/exif/tiffD.csv b/standards/exif/tiffD.csv
new file mode 100644 (file)
index 0000000..dc020a6
--- /dev/null
@@ -0,0 +1,8 @@
+# Other tags
+File change date and time  ; DateTime  ; 306  ; 132  ; ASCII  ; 20  ; 
+Image title  ; ImageDescription  ; 270  ; 10E  ; ASCII  ; Any  ; 
+Image input equipment manufacturer  ; Make  ; 271  ; 10F  ; ASCII  ; Any  ; 
+Image input equipment model  ; Model  ; 272  ; 110  ; ASCII  ; Any ; 
+Software used  ; Software  ; 305  ; 131  ; ASCII  ; Any  ; 
+Person who created the image  ; Artist  ; 315  ; 13B  ; ASCII  ; Any ; 
+Copyright holder  ; Copyright  ; 33432  ; 8298  ; ASCII  ; Any  ; 
diff --git a/standards/xmp/__init__.py b/standards/xmp/__init__.py
new file mode 100755 (executable)
index 0000000..604685f
--- /dev/null
@@ -0,0 +1,42 @@
+from os.path import join
+from Globals import package_home
+from _namespaces import namespaces
+
+home = package_home(globals())
+
+f = file(join(home, 'accessors.csv'))
+lines = f.readlines()
+f.close()
+
+accessors = {}
+accessorIds = {}
+rdfKwnowTypes = {'Seq':True, 'prop':True, 'Alt':True, 'Bag':True}
+
+prefix2Ns = dict([item[::-1] for item in namespaces.items()])
+
+for l in [l for l in lines if not l.startswith('#')] :
+       fields = [f.strip() for f in l.split(',')]
+
+       if not filter(None, fields) : continue
+       
+       cat, caption, name, root, rdfType = fields
+
+       accessor = { 'id' : name.split(':')[1]
+                          , 'root' : root
+                          , 'rdfType' : rdfType
+                          , 'namespace' : prefix2Ns.get(name.split(':')[0])
+                               }
+
+       assert not accessors.has_key(name), "Duplicate definition for %r" % name
+       assert name.count(':') <=1, "Ambiguous name %r" % name
+       assert not accessorIds.has_key(accessor['id']), "Ambiguous name: %r" % name
+       assert rdfKwnowTypes.has_key(rdfType), "Unknown rdf type: %r" % rdfType
+       if rdfType == 'prop' :
+               assert prefix2Ns.has_key(name.split(':')[0]), \
+                       "Attribute name %r don't match a known namespace prefix" % name
+       
+       accessors[name] = accessor
+       accessorIds[accessor['id']] = True
+
+
+__all__ = ('namespaces', 'prefix2Ns', 'accessors')
diff --git a/standards/xmp/_namespaces.py b/standards/xmp/_namespaces.py
new file mode 100755 (executable)
index 0000000..78d9324
--- /dev/null
@@ -0,0 +1,24 @@
+"""
+$Id: _namespaces.py 1251 2009-08-03 08:42:09Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/standards/xmp/_namespaces.py $
+"""
+namespaces = {
+        'http://purl.org/dc/elements/1.1/'                             : 'dc'
+       ,'http://ns.adobe.com/xap/1.0/'                                 : 'xmp'
+       ,'http://ns.adobe.com/xap/1.0/rights/'                  : 'xmpRights'
+       ,'http://ns.adobe.com/xap/1.0/mm/'                              : 'xmpMM'
+       ,'http://ns.adobe.com/xap/1.0/bj/'                              : 'xmpBJ'
+       ,'http://ns.adobe.com/xap/1.0/t/pg/'                    : 'xmpTPg'
+       ,'http://ns.adobe.com/xmp/1.0/DynamicMedia/'    : 'xmpDM'
+       ,'http://ns.adobe.com/pdf/1.3/'                                 : 'pdf'
+       ,'http://ns.adobe.com/photoshop/1.0/'                   : 'photoshop'
+       ,'http://ns.adobe.com/camera-raw-settings/1.0/' : 'crs'
+       ,'http://ns.adobe.com/tiff/1.0/'                                : 'tiff'
+       ,'http://ns.adobe.com/exif/1.0/'                                : 'exif'
+       ,'http://ns.adobe.com/exif/1.0/aux/'                    : 'aux'
+       ,'adobe:ns:meta/'                                                               : 'x'
+       ,'http://www.w3.org/1999/02/22-rdf-syntax-ns#'  : 'rdf'
+       ,'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'  : 'Iptc4xmpCore'
+       #'http://ns.adobe.com/xap/1.0/'                                 : 'xap'
+       #,'http://ns.adobe.com/xap/1.0/rights/'                 : 'xapRights'
+}
\ No newline at end of file
diff --git a/standards/xmp/accessors.csv b/standards/xmp/accessors.csv
new file mode 100644 (file)
index 0000000..6710d8a
--- /dev/null
@@ -0,0 +1,43 @@
+#Catégorie,Libellé,nom,root (from rdf:Description),Type RDF
+Contact,Créateur,dc:creator,,Seq
+Contact,Fonction,photoshop:AuthorsPosition,,prop
+Contact,Adresse,Iptc4xmpCore:CiAdrExtadr,Iptc4xmpCore:CreatorContactInfo,prop
+Contact,Ville,Iptc4xmpCore:CiAdrCity,Iptc4xmpCore:CreatorContactInfo,prop
+Contact,Région,Iptc4xmpCore:CiAdrRegion,Iptc4xmpCore:CreatorContactInfo,prop
+Contact,Code postal,Iptc4xmpCore:CiAdrPcode,Iptc4xmpCore:CreatorContactInfo,prop
+Contact,Pays,Iptc4xmpCore:CiAdrCtry,Iptc4xmpCore:CreatorContactInfo,prop
+Contact,Téléphone,Iptc4xmpCore:CiTelWork,Iptc4xmpCore:CreatorContactInfo,prop
+Contact,Adresse électronique,Iptc4xmpCore:CiEmailWork,Iptc4xmpCore:CreatorContactInfo,prop
+Contact,Site internet,Iptc4xmpCore:CiUrlWork,Iptc4xmpCore:CreatorContactInfo,prop
+,,,,
+Contenu,Titre,photoshop:Headline,,prop
+Contenu,Légende,dc:description,,Alt
+Contenu,Mots-clefs,dc:subject,,Bag
+Contenu,Code sujet IPTC,Iptc4xmpCore:SubjectCode,,Bag
+Contenu,Auteur de la description,photoshop:CaptionWriter,,prop
+Contenu,Catégorie,photoshop:Category,,prop
+Contenu,Autres catégories,photoshop:SupplementalCategories,,Bag
+,,,,
+Image,Date de création,photoshop:DateCreated,,prop
+Image,Catégorie intellectuelle,Iptc4xmpCore:IntellectualGenre,,prop
+Image,Scène,Iptc4xmpCore:Scene,,Bag
+Image,Emplacement,Iptc4xmpCore:Location,,prop
+Image,Ville,photoshop:City,,prop
+Image,Région,photoshop:State,,prop
+Image,Pays,photoshop:Country,,prop
+Image,Code pays ISO,Iptc4xmpCore:CountryCode,,prop
+,,,,
+État,Titre,dc:title,,Alt
+État,Identifiant de la fonction,photoshop:TransmissionReference,,prop
+État,Instructions,photoshop:Instructions,,prop
+État,Fournisseur,photoshop:Credit,,prop
+État,Source,photoshop:Source,,prop
+,,,,
+Copyright,État du copyright,xmpRights:Marked,,prop
+Copyright,Copyright,dc:rights,,Alt
+Copyright,Condit. d'utilis.,xmpRights:UsageTerms,,Alt
+Copyright,URL info copyright,xmpRights:WebStatement,,prop
+,,,,
+Exif,Date/heure origin.,exif:DateTimeOriginal,,prop
+,,,,
+Tiff,Orientation,tiff:Orientation,,prop
\ No newline at end of file
diff --git a/version.txt b/version.txt
new file mode 100755 (executable)
index 0000000..9f55b2c
--- /dev/null
@@ -0,0 +1 @@
+3.0
diff --git a/xmp.py b/xmp.py
new file mode 100755 (executable)
index 0000000..da36695
--- /dev/null
+++ b/xmp.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright © 2008  Benoît PIN <benoit.pin@ensmp.fr>                                #
+#                                                                                     #
+#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+# $Id: xmp.py 354 2008-02-13 13:30:53Z pin $
+# $URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/xmp.py $
+
+from types import StringTypes
+from logging import getLogger
+import re
+console = getLogger('Photo.xmp')
+
+class XMP(object) :
+       XMP_HEADER = u'<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?>'
+       XMP_HEADER_PATTERN = u'''<\?xpacket begin=['"]\ufeff['"] id=['"]W5M0MpCehiHzreSzNTczkc9d['"][^\?]*\?>'''
+       XMP_PADDING_LINE = u'\u0020' * 63 + u'\n'
+       XMP_TRAILER = u'<?xpacket end="w"?>'
+
+       _readers = {}
+       _writers = {}
+
+       
+       
+       def __init__(self, file, content_type='image/jpeg', encoding='utf-8') :
+               try :
+                       self.reader = self._readers[content_type]
+               except KeyError:
+                       raise NotImplementedError, "%r content type not supported by XMP" % content_type
+               
+               try :
+                       self.writer = self._writers[content_type]
+               except KeyError :
+                       self.writer = None
+                       console.info('XMP file opened on read-only mode.')
+               
+               self.file = file
+               self.encoding = encoding
+               self.xmp = None
+               self._open()
+       
+
+       def __del__(self) :
+               try :
+                       self.file.close()
+               except :
+                       pass
+       
+
+       def _open(self) :
+
+               if type(self.file) in StringTypes :
+                       self.file = file(self.file)
+
+               packet = self.reader(self.file)
+               
+               if packet is not None :
+                       # tests / unwrap
+                       reEncodedHeader = re.compile(self.XMP_HEADER_PATTERN.encode(self.encoding))
+                       m = reEncodedHeader.match(packet)
+                       assert m is not None, "No xmp header found"
+                       xmp = packet[m.end():]
+
+                       trailer = self.XMP_TRAILER[:-6].encode(self.encoding)  # TODO handle read-only mode
+                       trailerPos = xmp.find(trailer)
+                       assert trailerPos != -1, "No xmp trailer found"
+               
+                       xmp = xmp[:trailerPos]
+                       xmp = xmp.strip()
+                       self.xmp = xmp
+               else :
+                       self.xmp = None
+       
+       def save(self, f=None):
+               original = self.file
+               if f :
+                       if type(f) in StringTypes :
+                               new = file(f, 'w')
+                       else :
+                               new = f
+               elif f is None :
+                       new = self.file
+               
+               self.writer(original, new, self.xmp)
+                               
+       
+       def getXMP(self) :
+               return self.xmp
+       
+       
+       def setXMP(self, xmp) :
+               self.xmp = xmp
+
+       #
+       # xmp utils
+       #
+       
+       @staticmethod
+       def getXmpPadding(size) :
+               # size of trailer in kB
+               return (XMP.XMP_PADDING_LINE * 32 * size)
+       
+       
+       @staticmethod
+       def genXMPPacket(uXmpData, paddingSize):
+               packet = u''
+
+               packet += XMP.XMP_HEADER
+               packet += uXmpData
+               packet += XMP.getXmpPadding(paddingSize)
+               packet += XMP.XMP_TRAILER
+
+               return packet
+       
+
+       
+       #
+       # content type registry stuff
+       #
+       
+               
+       @classmethod
+       def registerReader(cls, content_type, reader) :
+               cls._readers[content_type] = reader
+
+       @classmethod
+       def registerWriter(cls, content_type, writer) :
+               cls._writers[content_type] = writer
+       
+       @classmethod
+       def registerWrapper(cls, content_type, wrapper) :
+               """ Registers specific wrapper to prepare data
+                       for embedding xmp into specific content_type file.
+               """
+               pass
+
+
+
+def test() :
+       from xml.dom.minidom import parse
+       data = parse('new.xmp').documentElement.toxml()
+
+       def test1() :
+               original = 'original.jpg'
+               modified = 'modified.jpg'
+       
+               x = XMP(original)
+               x.setXMP(data)
+               x.save(modified)
+
+       def test2() :
+               from cStringIO import StringIO
+               sio = StringIO()
+               sio.write(file('modified.jpg').read())
+               sio.seek(0)
+
+               x = XMP(sio)
+               x.setXMP(data)
+               x.save()
+               
+               f2 = open('modified2.jpg', 'w')
+               f2.write(sio.read())
+               f2.close()
+       
+       
+       test1()
+       test2()
+               
+
+               
+if __name__ == '__main__' :
+       test()
diff --git a/xmp_jpeg.py b/xmp_jpeg.py
new file mode 100755 (executable)
index 0000000..5c894ae
--- /dev/null
@@ -0,0 +1,256 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright (C) 2008  Benoît PIN <benoit.pin@ensmp.fr>                              #
+#                                                                                     #
+#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+""" Jpeg plugin for xmp read/write support.
+$Id: xmp_jpeg.py 999 2009-05-11 14:43:44Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/xmp_jpeg.py $
+"""
+
+from xmp import XMP
+from types import StringType
+
+class JpegXmpIO(object):
+               
+       JPEG_XMP_LEADIN = 'http://ns.adobe.com/xap/1.0/\x00'
+       JPEG_XMP_LEADIN_LENGTH = len(JPEG_XMP_LEADIN)
+               
+       MARKERS = {
+               0xFFC0: ("SOF0",        "Baseline DCT", True),
+               0xFFC1: ("SOF1",        "Extended Sequential DCT", True),
+               0xFFC2: ("SOF2",        "Progressive DCT", True),
+               0xFFC3: ("SOF3",        "Spatial lossless", True),
+               0xFFC4: ("DHT",         "Define Huffman table", True),
+               0xFFC5: ("SOF5",        "Differential sequential DCT", True),
+               0xFFC6: ("SOF6",        "Differential progressive DCT", True),
+               0xFFC7: ("SOF7",        "Differential spatial", True),
+               0xFFC8: ("JPG",         "Extension", False),
+               0xFFC9: ("SOF9",        "Extended sequential DCT (AC)", True),
+               0xFFCA: ("SOF10",       "Progressive DCT (AC)", True),
+               0xFFCB: ("SOF11",       "Spatial lossless DCT (AC)", True),
+               0xFFCC: ("DAC",         "Define arithmetic coding conditioning", True),
+               0xFFCD: ("SOF13",       "Differential sequential DCT (AC)", True),
+               0xFFCE: ("SOF14",       "Differential progressive DCT (AC)", True),
+               0xFFCF: ("SOF15",       "Differential spatial (AC)", True),
+               0xFFD0: ("RST0",        "Restart 0", False),
+               0xFFD1: ("RST1",        "Restart 1", False),
+               0xFFD2: ("RST2",        "Restart 2", False),
+               0xFFD3: ("RST3",        "Restart 3", False),
+               0xFFD4: ("RST4",        "Restart 4", False),
+               0xFFD5: ("RST5",        "Restart 5", False),
+               0xFFD6: ("RST6",        "Restart 6", False),
+               0xFFD7: ("RST7",        "Restart 7", False),
+               0xFFD8: ("SOI",         "Start of image", False),
+               0xFFD9: ("EOI",         "End of image", False),
+               0xFFDA: ("SOS",         "Start of scan", True),
+               0xFFDB: ("DQT",         "Define quantization table", True),
+               0xFFDC: ("DNL",         "Define number of lines", True),
+               0xFFDD: ("DRI",         "Define restart interval", True),
+               0xFFDE: ("DHP",         "Define hierarchical progression", True),
+               0xFFDF: ("EXP",         "Expand reference component", True),
+               0xFFE0: ("APP0",        "Application segment 0", True),
+               0xFFE1: ("APP1",        "Application segment 1", True),
+               0xFFE2: ("APP2",        "Application segment 2", True),
+               0xFFE3: ("APP3",        "Application segment 3", True),
+               0xFFE4: ("APP4",        "Application segment 4", True),
+               0xFFE5: ("APP5",        "Application segment 5", True),
+               0xFFE6: ("APP6",        "Application segment 6", True),
+               0xFFE7: ("APP7",        "Application segment 7", True),
+               0xFFE8: ("APP8",        "Application segment 8", True),
+               0xFFE9: ("APP9",        "Application segment 9", True),
+               0xFFEA: ("APP10",       "Application segment 10", True),
+               0xFFEB: ("APP11",       "Application segment 11", True),
+               0xFFEC: ("APP12",       "Application segment 12", True),
+               0xFFED: ("APP13",       "Application segment 13", True),
+               0xFFEE: ("APP14",       "Application segment 14", True),
+               0xFFEF: ("APP15",       "Application segment 15", True),
+               0xFFF0: ("JPG0",        "Extension 0", False),
+               0xFFF1: ("JPG1",        "Extension 1", False),
+               0xFFF2: ("JPG2",        "Extension 2", False),
+               0xFFF3: ("JPG3",        "Extension 3", False),
+               0xFFF4: ("JPG4",        "Extension 4", False),
+               0xFFF5: ("JPG5",        "Extension 5", False),
+               0xFFF6: ("JPG6",        "Extension 6", False),
+               0xFFF7: ("JPG7",        "Extension 7", False),
+               0xFFF8: ("JPG8",        "Extension 8", False),
+               0xFFF9: ("JPG9",        "Extension 9", False),
+               0xFFFA: ("JPG10",       "Extension 10", False),
+               0xFFFB: ("JPG11",       "Extension 11", False),
+               0xFFFC: ("JPG12",       "Extension 12", False),
+               0xFFFD: ("JPG13",       "Extension 13", False),
+               0xFFFE: ("COM",         "Comment", True)
+       }
+               
+               
+       @staticmethod
+       def i16(c,o=0):
+               return ord(c[o+1]) + (ord(c[o])<<8)
+       
+       @staticmethod
+       def getBlockInfo(marker, f):
+               start = f.tell()
+               length = JpegXmpIO.i16(f.read(2))
+               
+               markerInfo = JpegXmpIO.MARKERS[marker]
+               blockInfo = { 'name' : markerInfo[0]
+                                       , 'description' : markerInfo[1]
+                                       , 'start' : start
+                                       , 'length' : length}
+               
+               jump = start + length
+               f.seek(jump)
+               
+               return blockInfo
+       
+       @staticmethod
+       def getBlockInfos(f) :
+               f.seek(0)
+               s  = f.read(1)
+               
+               blockInfos = []
+               
+               while 1:
+                       s = s + f.read(1)
+                       i = JpegXmpIO.i16(s)
+                       
+                       if JpegXmpIO.MARKERS.has_key(i):
+                               name, desciption, handle = JpegXmpIO.MARKERS[i]
+                               
+                               if handle:
+                                       blockInfo = JpegXmpIO.getBlockInfo(i, f)
+                                       blockInfos.append(blockInfo)
+                               if i == 0xFFDA: # start of scan
+                                  break
+                               s = f.read(1)
+                       elif i == 0 or i == 65535:
+                               # padded marker or junk; move on
+                               s = "\xff"
+               
+               return blockInfos
+       
+       
+       @staticmethod
+       def genJpegXmpBlock(uXmpData, paddingSize=2) :
+               block = u''
+
+               block += JpegXmpIO.JPEG_XMP_LEADIN
+               block += XMP.genXMPPacket(uXmpData, paddingSize)
+               # utf-8 mandatory in jpeg files (xmp specification)
+               block = block.encode('utf-8')
+
+               length = len(block) + 2
+
+               # TODO : reduce padding size if this assertion occurs
+               assert length <= 0xfffd, "Jpeg block too long: %d (max: 0xfffd)" % hex(length)
+
+               chrlength = chr(length >> 8 & 0xff) + chr(length & 0xff)
+
+               block = chrlength + block
+
+               return block
+       
+
+
+       @staticmethod
+       def read(f) :
+
+               blockInfos = JpegXmpIO.getBlockInfos(f)
+               app1BlockInfos = [b for b in blockInfos if b['name'] == 'APP1']
+
+               xmpBlocks = []
+
+               for info in app1BlockInfos :
+                       f.seek(info['start'])
+                       data = f.read(info['length'])[2:]
+                       if data.startswith(JpegXmpIO.JPEG_XMP_LEADIN) :
+                               xmpBlocks.append(data)
+
+               assert len(xmpBlocks) <= 1, "Multiple xmp block data is not yet supported."
+
+               if len(xmpBlocks) == 1 :
+                       data = xmpBlocks[0]
+                       packet = data[len(JpegXmpIO.JPEG_XMP_LEADIN):]
+                       return packet
+               else :
+                       return None
+       
+       @staticmethod
+       def write(original, new, uxmp) :
+               
+               blockInfos = JpegXmpIO.getBlockInfos(original)
+               app1BlockInfos = [b for b in blockInfos if b['name'] == 'APP1']
+
+               xmpBlockInfos = []
+
+               for info in app1BlockInfos :
+                       original.seek(info['start'])
+                       lead = original.read(JpegXmpIO.JPEG_XMP_LEADIN_LENGTH+2)[2:]
+                       if lead == JpegXmpIO.JPEG_XMP_LEADIN :
+                               xmpBlockInfos.append(info)
+
+
+               assert len(xmpBlockInfos) <= 1, "Multiple xmp block data is not yet supported."
+
+               if isinstance(uxmp, StringType) :
+                       uxmp = unicode(uxmp, 'utf-8')
+                       
+               if len(xmpBlockInfos) == 0 :
+                       blockInfo = [b for b in blockInfos if b['name'] == 'APP13']
+
+                       if not blockInfo :
+                               blockInfo = [b for b in blockInfos if b['name'] == 'APP1']
+
+                       if not blockInfo :
+                               blockInfo = [b for b in blockInfos if b['name'] == 'APP0']
+                       
+                       if not blockInfo : raise ValueError, "No suitable place to write xmp segment"
+                       
+                       info = blockInfo[0]
+                       print 'create xmp after: %s' % info['name']
+                       
+                       original.seek(0)
+                       before = original.read(info['start'] + info['length'])
+                       after = original.read()
+                       
+                       jpegBlock = '\xFF\xE1' + JpegXmpIO.genJpegXmpBlock(uxmp)
+
+               else :
+                       info = xmpBlockInfos[0]
+
+                       original.seek(0)
+                       before = original.read(info['start'])
+
+                       original.seek(info['start'] + info['length'])
+                       after = original.read()
+
+                       jpegBlock = JpegXmpIO.genJpegXmpBlock(uxmp)
+               
+               new.seek(0)
+               new.write(before)
+               new.write(jpegBlock)
+               new.write(after)
+
+               # if original == new :
+               #       new.seek(0)
+               # else :
+               #       new.close()
+               #       original.close()
+
+
+XMP.registerReader('image/jpeg', JpegXmpIO.read)
+XMP.registerWriter('image/jpeg', JpegXmpIO.write)
diff --git a/xmputils.py b/xmputils.py
new file mode 100755 (executable)
index 0000000..4fd24bf
--- /dev/null
@@ -0,0 +1,354 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright (C) 2008 Benoît PIN <benoit.pin@ensmp.fr>                               #
+#                                                                                     #
+#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+""" XMP generation utilities.
+
+$Id: xmputils.py 1293 2009-08-14 16:48:18Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/xmputils.py $
+"""
+
+from libxml2 import newNode, parseDoc, treeError
+# prefix <-> namespaces mappings as defined in the official xmp documentation
+from standards.xmp import namespaces as xmpNs2Prefix
+from standards.xmp import prefix2Ns as xmpPrefix2Ns
+
+TIFF_ORIENTATIONS = {1 : (0, False)
+                                       ,2 : (0, True)
+                                       ,3 : (180, False)
+                                       ,4 : (180, True)
+                                       ,5 : (90, True)
+                                       ,6 : (90, False)
+                                       ,7 : (270, True)
+                                       ,8 : (270, False)}
+
+def _getRDFArrayValues(node, arrayType):
+       values = []
+       for element in iterElementChilds(node):
+               if element.name == arrayType and element.ns().content == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' :
+                       for value in iterElementChilds(element):
+                               if value.name == 'li':
+                                       values.append(value.content)
+                       return tuple(values)
+       else :
+               raise ValueError("No %s found" % arrayType )
+
+def getBagValues(node):
+       return _getRDFArrayValues(node, 'Bag')
+
+def getSeqValues(node):
+       return _getRDFArrayValues(node, 'Seq')
+
+
+def createRDFAlt(surrounded, defaultText, rootIndex):
+       """
+       returns (as libxml2 node):
+       <surrounded>
+          <rdf:Alt>
+             <rdf:li xml:lang="x-default">defaultText</rdf:li>
+          </rdf:Alt>
+       <surrounded>
+       """
+       docNs = rootIndex.getDocumentNs()
+       rdfPrefix = docNs['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
+       normalizedPrefix, name = surrounded.split(':')
+       ns = xmpPrefix2Ns[normalizedPrefix]
+       actualPrefix = docNs[ns]
+       
+       surrounded = newNode('%s:%s' % (actualPrefix, name))
+       alt = newNode('%s:Alt' % rdfPrefix)
+       li = newNode('%s:li' % rdfPrefix)
+       li.newProp('xml:lang', 'x-default')
+       li.setContent(defaultText)
+       
+       reduce(lambda a, b: a.addChild(b), (surrounded, alt, li))
+       
+       return surrounded
+
+
+def createRDFBag(surrounded, values, rootIndex):
+       """
+       returns (as libxml2 node):
+       <surrounded>
+       <rdf:Bag>
+          <rdf:li>values[0]</rdf:li>
+                       ...
+          <rdf:li>values[n]</rdf:li>
+       </rdf:Bag>
+    <surrounded>
+    """
+       return _createRDFArray(surrounded, values, False, rootIndex)
+
+def createRDFSeq(surrounded, values, rootIndex):
+       """
+       returns (as libxml2 node):
+       <surrounded>
+       <rdf:Seq>
+          <rdf:li>values[0]</rdf:li>
+                       ...
+          <rdf:li>values[n]</rdf:li>
+       </rdf:Seq>
+    <surrounded>
+    """
+       return _createRDFArray(surrounded, values, True, rootIndex)
+
+def _createRDFArray(surrounded, values, ordered, rootIndex):
+       docNs = rootIndex.getDocumentNs()
+       rdfPrefix = docNs['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
+       normalizedPrefix, name = surrounded.split(':')
+       ns = xmpPrefix2Ns[normalizedPrefix]
+       actualPrefix = docNs[ns]
+       
+       
+       surrounded = newNode('%s:%s' % (actualPrefix, name))
+       if ordered is True :
+               array = newNode('%s:Seq' % rdfPrefix)
+       elif ordered is False :
+               array = newNode('%s:Bag' % rdfPrefix)
+       else :
+               raise ValueError("'ordered' parameter must be a boolean value")
+       
+       surrounded.addChild(array)
+       
+       for v in values :
+               li = newNode('%s:li' % rdfPrefix)
+               li.setContent(v)
+               array.addChild(li)
+       
+       return surrounded
+
+def createEmptyXmpDoc() :
+       emptyDocument = """
+<x:xmpmeta xmlns:x='adobe:ns:meta/'>
+  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+    <rdf:Description/>
+  </rdf:RDF>
+</x:xmpmeta>
+       """
+       d = parseDoc(emptyDocument)
+       return d
+
+def getPathIndex(doc) :
+       root = doc.getRootElement()
+       index = PathIndex(root)
+       return index
+
+
+class PathIndex :
+       """\
+       Class used to provide a convenient tree access to xmp properties by paths.
+       Issues about namespaces and prefixes are normalized during the object
+       instanciation. Ns prefixes used to access elements are those recommended in the
+       official xmp documentation from Adobe.
+       """
+       
+       def __init__(self, element, parent=None) :
+               self.unique = True
+               self.element = element
+               self.parent = parent
+               
+               elementNs = element.ns().content
+               elementPrefix = element.ns().name
+               recommendedPrefix = xmpNs2Prefix.get(elementNs, elementPrefix)
+
+               self.name = '%s:%s' % (recommendedPrefix, element.name)
+               self.namespace = elementNs
+               self.prefix = elementPrefix
+               self._index = {}
+               
+               for prop in iterElementProperties(element) :
+                       self.addChildIndex(prop)
+               
+               for child in iterElementChilds(element) :
+                       self.addChildIndex(child)
+               
+               if self.parent is None:
+                       self.nsDeclarations = self._namespaceDeclarations()
+               
+       def addChildIndex(self, child) :
+               ns = child.ns()
+               if not ns :
+                       return
+                       
+               childNs = ns.content
+               childPrefix = ns.name
+               childRecommendedPrefix = xmpNs2Prefix.get(childNs, childPrefix)
+               childName = '%s:%s' % (childRecommendedPrefix, child.name)
+
+               if not self._index.has_key(childName) :
+                       self._index[childName] = PathIndex(child, parent=self)
+               else :
+                       childIndex = self._index[childName]
+                       childIndex.unique = False
+                       for prop in iterElementProperties(child) :
+                               childIndex.addChildIndex(prop)
+
+                       for c in iterElementChilds(child) :
+                               childIndex.addChildIndex(c)
+
+               self._index[childName].parent = self
+               return self._index[childName]
+       
+       def _namespaceDeclarations(self) :
+               """\
+               returns ns / prefix pairs as found in xmp packet
+               """
+               namespaces = {}
+               namespaces[self.namespace] = self.prefix
+               for child in self._index.values() :
+                       for namespace, prefix in child._namespaceDeclarations().items() :
+                               if namespaces.has_key(namespace) :
+                                       assert namespaces[namespace] == prefix, \
+                                                       "using several prefix for the same namespace is forbidden "\
+                                                       "in this implementation"
+                               else :
+                                       namespaces[namespace] = prefix
+               return namespaces
+       
+       def getDocumentNs(self) :
+               root = self.getRootIndex()
+               return root.nsDeclarations
+       
+       def exists(self, path) :
+               o = self
+               for part in path.split('/') :
+                       if o._index.has_key(part) :
+                               o = o._index[part]
+                       else :
+                               return False
+               return True
+       
+       def __getitem__(self, path) :
+               o = self
+               try :
+                       for part in path.split('/') :
+                               if part == '.' :
+                                       continue
+                               elif part == '..' :
+                                       o = o.parent
+                               o = o._index[part]
+               except ValueError :
+                       raise KeyError, path
+               return o
+       
+       def get(self, path, default=None) :
+               try :
+                       return self[path]
+               except KeyError :
+                       return default
+       
+       def getRootIndex(self) :
+               root = self
+               while root.parent is not None :
+                       root = root.parent
+               return root
+       
+       def createChildAndIndex(self, name, rdfType, nsDeclarationElement) :
+               recommandedPrefix, name = name.split(':', 1)
+
+               if rdfType == 'prop' :
+                       try :
+                               node = self.element.newProp(name, '')
+                       except treeError :
+                               raise ValueError, (self.element, name)
+               else :
+                       node = newNode(name)
+                       self.element.addChild(node)
+               
+               # bind namespace to new node
+               uri = xmpPrefix2Ns[recommandedPrefix]
+               docNamespaces = self.getDocumentNs()
+               if not docNamespaces.has_key(uri) :
+                       try :
+                               ns = nsDeclarationElement.newNs(uri, recommandedPrefix)
+                       except treeError :
+                               raise ValueError, (uri, prefix, self.element, list(nsDeclarationElement.nsDefs()))
+                       docNamespaces[uri] = recommandedPrefix
+               else :
+                       actualPrefix = docNamespaces[uri]
+                       try :
+                               ns = self.element.searchNs(None, actualPrefix)
+                       except treeError:
+                               # cas d'un xmp verbeux : le nouvel élément n'est pas ajouté
+                               # dans le rdf:Description du ns correspondant
+                               # (après tout, ce n'est pas une obligation)
+                               # => on ajoute le ns
+                               ns = nsDeclarationElement.newNs(uri, actualPrefix)
+                       
+               
+               node.setNs(ns)
+               return self.addChildIndex(node)
+
+       def getOrCreate(self, path, rdfType, preferedNsDeclaration='rdf:RDF/rdf:Description') :
+               parts = path.split('/')
+
+               if not parts :
+                       return self
+
+               name = parts[-1]
+               parts = parts[:-1]
+               root = self.getRootIndex()
+               nsDeclarationElement = root[preferedNsDeclaration].element
+               
+               parent = self
+               for p in parts :
+                       child = parent._index.get(p, None)
+                       if child is None :
+                               child = parent.createChildAndIndex(p, None, nsDeclarationElement)
+                       parent = child
+               
+               child = parent._index.get(name, None)
+               if child is None :
+                       child = parent.createChildAndIndex(name, rdfType, nsDeclarationElement)
+
+               return child
+               
+       def __str__(self) :
+               out = []
+               pr = out.append
+               path = [self.name]
+               parent = self.parent
+               while parent :
+                       path.append(parent.name)
+                       parent = parent.parent
+               path.reverse()
+               path = '/'.join(path)
+               pr(path)
+               pr(self.name)
+               pr(self.namespace)
+               pr(str(self.unique))
+               pr('-------')
+               
+               for child in self._index.values() :
+                       pr(str(child))
+
+               return '\n'.join(out)
+
+def iterElementChilds(parent) :
+       child  = parent.children
+       while child :
+               if child.type == 'element' :
+                       yield child
+               child = child.next
+
+def iterElementProperties(element) :
+       prop = element.properties
+       while prop :
+               if prop.type == 'attribute' :
+                       yield prop
+               prop = prop.next
diff --git a/xslt/xmp_merge_descriptions.xsl b/xslt/xmp_merge_descriptions.xsl
new file mode 100644 (file)
index 0000000..5d7ddd8
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<xsl:stylesheet version="1.0"
+               xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+               xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+               xmlns:x="adobe:ns:meta/"
+               xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
+               xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
+               xmlns:xmpDM="http://ns.adobe.com/xmp/1.0/DynamicMedia/"
+               xmlns:xapRights="http://ns.adobe.com/xap/1.0/rights/"
+               xmlns:exif="http://ns.adobe.com/exif/1.0/"
+               xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/"
+               xmlns:xap="http://ns.adobe.com/xap/1.0/"
+               xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
+               xmlns:dc="http://purl.org/dc/elements/1.1/"
+               xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
+               xmlns:xmpBJ="http://ns.adobe.com/xap/1.0/bj/"
+               xmlns:aux="http://ns.adobe.com/exif/1.0/aux/"
+               xmlns:xmpRights="http://ns.adobe.com/xap/1.0/rights/"
+               xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
+               xmlns:pdf="http://ns.adobe.com/pdf/1.3/"
+               xmlns:xmp="http://ns.adobe.com/xap/1.0/">
+
+  <xsl:output encoding="UTF-8" indent="yes" method="xml" />
+
+  <xsl:template match="*">
+    <xsl:element name="{name(.)}">
+       <xsl:for-each select="@*">
+        <xsl:attribute name="{name(.)}">
+          <xsl:value-of select="."/>
+        </xsl:attribute>
+       </xsl:for-each>
+       <xsl:choose>
+        <xsl:when test="name(.) = 'rdf:RDF' and count(rdf:Description)&gt;1">
+          <xsl:call-template name="fuse-descriptions" />
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:apply-templates />
+        </xsl:otherwise>
+       </xsl:choose>
+    </xsl:element>
+  </xsl:template>
+
+  <xsl:template name="fuse-descriptions">
+    <rdf:Description rdf:about="{rdf:Description[1]/rdf:about}">
+       <xsl:for-each select="rdf:Description">
+         <xsl:for-each select="@*[name(.)!='rdf:about']">
+           <xsl:attribute name="{name(.)}">
+             <xsl:value-of select="."/>
+           </xsl:attribute>
+         </xsl:for-each>
+         <xsl:copy-of select="*"/>
+       </xsl:for-each>
+    </rdf:Description>
+  </xsl:template>
+
+</xsl:stylesheet>