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 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
34 from logging
import getLogger
35 from cache
import memoizedmethod
36 from libxml2
import parseDoc
37 from standards
.xmp
import accessors
as xmpAccessors
39 from types
import TupleType
40 from subprocess
import Popen
, PIPE
41 from Products
.PortalTransforms
.libtransforms
.utils
import bin_search
, \
44 XPATH_EMPTY_TAGS
= "//node()[name()!='' and not(node()) and not(@*)]"
45 console
= getLogger('Photo.metadata')
53 except MissingBinary
:
55 console
.warn("xmpdump or xmpload not available.")
58 """ Photo metadata read / write mixin """
60 security
= ClassSecurityInfo()
67 security
.declarePrivate('getXMP')
71 """returns xmp metadata packet with xmpdump call
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()
79 raise SystemError, err
85 """returns xmp metadata packet with XMP object
91 x
= XMP(bf
, content_type
=self
.content_type
)
93 except NotImplementedError :
98 security
.declareProtected(view
, 'getXmpFile')
99 def getXmpFile(self
, REQUEST
):
100 """returns the xmp packet over http.
104 return File('xmp', 'xmp', xmp
, content_type
='text/xml').index_html(REQUEST
, REQUEST
.RESPONSE
)
108 security
.declarePrivate('getXmpBag')
109 def getXmpBag(self
, name
, root
, index
=None) :
110 index
= self
.getXmpPathIndex()
112 path
= '/'.join(filter(None, ['rdf:RDF/rdf:Description', root
, name
]))
113 node
= index
.get(path
)
116 values
= xmputils
.getBagValues(node
.element
)
120 security
.declarePrivate('getXmpSeq')
121 def getXmpSeq(self
, name
, root
) :
122 index
= self
.getXmpPathIndex()
124 path
= '/'.join(filter(None, ['rdf:RDF/rdf:Description', root
, name
]))
125 node
= index
.get(path
)
128 values
= xmputils
.getSeqValues(node
.element
)
132 security
.declarePrivate('getXmpAlt')
133 def getXmpAlt(self
, name
, root
) :
134 index
= self
.getXmpPathIndex()
136 path
= '/'.join(filter(None, ['rdf:RDF/rdf:Description', root
, name
]))
137 node
= index
.get(path
)
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
145 security
.declarePrivate('getXmpProp')
146 def getXmpProp(self
, name
, root
):
147 index
= self
.getXmpPathIndex()
149 path
= '/'.join(filter(None, ['rdf:RDF/rdf:Description', root
, name
]))
150 node
= index
.get(path
)
152 return node
.element
.content
156 security
.declarePrivate('getXmpPathIndex')
157 @memoizedmethod(volatile
=True)
158 def getXmpPathIndex(self
):
162 index
= xmputils
.getPathIndex(d
)
165 security
.declarePrivate('getXmpValue')
166 def getXmpValue(self
, name
):
167 """ returns pythonic version of xmp property """
168 info
= xmpAccessors
[name
]
170 rdfType
= info
['rdfType'].capitalize()
171 methName
= 'getXmp%s' % rdfType
172 meth
= getattr(aq_base(self
), methName
)
173 return meth(name
, root
)
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
}
190 security
.declarePrivate('setXMP')
192 def setXMP(self
, xmp
):
193 """setXMP with xmpload call
198 raise BlobError("Already opened for reading.")
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
)
206 filename
= blob
._p
_blob
_uncommitted
208 loadcmd
= '%s %s' % (XMPLOAD
, filename
)
209 p
= Popen(loadcmd
, stdin
=PIPE
, stderr
=PIPE
, shell
=True)
213 err
= p
.stderr
.read()
215 raise SystemError, err
219 self
.updateSize(size
=f
.tell())
221 self
.bdata
._p
_changed
= True
225 try : del self
._methodResultsCache
['getXMP']
226 except KeyError : pass
228 for name
in ('getXmpPathIndex',) :
230 del self
._v
__methodResultsCache
[name
]
231 except (AttributeError, KeyError):
234 self
.ZCacheable_invalidate()
235 self
.ZCacheable_set(None)
236 self
.http__refreshEtag()
239 def setXMP(self
, xmp
):
240 """setXMP with XMP object
244 x
= XMP(bf
, content_type
=self
.content_type
)
247 self
.updateSize(size
=bf
.tell())
249 # don't call update_data
250 self
.ZCacheable_invalidate()
251 self
.ZCacheable_set(None)
252 self
.http__refreshEtag()
255 try : del self
._methodResultsCache
['getXMP']
256 except KeyError : pass
257 for name
in ('getXmpPathIndex', ) :
259 del self
._v
__methodResultsCache
[name
]
260 except (AttributeError, KeyError):
265 security
.declarePrivate('setXmpField')
266 def setXmpFields(self
, **kw
):
271 doc
= xmputils
.createEmptyXmpDoc()
273 index
= xmputils
.getPathIndex(doc
)
275 pathPrefix
= 'rdf:RDF/rdf:Description'
276 preferedNsDeclaration
= 'rdf:RDF/rdf:Description'
278 for id, value
in kw
.items() :
279 name
= id.replace('_', ':')
280 info
= xmpAccessors
.get(name
)
281 if not info
: continue
283 rdfType
= info
['rdfType']
284 path
= '/'.join([p
for p
in [pathPrefix
, root
, name
] if p
])
286 Metadata
._setXmpField
(index
291 , preferedNsDeclaration
)
293 # clean empty tags without attributes
294 context
= doc
.xpathNewContext()
295 nodeset
= context
.xpathEval(XPATH_EMPTY_TAGS
)
300 nodeset
= context
.xpathEval(XPATH_EMPTY_TAGS
)
304 xmp
= doc
.serialize('utf-8')
305 # remove <?xml version="1.0" encoding="utf-8"?> header
306 xmp
= xmp
.split('?>', 1)[1].lstrip('\n')
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
)
319 xmpPropIndex
= index
.getOrCreate(path
321 , preferedNsDeclaration
)
322 if rdfType
== 'prop' :
323 xmpPropIndex
.element
.setContent(value
)
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
)
332 xmpPropIndex
= index
.get(path
)
333 if xmpPropIndex
is not None :
334 xmpPropIndex
.element
.unlinkNode()
335 xmpPropIndex
.element
.freeNode()
338 InitializeClass(Metadata
)