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