eggification
[Photo.git] / Products / Photo / metadata.py
diff --git a/Products/Photo/metadata.py b/Products/Photo/metadata.py
new file mode 100755 (executable)
index 0000000..310392b
--- /dev/null
@@ -0,0 +1,339 @@
+# -*- 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)