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)
|
implements(IState)
|
||||||
|
|
||||||
|
security = lambda context: None
|
||||||
icon = None
|
icon = None
|
||||||
color = 'blue'
|
color = 'blue'
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,8 @@ class IState(Interface):
|
||||||
title = Attribute('A user-readable name or title of the state')
|
title = Attribute('A user-readable name or title of the state')
|
||||||
transitions = Attribute('A sequence of strings naming the transitions '
|
transitions = Attribute('A sequence of strings naming the transitions '
|
||||||
'that can be executed from this state')
|
'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):
|
class ITransition(Interface):
|
||||||
|
|
|
@ -26,6 +26,8 @@ $Id$
|
||||||
import os
|
import os
|
||||||
from zope.interface import implements
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.util.jeep import Jeep
|
||||||
|
|
||||||
|
|
||||||
_not_found = object()
|
_not_found = object()
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue