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> #
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 """ Exif version 2.2 read/write module.
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 $
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.
37 # tags for parsing metadata
38 Exif_IFD_POINTER
= 0x8769
39 GPS_INFO_IFD_POINTER
= 0x8825
40 INTEROPERABILITY_IFD_POINTER
= 0xA005
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
48 STRIPBYTECOUNTS
= 0x117
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}
59 def __init__(self
, f
) :
60 # File Headers are 8 bytes as defined in the TIFF standard.
64 self
.byteOrder
= byteOrder
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))
73 raise ValueError, "Unkwnown byte order: %r" % byteOrder
75 assert r16() == 0x002A, "Incorrect exif header"
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
)]
89 self
.mergedTagInfos
= {}
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')]
100 self
.ifdnames
= ['IFD0']
102 for startfunc
, ifdname
in others
:
105 ret
= self
._loadTagsInfo
(start
, ifdname
)
107 self
.ifdnames
.append(ifdname
)
110 def _loadTagsInfo(self
, start
, ifdname
) :
111 r16
, r32
= self
.r16
, self
.r32
115 numberOfFields
= r16()
116 ifdInfos
= self
.tagInfos
[ifdname
] = {}
118 for i
in xrange(numberOfFields
) :
119 # 12 bytes of the field Interoperability
124 ts
= TYPES_SIZES
[typ
]
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.
133 offset
= self
.tagReaders
[typ
](count
)
136 noise
= self
.f
.read(4 - size
)
138 offsetIsValue
= False
141 ifdInfos
[tag
] = (typ
, count
, offset
, offsetIsValue
)
143 if ifdname
== 'GPS' :
144 self
.gpsTagInfos
.update(ifdInfos
)
146 self
.mergedTagInfos
.update(ifdInfos
)
148 # return nexf ifd offset
151 def getThumbnail(self
) :
152 if hasattr(self
, 'ifd1Offset') :
153 comp
= self
[COMPRESSION_SCHEME
]
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
]
174 return self
.mergedTagInfos
.keys()
176 def has_key(self
, key
) :
177 return self
.mergedTagInfos
.has_key(key
)
179 __contains__
= has_key
# necessary ?
181 def __getitem__(self
, key
) :
182 typ
, count
, offset
, offsetIsValue
= self
.mergedTagInfos
[key
]
187 value
= self
.tagReaders
[typ
](count
)
194 if self
.has_key(key
):
199 def getIFDNames(self
) :
203 def getIFDTags(self
, name
) :
204 tags
= [tag
for tag
in self
.tagInfos
[name
].keys()]
209 def save(self
, out
) :
210 byteOrder
= self
.byteOrder
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
))
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
]
232 out
.write(self
.byteOrder
)
235 r32(8) # offset of IFD0
238 isPtrTag
= POINTER_TAGS
.has_key
240 for ifdname
in self
.getIFDName() :
241 ifdInfos
= self
.tagInfos
[name
]
242 tags
= ifdInfos
.keys()
245 ifdStarts
[ifdname
] = out
.tell()
247 tiffOffset
= ifdStarts
[ifdname
] + INTEROPERABILITY_FIELD_LENGTH
* len(tags
) + 4
248 moreThan4bytesValuesTags
= []
250 for tag
, info
in ifdInfos
.items() :
252 pointerTags
.append((tag
, out
.tell()))
253 typ
, count
, offset
, offsetIsValue
= info
259 ts
= TYPES_SIZES
[typ
]
263 if count
== 1 : offset
= [offset
]
264 tagWriters
[typ
](offset
)
267 for i
in range(4 - size
) : out
.write('\0')
271 moreThan4bytesValuesTags
.append(tag
)
273 for tag
in moreThan4bytesValuesTags
:
274 typ
, count
, offset
, offsetIsValue
= ifdInfos
[tag
]
276 size
= TYPES_SIZES
[typ
] * count
277 out
.write(self
.f
.read(size
))
279 # write place-holder for next ifd offset (updated later)
284 return ord(c
[1]) + (ord(c
[0])<<8)
286 return chr(i
>> 8 & 255) + chr(i
& 255)
289 return ord(c
[3]) + (ord(c
[2])<<8) + (ord(c
[1])<<16) + (ord(c
[0])<<24)
291 return chr(i
>> 24 & 0xff) + chr(i
>> 16 & 0xff) + chr(i
>> 8 & 0xff) + chr(i
& 0xff)
295 return ord(c
[0]) + (ord(c
[1])<<8)
297 return chr(i
&255) + chr(i
>>8&255)
300 return ord(c
[0]) + (ord(c
[1])<<8) + (ord(c
[2])<<16) + (ord(c
[3])<<24)
302 return chr(i
&255) + chr(i
>>8&255) + chr(i
>>16&255) + chr(i
>>24&255)
307 def testRead(*paths
) :
308 from PIL
.Image
import open as imgopen
309 from standards
.exif
import TAGS
310 from cStringIO
import StringIO
314 paths
.extend(['testimages/%s'%name
for name
in os
.listdir('testimages') \
315 if name
.endswith('.jpg') and \
316 not name
.endswith('_thumb.jpg')])
324 exifBlock
= [a
[1] for a
in applist
if a
[0] == 'APP1' and a
[1].startswith("Exif\x00\x00")][0]
329 for name
in e
.getIFDNames() :
331 for tag
in e
.getIFDTags(name
) :
332 print hex(tag
), TAGS
.get(tag
), e
[tag
]
335 thumb
= e
.getThumbnail()
336 if thumb
is not None :
338 out
= open('%s_thumb.jpg' % path
[:-4], 'w')
342 def testWrite(*paths
) :
343 from PIL
.Image
import open as imgopen
344 from standards
.exif
import TAGS
345 from cStringIO
import StringIO
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')])
359 exifBlock
= [a
[1] for a
in applist
if a
[0] == 'APP1' and a
[1].startswith("Exif\x00\x00")][0]
361 from cStringIO
import StringIO
369 print '%r' % out
.read()
372 if __name__
== '__main__' :
373 testRead('testMM.jpg', 'testII.jpg')
374 #testWrite('testMM.jpg', 'testII.jpg')