--- /dev/null
+# -*- 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())
--- /dev/null
+Migration vers nouvelle version
+
+Attributs à virer :
+— _variants
+— _metadata
+
+Attributs à ajouter
+— self._methodResultsCache = OOBTree()
\ No newline at end of file
--- /dev/null
+# -*- 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)
+
--- /dev/null
+# -*- 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'
+ )
--- /dev/null
+# -*- 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)
--- /dev/null
+# -*- 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)
--- /dev/null
+PIL - Python Imaging Library - 1.1.4 or later
+http://www.pythonware.com/products/pil/
--- /dev/null
+<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>
+
--- /dev/null
+<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>
+
--- /dev/null
+<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>
+
--- /dev/null
+<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>
--- /dev/null
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<p>
+ <dtml-var tag>
+</p>
+
+<dtml-var manage_page_footer>
+
--- /dev/null
+<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>
--- /dev/null
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<p>
+ <img src="getJpegImage">
+</p>
+
+<dtml-var manage_page_footer>
+
--- /dev/null
+<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">
+ © 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>
+<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 " />
+
+</form>
+</div>
+</td>
+</tr>
+</table>
+</dtml-unless>
+
+<dtml-var manage_page_footer>
+
--- /dev/null
+# -*- 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')
--- /dev/null
+ 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.
--- /dev/null
+# -*- 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)
--- /dev/null
+#
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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()
--- /dev/null
+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
--- /dev/null
+#
\ No newline at end of file
--- /dev/null
+from _bridges import xmp2exif, exif2xmp
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+#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
--- /dev/null
+# 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 ;
--- /dev/null
+# 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 ;
--- /dev/null
+# 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 ;
--- /dev/null
+# 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 ;
--- /dev/null
+from _exif_tags import TAGS, TAG_TYPES
--- /dev/null
+""" 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)
--- /dev/null
+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 ;
--- /dev/null
+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 ;
--- /dev/null
+Exif version ; ExifVersion ; 36864 ; 9000 ; UNDEFINED ; 4 ;
+Supported Flashpix version ; FlashpixVersion ; 40960 ; A000 ; UNDEFINED ; 4 ;
--- /dev/null
+Color space information ; ColorSpace ; 40961 ; A001 ; SHORT ; 1 ;
--- /dev/null
+ 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 ;
--- /dev/null
+Manufacturer notes ; MakerNote ; 37500 ; 927C ; UNDEFINED ; Any ;
+User comments ; UserComment ; 37510 ; 9286 ; UNDEFINED ; Any ;
--- /dev/null
+Related audio file ; RelatedSoundFile ; 40964 ; A004 ; ASCII ; 13 ;
--- /dev/null
+ 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 ;
--- /dev/null
+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 ;
--- /dev/null
+Unique image ID ; ImageUniqueID ; 42016 ; A420 ; ASCII ; 33 ;
--- /dev/null
+# 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 ;
--- /dev/null
+# 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
--- /dev/null
+# 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 ;
--- /dev/null
+# 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 ;
--- /dev/null
+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')
--- /dev/null
+"""
+$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
--- /dev/null
+#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
--- /dev/null
+# -*- 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()
--- /dev/null
+# -*- 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)
--- /dev/null
+# -*- 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
--- /dev/null
+<?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)>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>