Ajout de l'accesseur xmp:CreateDate qui est désormais utilisé pour la date de prise...
[Photo.git] / xmputils.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Photo is a part of Plinn - http://plinn.org #
4 # Copyright (C) 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 """ XMP generation utilities.
21
22
23
24 """
25
26 from libxml2 import newNode, parseDoc, treeError
27 # prefix <-> namespaces mappings as defined in the official xmp documentation
28 from standards.xmp import namespaces as xmpNs2Prefix
29 from standards.xmp import prefix2Ns as xmpPrefix2Ns
30
31 TIFF_ORIENTATIONS = {1 : (0, False)
32 ,2 : (0, True)
33 ,3 : (180, False)
34 ,4 : (180, True)
35 ,5 : (90, True)
36 ,6 : (90, False)
37 ,7 : (270, True)
38 ,8 : (270, False)}
39
40 def _getRDFArrayValues(node, arrayType):
41 values = []
42 for element in iterElementChilds(node):
43 if element.name == arrayType and element.ns().content == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' :
44 for value in iterElementChilds(element):
45 if value.name == 'li':
46 values.append(value.content)
47 return tuple(values)
48 else :
49 raise ValueError("No %s found" % arrayType )
50
51 def getBagValues(node):
52 return _getRDFArrayValues(node, 'Bag')
53
54 def getSeqValues(node):
55 return _getRDFArrayValues(node, 'Seq')
56
57
58 def createRDFAlt(surrounded, defaultText, rootIndex):
59 """
60 returns (as libxml2 node):
61 <surrounded>
62 <rdf:Alt>
63 <rdf:li xml:lang="x-default">defaultText</rdf:li>
64 </rdf:Alt>
65 <surrounded>
66 """
67 docNs = rootIndex.getDocumentNs()
68 rdfPrefix = docNs['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
69 normalizedPrefix, name = surrounded.split(':')
70 ns = xmpPrefix2Ns[normalizedPrefix]
71 actualPrefix = docNs[ns]
72
73 surrounded = newNode('%s:%s' % (actualPrefix, name))
74 alt = newNode('%s:Alt' % rdfPrefix)
75 li = newNode('%s:li' % rdfPrefix)
76 li.newProp('xml:lang', 'x-default')
77 li.setContent(defaultText)
78
79 reduce(lambda a, b: a.addChild(b), (surrounded, alt, li))
80
81 return surrounded
82
83
84 def createRDFBag(surrounded, values, rootIndex):
85 """
86 returns (as libxml2 node):
87 <surrounded>
88 <rdf:Bag>
89 <rdf:li>values[0]</rdf:li>
90 ...
91 <rdf:li>values[n]</rdf:li>
92 </rdf:Bag>
93 <surrounded>
94 """
95 return _createRDFArray(surrounded, values, False, rootIndex)
96
97 def createRDFSeq(surrounded, values, rootIndex):
98 """
99 returns (as libxml2 node):
100 <surrounded>
101 <rdf:Seq>
102 <rdf:li>values[0]</rdf:li>
103 ...
104 <rdf:li>values[n]</rdf:li>
105 </rdf:Seq>
106 <surrounded>
107 """
108 return _createRDFArray(surrounded, values, True, rootIndex)
109
110 def _createRDFArray(surrounded, values, ordered, rootIndex):
111 docNs = rootIndex.getDocumentNs()
112 rdfPrefix = docNs['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
113 normalizedPrefix, name = surrounded.split(':')
114 ns = xmpPrefix2Ns[normalizedPrefix]
115 actualPrefix = docNs[ns]
116
117
118 surrounded = newNode('%s:%s' % (actualPrefix, name))
119 if ordered is True :
120 array = newNode('%s:Seq' % rdfPrefix)
121 elif ordered is False :
122 array = newNode('%s:Bag' % rdfPrefix)
123 else :
124 raise ValueError("'ordered' parameter must be a boolean value")
125
126 surrounded.addChild(array)
127
128 for v in values :
129 li = newNode('%s:li' % rdfPrefix)
130 li.setContent(v)
131 array.addChild(li)
132
133 return surrounded
134
135 def createEmptyXmpDoc() :
136 emptyDocument = """
137 <x:xmpmeta xmlns:x='adobe:ns:meta/'>
138 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
139 <rdf:Description/>
140 </rdf:RDF>
141 </x:xmpmeta>
142 """
143 d = parseDoc(emptyDocument)
144 return d
145
146 def getPathIndex(doc) :
147 root = doc.getRootElement()
148 index = PathIndex(root)
149 return index
150
151
152 class PathIndex :
153 """\
154 Class used to provide a convenient tree access to xmp properties by paths.
155 Issues about namespaces and prefixes are normalized during the object
156 instanciation. Ns prefixes used to access elements are those recommended in the
157 official xmp documentation from Adobe.
158 """
159
160 def __init__(self, element, parent=None) :
161 self.unique = True
162 self.element = element
163 self.parent = parent
164
165 elementNs = element.ns().content
166 elementPrefix = element.ns().name
167 recommendedPrefix = xmpNs2Prefix.get(elementNs, elementPrefix)
168
169 self.name = '%s:%s' % (recommendedPrefix, element.name)
170 self.namespace = elementNs
171 self.prefix = elementPrefix
172 self._index = {}
173
174 for prop in iterElementProperties(element) :
175 self.addChildIndex(prop)
176
177 for child in iterElementChilds(element) :
178 self.addChildIndex(child)
179
180 if self.parent is None:
181 self.nsDeclarations = self._namespaceDeclarations()
182
183 def addChildIndex(self, child) :
184 ns = child.ns()
185 if not ns :
186 return
187
188 childNs = ns.content
189 childPrefix = ns.name
190 childRecommendedPrefix = xmpNs2Prefix.get(childNs, childPrefix)
191 childName = '%s:%s' % (childRecommendedPrefix, child.name)
192
193 if not self._index.has_key(childName) :
194 self._index[childName] = PathIndex(child, parent=self)
195 else :
196 childIndex = self._index[childName]
197 childIndex.unique = False
198 for prop in iterElementProperties(child) :
199 childIndex.addChildIndex(prop)
200
201 for c in iterElementChilds(child) :
202 childIndex.addChildIndex(c)
203
204 self._index[childName].parent = self
205 return self._index[childName]
206
207 def _namespaceDeclarations(self) :
208 """\
209 returns ns / prefix pairs as found in xmp packet
210 """
211 namespaces = {}
212 namespaces[self.namespace] = self.prefix
213 for child in self._index.values() :
214 for namespace, prefix in child._namespaceDeclarations().items() :
215 if namespaces.has_key(namespace) :
216 assert namespaces[namespace] == prefix, \
217 "using several prefix for the same namespace is forbidden "\
218 "in this implementation"
219 else :
220 namespaces[namespace] = prefix
221 return namespaces
222
223 def getDocumentNs(self) :
224 root = self.getRootIndex()
225 return root.nsDeclarations
226
227 def exists(self, path) :
228 o = self
229 for part in path.split('/') :
230 if o._index.has_key(part) :
231 o = o._index[part]
232 else :
233 return False
234 return True
235
236 def __getitem__(self, path) :
237 o = self
238 try :
239 for part in path.split('/') :
240 if part == '.' :
241 continue
242 elif part == '..' :
243 o = o.parent
244 o = o._index[part]
245 except ValueError :
246 raise KeyError, path
247 return o
248
249 def get(self, path, default=None) :
250 try :
251 return self[path]
252 except KeyError :
253 return default
254
255 def getRootIndex(self) :
256 root = self
257 while root.parent is not None :
258 root = root.parent
259 return root
260
261 def createChildAndIndex(self, name, rdfType, nsDeclarationElement) :
262 recommandedPrefix, name = name.split(':', 1)
263
264 if rdfType == 'prop' :
265 try :
266 node = self.element.newProp(name, '')
267 except treeError :
268 raise ValueError, (self.element, name)
269 else :
270 node = newNode(name)
271 self.element.addChild(node)
272
273 # bind namespace to new node
274 uri = xmpPrefix2Ns[recommandedPrefix]
275 docNamespaces = self.getDocumentNs()
276 if not docNamespaces.has_key(uri) :
277 try :
278 ns = nsDeclarationElement.newNs(uri, recommandedPrefix)
279 except treeError :
280 raise ValueError, (uri, prefix, self.element, list(nsDeclarationElement.nsDefs()))
281 docNamespaces[uri] = recommandedPrefix
282 else :
283 actualPrefix = docNamespaces[uri]
284 try :
285 ns = self.element.searchNs(None, actualPrefix)
286 except treeError:
287 # cas d'un xmp verbeux : le nouvel élément n'est pas ajouté
288 # dans le rdf:Description du ns correspondant
289 # (après tout, ce n'est pas une obligation)
290 # => on ajoute le ns
291 ns = nsDeclarationElement.newNs(uri, actualPrefix)
292
293
294 node.setNs(ns)
295 return self.addChildIndex(node)
296
297 def getOrCreate(self, path, rdfType, preferedNsDeclaration='rdf:RDF/rdf:Description') :
298 parts = path.split('/')
299
300 if not parts :
301 return self
302
303 name = parts[-1]
304 parts = parts[:-1]
305 root = self.getRootIndex()
306 nsDeclarationElement = root[preferedNsDeclaration].element
307
308 parent = self
309 for p in parts :
310 child = parent._index.get(p, None)
311 if child is None :
312 child = parent.createChildAndIndex(p, None, nsDeclarationElement)
313 parent = child
314
315 child = parent._index.get(name, None)
316 if child is None :
317 child = parent.createChildAndIndex(name, rdfType, nsDeclarationElement)
318
319 return child
320
321 def __str__(self) :
322 out = []
323 pr = out.append
324 path = [self.name]
325 parent = self.parent
326 while parent :
327 path.append(parent.name)
328 parent = parent.parent
329 path.reverse()
330 path = '/'.join(path)
331 pr(path)
332 pr(self.name)
333 pr(self.namespace)
334 pr(str(self.unique))
335 pr('-------')
336
337 for child in self._index.values() :
338 pr(str(child))
339
340 return '\n'.join(out)
341
342 def iterElementChilds(parent) :
343 child = parent.children
344 while child :
345 if child.type == 'element' :
346 yield child
347 child = child.next
348
349 def iterElementProperties(element) :
350 prop = element.properties
351 while prop :
352 if prop.type == 'attribute' :
353 yield prop
354 prop = prop.next