[Zope3-checkins] CVS: zopeproducts/photo - LICENSE:1.1 README:1.1 TODO:1.1 __init__.py:1.1 configure.zcml:1.1 interfaces.py:1.1 utilities.py:1.1

Bjorn Tillenius bjorn at codeworks.lt
Fri Aug 15 09:10:52 EDT 2003


Update of /cvs-repository/zopeproducts/photo
In directory cvs.zope.org:/tmp/cvs-serv25590

Added Files:
	LICENSE README TODO __init__.py configure.zcml interfaces.py 
	utilities.py 
Log Message:
First checkin of the photo product.

This is intended to be similar to the photo product in Zope 2. It's not
quite finished yet, though it's already usable. Read the README for more
information.


=== Added File zopeproducts/photo/LICENSE ===
Zope Public License (ZPL) Version 2.0
-----------------------------------------------

This software is Copyright (c) Zope Corporation (tm) and
Contributors. All rights reserved.

This license has been certified as open source. It has also
been designated as GPL compatible by the Free Software
Foundation (FSF).

Redistribution and use in source and binary forms, with or
without modification, are permitted provided that the
following conditions are met:

1. Redistributions in source code must retain the above
   copyright notice, this list of conditions, and the following
   disclaimer.

2. Redistributions in binary form must reproduce the above
   copyright notice, this list of conditions, and the following
   disclaimer in the documentation and/or other materials
   provided with the distribution.

3. The name Zope Corporation (tm) must not be used to
   endorse or promote products derived from this software
   without prior written permission from Zope Corporation.

