add media asset management package

git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@2927 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2008-10-21 13:01:48 +00:00
parent 3baf1fabb5
commit c2624fb48e
10 changed files with 418 additions and 0 deletions

29
media/README.txt Normal file
View file

@ -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()

4
media/__init__.py Normal file
View file

@ -0,0 +1,4 @@
"""
$Id$
"""

170
media/asset.py Normal file
View file

@ -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

77
media/interfaces.py Normal file
View file

@ -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.
"""

91
media/piltransform.py Normal file
View file

@ -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)

BIN
media/testdata/test1.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

42
media/tests.py Normal file
View file

@ -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')

View file

@ -33,6 +33,7 @@ class State(object):
implements(IState)
security = lambda context: None
icon = None
color = 'blue'

View file

@ -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):

View file

@ -26,6 +26,8 @@ $Id$
import os
from zope.interface import implements
from cybertools.util.jeep import Jeep
_not_found = object()