comparison photologue/utils/EXIF.py @ 71:e2868ad47a1e

For Django 1.4, using the new manage.py.
author Brian Neal <bgneal@gmail.com>
date Sat, 14 Apr 2012 16:40:29 -0500
parents madeira/photologue/utils/EXIF.py@63e4211628e1
children
comparison
equal deleted inserted replaced
70:f26cdda0ad8b 71:e2868ad47a1e
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Library to extract EXIF information from digital camera image files
5 # http://sourceforge.net/projects/exif-py/
6 #
7 # VERSION 1.0.7
8 #
9 # To use this library call with:
10 # f = open(path_name, 'rb')
11 # tags = EXIF.process_file(f)
12 #
13 # To ignore makerNote tags, pass the -q or --quick
14 # command line arguments, or as
15 # f = open(path_name, 'rb')
16 # tags = EXIF.process_file(f, details=False)
17 #
18 # To stop processing after a certain tag is retrieved,
19 # pass the -t TAG or --stop-tag TAG argument, or as
20 # f = open(path_name, 'rb')
21 # tags = EXIF.process_file(f, stop_tag='TAG')
22 #
23 # where TAG is a valid tag name, ex 'DateTimeOriginal'
24 #
25 # These are useful when you are retrieving a large list of images
26 #
27 # Returned tags will be a dictionary mapping names of EXIF tags to their
28 # values in the file named by path_name. You can process the tags
29 # as you wish. In particular, you can iterate through all the tags with:
30 # for tag in tags.keys():
31 # if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename',
32 # 'EXIF MakerNote'):
33 # print "Key: %s, value %s" % (tag, tags[tag])
34 # (This code uses the if statement to avoid printing out a few of the
35 # tags that tend to be long or boring.)
36 #
37 # The tags dictionary will include keys for all of the usual EXIF
38 # tags, and will also include keys for Makernotes used by some
39 # cameras, for which we have a good specification.
40 #
41 # Note that the dictionary keys are the IFD name followed by the
42 # tag name. For example:
43 # 'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode'
44 #
45 # Copyright (c) 2002-2007 Gene Cash All rights reserved
46 # Copyright (c) 2007 Ianaré Sévi All rights reserved
47 #
48 # Redistribution and use in source and binary forms, with or without
49 # modification, are permitted provided that the following conditions
50 # are met:
51 #
52 # 1. Redistributions of source code must retain the above copyright
53 # notice, this list of conditions and the following disclaimer.
54 #
55 # 2. Redistributions in binary form must reproduce the above
56 # copyright notice, this list of conditions and the following
57 # disclaimer in the documentation and/or other materials provided
58 # with the distribution.
59 #
60 # 3. Neither the name of the authors nor the names of its contributors
61 # may be used to endorse or promote products derived from this
62 # software without specific prior written permission.
63 #
64 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
65 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
66 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
67 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
68 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
69 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
70 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
71 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
72 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
73 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
74 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
75 #
76 #
77 # ----- See 'changes.txt' file for all contributors and changes ----- #
78 #
79
80
81 # Don't throw an exception when given an out of range character.
82 def make_string(seq):
83 str = ""
84 for c in seq:
85 # Screen out non-printing characters
86 if 32 <= c and c < 256:
87 str += chr(c)
88 # If no printing chars
89 if not str:
90 return seq
91 return str
92
93 # Special version to deal with the code in the first 8 bytes of a user comment.
94 def make_string_uc(seq):
95 code = seq[0:8]
96 seq = seq[8:]
97 # Of course, this is only correct if ASCII, and the standard explicitly
98 # allows JIS and Unicode.
99 return make_string(seq)
100
101 # field type descriptions as (length, abbreviation, full name) tuples
102 FIELD_TYPES = (
103 (0, 'X', 'Proprietary'), # no such type
104 (1, 'B', 'Byte'),
105 (1, 'A', 'ASCII'),
106 (2, 'S', 'Short'),
107 (4, 'L', 'Long'),
108 (8, 'R', 'Ratio'),
109 (1, 'SB', 'Signed Byte'),
110 (1, 'U', 'Undefined'),
111 (2, 'SS', 'Signed Short'),
112 (4, 'SL', 'Signed Long'),
113 (8, 'SR', 'Signed Ratio'),
114 )
115
116 # dictionary of main EXIF tag names
117 # first element of tuple is tag name, optional second element is
118 # another dictionary giving names to values
119 EXIF_TAGS = {
120 0x0100: ('ImageWidth', ),
121 0x0101: ('ImageLength', ),
122 0x0102: ('BitsPerSample', ),
123 0x0103: ('Compression',
124 {1: 'Uncompressed TIFF',
125 6: 'JPEG Compressed'}),
126 0x0106: ('PhotometricInterpretation', ),
127 0x010A: ('FillOrder', ),
128 0x010D: ('DocumentName', ),
129 0x010E: ('ImageDescription', ),
130 0x010F: ('Make', ),
131 0x0110: ('Model', ),
132 0x0111: ('StripOffsets', ),
133 0x0112: ('Orientation',
134 {1: 'Horizontal (normal)',
135 2: 'Mirrored horizontal',
136 3: 'Rotated 180',
137 4: 'Mirrored vertical',
138 5: 'Mirrored horizontal then rotated 90 CCW',
139 6: 'Rotated 90 CW',
140 7: 'Mirrored horizontal then rotated 90 CW',
141 8: 'Rotated 90 CCW'}),
142 0x0115: ('SamplesPerPixel', ),
143 0x0116: ('RowsPerStrip', ),
144 0x0117: ('StripByteCounts', ),
145 0x011A: ('XResolution', ),
146 0x011B: ('YResolution', ),
147 0x011C: ('PlanarConfiguration', ),
148 0x0128: ('ResolutionUnit',
149 {1: 'Not Absolute',
150 2: 'Pixels/Inch',
151 3: 'Pixels/Centimeter'}),
152 0x012D: ('TransferFunction', ),
153 0x0131: ('Software', ),
154 0x0132: ('DateTime', ),
155 0x013B: ('Artist', ),
156 0x013E: ('WhitePoint', ),
157 0x013F: ('PrimaryChromaticities', ),
158 0x0156: ('TransferRange', ),
159 0x0200: ('JPEGProc', ),
160 0x0201: ('JPEGInterchangeFormat', ),
161 0x0202: ('JPEGInterchangeFormatLength', ),
162 0x0211: ('YCbCrCoefficients', ),
163 0x0212: ('YCbCrSubSampling', ),
164 0x0213: ('YCbCrPositioning', ),
165 0x0214: ('ReferenceBlackWhite', ),
166 0x828D: ('CFARepeatPatternDim', ),
167 0x828E: ('CFAPattern', ),
168 0x828F: ('BatteryLevel', ),
169 0x8298: ('Copyright', ),
170 0x829A: ('ExposureTime', ),
171 0x829D: ('FNumber', ),
172 0x83BB: ('IPTC/NAA', ),
173 0x8769: ('ExifOffset', ),
174 0x8773: ('InterColorProfile', ),
175 0x8822: ('ExposureProgram',
176 {0: 'Unidentified',
177 1: 'Manual',
178 2: 'Program Normal',
179 3: 'Aperture Priority',
180 4: 'Shutter Priority',
181 5: 'Program Creative',
182 6: 'Program Action',
183 7: 'Portrait Mode',
184 8: 'Landscape Mode'}),
185 0x8824: ('SpectralSensitivity', ),
186 0x8825: ('GPSInfo', ),
187 0x8827: ('ISOSpeedRatings', ),
188 0x8828: ('OECF', ),
189 # print as string
190 0x9000: ('ExifVersion', make_string),
191 0x9003: ('DateTimeOriginal', ),
192 0x9004: ('DateTimeDigitized', ),
193 0x9101: ('ComponentsConfiguration',
194 {0: '',
195 1: 'Y',
196 2: 'Cb',
197 3: 'Cr',
198 4: 'Red',
199 5: 'Green',
200 6: 'Blue'}),
201 0x9102: ('CompressedBitsPerPixel', ),
202 0x9201: ('ShutterSpeedValue', ),
203 0x9202: ('ApertureValue', ),
204 0x9203: ('BrightnessValue', ),
205 0x9204: ('ExposureBiasValue', ),
206 0x9205: ('MaxApertureValue', ),
207 0x9206: ('SubjectDistance', ),
208 0x9207: ('MeteringMode',
209 {0: 'Unidentified',
210 1: 'Average',
211 2: 'CenterWeightedAverage',
212 3: 'Spot',
213 4: 'MultiSpot'}),
214 0x9208: ('LightSource',
215 {0: 'Unknown',
216 1: 'Daylight',
217 2: 'Fluorescent',
218 3: 'Tungsten',
219 10: 'Flash',
220 17: 'Standard Light A',
221 18: 'Standard Light B',
222 19: 'Standard Light C',
223 20: 'D55',
224 21: 'D65',
225 22: 'D75',
226 255: 'Other'}),
227 0x9209: ('Flash', {0: 'No',
228 1: 'Fired',
229 5: 'Fired (?)', # no return sensed
230 7: 'Fired (!)', # return sensed
231 9: 'Fill Fired',
232 13: 'Fill Fired (?)',
233 15: 'Fill Fired (!)',
234 16: 'Off',
235 24: 'Auto Off',
236 25: 'Auto Fired',
237 29: 'Auto Fired (?)',
238 31: 'Auto Fired (!)',
239 32: 'Not Available'}),
240 0x920A: ('FocalLength', ),
241 0x9214: ('SubjectArea', ),
242 0x927C: ('MakerNote', ),
243 # print as string
244 0x9286: ('UserComment', make_string_uc), # First 8 bytes gives coding system e.g. ASCII vs. JIS vs Unicode
245 0x9290: ('SubSecTime', ),
246 0x9291: ('SubSecTimeOriginal', ),
247 0x9292: ('SubSecTimeDigitized', ),
248 # print as string
249 0xA000: ('FlashPixVersion', make_string),
250 0xA001: ('ColorSpace', ),
251 0xA002: ('ExifImageWidth', ),
252 0xA003: ('ExifImageLength', ),
253 0xA005: ('InteroperabilityOffset', ),
254 0xA20B: ('FlashEnergy', ), # 0x920B in TIFF/EP
255 0xA20C: ('SpatialFrequencyResponse', ), # 0x920C - -
256 0xA20E: ('FocalPlaneXResolution', ), # 0x920E - -
257 0xA20F: ('FocalPlaneYResolution', ), # 0x920F - -
258 0xA210: ('FocalPlaneResolutionUnit', ), # 0x9210 - -
259 0xA214: ('SubjectLocation', ), # 0x9214 - -
260 0xA215: ('ExposureIndex', ), # 0x9215 - -
261 0xA217: ('SensingMethod', ), # 0x9217 - -
262 0xA300: ('FileSource',
263 {3: 'Digital Camera'}),
264 0xA301: ('SceneType',
265 {1: 'Directly Photographed'}),
266 0xA302: ('CVAPattern', ),
267 0xA401: ('CustomRendered', ),
268 0xA402: ('ExposureMode',
269 {0: 'Auto Exposure',
270 1: 'Manual Exposure',
271 2: 'Auto Bracket'}),
272 0xA403: ('WhiteBalance',
273 {0: 'Auto',
274 1: 'Manual'}),
275 0xA404: ('DigitalZoomRatio', ),
276 0xA405: ('FocalLengthIn35mm', ),
277 0xA406: ('SceneCaptureType', ),
278 0xA407: ('GainControl', ),
279 0xA408: ('Contrast', ),
280 0xA409: ('Saturation', ),
281 0xA40A: ('Sharpness', ),
282 0xA40C: ('SubjectDistanceRange', ),
283 }
284
285 # interoperability tags
286 INTR_TAGS = {
287 0x0001: ('InteroperabilityIndex', ),
288 0x0002: ('InteroperabilityVersion', ),
289 0x1000: ('RelatedImageFileFormat', ),
290 0x1001: ('RelatedImageWidth', ),
291 0x1002: ('RelatedImageLength', ),
292 }
293
294 # GPS tags (not used yet, haven't seen camera with GPS)
295 GPS_TAGS = {
296 0x0000: ('GPSVersionID', ),
297 0x0001: ('GPSLatitudeRef', ),
298 0x0002: ('GPSLatitude', ),
299 0x0003: ('GPSLongitudeRef', ),
300 0x0004: ('GPSLongitude', ),
301 0x0005: ('GPSAltitudeRef', ),
302 0x0006: ('GPSAltitude', ),
303 0x0007: ('GPSTimeStamp', ),
304 0x0008: ('GPSSatellites', ),
305 0x0009: ('GPSStatus', ),
306 0x000A: ('GPSMeasureMode', ),
307 0x000B: ('GPSDOP', ),
308 0x000C: ('GPSSpeedRef', ),
309 0x000D: ('GPSSpeed', ),
310 0x000E: ('GPSTrackRef', ),
311 0x000F: ('GPSTrack', ),
312 0x0010: ('GPSImgDirectionRef', ),
313 0x0011: ('GPSImgDirection', ),
314 0x0012: ('GPSMapDatum', ),
315 0x0013: ('GPSDestLatitudeRef', ),
316 0x0014: ('GPSDestLatitude', ),
317 0x0015: ('GPSDestLongitudeRef', ),
318 0x0016: ('GPSDestLongitude', ),
319 0x0017: ('GPSDestBearingRef', ),
320 0x0018: ('GPSDestBearing', ),
321 0x0019: ('GPSDestDistanceRef', ),
322 0x001A: ('GPSDestDistance', ),
323 }
324
325 # Ignore these tags when quick processing
326 # 0x927C is MakerNote Tags
327 # 0x9286 is user comment
328 IGNORE_TAGS=(0x9286, 0x927C)
329
330 # http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp
331 def nikon_ev_bias(seq):
332 # First digit seems to be in steps of 1/6 EV.
333 # Does the third value mean the step size? It is usually 6,
334 # but it is 12 for the ExposureDifference.
335 #
336 if seq == [252, 1, 6, 0]:
337 return "-2/3 EV"
338 if seq == [253, 1, 6, 0]:
339 return "-1/2 EV"
340 if seq == [254, 1, 6, 0]:
341 return "-1/3 EV"
342 if seq == [0, 1, 6, 0]:
343 return "0 EV"
344 if seq == [2, 1, 6, 0]:
345 return "+1/3 EV"
346 if seq == [3, 1, 6, 0]:
347 return "+1/2 EV"
348 if seq == [4, 1, 6, 0]:
349 return "+2/3 EV"
350 # Handle combinations not in the table.
351 a = seq[0]
352 # Causes headaches for the +/- logic, so special case it.
353 if a == 0:
354 return "0 EV"
355 if a > 127:
356 a = 256 - a
357 ret_str = "-"
358 else:
359 ret_str = "+"
360 b = seq[2] # Assume third value means the step size
361 whole = a / b
362 a = a % b
363 if whole != 0:
364 ret_str = ret_str + str(whole) + " "
365 if a == 0:
366 ret_str = ret_str + "EV"
367 else:
368 r = Ratio(a, b)
369 ret_str = ret_str + r.__repr__() + " EV"
370 return ret_str
371
372 # Nikon E99x MakerNote Tags
373 MAKERNOTE_NIKON_NEWER_TAGS={
374 0x0001: ('MakernoteVersion', make_string), # Sometimes binary
375 0x0002: ('ISOSetting', ),
376 0x0003: ('ColorMode', ),
377 0x0004: ('Quality', ),
378 0x0005: ('Whitebalance', ),
379 0x0006: ('ImageSharpening', ),
380 0x0007: ('FocusMode', ),
381 0x0008: ('FlashSetting', ),
382 0x0009: ('AutoFlashMode', ),
383 0x000B: ('WhiteBalanceBias', ),
384 0x000C: ('WhiteBalanceRBCoeff', ),
385 0x000D: ('ProgramShift', nikon_ev_bias),
386 # Nearly the same as the other EV vals, but step size is 1/12 EV (?)
387 0x000E: ('ExposureDifference', nikon_ev_bias),
388 0x000F: ('ISOSelection', ),
389 0x0011: ('NikonPreview', ),
390 0x0012: ('FlashCompensation', nikon_ev_bias),
391 0x0013: ('ISOSpeedRequested', ),
392 0x0016: ('PhotoCornerCoordinates', ),
393 # 0x0017: Unknown, but most likely an EV value
394 0x0018: ('FlashBracketCompensationApplied', nikon_ev_bias),
395 0x0019: ('AEBracketCompensationApplied', ),
396 0x001A: ('ImageProcessing', ),
397 0x0080: ('ImageAdjustment', ),
398 0x0081: ('ToneCompensation', ),
399 0x0082: ('AuxiliaryLens', ),
400 0x0083: ('LensType', ),
401 0x0084: ('LensMinMaxFocalMaxAperture', ),
402 0x0085: ('ManualFocusDistance', ),
403 0x0086: ('DigitalZoomFactor', ),
404 0x0087: ('FlashMode',
405 {0x00: 'Did Not Fire',
406 0x01: 'Fired, Manual',
407 0x07: 'Fired, External',
408 0x08: 'Fired, Commander Mode ',
409 0x09: 'Fired, TTL Mode'}),
410 0x0088: ('AFFocusPosition',
411 {0x0000: 'Center',
412 0x0100: 'Top',
413 0x0200: 'Bottom',
414 0x0300: 'Left',
415 0x0400: 'Right'}),
416 0x0089: ('BracketingMode',
417 {0x00: 'Single frame, no bracketing',
418 0x01: 'Continuous, no bracketing',
419 0x02: 'Timer, no bracketing',
420 0x10: 'Single frame, exposure bracketing',
421 0x11: 'Continuous, exposure bracketing',
422 0x12: 'Timer, exposure bracketing',
423 0x40: 'Single frame, white balance bracketing',
424 0x41: 'Continuous, white balance bracketing',
425 0x42: 'Timer, white balance bracketing'}),
426 0x008A: ('AutoBracketRelease', ),
427 0x008B: ('LensFStops', ),
428 0x008C: ('NEFCurve2', ),
429 0x008D: ('ColorMode', ),
430 0x008F: ('SceneMode', ),
431 0x0090: ('LightingType', ),
432 0x0091: ('ShotInfo', ), # First 4 bytes are probably a version number in ASCII
433 0x0092: ('HueAdjustment', ),
434 # 0x0093: ('SaturationAdjustment', ),
435 0x0094: ('Saturation', # Name conflict with 0x00AA !!
436 {-3: 'B&W',
437 -2: '-2',
438 -1: '-1',
439 0: '0',
440 1: '1',
441 2: '2'}),
442 0x0095: ('NoiseReduction', ),
443 0x0096: ('NEFCurve2', ),
444 0x0097: ('ColorBalance', ),
445 0x0098: ('LensData', ), # First 4 bytes are a version number in ASCII
446 0x0099: ('RawImageCenter', ),
447 0x009A: ('SensorPixelSize', ),
448 0x009C: ('Scene Assist', ),
449 0x00A0: ('SerialNumber', ),
450 0x00A2: ('ImageDataSize', ),
451 # A4: In NEF, looks like a 4 byte ASCII version number
452 0x00A5: ('ImageCount', ),
453 0x00A6: ('DeletedImageCount', ),
454 0x00A7: ('TotalShutterReleases', ),
455 # A8: ExposureMode? JPG: First 4 bytes are probably a version number in ASCII
456 # But in a sample NEF, its 8 zeros, then the string "NORMAL"
457 0x00A9: ('ImageOptimization', ),
458 0x00AA: ('Saturation', ),
459 0x00AB: ('DigitalVariProgram', ),
460 0x00AC: ('ImageStabilization', ),
461 0x00AD: ('Responsive AF', ), # 'AFResponse'
462 0x0010: ('DataDump', ),
463 }
464
465 MAKERNOTE_NIKON_OLDER_TAGS = {
466 0x0003: ('Quality',
467 {1: 'VGA Basic',
468 2: 'VGA Normal',
469 3: 'VGA Fine',
470 4: 'SXGA Basic',
471 5: 'SXGA Normal',
472 6: 'SXGA Fine'}),
473 0x0004: ('ColorMode',
474 {1: 'Color',
475 2: 'Monochrome'}),
476 0x0005: ('ImageAdjustment',
477 {0: 'Normal',
478 1: 'Bright+',
479 2: 'Bright-',
480 3: 'Contrast+',
481 4: 'Contrast-'}),
482 0x0006: ('CCDSpeed',
483 {0: 'ISO 80',
484 2: 'ISO 160',
485 4: 'ISO 320',
486 5: 'ISO 100'}),
487 0x0007: ('WhiteBalance',
488 {0: 'Auto',
489 1: 'Preset',
490 2: 'Daylight',
491 3: 'Incandescent',
492 4: 'Fluorescent',
493 5: 'Cloudy',
494 6: 'Speed Light'}),
495 }
496
497 # decode Olympus SpecialMode tag in MakerNote
498 def olympus_special_mode(v):
499 a={
500 0: 'Normal',
501 1: 'Unknown',
502 2: 'Fast',
503 3: 'Panorama'}
504 b={
505 0: 'Non-panoramic',
506 1: 'Left to right',
507 2: 'Right to left',
508 3: 'Bottom to top',
509 4: 'Top to bottom'}
510 if v[0] not in a or v[2] not in b:
511 return v
512 return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]])
513
514 MAKERNOTE_OLYMPUS_TAGS={
515 # ah HAH! those sneeeeeaky bastids! this is how they get past the fact
516 # that a JPEG thumbnail is not allowed in an uncompressed TIFF file
517 0x0100: ('JPEGThumbnail', ),
518 0x0200: ('SpecialMode', olympus_special_mode),
519 0x0201: ('JPEGQual',
520 {1: 'SQ',
521 2: 'HQ',
522 3: 'SHQ'}),
523 0x0202: ('Macro',
524 {0: 'Normal',
525 1: 'Macro',
526 2: 'SuperMacro'}),
527 0x0203: ('BWMode',
528 {0: 'Off',
529 1: 'On'}),
530 0x0204: ('DigitalZoom', ),
531 0x0205: ('FocalPlaneDiagonal', ),
532 0x0206: ('LensDistortionParams', ),
533 0x0207: ('SoftwareRelease', ),
534 0x0208: ('PictureInfo', ),
535 0x0209: ('CameraID', make_string), # print as string
536 0x0F00: ('DataDump', ),
537 0x0300: ('PreCaptureFrames', ),
538 0x0404: ('SerialNumber', ),
539 0x1000: ('ShutterSpeedValue', ),
540 0x1001: ('ISOValue', ),
541 0x1002: ('ApertureValue', ),
542 0x1003: ('BrightnessValue', ),
543 0x1004: ('FlashMode', ),
544 0x1004: ('FlashMode',
545 {2: 'On',
546 3: 'Off'}),
547 0x1005: ('FlashDevice',
548 {0: 'None',
549 1: 'Internal',
550 4: 'External',
551 5: 'Internal + External'}),
552 0x1006: ('ExposureCompensation', ),
553 0x1007: ('SensorTemperature', ),
554 0x1008: ('LensTemperature', ),
555 0x100b: ('FocusMode',
556 {0: 'Auto',
557 1: 'Manual'}),
558 0x1017: ('RedBalance', ),
559 0x1018: ('BlueBalance', ),
560 0x101a: ('SerialNumber', ),
561 0x1023: ('FlashExposureComp', ),
562 0x1026: ('ExternalFlashBounce',
563 {0: 'No',
564 1: 'Yes'}),
565 0x1027: ('ExternalFlashZoom', ),
566 0x1028: ('ExternalFlashMode', ),
567 0x1029: ('Contrast int16u',
568 {0: 'High',
569 1: 'Normal',
570 2: 'Low'}),
571 0x102a: ('SharpnessFactor', ),
572 0x102b: ('ColorControl', ),
573 0x102c: ('ValidBits', ),
574 0x102d: ('CoringFilter', ),
575 0x102e: ('OlympusImageWidth', ),
576 0x102f: ('OlympusImageHeight', ),
577 0x1034: ('CompressionRatio', ),
578 0x1035: ('PreviewImageValid',
579 {0: 'No',
580 1: 'Yes'}),
581 0x1036: ('PreviewImageStart', ),
582 0x1037: ('PreviewImageLength', ),
583 0x1039: ('CCDScanMode',
584 {0: 'Interlaced',
585 1: 'Progressive'}),
586 0x103a: ('NoiseReduction',
587 {0: 'Off',
588 1: 'On'}),
589 0x103b: ('InfinityLensStep', ),
590 0x103c: ('NearLensStep', ),
591
592 # TODO - these need extra definitions
593 # http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html
594 0x2010: ('Equipment', ),
595 0x2020: ('CameraSettings', ),
596 0x2030: ('RawDevelopment', ),
597 0x2040: ('ImageProcessing', ),
598 0x2050: ('FocusInfo', ),
599 0x3000: ('RawInfo ', ),
600 }
601
602 # 0x2020 CameraSettings
603 MAKERNOTE_OLYMPUS_TAG_0x2020={
604 0x0100: ('PreviewImageValid',
605 {0: 'No',
606 1: 'Yes'}),
607 0x0101: ('PreviewImageStart', ),
608 0x0102: ('PreviewImageLength', ),
609 0x0200: ('ExposureMode', {
610 1: 'Manual',
611 2: 'Program',
612 3: 'Aperture-priority AE',
613 4: 'Shutter speed priority AE',
614 5: 'Program-shift'}),
615 0x0201: ('AELock',
616 {0: 'Off',
617 1: 'On'}),
618 0x0202: ('MeteringMode',
619 {2: 'Center Weighted',
620 3: 'Spot',
621 5: 'ESP',
622 261: 'Pattern+AF',
623 515: 'Spot+Highlight control',
624 1027: 'Spot+Shadow control'}),
625 0x0300: ('MacroMode',
626 {0: 'Off',
627 1: 'On'}),
628 0x0301: ('FocusMode',
629 {0: 'Single AF',
630 1: 'Sequential shooting AF',
631 2: 'Continuous AF',
632 3: 'Multi AF',
633 10: 'MF'}),
634 0x0302: ('FocusProcess',
635 {0: 'AF Not Used',
636 1: 'AF Used'}),
637 0x0303: ('AFSearch',
638 {0: 'Not Ready',
639 1: 'Ready'}),
640 0x0304: ('AFAreas', ),
641 0x0401: ('FlashExposureCompensation', ),
642 0x0500: ('WhiteBalance2',
643 {0: 'Auto',
644 16: '7500K (Fine Weather with Shade)',
645 17: '6000K (Cloudy)',
646 18: '5300K (Fine Weather)',
647 20: '3000K (Tungsten light)',
648 21: '3600K (Tungsten light-like)',
649 33: '6600K (Daylight fluorescent)',
650 34: '4500K (Neutral white fluorescent)',
651 35: '4000K (Cool white fluorescent)',
652 48: '3600K (Tungsten light-like)',
653 256: 'Custom WB 1',
654 257: 'Custom WB 2',
655 258: 'Custom WB 3',
656 259: 'Custom WB 4',
657 512: 'Custom WB 5400K',
658 513: 'Custom WB 2900K',
659 514: 'Custom WB 8000K', }),
660 0x0501: ('WhiteBalanceTemperature', ),
661 0x0502: ('WhiteBalanceBracket', ),
662 0x0503: ('CustomSaturation', ), # (3 numbers: 1. CS Value, 2. Min, 3. Max)
663 0x0504: ('ModifiedSaturation',
664 {0: 'Off',
665 1: 'CM1 (Red Enhance)',
666 2: 'CM2 (Green Enhance)',
667 3: 'CM3 (Blue Enhance)',
668 4: 'CM4 (Skin Tones)'}),
669 0x0505: ('ContrastSetting', ), # (3 numbers: 1. Contrast, 2. Min, 3. Max)
670 0x0506: ('SharpnessSetting', ), # (3 numbers: 1. Sharpness, 2. Min, 3. Max)
671 0x0507: ('ColorSpace',
672 {0: 'sRGB',
673 1: 'Adobe RGB',
674 2: 'Pro Photo RGB'}),
675 0x0509: ('SceneMode',
676 {0: 'Standard',
677 6: 'Auto',
678 7: 'Sport',
679 8: 'Portrait',
680 9: 'Landscape+Portrait',
681 10: 'Landscape',
682 11: 'Night scene',
683 13: 'Panorama',
684 16: 'Landscape+Portrait',
685 17: 'Night+Portrait',
686 19: 'Fireworks',
687 20: 'Sunset',
688 22: 'Macro',
689 25: 'Documents',
690 26: 'Museum',
691 28: 'Beach&Snow',
692 30: 'Candle',
693 35: 'Underwater Wide1',
694 36: 'Underwater Macro',
695 39: 'High Key',
696 40: 'Digital Image Stabilization',
697 44: 'Underwater Wide2',
698 45: 'Low Key',
699 46: 'Children',
700 48: 'Nature Macro'}),
701 0x050a: ('NoiseReduction',
702 {0: 'Off',
703 1: 'Noise Reduction',
704 2: 'Noise Filter',
705 3: 'Noise Reduction + Noise Filter',
706 4: 'Noise Filter (ISO Boost)',
707 5: 'Noise Reduction + Noise Filter (ISO Boost)'}),
708 0x050b: ('DistortionCorrection',
709 {0: 'Off',
710 1: 'On'}),
711 0x050c: ('ShadingCompensation',
712 {0: 'Off',
713 1: 'On'}),
714 0x050d: ('CompressionFactor', ),
715 0x050f: ('Gradation',
716 {'-1 -1 1': 'Low Key',
717 '0 -1 1': 'Normal',
718 '1 -1 1': 'High Key'}),
719 0x0520: ('PictureMode',
720 {1: 'Vivid',
721 2: 'Natural',
722 3: 'Muted',
723 256: 'Monotone',
724 512: 'Sepia'}),
725 0x0521: ('PictureModeSaturation', ),
726 0x0522: ('PictureModeHue?', ),
727 0x0523: ('PictureModeContrast', ),
728 0x0524: ('PictureModeSharpness', ),
729 0x0525: ('PictureModeBWFilter',
730 {0: 'n/a',
731 1: 'Neutral',
732 2: 'Yellow',
733 3: 'Orange',
734 4: 'Red',
735 5: 'Green'}),
736 0x0526: ('PictureModeTone',
737 {0: 'n/a',
738 1: 'Neutral',
739 2: 'Sepia',
740 3: 'Blue',
741 4: 'Purple',
742 5: 'Green'}),
743 0x0600: ('Sequence', ), # 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits
744 0x0601: ('PanoramaMode', ), # (2 numbers: 1. Mode, 2. Shot number)
745 0x0603: ('ImageQuality2',
746 {1: 'SQ',
747 2: 'HQ',
748 3: 'SHQ',
749 4: 'RAW'}),
750 0x0901: ('ManometerReading', ),
751 }
752
753
754 MAKERNOTE_CASIO_TAGS={
755 0x0001: ('RecordingMode',
756 {1: 'Single Shutter',
757 2: 'Panorama',
758 3: 'Night Scene',
759 4: 'Portrait',
760 5: 'Landscape'}),
761 0x0002: ('Quality',
762 {1: 'Economy',
763 2: 'Normal',
764 3: 'Fine'}),
765 0x0003: ('FocusingMode',
766 {2: 'Macro',
767 3: 'Auto Focus',
768 4: 'Manual Focus',
769 5: 'Infinity'}),
770 0x0004: ('FlashMode',
771 {1: 'Auto',
772 2: 'On',
773 3: 'Off',
774 4: 'Red Eye Reduction'}),
775 0x0005: ('FlashIntensity',
776 {11: 'Weak',
777 13: 'Normal',
778 15: 'Strong'}),
779 0x0006: ('Object Distance', ),
780 0x0007: ('WhiteBalance',
781 {1: 'Auto',
782 2: 'Tungsten',
783 3: 'Daylight',
784 4: 'Fluorescent',
785 5: 'Shade',
786 129: 'Manual'}),
787 0x000B: ('Sharpness',
788 {0: 'Normal',
789 1: 'Soft',
790 2: 'Hard'}),
791 0x000C: ('Contrast',
792 {0: 'Normal',
793 1: 'Low',
794 2: 'High'}),
795 0x000D: ('Saturation',
796 {0: 'Normal',
797 1: 'Low',
798 2: 'High'}),
799 0x0014: ('CCDSpeed',
800 {64: 'Normal',
801 80: 'Normal',
802 100: 'High',
803 125: '+1.0',
804 244: '+3.0',
805 250: '+2.0'}),
806 }
807
808 MAKERNOTE_FUJIFILM_TAGS={
809 0x0000: ('NoteVersion', make_string),
810 0x1000: ('Quality', ),
811 0x1001: ('Sharpness',
812 {1: 'Soft',
813 2: 'Soft',
814 3: 'Normal',
815 4: 'Hard',
816 5: 'Hard'}),
817 0x1002: ('WhiteBalance',
818 {0: 'Auto',
819 256: 'Daylight',
820 512: 'Cloudy',
821 768: 'DaylightColor-Fluorescent',
822 769: 'DaywhiteColor-Fluorescent',
823 770: 'White-Fluorescent',
824 1024: 'Incandescent',
825 3840: 'Custom'}),
826 0x1003: ('Color',
827 {0: 'Normal',
828 256: 'High',
829 512: 'Low'}),
830 0x1004: ('Tone',
831 {0: 'Normal',
832 256: 'High',
833 512: 'Low'}),
834 0x1010: ('FlashMode',
835 {0: 'Auto',
836 1: 'On',
837 2: 'Off',
838 3: 'Red Eye Reduction'}),
839 0x1011: ('FlashStrength', ),
840 0x1020: ('Macro',
841 {0: 'Off',
842 1: 'On'}),
843 0x1021: ('FocusMode',
844 {0: 'Auto',
845 1: 'Manual'}),
846 0x1030: ('SlowSync',
847 {0: 'Off',
848 1: 'On'}),
849 0x1031: ('PictureMode',
850 {0: 'Auto',
851 1: 'Portrait',
852 2: 'Landscape',
853 4: 'Sports',
854 5: 'Night',
855 6: 'Program AE',
856 256: 'Aperture Priority AE',
857 512: 'Shutter Priority AE',
858 768: 'Manual Exposure'}),
859 0x1100: ('MotorOrBracket',
860 {0: 'Off',
861 1: 'On'}),
862 0x1300: ('BlurWarning',
863 {0: 'Off',
864 1: 'On'}),
865 0x1301: ('FocusWarning',
866 {0: 'Off',
867 1: 'On'}),
868 0x1302: ('AEWarning',
869 {0: 'Off',
870 1: 'On'}),
871 }
872
873 MAKERNOTE_CANON_TAGS = {
874 0x0006: ('ImageType', ),
875 0x0007: ('FirmwareVersion', ),
876 0x0008: ('ImageNumber', ),
877 0x0009: ('OwnerName', ),
878 }
879
880 # this is in element offset, name, optional value dictionary format
881 MAKERNOTE_CANON_TAG_0x001 = {
882 1: ('Macromode',
883 {1: 'Macro',
884 2: 'Normal'}),
885 2: ('SelfTimer', ),
886 3: ('Quality',
887 {2: 'Normal',
888 3: 'Fine',
889 5: 'Superfine'}),
890 4: ('FlashMode',
891 {0: 'Flash Not Fired',
892 1: 'Auto',
893 2: 'On',
894 3: 'Red-Eye Reduction',
895 4: 'Slow Synchro',
896 5: 'Auto + Red-Eye Reduction',
897 6: 'On + Red-Eye Reduction',
898 16: 'external flash'}),
899 5: ('ContinuousDriveMode',
900 {0: 'Single Or Timer',
901 1: 'Continuous'}),
902 7: ('FocusMode',
903 {0: 'One-Shot',
904 1: 'AI Servo',
905 2: 'AI Focus',
906 3: 'MF',
907 4: 'Single',
908 5: 'Continuous',
909 6: 'MF'}),
910 10: ('ImageSize',
911 {0: 'Large',
912 1: 'Medium',
913 2: 'Small'}),
914 11: ('EasyShootingMode',
915 {0: 'Full Auto',
916 1: 'Manual',
917 2: 'Landscape',
918 3: 'Fast Shutter',
919 4: 'Slow Shutter',
920 5: 'Night',
921 6: 'B&W',
922 7: 'Sepia',
923 8: 'Portrait',
924 9: 'Sports',
925 10: 'Macro/Close-Up',
926 11: 'Pan Focus'}),
927 12: ('DigitalZoom',
928 {0: 'None',
929 1: '2x',
930 2: '4x'}),
931 13: ('Contrast',
932 {0xFFFF: 'Low',
933 0: 'Normal',
934 1: 'High'}),
935 14: ('Saturation',
936 {0xFFFF: 'Low',
937 0: 'Normal',
938 1: 'High'}),
939 15: ('Sharpness',
940 {0xFFFF: 'Low',
941 0: 'Normal',
942 1: 'High'}),
943 16: ('ISO',
944 {0: 'See ISOSpeedRatings Tag',
945 15: 'Auto',
946 16: '50',
947 17: '100',
948 18: '200',
949 19: '400'}),
950 17: ('MeteringMode',
951 {3: 'Evaluative',
952 4: 'Partial',
953 5: 'Center-weighted'}),
954 18: ('FocusType',
955 {0: 'Manual',
956 1: 'Auto',
957 3: 'Close-Up (Macro)',
958 8: 'Locked (Pan Mode)'}),
959 19: ('AFPointSelected',
960 {0x3000: 'None (MF)',
961 0x3001: 'Auto-Selected',
962 0x3002: 'Right',
963 0x3003: 'Center',
964 0x3004: 'Left'}),
965 20: ('ExposureMode',
966 {0: 'Easy Shooting',
967 1: 'Program',
968 2: 'Tv-priority',
969 3: 'Av-priority',
970 4: 'Manual',
971 5: 'A-DEP'}),
972 23: ('LongFocalLengthOfLensInFocalUnits', ),
973 24: ('ShortFocalLengthOfLensInFocalUnits', ),
974 25: ('FocalUnitsPerMM', ),
975 28: ('FlashActivity',
976 {0: 'Did Not Fire',
977 1: 'Fired'}),
978 29: ('FlashDetails',
979 {14: 'External E-TTL',
980 13: 'Internal Flash',
981 11: 'FP Sync Used',
982 7: '2nd("Rear")-Curtain Sync Used',
983 4: 'FP Sync Enabled'}),
984 32: ('FocusMode',
985 {0: 'Single',
986 1: 'Continuous'}),
987 }
988
989 MAKERNOTE_CANON_TAG_0x004 = {
990 7: ('WhiteBalance',
991 {0: 'Auto',
992 1: 'Sunny',
993 2: 'Cloudy',
994 3: 'Tungsten',
995 4: 'Fluorescent',
996 5: 'Flash',
997 6: 'Custom'}),
998 9: ('SequenceNumber', ),
999 14: ('AFPointUsed', ),
1000 15: ('FlashBias',
1001 {0XFFC0: '-2 EV',
1002 0XFFCC: '-1.67 EV',
1003 0XFFD0: '-1.50 EV',
1004 0XFFD4: '-1.33 EV',
1005 0XFFE0: '-1 EV',
1006 0XFFEC: '-0.67 EV',
1007 0XFFF0: '-0.50 EV',
1008 0XFFF4: '-0.33 EV',
1009 0X0000: '0 EV',
1010 0X000C: '0.33 EV',
1011 0X0010: '0.50 EV',
1012 0X0014: '0.67 EV',
1013 0X0020: '1 EV',
1014 0X002C: '1.33 EV',
1015 0X0030: '1.50 EV',
1016 0X0034: '1.67 EV',
1017 0X0040: '2 EV'}),
1018 19: ('SubjectDistance', ),
1019 }
1020
1021 # extract multibyte integer in Motorola format (little endian)
1022 def s2n_motorola(str):
1023 x = 0
1024 for c in str:
1025 x = (x << 8) | ord(c)
1026 return x
1027
1028 # extract multibyte integer in Intel format (big endian)
1029 def s2n_intel(str):
1030 x = 0
1031 y = 0L
1032 for c in str:
1033 x = x | (ord(c) << y)
1034 y = y + 8
1035 return x
1036
1037 # ratio object that eventually will be able to reduce itself to lowest
1038 # common denominator for printing
1039 def gcd(a, b):
1040 if b == 0:
1041 return a
1042 else:
1043 return gcd(b, a % b)
1044
1045 class Ratio:
1046 def __init__(self, num, den):
1047 self.num = num
1048 self.den = den
1049
1050 def __repr__(self):
1051 self.reduce()
1052 if self.den == 1:
1053 return str(self.num)
1054 return '%d/%d' % (self.num, self.den)
1055
1056 def reduce(self):
1057 div = gcd(self.num, self.den)
1058 if div > 1:
1059 self.num = self.num / div
1060 self.den = self.den / div
1061
1062 # for ease of dealing with tags
1063 class IFD_Tag:
1064 def __init__(self, printable, tag, field_type, values, field_offset,
1065 field_length):
1066 # printable version of data
1067 self.printable = printable
1068 # tag ID number
1069 self.tag = tag
1070 # field type as index into FIELD_TYPES
1071 self.field_type = field_type
1072 # offset of start of field in bytes from beginning of IFD
1073 self.field_offset = field_offset
1074 # length of data field in bytes
1075 self.field_length = field_length
1076 # either a string or array of data items
1077 self.values = values
1078
1079 def __str__(self):
1080 return self.printable
1081
1082 def __repr__(self):
1083 return '(0x%04X) %s=%s @ %d' % (self.tag,
1084 FIELD_TYPES[self.field_type][2],
1085 self.printable,
1086 self.field_offset)
1087
1088 # class that handles an EXIF header
1089 class EXIF_header:
1090 def __init__(self, file, endian, offset, fake_exif, debug=0):
1091 self.file = file
1092 self.endian = endian
1093 self.offset = offset
1094 self.fake_exif = fake_exif
1095 self.debug = debug
1096 self.tags = {}
1097
1098 # convert slice to integer, based on sign and endian flags
1099 # usually this offset is assumed to be relative to the beginning of the
1100 # start of the EXIF information. For some cameras that use relative tags,
1101 # this offset may be relative to some other starting point.
1102 def s2n(self, offset, length, signed=0):
1103 self.file.seek(self.offset+offset)
1104 slice=self.file.read(length)
1105 if self.endian == 'I':
1106 val=s2n_intel(slice)
1107 else:
1108 val=s2n_motorola(slice)
1109 # Sign extension ?
1110 if signed:
1111 msb=1L << (8*length-1)
1112 if val & msb:
1113 val=val-(msb << 1)
1114 return val
1115
1116 # convert offset to string
1117 def n2s(self, offset, length):
1118 s = ''
1119 for dummy in range(length):
1120 if self.endian == 'I':
1121 s = s + chr(offset & 0xFF)
1122 else:
1123 s = chr(offset & 0xFF) + s
1124 offset = offset >> 8
1125 return s
1126
1127 # return first IFD
1128 def first_IFD(self):
1129 return self.s2n(4, 4)
1130
1131 # return pointer to next IFD
1132 def next_IFD(self, ifd):
1133 entries=self.s2n(ifd, 2)
1134 return self.s2n(ifd+2+12*entries, 4)
1135
1136 # return list of IFDs in header
1137 def list_IFDs(self):
1138 i=self.first_IFD()
1139 a=[]
1140 while i:
1141 a.append(i)
1142 i=self.next_IFD(i)
1143 return a
1144
1145 # return list of entries in this IFD
1146 def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0, stop_tag='UNDEF'):
1147 entries=self.s2n(ifd, 2)
1148 for i in range(entries):
1149 # entry is index of start of this IFD in the file
1150 entry = ifd + 2 + 12 * i
1151 tag = self.s2n(entry, 2)
1152
1153 # get tag name early to avoid errors, help debug
1154 tag_entry = dict.get(tag)
1155 if tag_entry:
1156 tag_name = tag_entry[0]
1157 else:
1158 tag_name = 'Tag 0x%04X' % tag
1159
1160 # ignore certain tags for faster processing
1161 if not (not detailed and tag in IGNORE_TAGS):
1162 field_type = self.s2n(entry + 2, 2)
1163 if not 0 < field_type < len(FIELD_TYPES):
1164 # unknown field type
1165 raise ValueError('unknown type %d in tag 0x%04X' % (field_type, tag))
1166 typelen = FIELD_TYPES[field_type][0]
1167 count = self.s2n(entry + 4, 4)
1168 offset = entry + 8
1169 if count * typelen > 4:
1170 # offset is not the value; it's a pointer to the value
1171 # if relative we set things up so s2n will seek to the right
1172 # place when it adds self.offset. Note that this 'relative'
1173 # is for the Nikon type 3 makernote. Other cameras may use
1174 # other relative offsets, which would have to be computed here
1175 # slightly differently.
1176 if relative:
1177 tmp_offset = self.s2n(offset, 4)
1178 offset = tmp_offset + ifd - self.offset + 4
1179 if self.fake_exif:
1180 offset = offset + 18
1181 else:
1182 offset = self.s2n(offset, 4)
1183 field_offset = offset
1184 if field_type == 2:
1185 # special case: null-terminated ASCII string
1186 if count != 0:
1187 self.file.seek(self.offset + offset)
1188 values = self.file.read(count)
1189 values = values.strip().replace('\x00', '')
1190 else:
1191 values = ''
1192 else:
1193 values = []
1194 signed = (field_type in [6, 8, 9, 10])
1195 for dummy in range(count):
1196 if field_type in (5, 10):
1197 # a ratio
1198 value = Ratio(self.s2n(offset, 4, signed),
1199 self.s2n(offset + 4, 4, signed))
1200 else:
1201 value = self.s2n(offset, typelen, signed)
1202 values.append(value)
1203 offset = offset + typelen
1204 # now "values" is either a string or an array
1205 if count == 1 and field_type != 2:
1206 printable=str(values[0])
1207 else:
1208 printable=str(values)
1209 # compute printable version of values
1210 if tag_entry:
1211 if len(tag_entry) != 1:
1212 # optional 2nd tag element is present
1213 if callable(tag_entry[1]):
1214 # call mapping function
1215 printable = tag_entry[1](values)
1216 else:
1217 printable = ''
1218 for i in values:
1219 # use lookup table for this tag
1220 printable += tag_entry[1].get(i, repr(i))
1221
1222 self.tags[ifd_name + ' ' + tag_name] = IFD_Tag(printable, tag,
1223 field_type,
1224 values, field_offset,
1225 count * typelen)
1226 if self.debug:
1227 print ' debug: %s: %s' % (tag_name,
1228 repr(self.tags[ifd_name + ' ' + tag_name]))
1229
1230 if tag_name == stop_tag:
1231 break
1232
1233 # extract uncompressed TIFF thumbnail (like pulling teeth)
1234 # we take advantage of the pre-existing layout in the thumbnail IFD as
1235 # much as possible
1236 def extract_TIFF_thumbnail(self, thumb_ifd):
1237 entries = self.s2n(thumb_ifd, 2)
1238 # this is header plus offset to IFD ...
1239 if self.endian == 'M':
1240 tiff = 'MM\x00*\x00\x00\x00\x08'
1241 else:
1242 tiff = 'II*\x00\x08\x00\x00\x00'
1243 # ... plus thumbnail IFD data plus a null "next IFD" pointer
1244 self.file.seek(self.offset+thumb_ifd)
1245 tiff += self.file.read(entries*12+2)+'\x00\x00\x00\x00'
1246
1247 # fix up large value offset pointers into data area
1248 for i in range(entries):
1249 entry = thumb_ifd + 2 + 12 * i
1250 tag = self.s2n(entry, 2)
1251 field_type = self.s2n(entry+2, 2)
1252 typelen = FIELD_TYPES[field_type][0]
1253 count = self.s2n(entry+4, 4)
1254 oldoff = self.s2n(entry+8, 4)
1255 # start of the 4-byte pointer area in entry
1256 ptr = i * 12 + 18
1257 # remember strip offsets location
1258 if tag == 0x0111:
1259 strip_off = ptr
1260 strip_len = count * typelen
1261 # is it in the data area?
1262 if count * typelen > 4:
1263 # update offset pointer (nasty "strings are immutable" crap)
1264 # should be able to say "tiff[ptr:ptr+4]=newoff"
1265 newoff = len(tiff)
1266 tiff = tiff[:ptr] + self.n2s(newoff, 4) + tiff[ptr+4:]
1267 # remember strip offsets location
1268 if tag == 0x0111:
1269 strip_off = newoff
1270 strip_len = 4
1271 # get original data and store it
1272 self.file.seek(self.offset + oldoff)
1273 tiff += self.file.read(count * typelen)
1274
1275 # add pixel strips and update strip offset info
1276 old_offsets = self.tags['Thumbnail StripOffsets'].values
1277 old_counts = self.tags['Thumbnail StripByteCounts'].values
1278 for i in range(len(old_offsets)):
1279 # update offset pointer (more nasty "strings are immutable" crap)
1280 offset = self.n2s(len(tiff), strip_len)
1281 tiff = tiff[:strip_off] + offset + tiff[strip_off + strip_len:]
1282 strip_off += strip_len
1283 # add pixel strip to end
1284 self.file.seek(self.offset + old_offsets[i])
1285 tiff += self.file.read(old_counts[i])
1286
1287 self.tags['TIFFThumbnail'] = tiff
1288
1289 # decode all the camera-specific MakerNote formats
1290
1291 # Note is the data that comprises this MakerNote. The MakerNote will
1292 # likely have pointers in it that point to other parts of the file. We'll
1293 # use self.offset as the starting point for most of those pointers, since
1294 # they are relative to the beginning of the file.
1295 #
1296 # If the MakerNote is in a newer format, it may use relative addressing
1297 # within the MakerNote. In that case we'll use relative addresses for the
1298 # pointers.
1299 #
1300 # As an aside: it's not just to be annoying that the manufacturers use
1301 # relative offsets. It's so that if the makernote has to be moved by the
1302 # picture software all of the offsets don't have to be adjusted. Overall,
1303 # this is probably the right strategy for makernotes, though the spec is
1304 # ambiguous. (The spec does not appear to imagine that makernotes would
1305 # follow EXIF format internally. Once they did, it's ambiguous whether
1306 # the offsets should be from the header at the start of all the EXIF info,
1307 # or from the header at the start of the makernote.)
1308 def decode_maker_note(self):
1309 note = self.tags['EXIF MakerNote']
1310 make = self.tags['Image Make'].printable
1311 # model = self.tags['Image Model'].printable # unused
1312
1313 # Nikon
1314 # The maker note usually starts with the word Nikon, followed by the
1315 # type of the makernote (1 or 2, as a short). If the word Nikon is
1316 # not at the start of the makernote, it's probably type 2, since some
1317 # cameras work that way.
1318 if make in ('NIKON', 'NIKON CORPORATION'):
1319 if note.values[0:7] == [78, 105, 107, 111, 110, 0, 1]:
1320 if self.debug:
1321 print "Looks like a type 1 Nikon MakerNote."
1322 self.dump_IFD(note.field_offset+8, 'MakerNote',
1323 dict=MAKERNOTE_NIKON_OLDER_TAGS)
1324 elif note.values[0:7] == [78, 105, 107, 111, 110, 0, 2]:
1325 if self.debug:
1326 print "Looks like a labeled type 2 Nikon MakerNote"
1327 if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]:
1328 raise ValueError("Missing marker tag '42' in MakerNote.")
1329 # skip the Makernote label and the TIFF header
1330 self.dump_IFD(note.field_offset+10+8, 'MakerNote',
1331 dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1)
1332 else:
1333 # E99x or D1
1334 if self.debug:
1335 print "Looks like an unlabeled type 2 Nikon MakerNote"
1336 self.dump_IFD(note.field_offset, 'MakerNote',
1337 dict=MAKERNOTE_NIKON_NEWER_TAGS)
1338 return
1339
1340 # Olympus
1341 if make.startswith('OLYMPUS'):
1342 self.dump_IFD(note.field_offset+8, 'MakerNote',
1343 dict=MAKERNOTE_OLYMPUS_TAGS)
1344 # TODO
1345 #for i in (('MakerNote Tag 0x2020', MAKERNOTE_OLYMPUS_TAG_0x2020),):
1346 # self.decode_olympus_tag(self.tags[i[0]].values, i[1])
1347 #return
1348
1349 # Casio
1350 if make == 'Casio':
1351 self.dump_IFD(note.field_offset, 'MakerNote',
1352 dict=MAKERNOTE_CASIO_TAGS)
1353 return
1354
1355 # Fujifilm
1356 if make == 'FUJIFILM':
1357 # bug: everything else is "Motorola" endian, but the MakerNote
1358 # is "Intel" endian
1359 endian = self.endian
1360 self.endian = 'I'
1361 # bug: IFD offsets are from beginning of MakerNote, not
1362 # beginning of file header
1363 offset = self.offset
1364 self.offset += note.field_offset
1365 # process note with bogus values (note is actually at offset 12)
1366 self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS)
1367 # reset to correct values
1368 self.endian = endian
1369 self.offset = offset
1370 return
1371
1372 # Canon
1373 if make == 'Canon':
1374 self.dump_IFD(note.field_offset, 'MakerNote',
1375 dict=MAKERNOTE_CANON_TAGS)
1376 for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
1377 ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
1378 self.canon_decode_tag(self.tags[i[0]].values, i[1])
1379 return
1380
1381 # decode Olympus MakerNote tag based on offset within tag
1382 def olympus_decode_tag(self, value, dict):
1383 pass
1384
1385 # decode Canon MakerNote tag based on offset within tag
1386 # see http://www.burren.cx/david/canon.html by David Burren
1387 def canon_decode_tag(self, value, dict):
1388 for i in range(1, len(value)):
1389 x=dict.get(i, ('Unknown', ))
1390 if self.debug:
1391 print i, x
1392 name=x[0]
1393 if len(x) > 1:
1394 val=x[1].get(value[i], 'Unknown')
1395 else:
1396 val=value[i]
1397 # it's not a real IFD Tag but we fake one to make everybody
1398 # happy. this will have a "proprietary" type
1399 self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None,
1400 None, None)
1401
1402 # process an image file (expects an open file object)
1403 # this is the function that has to deal with all the arbitrary nasty bits
1404 # of the EXIF standard
1405 def process_file(f, stop_tag='UNDEF', details=True, debug=False):
1406 # yah it's cheesy...
1407 global detailed
1408 detailed = details
1409
1410 # by default do not fake an EXIF beginning
1411 fake_exif = 0
1412
1413 # determine whether it's a JPEG or TIFF
1414 data = f.read(12)
1415 if data[0:4] in ['II*\x00', 'MM\x00*']:
1416 # it's a TIFF file
1417 f.seek(0)
1418 endian = f.read(1)
1419 f.read(1)
1420 offset = 0
1421 elif data[0:2] == '\xFF\xD8':
1422 # it's a JPEG file
1423 while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM', 'Phot'):
1424 length = ord(data[4])*256+ord(data[5])
1425 f.read(length-8)
1426 # fake an EXIF beginning of file
1427 data = '\xFF\x00'+f.read(10)
1428 fake_exif = 1
1429 if data[2] == '\xFF' and data[6:10] == 'Exif':
1430 # detected EXIF header
1431 offset = f.tell()
1432 endian = f.read(1)
1433 else:
1434 # no EXIF information
1435 return {}
1436 else:
1437 # file format not recognized
1438 return {}
1439
1440 # deal with the EXIF info we found
1441 if debug:
1442 print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format'
1443 hdr = EXIF_header(f, endian, offset, fake_exif, debug)
1444 ifd_list = hdr.list_IFDs()
1445 ctr = 0
1446 for i in ifd_list:
1447 if ctr == 0:
1448 IFD_name = 'Image'
1449 elif ctr == 1:
1450 IFD_name = 'Thumbnail'
1451 thumb_ifd = i
1452 else:
1453 IFD_name = 'IFD %d' % ctr
1454 if debug:
1455 print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i)
1456 hdr.dump_IFD(i, IFD_name, stop_tag=stop_tag)
1457 # EXIF IFD
1458 exif_off = hdr.tags.get(IFD_name+' ExifOffset')
1459 if exif_off:
1460 if debug:
1461 print ' EXIF SubIFD at offset %d:' % exif_off.values[0]
1462 hdr.dump_IFD(exif_off.values[0], 'EXIF', stop_tag=stop_tag)
1463 # Interoperability IFD contained in EXIF IFD
1464 intr_off = hdr.tags.get('EXIF SubIFD InteroperabilityOffset')
1465 if intr_off:
1466 if debug:
1467 print ' EXIF Interoperability SubSubIFD at offset %d:' \
1468 % intr_off.values[0]
1469 hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
1470 dict=INTR_TAGS, stop_tag=stop_tag)
1471 # GPS IFD
1472 gps_off = hdr.tags.get(IFD_name+' GPSInfo')
1473 if gps_off:
1474 if debug:
1475 print ' GPS SubIFD at offset %d:' % gps_off.values[0]
1476 hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS, stop_tag=stop_tag)
1477 ctr += 1
1478
1479 # extract uncompressed TIFF thumbnail
1480 thumb = hdr.tags.get('Thumbnail Compression')
1481 if thumb and thumb.printable == 'Uncompressed TIFF':
1482 hdr.extract_TIFF_thumbnail(thumb_ifd)
1483
1484 # JPEG thumbnail (thankfully the JPEG data is stored as a unit)
1485 thumb_off = hdr.tags.get('Thumbnail JPEGInterchangeFormat')
1486 if thumb_off:
1487 f.seek(offset+thumb_off.values[0])
1488 size = hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0]
1489 hdr.tags['JPEGThumbnail'] = f.read(size)
1490
1491 # deal with MakerNote contained in EXIF IFD
1492 if 'EXIF MakerNote' in hdr.tags and detailed:
1493 hdr.decode_maker_note()
1494
1495 # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
1496 # since it's not allowed in a uncompressed TIFF IFD
1497 if 'JPEGThumbnail' not in hdr.tags:
1498 thumb_off=hdr.tags.get('MakerNote JPEGThumbnail')
1499 if thumb_off:
1500 f.seek(offset+thumb_off.values[0])
1501 hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length)
1502
1503 return hdr.tags
1504
1505
1506 # show command line usage
1507 def usage(exit_status):
1508 msg = 'Usage: EXIF.py [OPTIONS] file1 [file2 ...]\n'
1509 msg += 'Extract EXIF information from digital camera image files.\n\nOptions:\n'
1510 msg += '-q --quick Do not process MakerNotes.\n'
1511 msg += '-t TAG --stop-tag TAG Stop processing when this tag is retrieved.\n'
1512 msg += '-d --debug Run in debug mode.\n'
1513 print msg
1514 sys.exit(exit_status)
1515
1516 # library test/debug function (dump given files)
1517 if __name__ == '__main__':
1518 import sys
1519 import getopt
1520
1521 # parse command line options/arguments
1522 try:
1523 opts, args = getopt.getopt(sys.argv[1:], "hqdt:v", ["help", "quick", "debug", "stop-tag="])
1524 except getopt.GetoptError:
1525 usage(2)
1526 if args == []:
1527 usage(2)
1528 detailed = True
1529 stop_tag = 'UNDEF'
1530 debug = False
1531 for o, a in opts:
1532 if o in ("-h", "--help"):
1533 usage(0)
1534 if o in ("-q", "--quick"):
1535 detailed = False
1536 if o in ("-t", "--stop-tag"):
1537 stop_tag = a
1538 if o in ("-d", "--debug"):
1539 debug = True
1540
1541 # output info for each file
1542 for filename in args:
1543 try:
1544 file=open(filename, 'rb')
1545 except:
1546 print "'%s' is unreadable\n"%filename
1547 continue
1548 print filename + ':'
1549 # get the tags
1550 data = process_file(file, stop_tag=stop_tag, details=detailed, debug=debug)
1551 if not data:
1552 print 'No EXIF information found'
1553 continue
1554
1555 x=data.keys()
1556 x.sort()
1557 for i in x:
1558 if i in ('JPEGThumbnail', 'TIFFThumbnail'):
1559 continue
1560 try:
1561 print ' %s (%s): %s' % \
1562 (i, FIELD_TYPES[data[i].field_type][2], data[i].printable)
1563 except:
1564 print 'error', i, '"', data[i], '"'
1565 if 'JPEGThumbnail' in data:
1566 print 'File has JPEG thumbnail'
1567 print
1568