]> CRI, Mines Paris - PSL - Photo.git/blobdiff - Products/Photo/Photo.py
eggification
[Photo.git] / Products / Photo / Photo.py
diff --git a/Products/Photo/Photo.py b/Products/Photo/Photo.py
new file mode 100755 (executable)
index 0000000..787e4ee
--- /dev/null
@@ -0,0 +1,399 @@
+# -*- 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
+
+
+
+"""
+
+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
+               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', 180)
+               self.prop_filter = kw.get('prop_filter', 'ANTIALIAS')
+               super(Photo, self).__init__(id, title, file, content_type='', precondition='')
+
+               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.get(orientation, (0, False))
+               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())