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> #
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. #
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. #
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
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 $
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
33 from logging
import getLogger
34 from cache
import memoizedmethod
35 from libxml2
import parseDoc
36 from standards
.xmp
import accessors
as xmpAccessors
38 from types
import TupleType
39 from subprocess
import Popen
, PIPE
40 from Products
.PortalTransforms
.libtransforms
.utils
import bin_search
, \
43 XPATH_EMPTY_TAGS
= "//node()[name()!='' and not(node()) and not(@*)]"
44 console
= getLogger('Photo.metadata')
52 except MissingBinary
:
54 console
.warn("xmpdump or xmpload not available.")
57 """ Photo metadata read / write mixin """
59 security
= ClassSecurityInfo()
66 security
.declarePrivate('getXMP')
70 """returns xmp metadata packet with xmpdump call
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()
78 raise SystemError, err
84 """returns xmp metadata packet with XMP object
90 x
= XMP(bf
, content_type
=self
.content_type
)
92 except NotImplementedError :
97 security
.declareProtected(view
, 'getXmpFile')
98 def getXmpFile(self
, REQUEST
):
99 """returns the xmp packet over http.
103 return File('xmp', 'xmp', xmp
, content_type
='text/xml').index_html(REQUEST
, REQUEST
.RESPONSE
)
107 security
.declarePrivate('getXmpBag')
108 def getXmpBag(self
, name
, root
, index
=None) :
109 index
= self
.getXmpPathIndex()
111 path
= '/'.join(filter(None, ['rdf:RDF/rdf:Description', root
, name
]))
112 node
= index
.get(path
)
115 values
= xmputils
.getBagValues(node
.element
)
119 security
.declarePrivate('getXmpSeq')
120 def getXmpSeq(self
, name
, root
) :
121 index
= self
.getXmpPathIndex()
123 path
= '/'.join(filter(None, ['rdf:RDF/rdf:Description', root
, name
]))
124 node
= index
.get(path
)
127 values
= xmputils
.getSeqValues(node
.element
)
131 security
.declarePrivate('getXmpAlt')
132 def getXmpAlt(self
, name
, root
) :
133 index
= self
.getXmpPathIndex()
135 path
= '/'.join(filter(None, ['rdf:RDF/rdf:Description', root
, name
]))
136 node
= index
.get(path
)
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
144 security
.declarePrivate('getXmpProp')
145 def getXmpProp(self
, name
, root
):
146 index
= self
.getXmpPathIndex()
148 path
= '/'.join(filter(None, ['rdf:RDF/rdf:Description', root
, name
]))
149 node
= index
.get(path
)
151 return node
.element
.content
155 security
.declarePrivate('getXmpPathIndex')
156 @memoizedmethod(volatile
=True)
157 def getXmpPathIndex(self
):
161 index
= xmputils
.getPathIndex(d
)
164 security
.declarePrivate('getXmpValue')
165 def getXmpValue(self
, name
):
166 """ returns pythonic version of xmp property """
167 info
= xmpAccessors
[name
]
169 rdfType
= info
['rdfType'].capitalize()
170 methName
= 'getXmp%s' % rdfType
171 meth
= getattr(self
.aq_base
, methName
)
172 return meth(name
, root
)
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
}
189 security
.declarePrivate('setXMP')
191 def setXMP(self
, xmp
):
192 """setXMP with xmpload call
197 raise BlobError("Already opened for reading.")
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
)
205 filename
= blob
._p
_blob
_uncommitted
207 loadcmd
= '%s %s' % (XMPLOAD
, filename
)
208 p
= Popen(loadcmd
, stdin
=PIPE
, stderr
=PIPE
, shell
=True)
212 err
= p
.stderr
.read()
214 raise SystemError, err
218 self
.updateSize(size
=f
.tell())
220 self
.bdata
._p
_changed
= True
224 try : del self
._methodResultsCache
['getXMP']
225 except KeyError : pass
227 for name
in ('getXmpPathIndex',) :
229 del self
._v
__methodResultsCache
[name
]
230 except (AttributeError, KeyError):
233 self
.ZCacheable_invalidate()
234 self
.ZCacheable_set(None)
235 self
.http__refreshEtag()
238 def setXMP(self
, xmp
):
239 """setXMP with XMP object
243 x
= XMP(bf
, content_type
=self
.content_type
)
246 self
.updateSize(size
=bf
.tell())
248 # don't call update_data
249 self
.ZCacheable_invalidate()
250 self
.ZCacheable_set(None)
251 self
.http__refreshEtag()
254 try : del self
._methodResultsCache
['getXMP']
255 except KeyError : pass
256 for name
in ('getXmpPathIndex', ) :
258 del self
._v
__methodResultsCache
[name
]
259 except (AttributeError, KeyError):
264 security
.declarePrivate('setXmpField')
265 def setXmpFields(self
, **kw
):
270 doc
= xmputils
.createEmptyXmpDoc()
272 index
= xmputils
.getPathIndex(doc
)
274 pathPrefix
= 'rdf:RDF/rdf:Description'
275 preferedNsDeclaration
= 'rdf:RDF/rdf:Description'
277 for id, value
in kw
.items() :
278 name
= id.replace('_', ':')
279 info
= xmpAccessors
.get(name
)
280 if not info
: continue
282 rdfType
= info
['rdfType']
283 path
= '/'.join([p
for p
in [pathPrefix
, root
, name
] if p
])
285 Metadata
._setXmpField
(index
290 , preferedNsDeclaration
)
292 # clean empty tags without attributes
293 context
= doc
.xpathNewContext()
294 nodeset
= context
.xpathEval(XPATH_EMPTY_TAGS
)
299 nodeset
= context
.xpathEval(XPATH_EMPTY_TAGS
)
303 xmp
= doc
.serialize('utf-8')
304 # remove <?xml version="1.0" encoding="utf-8"?> header
305 xmp
= xmp
.split('?>', 1)[1].lstrip('\n')
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
)
318 xmpPropIndex
= index
.getOrCreate(path
320 , preferedNsDeclaration
)
321 if rdfType
== 'prop' :
322 xmpPropIndex
.element
.setContent(value
)
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
)
331 xmpPropIndex
= index
.get(path
)
332 if xmpPropIndex
is not None :
333 xmpPropIndex
.element
.unlinkNode()
334 xmpPropIndex
.element
.freeNode()
337 InitializeClass(Metadata
)