diff --git a/media/README.txt b/media/README.txt new file mode 100644 index 0000000..44b8692 --- /dev/null +++ b/media/README.txt @@ -0,0 +1,29 @@ +====================== +Media Asset Management +====================== + + ($Id$) + + >>> import os + >>> from cybertools.media.tests import dataDir, clearDataDir + >>> from cybertools.media.asset import MediaAssetFile + + >>> image1 = os.path.join(dataDir, 'test1.jpg') + + +Image Transformations +===================== + + >>> rules = dict( + ... minithumb='size(96, 72)', + ... ) + + >>> asset = MediaAssetFile(image1, rules, 'image/jpeg') + + >>> asset.transform() + + +Fin de Partie +============= + + >>> clearDataDir() diff --git a/media/__init__.py b/media/__init__.py new file mode 100644 index 0000000..4bc90fb --- /dev/null +++ b/media/__init__.py @@ -0,0 +1,4 @@ +""" +$Id$ +""" + diff --git a/media/asset.py b/media/asset.py new file mode 100644 index 0000000..f513735 --- /dev/null +++ b/media/asset.py @@ -0,0 +1,170 @@ +# +# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Media asset file adapter. + +Authors: Johann Schimpf, Erich Seifert. + +$Id$ +""" + +from logging import getLogger +import mimetypes +import os, re, sys + +from zope import component +from zope.interface import implements +from cybertools.media.interfaces import IMediaAsset, IFileTransform +from cybertools.media.piltransform import PILTransform +from cybertools.storage.filesystem import FileSystemStorage + +TRANSFORM_STATEMENT = re.compile(r"\s*(\+?)([\w]+[\w\d]*)\(([^\)]*)\)\s*") + +DEFAULT_FORMATS = { + "image": "image/jpeg" +} + +def parseTransformStatements(txStr): + """ Parse statements in transform chain strings.""" + statements = TRANSFORM_STATEMENT.findall(txStr) + return statements + +def getMimeBasetype(mimetype): + return mimetype.split("/",1)[0] + +def getMimetypeExt(mimetype): + exts = mimetypes.guess_all_extensions(mimetype) + return exts and exts[-1] or "" + + +class MediaAssetFile(object): + """ Class for extracting metadata from assets and to create transformed + variants using file representations in subdirectories. + """ + + implements(IMediaAsset) + + def __init__(self, dataPath, rules, contentType): + self.dataPath = dataPath + self.rules = rules + self.mimeType = contentType + + def getData(self, variant=None): + if variant == None: + return self.getOriginalData() + path = self.getPath(variant) + if not os.path.exists(path): + getLogger('Asset Manager').warn( + 'Media asset directory for transformation %r not found.' % variant) + return '' + f = open(path, 'rb') + data =f.read() + f.close() + return data + + def getContentType(self, variant=None): + contentType = self.getMimeType() + if variant == None: + return contentType + outputFormat = None + # Scan all statements for a defintion of an output format + optionsstr = self.rules.get(variant) + if optionsstr: + statements = parseTransformStatements(optionsstr) + for prefix, command, args in statements: + if command == "output": + outputFormat = args + break + # Return default type if no defintion was found + if not outputFormat: + baseType = getMimeBasetype(contentType) + return DEFAULT_FORMATS.get(baseType) + return outputFormat + + def transform(self, rules=None): + if rules is None: + rules = self.rules + for variant, commands in rules.items(): + self.createVariant(variant, commands) + + def createVariant(self, variant, commands): + oldassetdir = self.getDataPath() + # get or create output directory + path = self.getPath(variant) + assetdir = os.path.dirname(path) + if not os.path.exists(assetdir): + os.makedirs(assetdir) + excInfo = None # Save info of exceptions that may occure + try: + mediaFile = PILTransform() + mediaFile.open(oldassetdir) + statements = parseTransformStatements(commands) + for prefix, command, args in statements: + if command == "rotate": + rotArgs = args.split(",") + angle = float(rotArgs[0]) + resize = False + if len(rotArgs) > 1: + resize = bool(int(rotArgs[1])) + mediaFile.rotate(angle, resize) + elif command == "color": + mode = args + mediaFile.color(mode) + elif command == "crop": + dims = [float(i) for i in args.split(",")] + if dims and (2 <= len(dims) <= 4): + mediaFile.crop(*dims) + elif command == "size": + size = [int(i) for i in args.split(",")] + if size and len(size)==2: + mediaFile.resize(*size) + outputFormat = self.getContentType(variant) + mediaFile.save(path, outputFormat) + except Exception, e: + excInfo = sys.exc_info() + # Handle exceptions that have occured during the transformation + # in order to provide information on the affected asset + if excInfo: + eType, eValue, eTraceback = excInfo # Extract exception information + raise eType("Error transforming asset '%s': %s" % + (oldassetdir, eValue)), None, eTraceback + + def getPath(self, variant): + pathOrig = self.getDataPath() + dirOrig, fileOrig = os.path.split(pathOrig) + pathTx = os.path.join(dirOrig, variant, self.getName()) + outputFormat = self.getContentType(variant) + outputExt = getMimetypeExt(outputFormat) + pathTx = os.path.splitext(pathTx)[0] + outputExt + return pathTx + + def getMimeType(self): + return self.mimeType + + def getName(self): + return os.path.split(self.getDataPath())[1] + + def getDataPath(self): + return self.dataPath + + def getOriginalData(self): + f = self.getDataPath().open() + data = f.read() + f.close() + return data diff --git a/media/interfaces.py b/media/interfaces.py new file mode 100644 index 0000000..10fb246 --- /dev/null +++ b/media/interfaces.py @@ -0,0 +1,77 @@ +# +# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Media asset management interface definitions. + +$Id$ +""" + +from zope.interface import Interface, Attribute + +from loops.interfaces import IExternalFile + + +class IMediaAsset(Interface): + + def getData(variant=None): + """ Return the binary data of the media asset or one of its variants. + """ + + def getContentType(variant=None): + """ Return the mime-formatted content type of the media asset + or one of its variants. + """ + + def transform(rules): + """ Generate user-defined transformed variants of the media asset + according to the rules given. + """ + + +class IFileTransform(Interface): + """ Transformations using files in the filesystem. + """ + + def open(path): + """ Open the image under the given filename. + """ + + def save(path, mimetype): + """ Save the image under the given filename. + """ + + def rotate(rotangle): + """ Return a copy of an image rotated the given number of degrees + counter clockwise around its centre. + """ + + def color(mode): + """ Create image with specified color mode (e.g. 'greyscale'). + """ + + def crop(relWidth, relHeight, alignX=0.5, alignY=0.5): + """ Return a rectangular region from the current image. The box is defined + by a relative width and a relative height defining the crop aspect + as well as a horizontal (x) and a verical (y) alignment parameters. + """ + + def resize(width, height): + """ Modify the image to contain a thumbnail version of itself, no + larger than the given size. + """ diff --git a/media/piltransform.py b/media/piltransform.py new file mode 100644 index 0000000..86ebde7 --- /dev/null +++ b/media/piltransform.py @@ -0,0 +1,91 @@ +# +# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Views for displaying media assets. + +Authors: Johann Schimpf, Erich Seifert. + +$Id$ +""" + +from logging import getLogger +try: + import Image +except ImportError: + getLogger('Asset Manager').warn('Python Imaging Library could not be found.') + +from zope.interface import implements + +from cybertools.media.interfaces import IMediaAsset, IFileTransform +from cybertools.storage.filesystem import FileSystemStorage + + +def mimetypeToPIL(mimetype): + return mimetype.split("/",1)[-1] + + +class PILTransform(object): + """ Class for image transformation methods. + Based on the Python Imaging Library. + """ + + implements(IFileTransform) + + def open(self, path): + self.im = Image.open(path) + + def rotate(self, angle, resize): + self.im = self.im.rotate(angle,Image.BICUBIC) + + def color(self, mode): + if not mode: + return + mode = mode.upper() + if mode == "BITMAP": + mode = "1" + elif mode == "GRAYSCALE": + mode = "L" + self.im = self.im.convert(mode) + + def crop(self, relWidth, relHeight, alignX=0.5, alignY=0.5): + alignX = min(max(alignX, 0.0), 1.0) + alignY = min(max(alignY, 0.0), 1.0) + w, h = self.im.size + imgAspect = float(w) / float(h) + crpAspect = relWidth / relHeight + if imgAspect >= crpAspect: + crpWidth = h * crpAspect + crpHeight = h + else: + crpWidth = w + crpHeight = w / crpAspect + left = int((w - crpWidth) * alignX) + upper = int((h - crpHeight) * alignY) + right = int(left + crpWidth) + lower = int(upper + crpHeight) + box = (left, upper, right, lower) + self.im = self.im.crop(box) + + def resize(self, width, height): + dims = (width, height) + self.im.thumbnail(dims, Image.ANTIALIAS) + + def save(self, path, mimetype): + format = mimetypeToPIL(mimetype) + self.im.save(path) diff --git a/media/testdata/test1.jpg b/media/testdata/test1.jpg new file mode 100644 index 0000000..af8d63b Binary files /dev/null and b/media/testdata/test1.jpg differ diff --git a/media/tests.py b/media/tests.py new file mode 100644 index 0000000..acf1c02 --- /dev/null +++ b/media/tests.py @@ -0,0 +1,42 @@ +#! /usr/bin/python + +""" +Tests for the 'cybertools.media' package. + +$Id$ +""" + +import os +import unittest, doctest +from zope.testing.doctestunit import DocFileSuite +from zope.interface.verify import verifyClass + +from cybertools import media + +dataDir = os.path.join(os.path.dirname(media.__file__), 'testdata') + +def clearDataDir(): + for fn in os.listdir(dataDir): + path = os.path.join(dataDir, fn) + if os.path.isdir(path): + for subfn in os.listdir(path): + os.unlink(os.path.join(path, subfn)) + os.rmdir(path) + + +class Test(unittest.TestCase): + "Basic tests for the media package." + + def testSomething(self): + pass + + +def test_suite(): + flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + return unittest.TestSuite(( + unittest.makeSuite(Test), + DocFileSuite('README.txt', optionflags=flags), + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/stateful/definition.py b/stateful/definition.py index a7708ae..8cd2b2e 100644 --- a/stateful/definition.py +++ b/stateful/definition.py @@ -33,6 +33,7 @@ class State(object): implements(IState) + security = lambda context: None icon = None color = 'blue' diff --git a/stateful/interfaces.py b/stateful/interfaces.py index cf834d9..9e55e81 100644 --- a/stateful/interfaces.py +++ b/stateful/interfaces.py @@ -35,6 +35,8 @@ class IState(Interface): title = Attribute('A user-readable name or title of the state') transitions = Attribute('A sequence of strings naming the transitions ' 'that can be executed from this state') + security = Attribute('A callable setting the security settings for ' + 'an object in this state when executed.') class ITransition(Interface): diff --git a/util/config.py b/util/config.py index df4b94a..ec99d3f 100644 --- a/util/config.py +++ b/util/config.py @@ -26,6 +26,8 @@ $Id$ import os from zope.interface import implements +from cybertools.util.jeep import Jeep + _not_found = object()