0b11842e94885af72f27cbdfa6714a8b2ffde7f2
[Photo.git] / 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 $Id: metadata.py 1272 2009-08-11 08:57:35Z pin $
23 $URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/metadata.py $
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._current_filename()
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['rdf:Alt/rdf:li']
141 assert firstLi.unique, "More than one rdf:Alt (localisation not yet supported)"
142 return firstLi.element.content
143 return ''
144
145 security.declarePrivate('getXmpProp')
146 def getXmpProp(self, name, root):
147 index = self.getXmpPathIndex()
148 if index :
149 path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
150 node = index.get(path)
151 if node :
152 return node.element.content
153 return ''
154
155
156 security.declarePrivate('getXmpPathIndex')
157 @memoizedmethod(volatile=True)
158 def getXmpPathIndex(self):
159 xmp = self.getXMP()
160 if xmp :
161 d = parseDoc(xmp)
162 index = xmputils.getPathIndex(d)
163 return index
164
165 security.declarePrivate('getXmpValue')
166 def getXmpValue(self, name):
167 """ returns pythonic version of xmp property """
168 info = xmpAccessors[name]
169 root = info['root']
170 rdfType = info['rdfType'].capitalize()
171 methName = 'getXmp%s' % rdfType
172 meth = getattr(aq_base(self), methName)
173 return meth(name, root)
174
175
176 security.declareProtected(view, 'getXmpField')
177 def getXmpField(self, name):
178 """ returns data formated for a html form field """
179 editableValue = self.getXmpValue(name)
180 if type(editableValue) == TupleType :
181 editableValue = ', '.join(editableValue)
182 return {'id' : name.replace(':', '_'),
183 'value' : editableValue}
184
185
186 #
187 # writing api
188 #
189
190 security.declarePrivate('setXMP')
191 if xmpIO_OK :
192 def setXMP(self, xmp):
193 """setXMP with xmpload call
194 """
195 if self.size :
196 blob = self.bdata
197 if blob.readers :
198 raise BlobError("Already opened for reading.")
199
200 if blob._p_blob_uncommitted is None:
201 filename = blob._create_uncommitted_file()
202 uncommitted = file(filename, 'w')
203 cp(file(blob._p_blob_committed, 'rb'), uncommitted)
204 uncommitted.close()
205 else :
206 filename = blob._p_blob_uncommitted
207
208 loadcmd = '%s %s' % (XMPLOAD, filename)
209 p = Popen(loadcmd, stdin=PIPE, stderr=PIPE, shell=True)
210 p.stdin.write(xmp)
211 p.stdin.close()
212 p.wait()
213 err = p.stderr.read()
214 if err :
215 raise SystemError, err
216
217 f = file(filename)
218 f.seek(0,2)
219 self.updateSize(size=f.tell())
220 f.close()
221 self.bdata._p_changed = True
222
223
224 # purge caches
225 try : del self._methodResultsCache['getXMP']
226 except KeyError : pass
227
228 for name in ('getXmpPathIndex',) :
229 try :
230 del self._v__methodResultsCache[name]
231 except (AttributeError, KeyError):
232 continue
233
234 self.ZCacheable_invalidate()
235 self.ZCacheable_set(None)
236 self.http__refreshEtag()
237
238 else :
239 def setXMP(self, xmp):
240 """setXMP with XMP object
241 """
242 if self.size :
243 bf = self.open('r+')
244 x = XMP(bf, content_type=self.content_type)
245 x.setXMP(xmp)
246 x.save()
247 self.updateSize(size=bf.tell())
248
249 # don't call update_data
250 self.ZCacheable_invalidate()
251 self.ZCacheable_set(None)
252 self.http__refreshEtag()
253
254 # purge caches
255 try : del self._methodResultsCache['getXMP']
256 except KeyError : pass
257 for name in ('getXmpPathIndex', ) :
258 try :
259 del self._v__methodResultsCache[name]
260 except (AttributeError, KeyError):
261 continue
262
263
264
265 security.declarePrivate('setXmpField')
266 def setXmpFields(self, **kw):
267 xmp = self.getXMP()
268 if xmp :
269 doc = parseDoc(xmp)
270 else :
271 doc = xmputils.createEmptyXmpDoc()
272
273 index = xmputils.getPathIndex(doc)
274
275 pathPrefix = 'rdf:RDF/rdf:Description'
276 preferedNsDeclaration = 'rdf:RDF/rdf:Description'
277
278 for id, value in kw.items() :
279 name = id.replace('_', ':')
280 info = xmpAccessors.get(name)
281 if not info : continue
282 root = info['root']
283 rdfType = info['rdfType']
284 path = '/'.join([p for p in [pathPrefix, root, name] if p])
285
286 Metadata._setXmpField(index
287 , path
288 , rdfType
289 , name
290 , value
291 , preferedNsDeclaration)
292
293 # clean empty tags without attributes
294 context = doc.xpathNewContext()
295 nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
296 while nodeset :
297 for n in nodeset :
298 n.unlinkNode()
299 n.freeNode()
300 nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
301
302
303
304 xmp = doc.serialize('utf-8')
305 # remove <?xml version="1.0" encoding="utf-8"?> header
306 xmp = xmp.split('?>', 1)[1].lstrip('\n')
307 self.setXMP(xmp)
308
309 @staticmethod
310 def _setXmpField(index, path, rdfType, name, value, preferedNsDeclaration) :
311 if rdfType in ('Bag', 'Seq') :
312 value = value.replace(';', ',')
313 value = value.split(',')
314 value = [item.strip() for item in value]
315 value = filter(None, value)
316
317 if value :
318 # edit
319 xmpPropIndex = index.getOrCreate(path
320 , rdfType
321 , preferedNsDeclaration)
322 if rdfType == 'prop' :
323 xmpPropIndex.element.setContent(value)
324 else :
325 #rdfPrefix = index.getDocumentNs()['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
326 func = getattr(xmputils, 'createRDF%s' % rdfType)
327 newNode = func(name, value, index)
328 oldNode = xmpPropIndex.element
329 oldNode.replaceNode(newNode)
330 else :
331 # delete
332 xmpPropIndex = index.get(path)
333 if xmpPropIndex is not None :
334 xmpPropIndex.element.unlinkNode()
335 xmpPropIndex.element.freeNode()
336
337
338 InitializeClass(Metadata)