From: Benoît Pin <benoit.pin@gmail.com>
Date: Fri, 22 Oct 2010 16:12:20 +0000 (+0200)
Subject: Ajout du produit, sur la base du dépôt luxia r1390 :
X-Git-Url: https://scm.cri.minesparis.psl.eu/git/Photo.git/commitdiff_plain/a18ca54d896fa2f98cf2b7fb8955120ff5e0aa37?ds=sidebyside

Ajout du produit, sur la base du dépôt luxia r1390 :

URL : http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk
Racine du dépôt : http://svn.luxia.fr/svn/labo
UUID du dépôt : 7eb47c9a-6e02-46bb-968b-2b2bf1974b8d
Révision : 1390
Type de nœud : répertoire
Tâche programmée : normale
Auteur de la dernière modification : pin
Révision de la dernière modification : 1371
Date de la dernière modification: 2009-09-10 19:58:28 +0200 (Jeu 10 sep 2009)
---

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