X-Git-Url: https://scm.cri.minesparis.psl.eu/git/Photo.git/blobdiff_plain/b0a7e10b4f32cf74864bb53268ca4d3080f23bc0..6c41809185e322ce2d30e98234f71144f78f06c0:/Products/Photo/metadata.py?ds=inline diff --git a/Products/Photo/metadata.py b/Products/Photo/metadata.py new file mode 100755 index 0000000..310392b --- /dev/null +++ b/Products/Photo/metadata.py @@ -0,0 +1,339 @@ +# -*- coding: utf-8 -*- +####################################################################################### +# Photo is a part of Plinn - http://plinn.org # +# Copyright © 2004-2008 Benoît PIN # +# # +# 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 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)