X-Git-Url: https://scm.cri.minesparis.psl.eu/git/Photo.git/blobdiff_plain/b0a7e10b4f32cf74864bb53268ca4d3080f23bc0..6c41809185e322ce2d30e98234f71144f78f06c0:/Products/Photo/Photo.py?ds=inline diff --git a/Products/Photo/Photo.py b/Products/Photo/Photo.py new file mode 100755 index 0000000..787e4ee --- /dev/null +++ b/Products/Photo/Photo.py @@ -0,0 +1,399 @@ +# -*- coding: utf-8 -*- +####################################################################################### +# Photo is a part of Plinn - http://plinn.org # +# Copyright (C) 2004-2007 Benoît PIN # +# # +# 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 '%d%d' % 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())