Back to index

plone3  3.1.7
imagetransform.py
Go to the documentation of this file.
00001 #  ATContentTypes http://plone.org/products/atcontenttypes/
00002 #  Archetypes reimplementation of the CMF core types
00003 #  Copyright (c) 2003-2006 AT Content Types development team
00004 #
00005 #  This program is free software; you can redistribute it and/or modify
00006 #  it under the terms of the GNU General Public License as published by
00007 #  the Free Software Foundation; either version 2 of the License, or
00008 #  (at your option) any later version.
00009 #
00010 #  This program is distributed in the hope that it will be useful,
00011 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
00012 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00013 #  GNU General Public License for more details.
00014 #
00015 #  You should have received a copy of the GNU General Public License
00016 #  along with this program; if not, write to the Free Software
00017 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
00018 #
00019 """Image transformation code
00020 
00021 The basics for the EXIF information, orientation code and the rotation code
00022 were taken from CMFPhoto.
00023 """
00024 __author__  = 'Christian Heimes <tiran@cheimes.de>'
00025 __docformat__ = 'restructuredtext'
00026 
00027 import logging
00028 from cStringIO import StringIO
00029 
00030 from Products.CMFCore.permissions import View
00031 from Products.CMFCore.permissions import ModifyPortalContent
00032 from AccessControl import ClassSecurityInfo
00033 from ExtensionClass import Base
00034 from DateTime import DateTime
00035 from Globals import InitializeClass
00036 from OFS.Image import Image as OFSImage
00037 from OFS.Image import Pdata
00038 
00039 from Products.ATContentTypes.configuration import zconf
00040 from Products.ATContentTypes.config import HAS_PIL
00041 from Products.ATContentTypes import ATCTMessageFactory as _
00042 
00043 # third party extension
00044 import exif
00045 
00046 # the following code is based on the rotation code of Photo
00047 if HAS_PIL:
00048     import PIL.Image
00049 
00050 LOG = logging.getLogger('ATCT.image')
00051 
00052 # transpose constants, taken from PIL.Image to maintain compatibilty
00053 FLIP_LEFT_RIGHT = 0
00054 FLIP_TOP_BOTTOM = 1
00055 ROTATE_90 = 2
00056 ROTATE_180 = 3
00057 ROTATE_270 = 4
00058 
00059 
00060 TRANSPOSE_MAP = {
00061     FLIP_LEFT_RIGHT : _(u'Flip around vertical axis'),
00062     FLIP_TOP_BOTTOM : _(u'Flip around horizontal axis'),
00063     ROTATE_270      : _(u'Rotate 90 clockwise'),
00064     ROTATE_180      : _(u'Rotate 180'),
00065     ROTATE_90       : _(u'Rotate 90 counterclockwise'),
00066    }
00067    
00068 AUTO_ROTATE_MAP = {
00069     0   : None,
00070     90  : ROTATE_270,
00071     180 : ROTATE_180,
00072     270 : ROTATE_90,
00073     }
00074 
00075 
00076 
00077 class ATCTImageTransform(Base):
00078     """Base class for images containing transformation and exif functions
00079     
00080     * exif information
00081     * image rotation
00082     """
00083     
00084     actions = (
00085         {
00086         'id'          : 'transform',
00087         'name'        : 'Transform',
00088         'action'      : 'string:${object_url}/atct_image_transform',
00089         'permissions' : (ModifyPortalContent,),
00090         'condition'   : 'object/hasPIL',
00091          },
00092         )
00093     
00094     security = ClassSecurityInfo()
00095 
00096     security.declarePrivate('getImageAsFile')
00097     def getImageAsFile(self, img=None, scale=None):
00098         """Get the img as file like object
00099         """
00100         if img is None:
00101             f = self.getField('image')
00102             img = f.getScale(self, scale)
00103         # img.data contains the image as string or Pdata chain
00104         data = None
00105         if isinstance(img, OFSImage):
00106             data = str(img.data)
00107         elif isinstance(img, Pdata):
00108             data = str(img)
00109         elif isinstance(img, str):
00110             data = img
00111         elif isinstance(img, file) or (hasattr(img, 'read') and
00112           hasattr(img, 'seek')):
00113             img.seek(0)
00114             return img
00115         if data:
00116             return StringIO(data)
00117         else:
00118             return None
00119 
00120     # image related code like exif and rotation
00121     # partly based on CMFPhoto
00122     
00123     security.declareProtected(View, 'getEXIF')
00124     def getEXIF(self, img=None, refresh=False):
00125         """Get the exif informations of the file
00126         
00127         The information is cached in _v_image_exif
00128         """
00129         cache = '_image_exif'
00130         
00131         if refresh:
00132             setattr(self, cache, None)
00133         
00134         exif_data = getattr(self, cache, None)
00135         
00136         if exif_data is None or not isinstance(exif_data, dict):
00137             io = self.getImageAsFile(img, scale=None)
00138             if io is not None:
00139                 # some cameras are naughty :(
00140                 try:
00141                     io.seek(0)
00142                     exif_data = exif.process_file(io, debug=False)
00143                 except:
00144                     LOG.error('Failed to process EXIF information', exc_info=True)
00145                     exif_data = {}
00146                 # seek to 0 and do NOT close because we might work
00147                 # on a file upload which is required later
00148                 io.seek(0)
00149                 # remove some unwanted elements lik thumb nails
00150                 for key in ('JPEGThumbnail', 'TIFFThumbnail', 
00151                             'MakerNote JPEGThumbnail'):
00152                     if key in exif_data:
00153                         del exif_data[key]
00154 
00155         if not exif_data:
00156             # alawys return a dict
00157             exif_data = {}
00158         # set the EXIF cache even if the image has returned an empty
00159         # dict. This prevents regenerating the exif every time if an
00160         # image doesn't have exif information.
00161         setattr(self, cache, exif_data)
00162         return exif_data
00163 
00164     security.declareProtected(View, 'getEXIFOrientation')
00165     def getEXIFOrientation(self):
00166         """Get the rotation and mirror orientation from the EXIF data
00167         
00168         Some cameras are storing the informations about rotation and mirror in
00169         the exif data. It can be used for autorotation.
00170         """
00171         exif = self.getEXIF()
00172         mirror = 0
00173         rotation = 0
00174         code = exif.get('Image Orientation', None)
00175         
00176         if code is None:
00177             return (mirror, rotation)
00178         
00179         try:
00180             code = int(code)
00181         except ValueError:
00182             return (mirror, rotation)
00183             
00184         if code in (2, 4, 5, 7):
00185             mirror = 1
00186         if code in (1, 2):
00187             rotation = 0
00188         elif code in (3, 4):
00189             rotation = 180
00190         elif code in (5, 6):
00191             rotation = 90
00192         elif code in (7, 8):
00193             rotation = 270
00194        
00195         return (mirror, rotation)
00196     
00197     security.declareProtected(View, 'getEXIFOrigDate')
00198     def getEXIFOrigDate(self):
00199         """Get the EXIF DateTimeOriginal from the image (or None)
00200         """
00201         exif_data = self.getEXIF()
00202         raw_date = exif_data.get('EXIF DateTimeOriginal', None)
00203         if raw_date is not None:
00204             # some cameras are naughty ...
00205             try:
00206                 return DateTime(str(raw_date))
00207             except:
00208                 LOG.error('Failed to parse exif date %s' % raw_date, exc_info=True)
00209         return None
00210 
00211     security.declareProtected(ModifyPortalContent, 'transformImage')
00212     def transformImage(self, method, REQUEST=None):
00213         """
00214         Transform an Image:
00215             FLIP_LEFT_RIGHT
00216             FLIP_TOP_BOTTOM
00217             ROTATE_90 (rotate counterclockwise)
00218             ROTATE_180
00219             ROTATE_270 (rotate clockwise)
00220         """ 
00221         method = int(method)
00222         if method not in TRANSPOSE_MAP:
00223             raise RuntimeError, "Unknown method %s" % method
00224         
00225         target = self.absolute_url() + '/atct_image_transform'
00226         
00227         if not HAS_PIL:
00228             if REQUEST:
00229                 REQUEST.RESPONSE.redirect(target)
00230         
00231         image = self.getImageAsFile()
00232         image2 = StringIO()
00233         
00234         if image is not None:
00235             img = PIL.Image.open(image)
00236             del image
00237             fmt = img.format
00238             img = img.transpose(method)
00239             img.save(image2, fmt, quality=zconf.pil_config.quality)
00240             
00241             field = self.getField('image')
00242             mimetype = field.getContentType(self)
00243             filename = field.getFilename(self)
00244             
00245             # because AT tries to get mimetype and filename from a file like
00246             # object by attribute access I'm passing a string along
00247             self.setImage(image2.getvalue(), mimetype=mimetype,
00248                           filename=filename, refresh_exif=False)
00249         
00250         if REQUEST:
00251              REQUEST.RESPONSE.redirect(target)
00252 
00253     security.declareProtected(ModifyPortalContent, 'autoTransformImage')
00254     def autoTransformImage(self, REQUEST=None):
00255         """Auto transform image according to EXIF data
00256         
00257         Note: isn't using mirror
00258         """
00259         target = self.absolute_url() + '/atct_image_transform'
00260         mirror, rotation = self.getEXIFOrientation()
00261         transform = None
00262         if rotation:
00263             transform = AUTO_ROTATE_MAP.get(rotation, None)
00264             if transform is not None:
00265                 self.transformImage(transform)
00266         if REQUEST:
00267              REQUEST.RESPONSE.redirect(target)
00268         else:
00269             return mirror, rotation, transform
00270              
00271     security.declareProtected(View, 'getTransformMap')
00272     def getTransformMap(self):
00273         """Get map for tranforming the image
00274         """
00275         return [{'name' : n, 'value' : v} for v, n in TRANSPOSE_MAP.items()]
00276     
00277     security.declareProtected(View, 'hasPIL')
00278     def hasPIL(self):
00279         """Is PIL installed?
00280         """
00281         return HAS_PIL
00282 
00283 InitializeClass(ATCTImageTransform)