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:
parent
3baf1fabb5
commit
c2624fb48e
10 changed files with 418 additions and 0 deletions
29
media/README.txt
Normal file
29
media/README.txt
Normal 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
4
media/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
$Id$
|
||||
"""
|
||||
|
170
media/asset.py
Normal file
170
media/asset.py
Normal 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
77
media/interfaces.py
Normal 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
91
media/piltransform.py
Normal 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
BIN
media/testdata/test1.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
42
media/tests.py
Normal file
42
media/tests.py
Normal 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')
|
|
@ -33,6 +33,7 @@ class State(object):
|
|||
|
||||
implements(IState)
|
||||
|
||||
security = lambda context: None
|
||||
icon = None
|
||||
color = 'blue'
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -26,6 +26,8 @@ $Id$
|
|||
import os
|
||||
from zope.interface import implements
|
||||
|
||||
from cybertools.util.jeep import Jeep
|
||||
|
||||
|
||||
_not_found = object()
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue