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