source: trunk/src/testing/bin/fileServer/misc/mmpython/image/EXIF.py @ 4

Revision 4, 41.0 KB checked in by ajaworski, 13 years ago (diff)

Added modified SAGE sources

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