From c2624fb48eabfdec80dd1681b39d4982687a4663 Mon Sep 17 00:00:00 2001 From: helmutm Date: Tue, 21 Oct 2008 13:01:48 +0000 Subject: [PATCH] add media asset management package git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@2927 fd906abe-77d9-0310-91a1-e0d9ade77398 --- media/README.txt | 29 +++++++ media/__init__.py | 4 + media/asset.py | 170 +++++++++++++++++++++++++++++++++++++++ media/interfaces.py | 77 ++++++++++++++++++ media/piltransform.py | 91 +++++++++++++++++++++ media/testdata/test1.jpg | Bin 0 -> 11325 bytes media/tests.py | 42 ++++++++++ stateful/definition.py | 1 + stateful/interfaces.py | 2 + util/config.py | 2 + 10 files changed, 418 insertions(+) create mode 100644 media/README.txt create mode 100644 media/__init__.py create mode 100644 media/asset.py create mode 100644 media/interfaces.py create mode 100644 media/piltransform.py create mode 100644 media/testdata/test1.jpg create mode 100644 media/tests.py 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 0000000000000000000000000000000000000000..af8d63b01c99c2b674c63b49fc9352feb89e6eda GIT binary patch literal 11325 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 T;iC8swfssz+chm?Z;Sr}K8MHj literal 0 HcmV?d00001 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()