8738e500a0d97447e97f865c344b659bf75347c2
[Photo.git] / xmp.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Photo is a part of Plinn - http://plinn.org #
4 # Copyright © 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 #
21 #
22
23 from types import StringTypes
24 from logging import getLogger
25 import re
26 console = getLogger('Photo.xmp')
27
28 class XMP(object) :
29 XMP_HEADER = u'<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?>'
30 XMP_HEADER_PATTERN = u'''<\?xpacket begin=['"]\ufeff['"] id=['"]W5M0MpCehiHzreSzNTczkc9d['"][^\?]*\?>'''
31 XMP_PADDING_LINE = u'\u0020' * 63 + u'\n'
32 XMP_TRAILER = u'<?xpacket end="w"?>'
33
34 _readers = {}
35 _writers = {}
36
37
38
39 def __init__(self, file, content_type='image/jpeg', encoding='utf-8') :
40 try :
41 self.reader = self._readers[content_type]
42 except KeyError:
43 raise NotImplementedError, "%r content type not supported by XMP" % content_type
44
45 try :
46 self.writer = self._writers[content_type]
47 except KeyError :
48 self.writer = None
49 console.info('XMP file opened on read-only mode.')
50
51 self.file = file
52 self.encoding = encoding
53 self.xmp = None
54 self._open()
55
56
57 def __del__(self) :
58 try :
59 self.file.close()
60 except :
61 pass
62
63
64 def _open(self) :
65
66 if type(self.file) in StringTypes :
67 self.file = file(self.file)
68
69 packet = self.reader(self.file)
70
71 if packet is not None :
72 # tests / unwrap
73 reEncodedHeader = re.compile(self.XMP_HEADER_PATTERN.encode(self.encoding))
74 m = reEncodedHeader.match(packet)
75 if m is None :
76 console.warn('XMP packet wrapper not found')
77 self.xmp = packet
78 return
79
80 xmp = packet[m.end():]
81 trailer = self.XMP_TRAILER[:-6].encode(self.encoding) # TODO handle read-only mode
82 trailerPos = xmp.find(trailer)
83 assert trailerPos != -1, "No xmp trailer found"
84
85 xmp = xmp[:trailerPos]
86 xmp = xmp.strip()
87 self.xmp = xmp
88 else :
89 self.xmp = None
90
91 def save(self, f=None):
92 original = self.file
93 if f :
94 if type(f) in StringTypes :
95 new = file(f, 'w')
96 else :
97 new = f
98 elif f is None :
99 new = self.file
100
101 self.writer(original, new, self.xmp)
102
103
104 def getXMP(self) :
105 return self.xmp
106
107
108 def setXMP(self, xmp) :
109 self.xmp = xmp
110
111 #
112 # xmp utils
113 #
114
115 @staticmethod
116 def getXmpPadding(size) :
117 # size of trailer in kB
118 return (XMP.XMP_PADDING_LINE * 32 * size)
119
120
121 @staticmethod
122 def genXMPPacket(uXmpData, paddingSize):
123 packet = u''
124
125 packet += XMP.XMP_HEADER
126 packet += uXmpData
127 packet += XMP.getXmpPadding(paddingSize)
128 packet += XMP.XMP_TRAILER
129
130 return packet
131
132
133
134 #
135 # content type registry stuff
136 #
137
138
139 @classmethod
140 def registerReader(cls, content_type, reader) :
141 cls._readers[content_type] = reader
142
143 @classmethod
144 def registerWriter(cls, content_type, writer) :
145 cls._writers[content_type] = writer
146
147 @classmethod
148 def registerWrapper(cls, content_type, wrapper) :
149 """ Registers specific wrapper to prepare data
150 for embedding xmp into specific content_type file.
151 """
152 pass
153
154
155
156 def test() :
157 from xml.dom.minidom import parse
158 data = parse('new.xmp').documentElement.toxml()
159
160 def test1() :
161 original = 'original.jpg'
162 modified = 'modified.jpg'
163
164 x = XMP(original)
165 x.setXMP(data)
166 x.save(modified)
167
168 def test2() :
169 from cStringIO import StringIO
170 sio = StringIO()
171 sio.write(file('modified.jpg').read())
172 sio.seek(0)
173
174 x = XMP(sio)
175 x.setXMP(data)
176 x.save()
177
178 f2 = open('modified2.jpg', 'w')
179 f2.write(sio.read())
180 f2.close()
181
182
183 test1()
184 test2()
185
186
187
188 if __name__ == '__main__' :
189 test()