eggification
[Photo.git] / Products / Photo / metadata.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Photo is a part of Plinn - http://plinn.org #
4 # Copyright © 2004-2008 Benoît PIN <benoit.pin@ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """ Photo metadata read / write module
21
22
23
24 """
25
26 from AccessControl import ClassSecurityInfo
27 from Acquisition import aq_base
28 from Globals import InitializeClass
29 from AccessControl.Permissions import view
30 from ZODB.interfaces import BlobError
31 from ZODB.utils import cp
32 from OFS.Image import File
33 from xmp import XMP
34 from logging import getLogger
35 from cache import memoizedmethod
36 from libxml2 import parseDoc
37 from standards.xmp import accessors as xmpAccessors
38 import xmputils
39 from types import TupleType
40 from subprocess import Popen, PIPE
41 from Products.PortalTransforms.libtransforms.utils import bin_search, \
42 MissingBinary
43
44 XPATH_EMPTY_TAGS = "//node()[name()!='' and not(node()) and not(@*)]"
45 console = getLogger('Photo.metadata')
46
47 try :
48 XMPDUMP = 'xmpdump'
49 XMPLOAD = 'xmpload'
50 bin_search(XMPDUMP)
51 bin_search(XMPLOAD)
52 xmpIO_OK = True
53 except MissingBinary :
54 xmpIO_OK = False
55 console.warn("xmpdump or xmpload not available.")
56
57 class Metadata :
58 """ Photo metadata read / write mixin """
59
60 security = ClassSecurityInfo()
61
62
63 #
64 # reading api
65 #
66
67 security.declarePrivate('getXMP')
68 if xmpIO_OK :
69 @memoizedmethod()
70 def getXMP(self):
71 """returns xmp metadata packet with xmpdump call
72 """
73 if self.size :
74 blob_file_path = self.bdata._p_blob_uncommitted or self.bdata._p_blob_committed
75 dumpcmd = '%s %s' % (XMPDUMP, blob_file_path)
76 p = Popen(dumpcmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=True)
77 xmp, err = p.communicate()
78 if err :
79 raise SystemError, err
80 return xmp
81
82 else :
83 @memoizedmethod()
84 def getXMP(self):
85 """returns xmp metadata packet with XMP object
86 """
87 xmp = None
88 if self.size :
89 try :
90 bf = self.open('r')
91 x = XMP(bf, content_type=self.content_type)
92 xmp = x.getXMP()
93 except NotImplementedError :
94 pass
95
96 return xmp
97
98 security.declareProtected(view, 'getXmpFile')
99 def getXmpFile(self, REQUEST):
100 """returns the xmp packet over http.
101 """
102 xmp = self.getXMP()
103 if xmp is not None :
104 return File('xmp', 'xmp', xmp, content_type='text/xml').index_html(REQUEST, REQUEST.RESPONSE)
105 else :
106 return None
107
108 security.declarePrivate('getXmpBag')
109 def getXmpBag(self, name, root, index=None) :
110 index = self.getXmpPathIndex()
111 if index :
112 path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
113 node = index.get(path)
114
115 if node :
116 values = xmputils.getBagValues(node.element)
117 return values
118 return tuple()
119
120 security.declarePrivate('getXmpSeq')
121 def getXmpSeq(self, name, root) :
122 index = self.getXmpPathIndex()
123 if index :
124 path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
125 node = index.get(path)
126
127 if node :
128 values = xmputils.getSeqValues(node.element)
129 return values
130 return tuple()
131
132 security.declarePrivate('getXmpAlt')
133 def getXmpAlt(self, name, root) :
134 index = self.getXmpPathIndex()
135 if index :
136 path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
137 node = index.get(path)
138
139 if node :
140 firstLi = node.get('rdf:Alt/rdf:li')
141 if firstLi :
142 assert firstLi.unique, "More than one rdf:Alt (localisation not yet supported)"
143 return firstLi.element.content
144 return ''
145
146 security.declarePrivate('getXmpProp')
147 def getXmpProp(self, name, root):
148 index = self.getXmpPathIndex()
149 if index :
150 path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
151 node = index.get(path)
152 if node :
153 return node.element.content
154 return ''
155
156
157 security.declarePrivate('getXmpPathIndex')
158 @memoizedmethod(volatile=True)
159 def getXmpPathIndex(self):
160 xmp = self.getXMP()
161 if xmp :
162 d = parseDoc(xmp)
163 index = xmputils.getPathIndex(d)
164 return index
165
166 security.declarePrivate('getXmpValue')
167 def getXmpValue(self, name):
168 """ returns pythonic version of xmp property """
169 info = xmpAccessors[name]
170 root = info['root']
171 rdfType = info['rdfType'].capitalize()
172 methName = 'getXmp%s' % rdfType
173 meth = getattr(aq_base(self), methName)
174 return meth(name, root)
175
176
177 security.declareProtected(view, 'getXmpField')
178 def getXmpField(self, name):
179 """ returns data formated for a html form field """
180 editableValue = self.getXmpValue(name)
181 if type(editableValue) == TupleType :
182 editableValue = ', '.join(editableValue)
183 return {'id' : name.replace(':', '_'),
184 'value' : editableValue}
185
186
187 #
188 # writing api
189 #
190
191 security.declarePrivate('setXMP')
192 if xmpIO_OK :
193 def setXMP(self, xmp):
194 """setXMP with xmpload call
195 """
196 if self.size :
197 blob = self.bdata
198 if blob.readers :
199 raise BlobError("Already opened for reading.")
200
201 if blob._p_blob_uncommitted is None:
202 filename = blob._create_uncommitted_file()
203 uncommitted = file(filename, 'w')
204 cp(file(blob._p_blob_committed, 'rb'), uncommitted)
205 uncommitted.close()
206 else :
207 filename = blob._p_blob_uncommitted
208
209 loadcmd = '%s %s' % (XMPLOAD, filename)
210 p = Popen(loadcmd, stdin=PIPE, stderr=PIPE, shell=True)
211 p.stdin.write(xmp)
212 p.stdin.close()
213 p.wait()
214 err = p.stderr.read()
215 if err :
216 raise SystemError, err
217
218 f = file(filename)
219 f.seek(0,2)
220 self.updateSize(size=f.tell())
221 f.close()
222 self.bdata._p_changed = True
223
224
225 # purge caches
226 try : del self._methodResultsCache['getXMP']
227 except KeyError : pass
228
229 for name in ('getXmpPathIndex',) :
230 try :
231 del self._v__methodResultsCache[name]
232 except (AttributeError, KeyError):
233 continue
234
235 self.ZCacheable_invalidate()
236 self.ZCacheable_set(None)
237 self.http__refreshEtag()
238
239 else :
240 def setXMP(self, xmp):
241 """setXMP with XMP object
242 """
243 if self.size :
244 bf = self.open('r+')
245 x = XMP(bf, content_type=self.content_type)
246 x.setXMP(xmp)
247 x.save()
248 self.updateSize(size=bf.tell())
249
250 # don't call update_data
251 self.ZCacheable_invalidate()
252 self.ZCacheable_set(None)
253 self.http__refreshEtag()
254
255 # purge caches
256 try : del self._methodResultsCache['getXMP']
257 except KeyError : pass
258 for name in ('getXmpPathIndex', ) :
259 try :
260 del self._v__methodResultsCache[name]
261 except (AttributeError, KeyError):
262 continue
263
264
265
266 security.declarePrivate('setXmpField')
267 def setXmpFields(self, **kw):
268 xmp = self.getXMP()
269 if xmp :
270 doc = parseDoc(xmp)
271 else :
272 doc = xmputils.createEmptyXmpDoc()
273
274 index = xmputils.getPathIndex(doc)
275
276 pathPrefix = 'rdf:RDF/rdf:Description'
277 preferedNsDeclaration = 'rdf:RDF/rdf:Description'
278
279 for id, value in kw.items() :
280 name = id.replace('_', ':')
281 info = xmpAccessors.get(name)
282 if not info : continue
283 root = info['root']
284 rdfType = info['rdfType']
285 path = '/'.join([p for p in [pathPrefix, root, name] if p])
286
287 Metadata._setXmpField(index
288 , path
289 , rdfType
290 , name
291 , value
292 , preferedNsDeclaration)
293
294 # clean empty tags without attributes
295 context = doc.xpathNewContext()
296 nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
297 while nodeset :
298 for n in nodeset :
299 n.unlinkNode()
300 n.freeNode()
301 nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
302
303
304
305 xmp = doc.serialize('utf-8')
306 # remove <?xml version="1.0" encoding="utf-8"?> header
307 xmp = xmp.split('?>', 1)[1].lstrip('\n')
308 self.setXMP(xmp)
309
310 @staticmethod
311 def _setXmpField(index, path, rdfType, name, value, preferedNsDeclaration) :
312 if rdfType in ('Bag', 'Seq') :
313 value = value.replace(';', ',')
314 value = value.split(',')
315 value = [item.strip() for item in value]
316 value = filter(None, value)
317
318 if value :
319 # edit
320 xmpPropIndex = index.getOrCreate(path
321 , rdfType
322 , preferedNsDeclaration)
323 if rdfType == 'prop' :
324 xmpPropIndex.element.setContent(value)
325 else :
326 #rdfPrefix = index.getDocumentNs()['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
327 func = getattr(xmputils, 'createRDF%s' % rdfType)
328 newNode = func(name, value, index)
329 oldNode = xmpPropIndex.element
330 oldNode.replaceNode(newNode)
331 else :
332 # delete
333 xmpPropIndex = index.get(path)
334 if xmpPropIndex is not None :
335 xmpPropIndex.element.unlinkNode()
336 xmpPropIndex.element.freeNode()
337
338
339 InitializeClass(Metadata)