4. The right to distribute this software or to use it for
   any purpose does not give you the right to use Servicemarks
   (sm) or Trademarks (tm) of Zope Corporation. Use of them is
   covered in a separate agreement (see
   http://www.zope.com/Marks).

5. If any files are modified, you must cause the modified
   files to carry prominent notices stating that you changed
   the files and the date of any change.

Disclaimer

  THIS SOFTWARE IS PROVIDED BY ZOPE CORPORATION ``AS IS''
  AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
  NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN
  NO EVENT SHALL ZOPE CORPORATION OR ITS CONTRIBUTORS BE
  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
  DAMAGE.


This software consists of contributions made by Zope
Corporation and many individuals on behalf of Zope
Corporation.  Specific attributions are listed in the
accompanying credits file.


=== Added File zopeproducts/photo/README ===
Photo Product

  Overview

    This product enables you to have multiple sizes of an image. You
    only have to supply it with an image and it will automatically
    generate the size you ask it for. It also can store some meta
    data about the image.

    It is much influenced by the Photo product in Zope 2, but it
    is not a direct port of it.

  Display Sizes

    Right now it contains a static set of different sizes of the image.
    This will change though, so that you can specify the sizes you
    want.

  Image Utilities

    In order to resize the image it uses an image utility. It will give
    you a list of available image utilities. You can choose any utility
    which implements IImageResizeUtility. Two utilites are provided in
    this package. One which uses ImageMagick and another which uses PIL.

    Note that in order to use the ImageMagick utility the 'convert'
    program has to be in your PATH. To use the PIL utility the PIL
    module has to be installed. 

  Uploading Photos

    There are two ways of uploading photos. One is to do it through
    the browser by adding a photo in the ZMI. Though if you want to
    upload many photos you're better off creating a photo aware folder,
    like the PhotoSlide product. You can also create a normal folder
    and mark it with the IPhotoFolder interface. Then every image
    you upload to that folder will becom a photo.

  Current Status

    At the moment this product is quite usable, though there's still
    a lot to be done. See the TODO for more information

  Other Notes

    This product was created by Bjorn Tillenius, if you have any
    questions you can reach me at <bjorn(at)codeworks.lt>.


=== Added File zopeproducts/photo/TODO ===
* Provide a nice icon

    Currently there's no icon associated with this product.
    
* Support for external storage

    Storing a lot of images inside ZODB makes it unnecessary big and
    slow. It would be better to only store meta data there and store
    the actual images somewhere else, like the file system or an
    external database.

* Fix generation of new image sizes

    At the moment new image sizes only get created when someone requests
    it and the new image never gets deleted. There should be an option
    to pre-generate certain sizes. There should also be some kind of
    cache timeout, so that the images get deleted after a while.

* Customization of display sizes

    Now there are hard coded list of display sizes. It should be possible
    to edit this list.

* EXIF support

    Extract EXIF data from the image.

* Run images through filters while resizing

    It would be nice to say, for example, that the thumbnails should
    have rounded corners. My main idea here is that for each display
    size you can specify a number of filter through which the resized
    image will be processed.

* Put PILImageUtility and ImageMagickUtility into seperate packages

    Those are not specific to this product and could be useful for others
    to use. Of course they would need some more functionality as well.

* More functional tests

    I noticed that quite often when I developed this product, all the unit
    tests passed, but in reality the product was broken. Would be nice to
    have some tests which can tell that it is broken.


=== Added File zopeproducts/photo/__init__.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
##############################################################################
"""The Photo package

$Id: __init__.py,v 1.1 2003/08/15 12:10:43 BjornT Exp $
"""

from zope.interface import implements
from zope.app.container.btree import BTreeContainer
from zope.app.content.image import Image
from zope.app.interfaces.content.file import IFileContent
from zope.app.interfaces.file import IDirectoryFactory, IFileFactory
from zope.app.interfaces.size import ISized
from zope.app.interfaces.container import IContainer
from zope.app.interfaces.dublincore import ICMFDublinCore
from zope.app import zapi
from zope.i18n import MessageIDFactory

from zopeproducts.photo.interfaces import IPhoto, IImageResizeUtility
from zopeproducts.photo.interfaces import IPhotoContainer

_ = MessageIDFactory("photo")

defaultDisplayIds = {u'thumbnail': (128,128),
                     u'xsmall': (200,200),
                     u'small': (320,320),
                     u'medium': (480,480),
                     u'large': (768,768),
                     u'xlarge': (1024,1024),
                     u'original': (0,0)
                     }

defaultImageResizer = 'ImageMagick Image Utility'

class Photo(BTreeContainer):

    implements(IPhoto, IContainer, IFileContent)

    # ATTRIBUTES
    useParentOptions = True
    
    # CONSTRUCTOR
    def __init__(self):
        super(Photo, self).__init__()
        self._displayIds = defaultDisplayIds
        self.resizeUtility = defaultImageResizer

    # PROPERTIES
    def getTitle(self):
        """Gets the title of the photo.

        Since we are annotatable we use the title in the dublin core"""
        return zapi.getAdapter(self, ICMFDublinCore).title

    def setTitle(self, title):
        """Sets the title of the photo

        Since we are annotatable we use the title in the dublin core"""
        zapi.getAdapter(self, ICMFDublinCore).title = title

    title = property(getTitle, setTitle)

    def getDescription(self):
        """Gets the description of the photo

        Since we are annotatable we use the description in the dublin core"""
        return zapi.getAdapter(self, ICMFDublinCore).description

    def setDescription(self, description):
        """Sets the description of the photo

        Since we are annotatable we use the description in the dublin core"""
        zapi.getAdapter(self, ICMFDublinCore).description = description

    description = property(getDescription, setDescription)

    _data = ''

    def getData(self):
        """Gets the data of the original photo"""
        return self._data

    def setData(self, data):
        """Sets the data of the original photo"""
        self._data = data
        self._deleteGeneratedDisplays()
        im = Image(data)
        self.setObject(u'original', im)
        self._displayIds['original'] = im.getImageSize()

    data = property(getData, setData)


    _currentDisplayId = 'medium'

    def setCurrentDisplayId(self, displayId):
        """Set the current display id, so it's not necessary to specify
        it to getImage() every time.
        """
        if displayId in self.getDisplayIds():
            self._currentDisplayId = displayId

    def getCurrentDisplayId(self):
        """Gets the current display id"""
        return self._getPhotoOption('currentDisplayId')


    currentDisplayId = zapi.ContextProperty(getCurrentDisplayId,
                                            setCurrentDisplayId)
    _resizeUtility = defaultImageResizer

    def setResizeUtility(self, val):
        """Sets the resize utility."""
        self._resizeUtility = val

    def getResizeUtility(self):
        """Gets the resize utility."""
        return self._getPhotoOption('resizeUtility')

    resizeUtility = zapi.ContextProperty(getResizeUtility,
                                         setResizeUtility)

    # PUBLIC METHODS
    def getDisplayIds(self):
        """See IPhoto"""
        result = self._displayIds.keys()
        result.reverse()
        return result
    
    def getDisplaySize(self, displayId):
        """See IPhoto"""
        if  not self._displayIds.has_key(displayId):
            return None
        else:
            return self._displayIds[displayId]

    def getImage(self, displayId = None):
        """See IPhoto"""
        if not self._data:
            return
        if displayId == None:
            displayId = self.currentDisplayId
        if displayId not in self.getDisplayIds():
            return None
        if not self.__contains__(displayId):
            self._generateDisplay(displayId)

        return self.get(displayId)

    getImage = zapi.ContextMethod(getImage)


    # PRIVATE METHODS
    def _getPhotoOption(self, option):
        """Returns the photo option, considering useParentOptions."""
        if self.useParentOptions:
            pc = zapi.getParent(self)
            if IPhotoContainer.isImplementedBy(pc):
                return getattr(pc, option)
            else:
                return getattr(self, '_' + option)
        else:
            return getattr(self, '_' + option)
        
    _getPhotoOption = zapi.ContextMethod(_getPhotoOption)

    def _deleteGeneratedDisplays(self):
        for dispId in self.getDisplayIds():
            if self.__contains__(dispId):
                self.__delitem__(dispId)
        

    def _generateDisplay(self, displayId):
        """Generate a new size of the image"""
        im_size = self._displayIds[displayId]
        resizer = zapi.getUtility(self, IImageResizeUtility,
                                  self.resizeUtility)

        image = resizer.resize(Image(self.data), im_size, keep_aspect=True)
        self.setObject(displayId, image)
        
    _generateDisplay = zapi.ContextMethod(_generateDisplay)

class PhotoSized:

    implements(ISized)

    def __init__(self, photo):
        self._photo = photo

    def sizeForSorting(self):
        """See ISized"""
        image = Image(self._photo.data)
        return ('byte', image.getSize())
        
    def sizeForDisplay(self):
        """See ISized"""
        image = Image(self._photo.data)
        length = len(self._photo.data)
        size = image.getSize() 
        if size <= 0:
            return u''
        else:
            # XXX size should be localized (the formatting of the number)
            if size > 1024*1024:
                size = "%0.02f" % (size / (1024*1024.0))
                unit = _('MB')
            elif size > 1024:
                size = "%0.02f" % (size / 1024.0)
                unit = _('kB')
            else:
                unit = _('bytes')
                
            x, y = image.getImageSize()
            result = '${size} ${unit} ${x}x${y}'
            result = _(result)
            result.mapping = {'size': size, 'unit': unit, 'x': x, 'y': y}
            return result

class PhotoFactory(object):
    """Creates photos in the file system representaion.

    This class can create photos instead of both directories and
    files in the file system representation.
    """

    implements(IDirectoryFactory, IFileFactory)
        
    def __call__(self, name, content_type='', data=None):
        photo = Photo()
        if data:
            photo.data = data
        return photo
        


=== Added File zopeproducts/photo/configure.zcml ===
<configure
    xmlns="http://namespaces.zope.org/zope"
    i18n_domain="photo"
    >

<content class=".Photo">
  <implements
      interface="zope.app.interfaces.annotation.IAttributeAnnotatable"
      />
  <factory
      id="Photo"
      permission="zope.ManageContent"
      description="Photo"
      />
  <require
      permission="zope.View"
      interface=".interfaces.IPhoto"
      />
  <require
      permission="zope.ManageContent"
      set_schema=".interfaces.IPhoto"
      />
</content>

<!-- ADAPTERS -->

<adapter
    factory=".PhotoSized"
    provides="zope.app.interfaces.size.ISized"
    for=".interfaces.IPhoto"
    />

<adapter
    factory=".PhotoFactory"
    provides="zope.app.interfaces.file.IDirectoryFactory"
    for=".interfaces.IPhotoFolder"
    permission="zope.ManageContent"
    />

<adapter
    factory=".PhotoFactory"
    provides="zope.app.interfaces.file.IFileFactory"
    for=".interfaces.IPhotoFolder"
    permission="zope.ManageContent"
    />

<adapter
   for=".interfaces.IPhoto"
   provides="zope.app.interfaces.file.IReadDirectory"
   factory="zope.app.container.directory.noop"
   permission="zope.ManageContent"
   />


<!-- UTILITIES -->

<utility
    factory=".utilities.PILImageUtility"
    provides=".interfaces.IImageResizeUtility"
    name="PIL Image Utility"
    />

<utility
    factory=".utilities.ImageMagickUtility"
    provides=".interfaces.IImageResizeUtility"
    name="ImageMagick Image Utility"
    />

<include package=".browser" />

</configure>



=== Added File zopeproducts/photo/interfaces.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
##############################################################################
"""Interface descriptions for the Photo package

$Id: interfaces.py,v 1.1 2003/08/15 12:10:43 BjornT Exp $
"""

from zope.app.interfaces.container import IContainer
from zope.app.services.servicenames import Utilities
from zope.context import ContextProperty
from zope.component import getService, ComponentLookupError
from zope.interface import Interface, Attribute
from zope.schema import Text, TextLine, Bytes, EnumeratedTextLine, Bool
from zope.i18n import MessageIDFactory

_ = MessageIDFactory("photo")


dispIds = [u'thumbnail',
           u'xsmall',
           u'small',
           u'medium',
           u'large',
           u'xlarge',
           u'original'
           ]

class ResizeUtilityName(TextLine):
    """Field which lists all available resize utilities."""
    
    def __allowed(self):
        """Finds all resize utility names and returns them.
        
        Note that this method works only if the Field is context wrapped.
        """
        resizeUtilities = []
        try:
            us = getService(self, Utilities)
            for resize_util_reg in us.getUtilitiesFor(IImageResizeUtility):
                resizeUtilities.append(resize_util_reg[0])
        except ComponentLookupError:
            resizeUtilities = []
        return resizeUtilities

    allowed_values = ContextProperty(__allowed)

class IPhotoFolder(IContainer):
    """Marker interface in order to make it easier to upload photos.

    If a container is marked with this interface it will create 
    photos when an images are uploaded in it.
    """

class IPhotoContainer(Interface):
    """Schema for certain photo options.
    
    Objects that will contain photos should implement this schema.
    """
    
    useParentOptions = Bool(
        title=_('Use Parent Options'),
        description=
        _("If inside a photo container, ignore the following options."),
        default=True,
        required=True
        )

    currentDisplayId = EnumeratedTextLine(
        title=_('Default Size'),
        description=_('The defualt size of the photo(s).'),
        allowed_values=dispIds,
        default=_('medium'),
        required=True
        )

    resizeUtility = ResizeUtilityName(
        title=_('Resize Utility'),
        description=_('The utility used for resizing the images'),
        required=True
        )


class IPhoto(IContainer, IPhotoContainer):
    """Provides several sizes of an Image.
    """

    title = TextLine(title=_('Title'),
                     description=_('The title of the photo'),
                     default=u'',
                     required=False)

    description = Text(title=_('Description'),
                       description=_('The description of the photo'),
                       default=u'',
                       required=False)
                               
    data = Bytes(title=_('Image File'),
                 description=_('The original image'),
                 required=True)

    def getDisplayIds():
        """Gets a list of available display ids"""

    def getDisplaySize(dispId):
        """Gets the geometry of the specified display id"""

    def getImage(dispId):
        """Returns the image of the specified display id"""
        

class IImageResizeUtility(Interface):
    """Resizes an given image to a give size"""

    def resize(image, size, keep_aspect):
        """Returns a new image resized to the given size

        If keep_aspect is true the image should be resized as close
        as possible to size while retaining its aspect ratio.
        """

class IPILImageUtility(IImageResizeUtility):
    """An image utility using PIL.

    For now it only resizes images.
    """

class IImageMagickUtility(IImageResizeUtility):
    """An image utility using ImageMagick.

    For now it only resizes images.
    """


=== Added File zopeproducts/photo/utilities.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
##############################################################################
"""Utilities for resizing images.

$Id: utilities.py,v 1.1 2003/08/15 12:10:43 BjornT Exp $
"""

import popen2

from zope.interface import implements
from zope.app.content.image import Image
from zopeproducts.photo.interfaces import IPILImageUtility, IImageMagickUtility

from PIL import Image as PILImage
from cStringIO import StringIO


class PILImageUtility:
    """An image utility which uses PIL.
    """
    implements(IPILImageUtility)

    def resize(self, image, size, keep_aspect=False):
        """See IPILImageResizeUtility"""
        im = PILImage.open(StringIO(image.getData()))
        fmt = im.format
        new_size = self._getNewSize(image.getImageSize(), size, keep_aspect)
        new_data = StringIO()
        (im.resize(new_size)).save(new_data, fmt)
        return Image(new_data.getvalue())

    def _getNewSize(self, image_size, desired_size, keep_aspect):
        """Resizes image_size to desired_size, optionally keeping the
        aspect ratio.
        """
        if keep_aspect:
            x_ratio = float(desired_size[0])/image_size[0]
            y_ratio = float(1.0*desired_size[1])/image_size[1]
            if x_ratio < y_ratio:
                new_size = (round(x_ratio*image_size[0]),
                            round(x_ratio*image_size[1]))
            else:
                new_size = (round(y_ratio*image_size[0]),
                            round(y_ratio*image_size[1]))
            return new_size
        else:
            return desired_size


class ImageMagickUtility:
    """An image utility which uses ImageMagick.

    It needs the convert (or convert.exe) to be in the path.
    """
    implements(IImageMagickUtility)

    def resize(self, image, size, keep_aspect=False):
        """See IImageMacickResizeUtility"""
        instream, outstream = self._getConvertResizePipe(size, keep_aspect)
        outstream.write(image.getData())
        outstream.close()
        new_data = StringIO()
        new_data.write(instream.read())
        return Image(new_data.getvalue())

    def _getConvertResizePipe(self, size, keep_aspect):
        """Returns a pipe communicating with the convert program."""
        # XXX this code isn't tested much, may not work on every system
        if keep_aspect:
            convert_call = 'convert -resize %sx%s - -' % size
        else:
            convert_call = 'convert -resize %sx%s! - -' % size
            
        convert = popen2.Popen3(convert_call,
                                True)
        if convert.poll() == -1:
            return popen2.popen2(convert_call, mode='b') 
        else:
            # XXX here we should raise some error since we couldn't find the
            #     convert executable.
            pass




More information about the Zope3-Checkins mailing list