Ajout d'un test pour getXmpAlt : il arrive que Lr3 produise des Alt vides.
[Photo.git] / exif.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 """ Exif version 2.2 read/write module.
21
22 $Id: exif.py 360 2008-02-21 09:17:32Z pin $
23 $URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/exif.py $
24 """
25
26 TYPES_SIZES = {
27 1: 1 # BYTE An 8-bit unsigned integer.,
28 , 2: 1 # ASCII An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.,
29 , 3: 2 # SHORT A 16-bit (2-byte) unsigned integer,
30 , 4: 4 # LONG A 32-bit (4-byte) unsigned integer,
31 , 5: 8 # RATIONAL Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator.,
32 , 7: 1 # UNDEFINED An 8-bit byte that can take any value depending on the field definition,
33 , 9: 4 # SLONG A 32-bit (4-byte) signed integer (2's complement notation),
34 , 10 : 8 # SRATIONAL Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
35 }
36
37 # tags for parsing metadata
38 Exif_IFD_POINTER = 0x8769
39 GPS_INFO_IFD_POINTER = 0x8825
40 INTEROPERABILITY_IFD_POINTER = 0xA005
41
42 # tags to get thumbnail
43 COMPRESSION_SCHEME = 0x103
44 COMPRESSION_SCHEME_TYPES = {1:'image/bmp', 6:'image/jpeg'}
45 OFFSET_TO_JPEG_SOI = 0x201
46 BYTES_OF_JPEG_DATA = 0x202
47 STRIPOFFSETS = 0x111
48 STRIPBYTECOUNTS = 0x117
49
50 # constants for writing
51 INTEROPERABILITY_FIELD_LENGTH = 12
52 POINTER_TAGS = { Exif_IFD_POINTER:True
53 , GPS_INFO_IFD_POINTER:True
54 , INTEROPERABILITY_IFD_POINTER:True}
55
56
57 class Exif(dict) :
58
59 def __init__(self, f) :
60 # File Headers are 8 bytes as defined in the TIFF standard.
61 self.f = f
62
63 byteOrder = f.read(2)
64 self.byteOrder = byteOrder
65
66 if byteOrder == 'MM' :
67 r16 = self.r16 = lambda:ib16(f.read(2))
68 r32 = self.r32 = lambda:ib32(f.read(4))
69 elif byteOrder == 'II' :
70 r16 = self.r16 = lambda:il16(f.read(2))
71 r32 = self.r32 = lambda:il32(f.read(4))
72 else :
73 raise ValueError, "Unkwnown byte order: %r" % byteOrder
74
75 assert r16() == 0x002A, "Incorrect exif header"
76
77 self.tagReaders = {
78 1: lambda c : [ord(f.read(1)) for i in xrange(c)]
79 , 2: lambda c : f.read(c)
80 , 3: lambda c : [r16() for i in xrange(c)]
81 , 4: lambda c : [r32() for i in xrange(c)]
82 , 5: lambda c : [(r32(), r32()) for i in xrange(c)]
83 , 7: lambda c : f.read(c)
84 , 9: lambda c : [r32() for i in xrange(c)]
85 , 10: lambda c : [(r32(), r32()) for i in xrange(c)]
86 }
87
88 self.tagInfos = {}
89 self.mergedTagInfos = {}
90 self.gpsTagInfos = {}
91
92 ifd0Offset = r32()
93
94 ifd1Offset = self._loadTagsInfo(ifd0Offset, 'IFD0')
95 others = [(lambda:self[Exif_IFD_POINTER], 'Exif'),
96 (lambda:self.get(GPS_INFO_IFD_POINTER), 'GPS'),
97 (lambda:self.get(INTEROPERABILITY_IFD_POINTER), 'Interoperability'),
98 (lambda:ifd1Offset, 'IFD1')]
99
100 self.ifdnames = ['IFD0']
101
102 for startfunc, ifdname in others :
103 start = startfunc()
104 if start :
105 ret = self._loadTagsInfo(start, ifdname)
106 assert ret == 0
107 self.ifdnames.append(ifdname)
108
109
110 def _loadTagsInfo(self, start, ifdname) :
111 r16, r32 = self.r16, self.r32
112
113 self.f.seek(start)
114
115 numberOfFields = r16()
116 ifdInfos = self.tagInfos[ifdname] = {}
117
118 for i in xrange(numberOfFields) :
119 # 12 bytes of the field Interoperability
120 tag = r16()
121 typ = r16()
122 count = r32()
123
124 ts = TYPES_SIZES[typ]
125 size = ts * count
126
127 # In cases where the value fits in 4 bytes,
128 # the value itself is recorded.
129 # If the value is smaller than 4 bytes, the value is
130 # stored in the 4-byte area starting from the left.
131 if size <= 4 :
132 offsetIsValue = True
133 offset = self.tagReaders[typ](count)
134 if count == 1:
135 offset = offset[0]
136 noise = self.f.read(4 - size)
137 else :
138 offsetIsValue = False
139 offset = r32()
140
141 ifdInfos[tag] = (typ, count, offset, offsetIsValue)
142
143 if ifdname == 'GPS' :
144 self.gpsTagInfos.update(ifdInfos)
145 else :
146 self.mergedTagInfos.update(ifdInfos)
147
148 # return nexf ifd offset
149 return r32()
150
151 def getThumbnail(self) :
152 if hasattr(self, 'ifd1Offset') :
153 comp = self[COMPRESSION_SCHEME]
154 if comp == 6 :
155 # TODO : handle uncompressed thumbnails
156 mime = COMPRESSION_SCHEME_TYPES.get(comp, 'unknown')
157 start = self[OFFSET_TO_JPEG_SOI]
158 count = self[BYTES_OF_JPEG_DATA]
159 f = self.f
160 f.seek(start)
161 data = f.read(count)
162 return data, mime
163 else :
164 return None
165 else :
166 return None
167
168
169
170 #
171 # dict interface
172 #
173 def keys(self) :
174 return self.mergedTagInfos.keys()
175
176 def has_key(self, key) :
177 return self.mergedTagInfos.has_key(key)
178
179 __contains__ = has_key # necessary ?
180
181 def __getitem__(self, key) :
182 typ, count, offset, offsetIsValue = self.mergedTagInfos[key]
183 if offsetIsValue :
184 return offset
185 else :
186 self.f.seek(offset)
187 value = self.tagReaders[typ](count)
188 if count == 1:
189 return value[0]
190 else :
191 return value
192
193 def get(self, key) :
194 if self.has_key(key):
195 return self[key]
196 else :
197 return None
198
199 def getIFDNames(self) :
200 return self.ifdnames
201
202
203 def getIFDTags(self, name) :
204 tags = [tag for tag in self.tagInfos[name].keys()]
205 tags.sort()
206 return tags
207
208
209 def save(self, out) :
210 byteOrder = self.byteOrder
211
212 if byteOrder == 'MM' :
213 w16 = self.w16 = lambda i : out.write(ob16(i))
214 w32 = self.w32 = lambda i : out.write(ob32(i))
215 elif byteOrder == 'II' :
216 w16 = self.w16 = lambda i : out.write(ol16(i))
217 w32 = self.w32 = lambda i : out.write(ol32(i))
218
219 tagWriters = {
220 1: lambda l : [out.write(chr(i)) for i in l]
221 , 2: lambda l : out.write(l)
222 , 3: lambda l : [w16(i) for i in l]
223 , 4: lambda l : [w32(i) for i in l]
224 , 5: lambda l : [(w32(i[0]), w32(i[1])) for i in l]
225 , 7: lambda l : out.write(l)
226 , 9: lambda l : [w32(i) for i in l]
227 , 10: lambda l : [(w32(i[0]), w32(i[1])) for i in l]
228 }
229
230
231 # tiff header
232 out.write(self.byteOrder)
233 w16(0x002A)
234 tags = self.keys()
235 r32(8) # offset of IFD0
236 ifdStarts = {}
237 pointerTags = []
238 isPtrTag = POINTER_TAGS.has_key
239
240 for ifdname in self.getIFDName() :
241 ifdInfos = self.tagInfos[name]
242 tags = ifdInfos.keys()
243 tags.sort()
244
245 ifdStarts[ifdname] = out.tell()
246
247 tiffOffset = ifdStarts[ifdname] + INTEROPERABILITY_FIELD_LENGTH * len(tags) + 4
248 moreThan4bytesValuesTags = []
249
250 for tag, info in ifdInfos.items() :
251 if isPtrTag(tag) :
252 pointerTags.append((tag, out.tell()))
253 typ, count, offset, offsetIsValue = info
254
255 w16(tag)
256 w16(typ)
257 w32(count)
258
259 ts = TYPES_SIZES[typ]
260 size = ts * count
261
262 if size <= 4 :
263 if count == 1 : offset = [offset]
264 tagWriters[typ](offset)
265
266 # padding
267 for i in range(4 - size) : out.write('\0')
268 else :
269 w32(tiffOffset)
270 tiffOffset += size
271 moreThan4bytesValuesTags.append(tag)
272
273 for tag in moreThan4bytesValuesTags :
274 typ, count, offset, offsetIsValue = ifdInfos[tag]
275 self.f.seek(offset)
276 size = TYPES_SIZES[typ] * count
277 out.write(self.f.read(size))
278
279 # write place-holder for next ifd offset (updated later)
280 r32(0)
281
282
283 def ib16(c):
284 return ord(c[1]) + (ord(c[0])<<8)
285 def ob16(i) :
286 return chr(i >> 8 & 255) + chr(i & 255)
287
288 def ib32(c):
289 return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24)
290 def ob32(c):
291 return chr(i >> 24 & 0xff) + chr(i >> 16 & 0xff) + chr(i >> 8 & 0xff) + chr(i & 0xff)
292
293
294 def il16(c):
295 return ord(c[0]) + (ord(c[1])<<8)
296 def ol16(i):
297 return chr(i&255) + chr(i>>8&255)
298
299 def il32(c):
300 return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24)
301 def ol32(i):
302 return chr(i&255) + chr(i>>8&255) + chr(i>>16&255) + chr(i>>24&255)
303
304
305
306
307 def testRead(*paths) :
308 from PIL.Image import open as imgopen
309 from standards.exif import TAGS
310 from cStringIO import StringIO
311
312 import os
313 paths = list(paths)
314 paths.extend(['testimages/%s'%name for name in os.listdir('testimages') \
315 if name.endswith('.jpg') and \
316 not name.endswith('_thumb.jpg')])
317
318 for path in paths :
319 print '------------'
320 print path
321 print '------------'
322 im = imgopen(path)
323 applist = im.applist
324 exifBlock = [a[1] for a in applist if a[0] == 'APP1' and a[1].startswith("Exif\x00\x00")][0]
325 exif = exifBlock[6:]
326 sio = StringIO(exif)
327
328 e = Exif(sio)
329 for name in e.getIFDNames() :
330 print '%s: ' %name
331 for tag in e.getIFDTags(name) :
332 print hex(tag), TAGS.get(tag), e[tag]
333 print
334
335 thumb = e.getThumbnail()
336 if thumb is not None :
337 data, mime = thumb
338 out = open('%s_thumb.jpg' % path[:-4], 'w')
339 out.write(data)
340 out.close()
341
342 def testWrite(*paths) :
343 from PIL.Image import open as imgopen
344 from standards.exif import TAGS
345 from cStringIO import StringIO
346
347 # import os
348 # paths = list(paths)
349 # paths.extend(['testimages/%s'%name for name in os.listdir('testimages') \
350 # if name.endswith('.jpg') and \
351 # not name.endswith('_thumb.jpg')])
352
353 for path in paths :
354 print '------------'
355 print path
356 print '------------'
357 im = imgopen(path)
358 applist = im.applist
359 exifBlock = [a[1] for a in applist if a[0] == 'APP1' and a[1].startswith("Exif\x00\x00")][0]
360 exif = exifBlock[6:]
361 from cStringIO import StringIO
362 sio = StringIO(exif)
363
364 e = Exif(sio)
365
366 out = StringIO()
367 e.save(out)
368 out.seek(0)
369 print '%r' % out.read()
370
371
372 if __name__ == '__main__' :
373 testRead('testMM.jpg', 'testII.jpg')
374 #testWrite('testMM.jpg', 'testII.jpg')