--- /dev/null
+# -*- coding: utf-8 -*-
+#######################################################################################
+# Photo is a part of Plinn - http://plinn.org #
+# Copyright © 2004-2008 Benoît PIN <benoit.pin@ensmp.fr> #
+# #
+# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
+#######################################################################################
+""" Photo metadata read / write module
+
+
+
+"""
+
+from AccessControl import ClassSecurityInfo
+from Acquisition import aq_base
+from Globals import InitializeClass
+from AccessControl.Permissions import view
+from ZODB.interfaces import BlobError
+from ZODB.utils import cp
+from OFS.Image import File
+from xmp import XMP
+from logging import getLogger
+from cache import memoizedmethod
+from libxml2 import parseDoc
+from standards.xmp import accessors as xmpAccessors
+import xmputils
+from types import TupleType
+from subprocess import Popen, PIPE
+from Products.PortalTransforms.libtransforms.utils import bin_search, \
+ MissingBinary
+
+XPATH_EMPTY_TAGS = "//node()[name()!='' and not(node()) and not(@*)]"
+console = getLogger('Photo.metadata')
+
+try :
+ XMPDUMP = 'xmpdump'
+ XMPLOAD = 'xmpload'
+ bin_search(XMPDUMP)
+ bin_search(XMPLOAD)
+ xmpIO_OK = True
+except MissingBinary :
+ xmpIO_OK = False
+ console.warn("xmpdump or xmpload not available.")
+
+class Metadata :
+ """ Photo metadata read / write mixin """
+
+ security = ClassSecurityInfo()
+
+
+ #
+ # reading api
+ #
+
+ security.declarePrivate('getXMP')
+ if xmpIO_OK :
+ @memoizedmethod()
+ def getXMP(self):
+ """returns xmp metadata packet with xmpdump call
+ """
+ if self.size :
+ blob_file_path = self.bdata._p_blob_uncommitted or self.bdata._p_blob_committed
+ dumpcmd = '%s %s' % (XMPDUMP, blob_file_path)
+ p = Popen(dumpcmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=True)
+ xmp, err = p.communicate()
+ if err :
+ raise SystemError, err
+ return xmp
+
+ else :
+ @memoizedmethod()
+ def getXMP(self):
+ """returns xmp metadata packet with XMP object
+ """
+ xmp = None
+ if self.size :
+ try :
+ bf = self.open('r')
+ x = XMP(bf, content_type=self.content_type)
+ xmp = x.getXMP()
+ except NotImplementedError :
+ pass
+
+ return xmp
+
+ security.declareProtected(view, 'getXmpFile')
+ def getXmpFile(self, REQUEST):
+ """returns the xmp packet over http.
+ """
+ xmp = self.getXMP()
+ if xmp is not None :
+ return File('xmp', 'xmp', xmp, content_type='text/xml').index_html(REQUEST, REQUEST.RESPONSE)
+ else :
+ return None
+
+ security.declarePrivate('getXmpBag')
+ def getXmpBag(self, name, root, index=None) :
+ index = self.getXmpPathIndex()
+ if index :
+ path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
+ node = index.get(path)
+
+ if node :
+ values = xmputils.getBagValues(node.element)
+ return values
+ return tuple()
+
+ security.declarePrivate('getXmpSeq')
+ def getXmpSeq(self, name, root) :
+ index = self.getXmpPathIndex()
+ if index :
+ path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
+ node = index.get(path)
+
+ if node :
+ values = xmputils.getSeqValues(node.element)
+ return values
+ return tuple()
+
+ security.declarePrivate('getXmpAlt')
+ def getXmpAlt(self, name, root) :
+ index = self.getXmpPathIndex()
+ if index :
+ path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
+ node = index.get(path)
+
+ if node :
+ firstLi = node.get('rdf:Alt/rdf:li')
+ if firstLi :
+ assert firstLi.unique, "More than one rdf:Alt (localisation not yet supported)"
+ return firstLi.element.content
+ return ''
+
+ security.declarePrivate('getXmpProp')
+ def getXmpProp(self, name, root):
+ index = self.getXmpPathIndex()
+ if index :
+ path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
+ node = index.get(path)
+ if node :
+ return node.element.content
+ return ''
+
+
+ security.declarePrivate('getXmpPathIndex')
+ @memoizedmethod(volatile=True)
+ def getXmpPathIndex(self):
+ xmp = self.getXMP()
+ if xmp :
+ d = parseDoc(xmp)
+ index = xmputils.getPathIndex(d)
+ return index
+
+ security.declarePrivate('getXmpValue')
+ def getXmpValue(self, name):
+ """ returns pythonic version of xmp property """
+ info = xmpAccessors[name]
+ root = info['root']
+ rdfType = info['rdfType'].capitalize()
+ methName = 'getXmp%s' % rdfType
+ meth = getattr(aq_base(self), methName)
+ return meth(name, root)
+
+
+ security.declareProtected(view, 'getXmpField')
+ def getXmpField(self, name):
+ """ returns data formated for a html form field """
+ editableValue = self.getXmpValue(name)
+ if type(editableValue) == TupleType :
+ editableValue = ', '.join(editableValue)
+ return {'id' : name.replace(':', '_'),
+ 'value' : editableValue}
+
+
+ #
+ # writing api
+ #
+
+ security.declarePrivate('setXMP')
+ if xmpIO_OK :
+ def setXMP(self, xmp):
+ """setXMP with xmpload call
+ """
+ if self.size :
+ blob = self.bdata
+ if blob.readers :
+ raise BlobError("Already opened for reading.")
+
+ if blob._p_blob_uncommitted is None:
+ filename = blob._create_uncommitted_file()
+ uncommitted = file(filename, 'w')
+ cp(file(blob._p_blob_committed, 'rb'), uncommitted)
+ uncommitted.close()
+ else :
+ filename = blob._p_blob_uncommitted
+
+ loadcmd = '%s %s' % (XMPLOAD, filename)
+ p = Popen(loadcmd, stdin=PIPE, stderr=PIPE, shell=True)
+ p.stdin.write(xmp)
+ p.stdin.close()
+ p.wait()
+ err = p.stderr.read()
+ if err :
+ raise SystemError, err
+
+ f = file(filename)
+ f.seek(0,2)
+ self.updateSize(size=f.tell())
+ f.close()
+ self.bdata._p_changed = True
+
+
+ # purge caches
+ try : del self._methodResultsCache['getXMP']
+ except KeyError : pass
+
+ for name in ('getXmpPathIndex',) :
+ try :
+ del self._v__methodResultsCache[name]
+ except (AttributeError, KeyError):
+ continue
+
+ self.ZCacheable_invalidate()
+ self.ZCacheable_set(None)
+ self.http__refreshEtag()
+
+ else :
+ def setXMP(self, xmp):
+ """setXMP with XMP object
+ """
+ if self.size :
+ bf = self.open('r+')
+ x = XMP(bf, content_type=self.content_type)
+ x.setXMP(xmp)
+ x.save()
+ self.updateSize(size=bf.tell())
+
+ # don't call update_data
+ self.ZCacheable_invalidate()
+ self.ZCacheable_set(None)
+ self.http__refreshEtag()
+
+ # purge caches
+ try : del self._methodResultsCache['getXMP']
+ except KeyError : pass
+ for name in ('getXmpPathIndex', ) :
+ try :
+ del self._v__methodResultsCache[name]
+ except (AttributeError, KeyError):
+ continue
+
+
+
+ security.declarePrivate('setXmpField')
+ def setXmpFields(self, **kw):
+ xmp = self.getXMP()
+ if xmp :
+ doc = parseDoc(xmp)
+ else :
+ doc = xmputils.createEmptyXmpDoc()
+
+ index = xmputils.getPathIndex(doc)
+
+ pathPrefix = 'rdf:RDF/rdf:Description'
+ preferedNsDeclaration = 'rdf:RDF/rdf:Description'
+
+ for id, value in kw.items() :
+ name = id.replace('_', ':')
+ info = xmpAccessors.get(name)
+ if not info : continue
+ root = info['root']
+ rdfType = info['rdfType']
+ path = '/'.join([p for p in [pathPrefix, root, name] if p])
+
+ Metadata._setXmpField(index
+ , path
+ , rdfType
+ , name
+ , value
+ , preferedNsDeclaration)
+
+ # clean empty tags without attributes
+ context = doc.xpathNewContext()
+ nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
+ while nodeset :
+ for n in nodeset :
+ n.unlinkNode()
+ n.freeNode()
+ nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
+
+
+
+ xmp = doc.serialize('utf-8')
+ # remove <?xml version="1.0" encoding="utf-8"?> header
+ xmp = xmp.split('?>', 1)[1].lstrip('\n')
+ self.setXMP(xmp)
+
+ @staticmethod
+ def _setXmpField(index, path, rdfType, name, value, preferedNsDeclaration) :
+ if rdfType in ('Bag', 'Seq') :
+ value = value.replace(';', ',')
+ value = value.split(',')
+ value = [item.strip() for item in value]
+ value = filter(None, value)
+
+ if value :
+ # edit
+ xmpPropIndex = index.getOrCreate(path
+ , rdfType
+ , preferedNsDeclaration)
+ if rdfType == 'prop' :
+ xmpPropIndex.element.setContent(value)
+ else :
+ #rdfPrefix = index.getDocumentNs()['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
+ func = getattr(xmputils, 'createRDF%s' % rdfType)
+ newNode = func(name, value, index)
+ oldNode = xmpPropIndex.element
+ oldNode.replaceNode(newNode)
+ else :
+ # delete
+ xmpPropIndex = index.get(path)
+ if xmpPropIndex is not None :
+ xmpPropIndex.element.unlinkNode()
+ xmpPropIndex.element.freeNode()
+
+
+InitializeClass(Metadata)