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
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
._p
_blob
_uncommitted
or self
.bdata
._p
_blob
_committed
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
.get('rdf:Alt/rdf:li')
142 assert firstLi
.unique
, "More than one rdf:Alt (localisation not yet supported)"
143 return firstLi
.element
.content
146 security
.declarePrivate('getXmpProp')
147 def getXmpProp(self
, name
, root
):
148 index
= self
.getXmpPathIndex()
150 path
= '/'.join(filter(None, ['rdf:RDF/rdf:Description', root
, name
]))
151 node
= index
.get(path
)
153 return node
.element
.content
157 security
.declarePrivate('getXmpPathIndex')
158 @memoizedmethod(volatile
=True)
159 def getXmpPathIndex(self
):
163 index
= xmputils
.getPathIndex(d
)
166 security
.declarePrivate('getXmpValue')
167 def getXmpValue(self
, name
):
168 """ returns pythonic version of xmp property """
169 info
= xmpAccessors
[name
]
171 rdfType
= info
['rdfType'].capitalize()
172 methName
= 'getXmp%s' % rdfType
173 meth
= getattr(aq_base(self
), methName
)
174 return meth(name
, root
)
177 security
.declareProtected(view
, 'getXmpField')
178 def getXmpField(self
, name
):
179 """ returns data formated for a html form field """
180 editableValue
= self
.getXmpValue(name
)
181 if type(editableValue
) == TupleType
:
182 editableValue
= ', '.join(editableValue
)
183 return {'id' : name
.replace(':', '_'),
184 'value' : editableValue
}
191 security
.declarePrivate('setXMP')
193 def setXMP(self
, xmp
):
194 """setXMP with xmpload call
199 raise BlobError("Already opened for reading.")
201 if blob
._p
_blob
_uncommitted
is None:
202 filename
= blob
._create
_uncommitted
_file
()
203 uncommitted
= file(filename
, 'w')
204 cp(file(blob
._p
_blob
_committed
, 'rb'), uncommitted
)
207 filename
= blob
._p
_blob
_uncommitted
209 loadcmd
= '%s %s' % (XMPLOAD
, filename
)
210 p
= Popen(loadcmd
, stdin
=PIPE
, stderr
=PIPE
, shell
=True)
214 err
= p
.stderr
.read()
216 raise SystemError, err
220 self
.updateSize(size
=f
.tell())
222 self
.bdata
._p
_changed
= True
226 try : del self
._methodResultsCache
['getXMP']
227 except KeyError : pass
229 for name
in ('getXmpPathIndex',) :
231 del self
._v
__methodResultsCache
[name
]
232 except (AttributeError, KeyError):
235 self
.ZCacheable_invalidate()
236 self
.ZCacheable_set(None)
237 self
.http__refreshEtag()
240 def setXMP(self
, xmp
):
241 """setXMP with XMP object
245 x
= XMP(bf
, content_type
=self
.content_type
)
248 self
.updateSize(size
=bf
.tell())
250 # don't call update_data
251 self
.ZCacheable_invalidate()
252 self
.ZCacheable_set(None)
253 self
.http__refreshEtag()
256 try : del self
._methodResultsCache
['getXMP']
257 except KeyError : pass
258 for name
in ('getXmpPathIndex', ) :
260 del self
._v
__methodResultsCache
[name
]
261 except (AttributeError, KeyError):
266 security
.declarePrivate('setXmpField')
267 def setXmpFields(self
, **kw
):
272 doc
= xmputils
.createEmptyXmpDoc()
274 index
= xmputils
.getPathIndex(doc
)
276 pathPrefix
= 'rdf:RDF/rdf:Description'
277 preferedNsDeclaration
= 'rdf:RDF/rdf:Description'
279 for id, value
in kw
.items() :
280 name
= id.replace('_', ':')
281 info
= xmpAccessors
.get(name
)
282 if not info
: continue
284 rdfType
= info
['rdfType']
285 path
= '/'.join([p
for p
in [pathPrefix
, root
, name
] if p
])
287 Metadata
._setXmpField
(index
292 , preferedNsDeclaration
)
294 # clean empty tags without attributes
295 context
= doc
.xpathNewContext()
296 nodeset
= context
.xpathEval(XPATH_EMPTY_TAGS
)
301 nodeset
= context
.xpathEval(XPATH_EMPTY_TAGS
)
305 xmp
= doc
.serialize('utf-8')
306 # remove <?xml version="1.0" encoding="utf-8"?> header
307 xmp
= xmp
.split('?>', 1)[1].lstrip('\n')
311 def _setXmpField(index
, path
, rdfType
, name
, value
, preferedNsDeclaration
) :
312 if rdfType
in ('Bag', 'Seq') :
313 value
= value
.replace(';', ',')
314 value
= value
.split(',')
315 value
= [item
.strip() for item
in value
]
316 value
= filter(None, value
)
320 xmpPropIndex
= index
.getOrCreate(path
322 , preferedNsDeclaration
)
323 if rdfType
== 'prop' :
324 xmpPropIndex
.element
.setContent(value
)
326 #rdfPrefix = index.getDocumentNs()['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
327 func
= getattr(xmputils
, 'createRDF%s' % rdfType
)
328 newNode
= func(name
, value
, index
)
329 oldNode
= xmpPropIndex
.element
330 oldNode
.replaceNode(newNode
)
333 xmpPropIndex
= index
.get(path
)
334 if xmpPropIndex
is not None :
335 xmpPropIndex
.element
.unlinkNode()
336 xmpPropIndex
.element
.freeNode()
339 InitializeClass(Metadata
)