Attachment 'EXIF.py'
Download 1 # Library to extract EXIF information in digital camera image files
2 #
3 # To use this library call with:
4 # f=open(path_name, 'rb')
5 # tags=EXIF.process_file(f)
6 # tags will now be a dictionary mapping names of EXIF tags to their
7 # values in the file named by path_name. You can process the tags
8 # as you wish. In particular, you can iterate through all the tags with:
9 # for tag in tags.keys():
10 # if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename',
11 # 'EXIF MakerNote'):
12 # print "Key: %s, value %s" % (tag, tags[tag])
13 # (This code uses the if statement to avoid printing out a few of the
14 # tags that tend to be long or boring.)
15 #
16 # The tags dictionary will include keys for all of the usual EXIF
17 # tags, and will also include keys for Makernotes used by some
18 # cameras, for which we have a good specification.
19 #
20 # Contains code from "exifdump.py" originally written by Thierry Bousch
21 # <bousch@topo.math.u-psud.fr> and released into the public domain.
22 #
23 # Updated and turned into general-purpose library by Gene Cash
24 #
25 # This copyright license is intended to be similar to the FreeBSD license.
26 #
27 # Copyright 2002 Gene Cash All rights reserved.
28 #
29 # Redistribution and use in source and binary forms, with or without
30 # modification, are permitted provided that the following conditions are
31 # met:
32 #
33 # 1. Redistributions of source code must retain the above copyright
34 # notice, this list of conditions and the following disclaimer.
35 # 2. Redistributions in binary form must reproduce the above copyright
36 # notice, this list of conditions and the following disclaimer in the
37 # documentation and/or other materials provided with the
38 # distribution.
39 #
40 # THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR
41 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
42 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
43 # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
44 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
45 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
46 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
47 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
48 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
49 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
50 # POSSIBILITY OF SUCH DAMAGE.
51 #
52 # This means you may do anything you want with this code, except claim you
53 # wrote it. Also, if it breaks you get to keep both pieces.
54 #
55 # Patch Contributors:
56 # * Simon J. Gerraty <sjg@crufty.net>
57 # s2n fix & orientation decode
58 # * John T. Riedl <riedl@cs.umn.edu>
59 # Added support for newer Nikon type 3 Makernote format for D70 and some
60 # other Nikon cameras.
61 # * Joerg Schaefer <schaeferj@gmx.net>
62 # Fixed subtle bug when faking an EXIF header, which affected maker notes
63 # using relative offsets, and a fix for Nikon D100.
64 #
65 # 21-AUG-99 TB Last update by Thierry Bousch to his code.
66 # 17-JAN-02 CEC Discovered code on web.
67 # Commented everything.
68 # Made small code improvements.
69 # Reformatted for readability.
70 # 19-JAN-02 CEC Added ability to read TIFFs and JFIF-format JPEGs.
71 # Added ability to extract JPEG formatted thumbnail.
72 # Added ability to read GPS IFD (not tested).
73 # Converted IFD data structure to dictionaries indexed by
74 # tag name.
75 # Factored into library returning dictionary of IFDs plus
76 # thumbnail, if any.
77 # 20-JAN-02 CEC Added MakerNote processing logic.
78 # Added Olympus MakerNote.
79 # Converted data structure to single-level dictionary, avoiding
80 # tag name collisions by prefixing with IFD name. This makes
81 # it much easier to use.
82 # 23-JAN-02 CEC Trimmed nulls from end of string values.
83 # 25-JAN-02 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote.
84 # 26-JAN-02 CEC Added ability to extract TIFF thumbnails.
85 # Added Nikon, Fujifilm, Casio MakerNotes.
86 # 30-NOV-03 CEC Fixed problem with canon_decode_tag() not creating an
87 # IFD_Tag() object.
88 # 15-FEB-04 CEC Finally fixed bit shift warning by converting Y to 0L.
89 #
90
91 # field type descriptions as (length, abbreviation, full name) tuples
92 FIELD_TYPES=(
93 (0, 'X', 'Proprietary'), # no such type
94 (1, 'B', 'Byte'),
95 (1, 'A', 'ASCII'),
96 (2, 'S', 'Short'),
97 (4, 'L', 'Long'),
98 (8, 'R', 'Ratio'),
99 (1, 'SB', 'Signed Byte'),
100 (1, 'U', 'Undefined'),
101 (2, 'SS', 'Signed Short'),
102 (4, 'SL', 'Signed Long'),
103 (8, 'SR', 'Signed Ratio')
104 )
105
106 def cu(x):
107 if isinstance(x, str):
108 return x
109 return ''.join(map(chr, x))
110
111 # dictionary of main EXIF tag names
112 # first element of tuple is tag name, optional second element is
113 # another dictionary giving names to values
114 EXIF_TAGS={
115 0x0100: ('ImageWidth', ),
116 0x0101: ('ImageLength', ),
117 0x0102: ('BitsPerSample', ),
118 0x0103: ('Compression',
119 {1: 'Uncompressed TIFF',
120 6: 'JPEG Compressed'}),
121 0x0106: ('PhotometricInterpretation', ),
122 0x010A: ('FillOrder', ),
123 0x010D: ('DocumentName', ),
124 0x010E: ('ImageDescription', ),
125 0x010F: ('Make', ),
126 0x0110: ('Model', ),
127 0x0111: ('StripOffsets', ),
128 0x0112: ('Orientation',
129 {1: 'Horizontal (normal)',
130 2: 'Mirrored horizontal',
131 3: 'Rotated 180',
132 4: 'Mirrored vertical',
133 5: 'Mirrored horizontal then rotated 90 CCW',
134 6: 'Rotated 90 CW',
135 7: 'Mirrored horizontal then rotated 90 CW',
136 8: 'Rotated 90 CCW'}),
137 0x0115: ('SamplesPerPixel', ),
138 0x0116: ('RowsPerStrip', ),
139 0x0117: ('StripByteCounts', ),
140 0x011A: ('XResolution', ),
141 0x011B: ('YResolution', ),
142 0x011C: ('PlanarConfiguration', ),
143 0x0128: ('ResolutionUnit',
144 {1: 'Not Absolute',
145 2: 'Pixels/Inch',
146 3: 'Pixels/Centimeter'}),
147 0x012D: ('TransferFunction', ),
148 0x0131: ('Software', ),
149 0x0132: ('DateTime', ),
150 0x013B: ('Artist', ),
151 0x013E: ('WhitePoint', ),
152 0x013F: ('PrimaryChromaticities', ),
153 0x0156: ('TransferRange', ),
154 0x0200: ('JPEGProc', ),
155 0x0201: ('JPEGInterchangeFormat', ),
156 0x0202: ('JPEGInterchangeFormatLength', ),
157 0x0211: ('YCbCrCoefficients', ),
158 0x0212: ('YCbCrSubSampling', ),
159 0x0213: ('YCbCrPositioning', ),
160 0x0214: ('ReferenceBlackWhite', ),
161 0x828D: ('CFARepeatPatternDim', ),
162 0x828E: ('CFAPattern', ),
163 0x828F: ('BatteryLevel', ),
164 0x8298: ('Copyright', ),
165 0x829A: ('ExposureTime', ),
166 0x829D: ('FNumber', ),
167 0x83BB: ('IPTC/NAA', ),
168 0x8769: ('ExifOffset', ),
169 0x8773: ('InterColorProfile', ),
170 0x8822: ('ExposureProgram',
171 {0: 'Unidentified',
172 1: 'Manual',
173 2: 'Program Normal',
174 3: 'Aperture Priority',
175 4: 'Shutter Priority',
176 5: 'Program Creative',
177 6: 'Program Action',
178 7: 'Portrait Mode',
179 8: 'Landscape Mode'}),
180 0x8824: ('SpectralSensitivity', ),
181 0x8825: ('GPSInfo', ),
182 0x8827: ('ISOSpeedRatings', ),
183 0x8828: ('OECF', ),
184 # print as string
185 0x9000: ('ExifVersion', cu),
186 0x9003: ('DateTimeOriginal', ),
187 0x9004: ('DateTimeDigitized', ),
188 0x9101: ('ComponentsConfiguration',
189 {0: '',
190 1: 'Y',
191 2: 'Cb',
192 3: 'Cr',
193 4: 'Red',
194 5: 'Green',
195 6: 'Blue'}),
196 0x9102: ('CompressedBitsPerPixel', ),
197 0x9201: ('ShutterSpeedValue', ),
198 0x9202: ('ApertureValue', ),
199 0x9203: ('BrightnessValue', ),
200 0x9204: ('ExposureBiasValue', ),
201 0x9205: ('MaxApertureValue', ),
202 0x9206: ('SubjectDistance', ),
203 0x9207: ('MeteringMode',
204 {0: 'Unidentified',
205 1: 'Average',
206 2: 'CenterWeightedAverage',
207 3: 'Spot',
208 4: 'MultiSpot'}),
209 0x9208: ('LightSource',
210 {0: 'Unknown',
211 1: 'Daylight',
212 2: 'Fluorescent',
213 3: 'Tungsten',
214 10: 'Flash',
215 17: 'Standard Light A',
216 18: 'Standard Light B',
217 19: 'Standard Light C',
218 20: 'D55',
219 21: 'D65',
220 22: 'D75',
221 255: 'Other'}),
222 0x9209: ('Flash', {0: 'No',
223 1: 'Fired',
224 5: 'Fired (?)', # no return sensed
225 7: 'Fired (!)', # return sensed
226 9: 'Fill Fired',
227 13: 'Fill Fired (?)',
228 15: 'Fill Fired (!)',
229 16: 'Off',
230 24: 'Auto Off',
231 25: 'Auto Fired',
232 29: 'Auto Fired (?)',
233 31: 'Auto Fired (!)',
234 32: 'Not Available'}),
235 0x920A: ('FocalLength', ),
236 0x927C: ('MakerNote', ),
237 # print as string
238 0x9286: ('UserComment', cu),
239 # 0x9286: ('UserComment', ),
240 0x9290: ('SubSecTime', ),
241 0x9291: ('SubSecTimeOriginal', ),
242 0x9292: ('SubSecTimeDigitized', ),
243 # print as string
244 0xA000: ('FlashPixVersion', cu),
245 0xA001: ('ColorSpace', ),
246 0xA002: ('ExifImageWidth', ),
247 0xA003: ('ExifImageLength', ),
248 0xA005: ('InteroperabilityOffset', ),
249 0xA20B: ('FlashEnergy', ), # 0x920B in TIFF/EP
250 0xA20C: ('SpatialFrequencyResponse', ), # 0x920C - -
251 0xA20E: ('FocalPlaneXResolution', ), # 0x920E - -
252 0xA20F: ('FocalPlaneYResolution', ), # 0x920F - -
253 0xA210: ('FocalPlaneResolutionUnit', ), # 0x9210 - -
254 0xA214: ('SubjectLocation', ), # 0x9214 - -
255 0xA215: ('ExposureIndex', ), # 0x9215 - -
256 0xA217: ('SensingMethod', ), # 0x9217 - -
257 0xA300: ('FileSource',
258 {3: 'Digital Camera'}),
259 0xA301: ('SceneType',
260 {1: 'Directly Photographed'}),
261 0xA302: ('CVAPattern',),
262 }
263
264 # interoperability tags
265 INTR_TAGS={
266 0x0001: ('InteroperabilityIndex', ),
267 0x0002: ('InteroperabilityVersion', ),
268 0x1000: ('RelatedImageFileFormat', ),
269 0x1001: ('RelatedImageWidth', ),
270 0x1002: ('RelatedImageLength', ),
271 }
272
273 # GPS tags (not used yet, haven't seen camera with GPS)
274 GPS_TAGS={
275 0x0000: ('GPSVersionID', ),
276 0x0001: ('GPSLatitudeRef', ),
277 0x0002: ('GPSLatitude', ),
278 0x0003: ('GPSLongitudeRef', ),
279 0x0004: ('GPSLongitude', ),
280 0x0005: ('GPSAltitudeRef', ),
281 0x0006: ('GPSAltitude', ),
282 0x0007: ('GPSTimeStamp', ),
283 0x0008: ('GPSSatellites', ),
284 0x0009: ('GPSStatus', ),
285 0x000A: ('GPSMeasureMode', ),
286 0x000B: ('GPSDOP', ),
287 0x000C: ('GPSSpeedRef', ),
288 0x000D: ('GPSSpeed', ),
289 0x000E: ('GPSTrackRef', ),
290 0x000F: ('GPSTrack', ),
291 0x0010: ('GPSImgDirectionRef', ),
292 0x0011: ('GPSImgDirection', ),
293 0x0012: ('GPSMapDatum', ),
294 0x0013: ('GPSDestLatitudeRef', ),
295 0x0014: ('GPSDestLatitude', ),
296 0x0015: ('GPSDestLongitudeRef', ),
297 0x0016: ('GPSDestLongitude', ),
298 0x0017: ('GPSDestBearingRef', ),
299 0x0018: ('GPSDestBearing', ),
300 0x0019: ('GPSDestDistanceRef', ),
301 0x001A: ('GPSDestDistance', )
302 }
303
304 # Nikon E99x MakerNote Tags
305 # http://members.tripod.com/~tawba/990exif.htm
306 MAKERNOTE_NIKON_NEWER_TAGS={
307 0x0002: ('ISOSetting', ),
308 0x0003: ('ColorMode', ),
309 0x0004: ('Quality', ),
310 0x0005: ('Whitebalance', ),
311 0x0006: ('ImageSharpening', ),
312 0x0007: ('FocusMode', ),
313 0x0008: ('FlashSetting', ),
314 0x0009: ('AutoFlashMode', ),
315 0x000B: ('WhiteBalanceBias', ),
316 0x000C: ('WhiteBalanceRBCoeff', ),
317 0x000F: ('ISOSelection', ),
318 0x0012: ('FlashCompensation', ),
319 0x0013: ('ISOSpeedRequested', ),
320 0x0016: ('PhotoCornerCoordinates', ),
321 0x0018: ('FlashBracketCompensationApplied', ),
322 0x0019: ('AEBracketCompensationApplied', ),
323 0x0080: ('ImageAdjustment', ),
324 0x0081: ('ToneCompensation', ),
325 0x0082: ('AuxiliaryLens', ),
326 0x0083: ('LensType', ),
327 0x0084: ('LensMinMaxFocalMaxAperture', ),
328 0x0085: ('ManualFocusDistance', ),
329 0x0086: ('DigitalZoomFactor', ),
330 0x0088: ('AFFocusPosition',
331 {0x0000: 'Center',
332 0x0100: 'Top',
333 0x0200: 'Bottom',
334 0x0300: 'Left',
335 0x0400: 'Right'}),
336 0x0089: ('BracketingMode',
337 {0x00: 'Single frame, no bracketing',
338 0x01: 'Continuous, no bracketing',
339 0x02: 'Timer, no bracketing',
340 0x10: 'Single frame, exposure bracketing',
341 0x11: 'Continuous, exposure bracketing',
342 0x12: 'Timer, exposure bracketing',
343 0x40: 'Single frame, white balance bracketing',
344 0x41: 'Continuous, white balance bracketing',
345 0x42: 'Timer, white balance bracketing'}),
346 0x008D: ('ColorMode', ),
347 0x008F: ('SceneMode?', ),
348 0x0090: ('LightingType', ),
349 0x0092: ('HueAdjustment', ),
350 0x0094: ('Saturation',
351 {-3: 'B&W',
352 -2: '-2',
353 -1: '-1',
354 0: '0',
355 1: '1',
356 2: '2'}),
357 0x0095: ('NoiseReduction', ),
358 0x00A7: ('TotalShutterReleases', ),
359 0x00A9: ('ImageOptimization', ),
360 0x00AA: ('Saturation', ),
361 0x00AB: ('DigitalVariProgram', ),
362 0x0010: ('DataDump', )
363 }
364
365 MAKERNOTE_NIKON_OLDER_TAGS={
366 0x0003: ('Quality',
367 {1: 'VGA Basic',
368 2: 'VGA Normal',
369 3: 'VGA Fine',
370 4: 'SXGA Basic',
371 5: 'SXGA Normal',
372 6: 'SXGA Fine'}),
373 0x0004: ('ColorMode',
374 {1: 'Color',
375 2: 'Monochrome'}),
376 0x0005: ('ImageAdjustment',
377 {0: 'Normal',
378 1: 'Bright+',
379 2: 'Bright-',
380 3: 'Contrast+',
381 4: 'Contrast-'}),
382 0x0006: ('CCDSpeed',
383 {0: 'ISO 80',
384 2: 'ISO 160',
385 4: 'ISO 320',
386 5: 'ISO 100'}),
387 0x0007: ('WhiteBalance',
388 {0: 'Auto',
389 1: 'Preset',
390 2: 'Daylight',
391 3: 'Incandescent',
392 4: 'Fluorescent',
393 5: 'Cloudy',
394 6: 'Speed Light'})
395 }
396
397 # decode Olympus SpecialMode tag in MakerNote
398 def olympus_special_mode(v):
399 try:
400 a={
401 0: 'Normal',
402 1: 'Unknown',
403 2: 'Fast',
404 3: 'Panorama'}
405 b={
406 0: 'Non-panoramic',
407 1: 'Left to right',
408 2: 'Right to left',
409 3: 'Bottom to top',
410 4: 'Top to bottom'}
411 return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]])
412 except KeyError:
413 return v
414
415 MAKERNOTE_OLYMPUS_TAGS={
416 # ah HAH! those sneeeeeaky bastids! this is how they get past the fact
417 # that a JPEG thumbnail is not allowed in an uncompressed TIFF file
418 0x0100: ('JPEGThumbnail', ),
419 0x0200: ('SpecialMode', olympus_special_mode),
420 0x0201: ('JPEGQual',
421 {1: 'SQ',
422 2: 'HQ',
423 3: 'SHQ'}),
424 0x0202: ('Macro',
425 {0: 'Normal',
426 1: 'Macro'}),
427 0x0204: ('DigitalZoom', ),
428 0x0207: ('SoftwareRelease', ),
429 0x0208: ('PictureInfo', ),
430 # print as string
431 0x0209: ('CameraID', cu),
432 0x0F00: ('DataDump', )
433 }
434
435 MAKERNOTE_CASIO_TAGS={
436 0x0001: ('RecordingMode',
437 {1: 'Single Shutter',
438 2: 'Panorama',
439 3: 'Night Scene',
440 4: 'Portrait',
441 5: 'Landscape'}),
442 0x0002: ('Quality',
443 {1: 'Economy',
444 2: 'Normal',
445 3: 'Fine'}),
446 0x0003: ('FocusingMode',
447 {2: 'Macro',
448 3: 'Auto Focus',
449 4: 'Manual Focus',
450 5: 'Infinity'}),
451 0x0004: ('FlashMode',
452 {1: 'Auto',
453 2: 'On',
454 3: 'Off',
455 4: 'Red Eye Reduction'}),
456 0x0005: ('FlashIntensity',
457 {11: 'Weak',
458 13: 'Normal',
459 15: 'Strong'}),
460 0x0006: ('Object Distance', ),
461 0x0007: ('WhiteBalance',
462 {1: 'Auto',
463 2: 'Tungsten',
464 3: 'Daylight',
465 4: 'Fluorescent',
466 5: 'Shade',
467 129: 'Manual'}),
468 0x000B: ('Sharpness',
469 {0: 'Normal',
470 1: 'Soft',
471 2: 'Hard'}),
472 0x000C: ('Contrast',
473 {0: 'Normal',
474 1: 'Low',
475 2: 'High'}),
476 0x000D: ('Saturation',
477 {0: 'Normal',
478 1: 'Low',
479 2: 'High'}),
480 0x0014: ('CCDSpeed',
481 {64: 'Normal',
482 80: 'Normal',
483 100: 'High',
484 125: '+1.0',
485 244: '+3.0',
486 250: '+2.0',})
487 }
488
489 MAKERNOTE_FUJIFILM_TAGS={
490 0x0000: ('NoteVersion', cu),
491 0x1000: ('Quality', ),
492 0x1001: ('Sharpness',
493 {1: 'Soft',
494 2: 'Soft',
495 3: 'Normal',
496 4: 'Hard',
497 5: 'Hard'}),
498 0x1002: ('WhiteBalance',
499 {0: 'Auto',
500 256: 'Daylight',
501 512: 'Cloudy',
502 768: 'DaylightColor-Fluorescent',
503 769: 'DaywhiteColor-Fluorescent',
504 770: 'White-Fluorescent',
505 1024: 'Incandescent',
506 3840: 'Custom'}),
507 0x1003: ('Color',
508 {0: 'Normal',
509 256: 'High',
510 512: 'Low'}),
511 0x1004: ('Tone',
512 {0: 'Normal',
513 256: 'High',
514 512: 'Low'}),
515 0x1010: ('FlashMode',
516 {0: 'Auto',
517 1: 'On',
518 2: 'Off',
519 3: 'Red Eye Reduction'}),
520 0x1011: ('FlashStrength', ),
521 0x1020: ('Macro',
522 {0: 'Off',
523 1: 'On'}),
524 0x1021: ('FocusMode',
525 {0: 'Auto',
526 1: 'Manual'}),
527 0x1030: ('SlowSync',
528 {0: 'Off',
529 1: 'On'}),
530 0x1031: ('PictureMode',
531 {0: 'Auto',
532 1: 'Portrait',
533 2: 'Landscape',
534 4: 'Sports',
535 5: 'Night',
536 6: 'Program AE',
537 256: 'Aperture Priority AE',
538 512: 'Shutter Priority AE',
539 768: 'Manual Exposure'}),
540 0x1100: ('MotorOrBracket',
541 {0: 'Off',
542 1: 'On'}),
543 0x1300: ('BlurWarning',
544 {0: 'Off',
545 1: 'On'}),
546 0x1301: ('FocusWarning',
547 {0: 'Off',
548 1: 'On'}),
549 0x1302: ('AEWarning',
550 {0: 'Off',
551 1: 'On'})
552 }
553
554 MAKERNOTE_CANON_TAGS={
555 0x0006: ('ImageType', ),
556 0x0007: ('FirmwareVersion', ),
557 0x0008: ('ImageNumber', ),
558 0x0009: ('OwnerName', )
559 }
560
561 # see http://www.burren.cx/david/canon.html by David Burren
562 # this is in element offset, name, optional value dictionary format
563 MAKERNOTE_CANON_TAG_0x001={
564 1: ('Macromode',
565 {1: 'Macro',
566 2: 'Normal'}),
567 2: ('SelfTimer', ),
568 3: ('Quality',
569 {2: 'Normal',
570 3: 'Fine',
571 5: 'Superfine'}),
572 4: ('FlashMode',
573 {0: 'Flash Not Fired',
574 1: 'Auto',
575 2: 'On',
576 3: 'Red-Eye Reduction',
577 4: 'Slow Synchro',
578 5: 'Auto + Red-Eye Reduction',
579 6: 'On + Red-Eye Reduction',
580 16: 'external flash'}),
581 5: ('ContinuousDriveMode',
582 {0: 'Single Or Timer',
583 1: 'Continuous'}),
584 7: ('FocusMode',
585 {0: 'One-Shot',
586 1: 'AI Servo',
587 2: 'AI Focus',
588 3: 'MF',
589 4: 'Single',
590 5: 'Continuous',
591 6: 'MF'}),
592 10: ('ImageSize',
593 {0: 'Large',
594 1: 'Medium',
595 2: 'Small'}),
596 11: ('EasyShootingMode',
597 {0: 'Full Auto',
598 1: 'Manual',
599 2: 'Landscape',
600 3: 'Fast Shutter',
601 4: 'Slow Shutter',
602 5: 'Night',
603 6: 'B&W',
604 7: 'Sepia',
605 8: 'Portrait',
606 9: 'Sports',
607 10: 'Macro/Close-Up',
608 11: 'Pan Focus'}),
609 12: ('DigitalZoom',
610 {0: 'None',
611 1: '2x',
612 2: '4x'}),
613 13: ('Contrast',
614 {0xFFFF: 'Low',
615 0: 'Normal',
616 1: 'High'}),
617 14: ('Saturation',
618 {0xFFFF: 'Low',
619 0: 'Normal',
620 1: 'High'}),
621 15: ('Sharpness',
622 {0xFFFF: 'Low',
623 0: 'Normal',
624 1: 'High'}),
625 16: ('ISO',
626 {0: 'See ISOSpeedRatings Tag',
627 15: 'Auto',
628 16: '50',
629 17: '100',
630 18: '200',
631 19: '400'}),
632 17: ('MeteringMode',
633 {3: 'Evaluative',
634 4: 'Partial',
635 5: 'Center-weighted'}),
636 18: ('FocusType',
637 {0: 'Manual',
638 1: 'Auto',
639 3: 'Close-Up (Macro)',
640 8: 'Locked (Pan Mode)'}),
641 19: ('AFPointSelected',
642 {0x3000: 'None (MF)',
643 0x3001: 'Auto-Selected',
644 0x3002: 'Right',
645 0x3003: 'Center',
646 0x3004: 'Left'}),
647 20: ('ExposureMode',
648 {0: 'Easy Shooting',
649 1: 'Program',
650 2: 'Tv-priority',
651 3: 'Av-priority',
652 4: 'Manual',
653 5: 'A-DEP'}),
654 23: ('LongFocalLengthOfLensInFocalUnits', ),
655 24: ('ShortFocalLengthOfLensInFocalUnits', ),
656 25: ('FocalUnitsPerMM', ),
657 28: ('FlashActivity',
658 {0: 'Did Not Fire',
659 1: 'Fired'}),
660 29: ('FlashDetails',
661 {14: 'External E-TTL',
662 13: 'Internal Flash',
663 11: 'FP Sync Used',
664 7: '2nd("Rear")-Curtain Sync Used',
665 4: 'FP Sync Enabled'}),
666 32: ('FocusMode',
667 {0: 'Single',
668 1: 'Continuous'})
669 }
670
671 MAKERNOTE_CANON_TAG_0x004={
672 7: ('WhiteBalance',
673 {0: 'Auto',
674 1: 'Sunny',
675 2: 'Cloudy',
676 3: 'Tungsten',
677 4: 'Fluorescent',
678 5: 'Flash',
679 6: 'Custom'}),
680 9: ('SequenceNumber', ),
681 14: ('AFPointUsed', ),
682 15: ('FlashBias',
683 {0XFFC0: '-2 EV',
684 0XFFCC: '-1.67 EV',
685 0XFFD0: '-1.50 EV',
686 0XFFD4: '-1.33 EV',
687 0XFFE0: '-1 EV',
688 0XFFEC: '-0.67 EV',
689 0XFFF0: '-0.50 EV',
690 0XFFF4: '-0.33 EV',
691 0X0000: '0 EV',
692 0X000C: '0.33 EV',
693 0X0010: '0.50 EV',
694 0X0014: '0.67 EV',
695 0X0020: '1 EV',
696 0X002C: '1.33 EV',
697 0X0030: '1.50 EV',
698 0X0034: '1.67 EV',
699 0X0040: '2 EV'}),
700 19: ('SubjectDistance', )
701 }
702
703 # extract multibyte integer in Motorola format (little endian)
704 def s2n_motorola(str):
705 x=0
706 for c in str:
707 x=(x << 8) | ord(c)
708 return x
709
710 # extract multibyte integer in Intel format (big endian)
711 def s2n_intel(str):
712 x=0
713 y=0L
714 for c in str:
715 x=x | (ord(c) << y)
716 y=y+8
717 return x
718
719 # ratio object that eventually will be able to reduce itself to lowest
720 # common denominator for printing
721 def gcd(a, b):
722 if b == 0:
723 return a
724 else:
725 return gcd(b, a % b)
726
727 class Ratio:
728 def __init__(self, num, den):
729 self.num=num
730 self.den=den
731
732 def __repr__(self):
733 self.reduce()
734 if self.den == 1:
735 return str(self.num)
736 return '%d/%d' % (self.num, self.den)
737
738 def reduce(self):
739 div=gcd(self.num, self.den)
740 if div > 1:
741 self.num=self.num/div
742 self.den=self.den/div
743
744 # for ease of dealing with tags
745 class IFD_Tag:
746 def __init__(self, printable, tag, field_type, values, field_offset,
747 field_length):
748 # printable version of data
749 self.printable=printable
750 # tag ID number
751 self.tag=tag
752 # field type as index into FIELD_TYPES
753 self.field_type=field_type
754 # offset of start of field in bytes from beginning of IFD
755 self.field_offset=field_offset
756 # length of data field in bytes
757 self.field_length=field_length
758 # either a string or array of data items
759 self.values=values
760
761 def __str__(self):
762 return self.printable
763
764 def __repr__(self):
765 return '(0x%04X) %s=%s @ %d' % (self.tag,
766 FIELD_TYPES[self.field_type][2],
767 self.printable,
768 self.field_offset)
769
770 # class that handles an EXIF header
771 class EXIF_header:
772 def __init__(self, file, endian, offset, fake_exif, debug=0):
773 self.file=file
774 self.endian=endian
775 self.offset=offset
776 self.fake_exif=fake_exif
777 self.debug=debug
778 self.tags={}
779
780 # convert slice to integer, based on sign and endian flags
781 # usually this offset is assumed to be relative to the beginning of the
782 # start of the EXIF information. For some cameras that use relative tags,
783 # this offset may be relative to some other starting point.
784 def s2n(self, offset, length, signed=0):
785 self.file.seek(self.offset+offset)
786 slice=self.file.read(length)
787 if self.endian == 'I':
788 val=s2n_intel(slice)
789 else:
790 val=s2n_motorola(slice)
791 # Sign extension ?
792 if signed:
793 msb=1L << (8*length-1)
794 if val & msb:
795 val=val-(msb << 1)
796 return val
797
798 # convert offset to string
799 def n2s(self, offset, length):
800 s=''
801 for i in range(length):
802 if self.endian == 'I':
803 s=s+chr(offset & 0xFF)
804 else:
805 s=chr(offset & 0xFF)+s
806 offset=offset >> 8
807 return s
808
809 # return first IFD
810 def first_IFD(self):
811 return self.s2n(4, 4)
812
813 # return pointer to next IFD
814 def next_IFD(self, ifd):
815 entries=self.s2n(ifd, 2)
816 return self.s2n(ifd+2+12*entries, 4)
817
818 # return list of IFDs in header
819 def list_IFDs(self):
820 i=self.first_IFD()
821 a=[]
822 while i:
823 a.append(i)
824 i=self.next_IFD(i)
825 return a
826
827 # return list of entries in this IFD
828 def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0):
829 entries=self.s2n(ifd, 2)
830 for i in range(entries):
831 # entry is index of start of this IFD in the file
832 entry=ifd+2+12*i
833 tag=self.s2n(entry, 2)
834 # get tag name. We do it early to make debugging easier
835 tag_entry=dict.get(tag)
836 if tag_entry:
837 tag_name=tag_entry[0]
838 else:
839 tag_name='Tag 0x%04X' % tag
840 field_type=self.s2n(entry+2, 2)
841 if not 0 < field_type < len(FIELD_TYPES):
842 # unknown field type
843 raise ValueError, \
844 'unknown type %d in tag 0x%04X' % (field_type, tag)
845 typelen=FIELD_TYPES[field_type][0]
846 count=self.s2n(entry+4, 4)
847 offset=entry+8
848 if count*typelen > 4:
849 # offset is not the value; it's a pointer to the value
850 # if relative we set things up so s2n will seek to the right
851 # place when it adds self.offset. Note that this 'relative'
852 # is for the Nikon type 3 makernote. Other cameras may use
853 # other relative offsets, which would have to be computed here
854 # slightly differently.
855 if relative:
856 tmp_offset=self.s2n(offset, 4)
857 offset=tmp_offset+ifd-self.offset+4
858 if self.fake_exif:
859 offset=offset+18
860 else:
861 offset=self.s2n(offset, 4)
862 field_offset=offset
863 if field_type == 2:
864 # special case: null-terminated ASCII string
865 if count != 0:
866 self.file.seek(self.offset+offset)
867 values=self.file.read(count)
868 values=values.strip().replace('\x00','')
869 else:
870 values=''
871 else:
872 values=[]
873 signed=(field_type in [6, 8, 9, 10])
874 for j in range(count):
875 if field_type in (5, 10):
876 # a ratio
877 value_j=Ratio(self.s2n(offset, 4, signed),
878 self.s2n(offset+4, 4, signed))
879 else:
880 value_j=self.s2n(offset, typelen, signed)
881 values.append(value_j)
882 offset=offset+typelen
883 # now "values" is either a string or an array
884 if count == 1 and field_type != 2:
885 printable=str(values[0])
886 else:
887 printable=str(values)
888 # compute printable version of values
889 if tag_entry:
890 if len(tag_entry) != 1:
891 # optional 2nd tag element is present
892 if callable(tag_entry[1]):
893 # call mapping function
894 printable=tag_entry[1](values)
895 else:
896 printable=''
897 for i in values:
898 # use lookup table for this tag
899 printable+=tag_entry[1].get(i, repr(i))
900 self.tags[ifd_name+' '+tag_name]=IFD_Tag(printable, tag,
901 field_type,
902 values, field_offset,
903 count*typelen)
904 if self.debug:
905 print ' debug: %s: %s' % (tag_name,
906 repr(self.tags[ifd_name+' '+tag_name]))
907
908 # extract uncompressed TIFF thumbnail (like pulling teeth)
909 # we take advantage of the pre-existing layout in the thumbnail IFD as
910 # much as possible
911 def extract_TIFF_thumbnail(self, thumb_ifd):
912 entries=self.s2n(thumb_ifd, 2)
913 # this is header plus offset to IFD ...
914 if self.endian == 'M':
915 tiff='MM\x00*\x00\x00\x00\x08'
916 else:
917 tiff='II*\x00\x08\x00\x00\x00'
918 # ... plus thumbnail IFD data plus a null "next IFD" pointer
919 self.file.seek(self.offset+thumb_ifd)
920 tiff+=self.file.read(entries*12+2)+'\x00\x00\x00\x00'
921
922 # fix up large value offset pointers into data area
923 for i in range(entries):
924 entry=thumb_ifd+2+12*i
925 tag=self.s2n(entry, 2)
926 field_type=self.s2n(entry+2, 2)
927 typelen=FIELD_TYPES[field_type][0]
928 count=self.s2n(entry+4, 4)
929 oldoff=self.s2n(entry+8, 4)
930 # start of the 4-byte pointer area in entry
931 ptr=i*12+18
932 # remember strip offsets location
933 if tag == 0x0111:
934 strip_off=ptr
935 strip_len=count*typelen
936 # is it in the data area?
937 if count*typelen > 4:
938 # update offset pointer (nasty "strings are immutable" crap)
939 # should be able to say "tiff[ptr:ptr+4]=newoff"
940 newoff=len(tiff)
941 tiff=tiff[:ptr]+self.n2s(newoff, 4)+tiff[ptr+4:]
942 # remember strip offsets location
943 if tag == 0x0111:
944 strip_off=newoff
945 strip_len=4
946 # get original data and store it
947 self.file.seek(self.offset+oldoff)
948 tiff+=self.file.read(count*typelen)
949
950 # add pixel strips and update strip offset info
951 old_offsets=self.tags['Thumbnail StripOffsets'].values
952 old_counts=self.tags['Thumbnail StripByteCounts'].values
953 for i in range(len(old_offsets)):
954 # update offset pointer (more nasty "strings are immutable" crap)
955 offset=self.n2s(len(tiff), strip_len)
956 tiff=tiff[:strip_off]+offset+tiff[strip_off+strip_len:]
957 strip_off+=strip_len
958 # add pixel strip to end
959 self.file.seek(self.offset+old_offsets[i])
960 tiff+=self.file.read(old_counts[i])
961
962 self.tags['TIFFThumbnail']=tiff
963
964 # decode all the camera-specific MakerNote formats
965
966 # Note is the data that comprises this MakerNote. The MakerNote will
967 # likely have pointers in it that point to other parts of the file. We'll
968 # use self.offset as the starting point for most of those pointers, since
969 # they are relative to the beginning of the file.
970 #
971 # If the MakerNote is in a newer format, it may use relative addressing
972 # within the MakerNote. In that case we'll use relative addresses for the
973 # pointers.
974 #
975 # As an aside: it's not just to be annoying that the manufacturers use
976 # relative offsets. It's so that if the makernote has to be moved by the
977 # picture software all of the offsets don't have to be adjusted. Overall,
978 # this is probably the right strategy for makernotes, though the spec is
979 # ambiguous. (The spec does not appear to imagine that makernotes would
980 # follow EXIF format internally. Once they did, it's ambiguous whether
981 # the offsets should be from the header at the start of all the EXIF info,
982 # or from the header at the start of the makernote.)
983 def decode_maker_note(self):
984 note=self.tags['EXIF MakerNote']
985 make=self.tags['Image Make'].printable
986 model=self.tags['Image Model'].printable
987
988 # Nikon
989 # The maker note usually starts with the word Nikon, followed by the
990 # type of the makernote (1 or 2, as a short). If the word Nikon is
991 # not at the start of the makernote, it's probably type 2, since some
992 # cameras work that way.
993 if make in ('NIKON', 'NIKON CORPORATION'):
994 if note.values[0:7] == [78, 105, 107, 111, 110, 00, 01]:
995 if self.debug:
996 print "Looks like a type 1 Nikon MakerNote."
997 self.dump_IFD(note.field_offset+8, 'MakerNote',
998 dict=MAKERNOTE_NIKON_OLDER_TAGS)
999 elif note.values[0:7] == [78, 105, 107, 111, 110, 00, 02]:
1000 if self.debug:
1001 print "Looks like a labeled type 2 Nikon MakerNote"
1002 if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]:
1003 raise ValueError, "Missing marker tag '42' in MakerNote."
1004 # skip the Makernote label and the TIFF header
1005 self.dump_IFD(note.field_offset+10+8, 'MakerNote',
1006 dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1)
1007 else:
1008 # E99x or D1
1009 if self.debug:
1010 print "Looks like an unlabeled type 2 Nikon MakerNote"
1011 self.dump_IFD(note.field_offset, 'MakerNote',
1012 dict=MAKERNOTE_NIKON_NEWER_TAGS)
1013 return
1014
1015 # Olympus
1016 if make[:7] == 'OLYMPUS':
1017 self.dump_IFD(note.field_offset+8, 'MakerNote',
1018 dict=MAKERNOTE_OLYMPUS_TAGS)
1019 return
1020
1021 # Casio
1022 if make == 'Casio':
1023 self.dump_IFD(note.field_offset, 'MakerNote',
1024 dict=MAKERNOTE_CASIO_TAGS)
1025 return
1026
1027 # Fujifilm
1028 if make == 'FUJIFILM':
1029 # bug: everything else is "Motorola" endian, but the MakerNote
1030 # is "Intel" endian
1031 endian=self.endian
1032 self.endian='I'
1033 # bug: IFD offsets are from beginning of MakerNote, not
1034 # beginning of file header
1035 offset=self.offset
1036 self.offset+=note.field_offset
1037 # process note with bogus values (note is actually at offset 12)
1038 self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS)
1039 # reset to correct values
1040 self.endian=endian
1041 self.offset=offset
1042 return
1043
1044 # Canon
1045 if make == 'Canon':
1046 self.dump_IFD(note.field_offset, 'MakerNote',
1047 dict=MAKERNOTE_CANON_TAGS)
1048 for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
1049 ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
1050 self.canon_decode_tag(self.tags[i[0]].values, i[1])
1051 return
1052
1053 # decode Canon MakerNote tag based on offset within tag
1054 # see http://www.burren.cx/david/canon.html by David Burren
1055 def canon_decode_tag(self, value, dict):
1056 for i in range(1, len(value)):
1057 x=dict.get(i, ('Unknown', ))
1058 if self.debug:
1059 print i, x
1060 name=x[0]
1061 if len(x) > 1:
1062 val=x[1].get(value[i], 'Unknown')
1063 else:
1064 val=value[i]
1065 # it's not a real IFD Tag but we fake one to make everybody
1066 # happy. this will have a "proprietary" type
1067 self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None,
1068 None, None)
1069
1070 # process an image file (expects an open file object)
1071 # this is the function that has to deal with all the arbitrary nasty bits
1072 # of the EXIF standard
1073 def process_file(file, debug=0):
1074 # determine whether it's a JPEG or TIFF
1075 data=file.read(12)
1076 if data[0:4] in ['II*\x00', 'MM\x00*']:
1077 # it's a TIFF file
1078 file.seek(0)
1079 endian=file.read(1)
1080 file.read(1)
1081 offset=0
1082 elif data[0:2] == '\xFF\xD8':
1083 # it's a JPEG file
1084 # skip JFIF style header(s)
1085 fake_exif=0
1086 while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM'):
1087 length=ord(data[4])*256+ord(data[5])
1088 file.read(length-8)
1089 # fake an EXIF beginning of file
1090 data='\xFF\x00'+file.read(10)
1091 fake_exif=1
1092 if data[2] == '\xFF' and data[6:10] == 'Exif':
1093 # detected EXIF header
1094 offset=file.tell()
1095 endian=file.read(1)
1096 else:
1097 # no EXIF information
1098 return {}
1099 else:
1100 # file format not recognized
1101 return {}
1102
1103 # deal with the EXIF info we found
1104 if debug:
1105 print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format'
1106 hdr=EXIF_header(file, endian, offset, fake_exif, debug)
1107 ifd_list=hdr.list_IFDs()
1108 ctr=0
1109 for i in ifd_list:
1110 if ctr == 0:
1111 IFD_name='Image'
1112 elif ctr == 1:
1113 IFD_name='Thumbnail'
1114 thumb_ifd=i
1115 else:
1116 IFD_name='IFD %d' % ctr
1117 if debug:
1118 print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i)
1119 hdr.dump_IFD(i, IFD_name)
1120 # EXIF IFD
1121 exif_off=hdr.tags.get(IFD_name+' ExifOffset')
1122 if exif_off:
1123 if debug:
1124 print ' EXIF SubIFD at offset %d:' % exif_off.values[0]
1125 hdr.dump_IFD(exif_off.values[0], 'EXIF')
1126 # Interoperability IFD contained in EXIF IFD
1127 intr_off=hdr.tags.get('EXIF SubIFD InteroperabilityOffset')
1128 if intr_off:
1129 if debug:
1130 print ' EXIF Interoperability SubSubIFD at offset %d:' \
1131 % intr_off.values[0]
1132 hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
1133 dict=INTR_TAGS)
1134 # GPS IFD
1135 gps_off=hdr.tags.get(IFD_name+' GPSInfo')
1136 if gps_off:
1137 if debug:
1138 print ' GPS SubIFD at offset %d:' % gps_off.values[0]
1139 hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS)
1140 ctr+=1
1141
1142 # extract uncompressed TIFF thumbnail
1143 thumb=hdr.tags.get('Thumbnail Compression')
1144 if thumb and thumb.printable == 'Uncompressed TIFF':
1145 hdr.extract_TIFF_thumbnail(thumb_ifd)
1146
1147 # JPEG thumbnail (thankfully the JPEG data is stored as a unit)
1148 thumb_off=hdr.tags.get('Thumbnail JPEGInterchangeFormat')
1149 if thumb_off:
1150 file.seek(offset+thumb_off.values[0])
1151 size=hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0]
1152 hdr.tags['JPEGThumbnail']=file.read(size)
1153
1154 # deal with MakerNote contained in EXIF IFD
1155 if hdr.tags.has_key('EXIF MakerNote'):
1156 hdr.decode_maker_note()
1157
1158 # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
1159 # since it's not allowed in a uncompressed TIFF IFD
1160 if not hdr.tags.has_key('JPEGThumbnail'):
1161 thumb_off=hdr.tags.get('MakerNote JPEGThumbnail')
1162 if thumb_off:
1163 file.seek(offset+thumb_off.values[0])
1164 hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length)
1165
1166 return hdr.tags
1167
1168 # library test/debug function (dump given files)
1169 if __name__ == '__main__':
1170 import sys
1171
1172 if len(sys.argv) < 2:
1173 print 'Usage: %s files...\n' % sys.argv[0]
1174 sys.exit(0)
1175
1176 for filename in sys.argv[1:]:
1177 try:
1178 file=open(filename, 'rb')
1179 except:
1180 print filename, 'unreadable'
1181 print
1182 continue
1183 print filename+':'
1184 # data=process_file(file, 1) # with debug info
1185 data=process_file(file)
1186 if not data:
1187 print 'No EXIF information found'
1188 continue
1189
1190 x=data.keys()
1191 x.sort()
1192 for i in x:
1193 if i in ('JPEGThumbnail', 'TIFFThumbnail'):
1194 continue
1195 try:
1196 print ' %s (%s): %s' % \
1197 (i, FIELD_TYPES[data[i].field_type][2], data[i].printable)
1198 except:
1199 print 'error', i, '"', data[i], '"'
1200 if data.has_key('JPEGThumbnail'):
1201 print 'File has JPEG thumbnail'
1202 print
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.