author | Tero Marttila <terom@fixme.fi> |
Sat, 13 Jun 2009 20:31:51 +0300 | |
branch | new-exif |
changeset 104 | 6afe59e5ffae |
parent 103 | 63e89dc2d6f1 |
child 105 | effae6f38749 |
permissions | -rw-r--r-- |
102 | 1 |
""" |
2 |
A custom EXIF parsing module, aimed at high performance. |
|
3 |
""" |
|
4 |
||
5 |
import struct, mmap, os |
|
6 |
||
7 |
def read_struct (file, fmt) : |
|
8 |
""" |
|
9 |
Utility function to read data from the a file using struct |
|
10 |
""" |
|
11 |
||
12 |
# length of data |
|
13 |
fmt_size = struct.calcsize(fmt) |
|
14 |
||
15 |
# get data |
|
16 |
file_data = file.read(fmt_size) |
|
17 |
||
18 |
# unpack single item, this should raise an error if file_data is too short |
|
19 |
return struct.unpack(fmt, file_data) |
|
20 |
||
21 |
class Buffer (object) : |
|
22 |
""" |
|
23 |
Wraps a buffer object (anything that supports the python buffer protocol) for read-only access. |
|
24 |
||
25 |
Includes an offset for relative values, and an endianess for reading binary data. |
|
26 |
""" |
|
27 |
||
28 |
def __init__ (self, obj, offset=None, size=None, struct_prefix='=') : |
|
29 |
""" |
|
30 |
Create a new Buffer object with a new underlying buffer, created from the given object, offset and size. |
|
31 |
||
32 |
The endiannes is given in the form of a struct-module prefix, which should be one of '<' or '>'. |
|
33 |
Standard size/alignment are assumed. |
|
34 |
""" |
|
35 |
||
36 |
# store |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
37 |
self.buf = buffer(obj, *(arg for arg in (offset, size) if arg is not None)) |
102 | 38 |
self.offset = offset |
39 |
self.size = size |
|
40 |
self.prefix = struct_prefix |
|
41 |
||
42 |
def subregion (self, offset, length=None) : |
|
43 |
""" |
|
44 |
Create a new sub-Buffer referencing a view of this buffer, at the given offset, and with the given |
|
45 |
length, if any, and the same struct_prefix. |
|
46 |
""" |
|
47 |
||
48 |
return Buffer(self.buf, offset, length, struct_prefix=self.prefix) |
|
49 |
||
50 |
def pread (self, offset, length) : |
|
51 |
""" |
|
52 |
Read a random-access region of raw data |
|
53 |
""" |
|
54 |
||
55 |
return self.buf[offset:offset + length] |
|
56 |
||
57 |
def pread_struct (self, offset, fmt) : |
|
58 |
""" |
|
59 |
Read structured data using the given struct format from the given offset. |
|
60 |
""" |
|
61 |
||
62 |
return struct.unpack_from(self.prefix + fmt, self.buf, offset=offset) |
|
63 |
||
64 |
def pread_item (self, offset, fmt) : |
|
65 |
""" |
|
66 |
Read a single item of structured data from the given offset. |
|
67 |
""" |
|
68 |
||
69 |
value, = self.pread_struct(offset, fmt) |
|
70 |
||
71 |
return value |
|
72 |
||
73 |
def iter_offsets (self, count, size, offset=0) : |
|
74 |
""" |
|
75 |
Yield a series of offsets for `count` items of `size` bytes, beginning at `offset`. |
|
76 |
""" |
|
77 |
||
78 |
return xrange(offset, offset + count * size, size) |
|
79 |
||
80 |
def item_size (self, fmt) : |
|
81 |
""" |
|
82 |
Returns the size in bytes of the given item format |
|
83 |
""" |
|
84 |
||
85 |
return struct.calcsize(self.prefix + fmt) |
|
86 |
||
87 |
def unpack_item (self, fmt, data) : |
|
88 |
""" |
|
89 |
Unpacks a single item from the given data |
|
90 |
""" |
|
91 |
||
92 |
value, = struct.unpack(self.prefix + fmt, data) |
|
93 |
||
94 |
return value |
|
95 |
||
96 |
def mmap_buffer (file, size) : |
|
97 |
""" |
|
98 |
Create and return a new read-only mmap'd region |
|
99 |
""" |
|
100 |
||
101 |
return mmap.mmap(file.fileno(), size, access=mmap.ACCESS_READ) |
|
102 |
||
103 |
import exif_data |
|
104 |
||
105 |
class Tag (object) : |
|
106 |
""" |
|
107 |
Represents a single Tag in an IFD |
|
108 |
""" |
|
109 |
||
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
110 |
def __init__ (self, offset, tag, type, count, data_raw) : |
102 | 111 |
""" |
112 |
Build a Tag with the given binary items from the IFD entry |
|
113 |
""" |
|
114 |
||
115 |
self.offset = offset |
|
116 |
self.tag = tag |
|
117 |
self.type = type |
|
118 |
self.count = count |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
119 |
self.data_raw = data_raw |
102 | 120 |
|
121 |
# lookup the type for this tag |
|
122 |
self.type_data = exif_data.FIELD_TYPES.get(type) |
|
123 |
||
124 |
# unpack it |
|
125 |
if self.type_data : |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
126 |
self.type_format, self.type_name, self.type_func = self.type_data |
102 | 127 |
|
128 |
# lookup the tag data for this tag |
|
129 |
self.tag_data = exif_data.EXIF_TAGS.get(tag) |
|
130 |
||
131 |
# unpack it |
|
132 |
if self.tag_data : |
|
133 |
# the EXIF tag name |
|
104 | 134 |
self.tag_name, self.tag_value_spec = self.tag_data |
102 | 135 |
|
136 |
@property |
|
137 |
def name (self) : |
|
138 |
""" |
|
139 |
Lookup the name of this tag via its code, returns None if unknown. |
|
140 |
""" |
|
141 |
||
142 |
if self.tag_data : |
|
143 |
return self.tag_name |
|
144 |
||
145 |
else : |
|
146 |
return None |
|
147 |
||
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
148 |
def process_values (self, raw_values) : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
149 |
""" |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
150 |
Process the given raw values unpacked from the file. |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
151 |
""" |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
152 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
153 |
if self.type_data and self.type_func : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
154 |
# use the filter func |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
155 |
return self.type_func(raw_values) |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
156 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
157 |
else : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
158 |
# nada, just leave them |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
159 |
return raw_values |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
160 |
|
104 | 161 |
def readable_value (self, values) : |
102 | 162 |
""" |
104 | 163 |
Convert the given values for this tag into a human-readable string. |
102 | 164 |
|
104 | 165 |
Returns the comma-separated values by default. |
102 | 166 |
""" |
167 |
||
104 | 168 |
if self.tag_data : |
169 |
spec = self.tag_value_spec |
|
102 | 170 |
|
171 |
else : |
|
104 | 172 |
# fallback to default |
173 |
spec = None |
|
174 |
||
175 |
# map it |
|
176 |
return exif_data.map_values(spec, values) |
|
102 | 177 |
|
178 |
# size of an IFD entry in bytes |
|
179 |
IFD_ENTRY_SIZE = 12 |
|
180 |
||
181 |
class IFD (Buffer) : |
|
182 |
""" |
|
183 |
Represents an IFD (Image file directory) region in EXIF data. |
|
184 |
""" |
|
185 |
||
186 |
def __init__ (self, buffer, **buffer_opts) : |
|
187 |
""" |
|
188 |
Access the IFD data from the given bufferable object with given buffer opts. |
|
189 |
||
190 |
This will read the `count` and `next_offset` values. |
|
191 |
""" |
|
192 |
||
193 |
# init |
|
194 |
super(IFD, self).__init__(buffer, **buffer_opts) |
|
195 |
||
196 |
# read header |
|
197 |
self.count = self.pread_item(0, 'H') |
|
198 |
||
199 |
# read next-offset |
|
200 |
self.next_offset = self.pread_item(0x02 + self.count * IFD_ENTRY_SIZE, 'I') |
|
201 |
||
202 |
def iter_tags (self) : |
|
203 |
""" |
|
204 |
Iterate over all the Tag objects in this IFD |
|
205 |
""" |
|
206 |
||
207 |
# read each tag |
|
208 |
for offset in self.iter_offsets(self.count, IFD_ENTRY_SIZE, 0x02) : |
|
209 |
# read the tag data |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
210 |
tag, type, count, data_raw = self.pread_struct(offset, 'HHI4s') |
102 | 211 |
|
212 |
# yield the new Tag |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
213 |
yield Tag(offset, tag, type, count, data_raw) |
102 | 214 |
|
215 |
class EXIF (Buffer) : |
|
216 |
""" |
|
217 |
Represents the EXIF data embedded in some image file in the form of a Region. |
|
218 |
""" |
|
219 |
||
220 |
def __init__ (self, buffer, tags=None, **buffer_opts) : |
|
221 |
""" |
|
222 |
Access the EXIF data from the given bufferable object with the given buffer options. |
|
223 |
||
224 |
`tags`, if given, specifies that only the given named tags should be loaded. |
|
225 |
""" |
|
226 |
||
227 |
# init Buffer |
|
228 |
super(EXIF, self).__init__(buffer, **buffer_opts) |
|
229 |
||
230 |
# store |
|
231 |
self.buffer = buffer |
|
232 |
||
233 |
def iter_ifds (self) : |
|
234 |
""" |
|
104 | 235 |
Iterate over the primary IFDs in this EXIF. |
102 | 236 |
""" |
237 |
||
238 |
# starting offset |
|
239 |
offset = self.pread_item(0x04, 'I') |
|
240 |
||
241 |
while offset : |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
242 |
# create and read the IFD, operating on the right sub-buffer |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
243 |
ifd = IFD(self.buf, offset=offset) |
102 | 244 |
|
245 |
# yield it |
|
246 |
yield ifd |
|
247 |
||
248 |
# skip to next offset |
|
249 |
offset = ifd.next_offset |
|
250 |
||
104 | 251 |
def iter_all_ifds (self) : |
252 |
""" |
|
253 |
Iterate over all of the IFDs contained within this EXIF, or within other IFDs. |
|
254 |
""" |
|
255 |
||
102 | 256 |
__iter__ = iter_ifds |
257 |
||
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
258 |
def tag_data_info (self, tag) : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
259 |
""" |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
260 |
Calculate the location, format and size of the given tag's data. |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
261 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
262 |
Returns a (fmt, offset, size) tuple. |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
263 |
""" |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
264 |
# unknown tag? |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
265 |
if not tag.type_data : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
266 |
return None |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
267 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
268 |
# data format |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
269 |
if len(tag.type_format) == 1 : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
270 |
# let struct handle the count |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
271 |
fmt = "%d%s" % (tag.count, tag.type_format) |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
272 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
273 |
else : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
274 |
# handle the count ourselves |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
275 |
fmt = tag.type_format * tag.count |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
276 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
277 |
# size of the data |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
278 |
size = self.item_size(fmt) |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
279 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
280 |
# inline or external? |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
281 |
if size > 0x04 : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
282 |
# point at the external data |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
283 |
offset = self.unpack_item('I', tag.data_raw) |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
284 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
285 |
else : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
286 |
# point at the inline data |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
287 |
offset = tag.offset + 0x08 |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
288 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
289 |
return fmt, offset, size |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
290 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
291 |
def tag_values_raw (self, tag) : |
102 | 292 |
""" |
293 |
Get the raw values for the given tag as a tuple. |
|
294 |
||
295 |
Returns None if the tag could not be recognized. |
|
296 |
""" |
|
297 |
||
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
298 |
# find the data |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
299 |
data_info = self.tag_data_info(tag) |
102 | 300 |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
301 |
# not found? |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
302 |
if not data_info : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
303 |
return None |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
304 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
305 |
# unpack |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
306 |
data_fmt, data_offset, data_size = data_info |
102 | 307 |
|
308 |
# read values |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
309 |
return self.pread_struct(data_offset, data_fmt) |
102 | 310 |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
311 |
def tag_values (self, tag) : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
312 |
""" |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
313 |
Gets the processed values for the given tag as a list. |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
314 |
""" |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
315 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
316 |
# read + process |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
317 |
return tag.process_values(self.tag_values_raw(tag)) |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
318 |
|
102 | 319 |
def tag_value (self, tag) : |
320 |
""" |
|
321 |
Return the human-readable string value for the given tag. |
|
322 |
""" |
|
323 |
||
324 |
# load the raw values |
|
325 |
values = self.tag_values(tag) |
|
326 |
||
327 |
# unknown? |
|
328 |
if not values : |
|
329 |
return "" |
|
330 |
||
331 |
# return as comma-separated formatted string, yes |
|
104 | 332 |
return tag.readable_value(values) |
102 | 333 |
|
334 |
# mapping from two-byte TIFF byte order marker to struct prefix |
|
335 |
TIFF_BYTE_ORDER = { |
|
336 |
'II': '<', |
|
337 |
'MM': '>', |
|
338 |
} |
|
339 |
||
340 |
# "An arbitrary but carefully chosen number (42) that further identifies the file as a TIFF file" |
|
341 |
TIFF_BYTEORDER_MAGIC = 42 |
|
342 |
||
343 |
def tiff_load (file, length=0, **opts) : |
|
344 |
""" |
|
345 |
Load the Exif/TIFF data from the given file at its current position with optional length, using exif_load. |
|
346 |
""" |
|
347 |
||
348 |
# all Exif data offsets are relative to the beginning of this TIFF header |
|
349 |
offset = file.tell() |
|
350 |
||
351 |
# mmap the region for the EXIF data |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
352 |
buffer = mmap_buffer(file, length) |
102 | 353 |
|
354 |
# read byte-order header |
|
355 |
byte_order = file.read(2) |
|
356 |
||
357 |
# map to struct prefix |
|
358 |
struct_prefix = TIFF_BYTE_ORDER[byte_order] |
|
359 |
||
360 |
# validate |
|
361 |
check_value, = read_struct(file, struct_prefix + 'H') |
|
362 |
||
363 |
if check_value != TIFF_BYTEORDER_MAGIC : |
|
364 |
raise Exception("Invalid byte-order for TIFF: %2c -> %d" % (byte_order, check_value)) |
|
365 |
||
366 |
# build and return the EXIF object with the correct offset/size from the mmap region |
|
367 |
return EXIF(buffer, offset=offset, size=length, **opts) |
|
368 |
||
369 |
# the JPEG markers that don't have any data |
|
370 |
JPEG_NOSIZE_MARKERS = (0xD8, 0xD9) |
|
371 |
||
372 |
# the first marker in a JPEG File |
|
373 |
JPEG_START_MARKER = 0xD8 |
|
374 |
||
375 |
# the JPEG APP1 marker used for EXIF |
|
376 |
JPEG_EXIF_MARKER = 0xE1 |
|
377 |
||
378 |
# the JPEG APP1 Exif header |
|
379 |
JPEG_EXIF_HEADER = "Exif\x00\x00" |
|
380 |
||
381 |
def jpeg_markers (file) : |
|
382 |
""" |
|
383 |
Iterate over the JPEG markers in the given file, yielding (type_byte, size) tuples. |
|
384 |
||
385 |
The size fields will be 0 for markers with no data. The file will be positioned at the beginning of the data |
|
386 |
region, and may be seek'd around if needed. |
|
387 |
||
388 |
XXX: find a real implementation of this somewhere? |
|
389 |
""" |
|
390 |
||
391 |
while True : |
|
392 |
# read type |
|
393 |
marker_byte, marker_type = read_struct(file, '!BB') |
|
394 |
||
395 |
# validate |
|
396 |
if marker_byte != 0xff : |
|
397 |
raise Exception("Not a JPEG marker: %x%x" % (marker_byte, marker_type)) |
|
398 |
||
399 |
# special cases for no data |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
400 |
if marker_type in JPEG_NOSIZE_MARKERS : |
102 | 401 |
size = 0 |
402 |
||
403 |
else : |
|
404 |
# read size field |
|
405 |
size, = read_struct(file, '!H') |
|
406 |
||
407 |
# validate |
|
408 |
if size < 0x02 : |
|
409 |
raise Exception("Invalid size for marker %x%x: %x" % (marker_byte, marker_type, size)) |
|
410 |
||
411 |
else : |
|
412 |
# do not count the size field itself |
|
413 |
size = size - 2 |
|
414 |
||
415 |
# ok, data is at current position |
|
416 |
offset = file.tell() |
|
417 |
||
418 |
# yield |
|
419 |
yield marker_type, size |
|
420 |
||
421 |
# absolute seek to next marker |
|
422 |
file.seek(offset + size) |
|
423 |
||
424 |
def jpeg_find_exif (file) : |
|
425 |
""" |
|
426 |
Find the Exif/TIFF section in the given JPEG file. |
|
427 |
||
428 |
If found, the file will be seek'd to the start of the Exif/TIFF header, and the size of the Exif/TIFF data will |
|
429 |
be returned. |
|
430 |
||
431 |
Returns None if no EXIF section was found. |
|
432 |
""" |
|
433 |
||
434 |
for count, (marker, size) in enumerate(jpeg_markers(file)) : |
|
435 |
# verify that it's a JPEG file |
|
436 |
if count == 0 : |
|
437 |
# must start with the right marker |
|
438 |
if marker != JPEG_START_MARKER : |
|
439 |
raise Exception("JPEG file must start with 0xFF%02x marker" % (marker, )) |
|
440 |
||
441 |
# look for APP1 marker (0xE1) with EXIF signature |
|
442 |
elif marker == JPEG_EXIF_MARKER and file.read(len(JPEG_EXIF_HEADER)) == JPEG_EXIF_HEADER: |
|
443 |
# skipped the initial Exif marker signature |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
444 |
return size - len(JPEG_EXIF_HEADER) |
102 | 445 |
|
446 |
# nothing |
|
447 |
return None |
|
448 |
||
449 |
def jpeg_load (file, **opts) : |
|
450 |
""" |
|
451 |
Loads the embedded Exif TIFF data from the given JPEG file using tiff_load. |
|
452 |
||
453 |
Returns None if no EXIF data could be found. |
|
454 |
""" |
|
455 |
||
456 |
# look for the right section |
|
457 |
size = jpeg_find_exif(file) |
|
458 |
||
459 |
# not found? |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
460 |
if not size : |
102 | 461 |
# nothing |
462 |
return |
|
463 |
||
464 |
else : |
|
465 |
# load it as TIFF data |
|
466 |
return tiff_load(file, size, **opts) |
|
467 |
||
468 |
def load_path (path, **opts) : |
|
469 |
""" |
|
470 |
Loads an EXIF object from the given filesystem path. |
|
471 |
||
472 |
Returns None if it could not be parsed. |
|
473 |
""" |
|
474 |
||
475 |
# file extension |
|
476 |
root, fext = os.path.splitext(path) |
|
477 |
||
478 |
# map |
|
479 |
func = { |
|
480 |
'.jpeg': jpeg_load, |
|
481 |
'.jpg': jpeg_load, |
|
482 |
'.tiff': tiff_load, # XXX: untested |
|
483 |
}.get(fext.lower()) |
|
484 |
||
485 |
# not recognized? |
|
486 |
if not func : |
|
487 |
# XXX: sniff the file |
|
488 |
return None |
|
489 |
||
490 |
# open it |
|
491 |
file = open(path, 'rb') |
|
492 |
||
493 |
# try and load it |
|
494 |
return func(file, **opts) |
|
495 |
||
496 |
def dump_exif (exif) : |
|
497 |
""" |
|
498 |
Dump all tags from the given EXIF object to stdout |
|
499 |
""" |
|
500 |
||
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
501 |
print "EXIF offset=%#08x, size=%d:" % (exif.offset, exif.size) |
102 | 502 |
|
503 |
for i, ifd in enumerate(exif.iter_ifds()) : |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
504 |
print "\tIFD:%d offset=%#04x(%#08x), count=%d, next=%d:" % ( |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
505 |
i, |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
506 |
ifd.offset, ifd.offset + exif.offset, |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
507 |
ifd.count, |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
508 |
ifd.next_offset |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
509 |
) |
102 | 510 |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
511 |
for i, tag in enumerate(ifd.iter_tags()) : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
512 |
data_info = exif.tag_data_info(tag) |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
513 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
514 |
if data_info : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
515 |
data_fmt, data_offset, data_size = data_info |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
516 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
517 |
else : |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
518 |
data_fmt = data_offset = data_size = None |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
519 |
|
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
520 |
print "\t\tTag:%d offset=%#04x(%#08x), tag=%d/%s, type=%d/%s, count=%d, fmt=%s, offset=%#04x, size=%s:" % ( |
102 | 521 |
i, |
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
522 |
tag.offset, tag.offset + exif.offset, |
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
523 |
tag.tag, tag.name or '???', |
102 | 524 |
tag.type, tag.type_name if tag.type_data else '???', |
525 |
tag.count, |
|
103
63e89dc2d6f1
new exif.py seems to work now, although still missing sub-IFDs
Tero Marttila <terom@fixme.fi>
parents:
102
diff
changeset
|
526 |
data_fmt, data_offset, data_size, |
102 | 527 |
) |
104 | 528 |
|
529 |
values = exif.tag_values(tag) |
|
102 | 530 |
|
104 | 531 |
for i, value in enumerate(values) : |
532 |
print "\t\t\t%02d: %r" % (i, value) |
|
102 | 533 |
|
104 | 534 |
print "\t\t\t-> %s" % (tag.readable_value(values), ) |
535 |
||
536 |
def main (path, quiet=False) : |
|
102 | 537 |
""" |
538 |
Load and dump EXIF data from the given path |
|
539 |
""" |
|
540 |
||
541 |
# try and load it |
|
542 |
exif = load_path(path) |
|
543 |
||
544 |
if not exif : |
|
545 |
raise Exception("No EXIF data found") |
|
546 |
||
104 | 547 |
if not quiet : |
548 |
# dump it |
|
549 |
print "%s: " % path |
|
550 |
||
102 | 551 |
|
104 | 552 |
dump_exif(exif) |
102 | 553 |
|
554 |
if __name__ == '__main__' : |
|
555 |
from sys import argv |
|
556 |
||
104 | 557 |
main(argv[1], '-q' in argv) |
102 | 558 |