File indexing completed on 2024-04-14 04:52:53

0001 #! /usr/bin/env python
0002 # Library to extract EXIF information in digital camera image files
0003 #
0004 # Contains code from "exifdump.py" originally written by Thierry Bousch
0005 # <bousch@topo.math.u-psud.fr> and released into the public domain.
0006 #
0007 # Updated and turned into general-purpose library by Gene Cash
0008 # <gcash@cfl.rr.com>
0009 #
0010 # NOTE: This version has been modified by Leif Jensen
0011 #
0012 # This copyright license is intended to be similar to the FreeBSD license. 
0013 #
0014 # SPDX-FileCopyrightText: 2002 Gene Cash All rights reserved.
0015 #
0016 # SPDX-License-Identifier: BSD-2-Clause
0017 #
0018 # 21-AUG-99 TB  Last update by Thierry Bousch to his code.
0019 # 17-JAN-02 CEC Discovered code on web.
0020 #               Commented everything.
0021 #               Made small code improvements.
0022 #               Reformatted for readability.
0023 # 19-JAN-02 CEC Added ability to read TIFFs and JFIF-format JPEGs.
0024 #               Added ability to extract JPEG formatted thumbnail.
0025 #               Added ability to read GPS IFD (not tested).
0026 #               Converted IFD data structure to dictionaries indexed by
0027 #               tag name.
0028 #               Factored into library returning dictionary of IFDs plus
0029 #               thumbnail, if any.
0030 # 20-JAN-02 CEC Added MakerNote processing logic.
0031 #               Added Olympus MakerNote.
0032 #               Converted data structure to single-level dictionary, avoiding
0033 #               tag name collisions by prefixing with IFD name.  This makes
0034 #               it much easier to use.
0035 # 23-JAN-02 CEC Trimmed nulls from end of string values.
0036 # 25-JAN-02 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote.
0037 # 26-JAN-02 CEC Added ability to extract TIFF thumbnails.
0038 #               Added Nikon, Fujifilm, Casio MakerNotes.
0039 #
0040 # To do:
0041 # * Finish Canon MakerNote format
0042 # * Better printing of ratios
0043 
0044 # field type descriptions as (length, abbreviation, full name) tuples
0045 FIELD_TYPES=(
0046     (0, 'X',  'Dummy'), # no such type
0047     (1, 'B',  'Byte'),
0048     (1, 'A',  'ASCII'),
0049     (2, 'S',  'Short'),
0050     (4, 'L',  'Long'),
0051     (8, 'R',  'Ratio'),
0052     (1, 'SB', 'Signed Byte'),
0053     (1, 'U',  'Undefined'),
0054     (2, 'SS', 'Signed Short'),
0055     (4, 'SL', 'Signed Long'),
0056     (8, 'SR', 'Signed Ratio')
0057     )
0058 
0059 # dictionary of main EXIF tag names
0060 # first element of tuple is tag name, optional second element is
0061 # another dictionary giving names to values
0062 EXIF_TAGS={
0063     0x0100: ('ImageWidth', ),
0064     0x0101: ('ImageLength', ),
0065     0x0102: ('BitsPerSample', ),
0066     0x0103: ('Compression',
0067              {1: 'Uncompressed TIFF',
0068               6: 'JPEG Compressed'}),
0069     0x0106: ('PhotometricInterpretation', ),
0070     0x010A: ('FillOrder', ),
0071     0x010D: ('DocumentName', ),
0072     0x010E: ('ImageDescription', ),
0073     0x010F: ('Make', ),
0074     0x0110: ('Model', ),
0075     0x0111: ('StripOffsets', ),
0076     0x0112: ('Orientation', ),
0077     0x0115: ('SamplesPerPixel', ),
0078     0x0116: ('RowsPerStrip', ),
0079     0x0117: ('StripByteCounts', ),
0080     0x011A: ('XResolution', ),
0081     0x011B: ('YResolution', ),
0082     0x011C: ('PlanarConfiguration', ),
0083     0x0128: ('ResolutionUnit',
0084              {1: 'Not Absolute',
0085               2: 'Pixels/Inch',
0086               3: 'Pixels/Centimeter'}),
0087     0x012D: ('TransferFunction', ),
0088     0x0131: ('Software', ),
0089     0x0132: ('DateTime', ),
0090     0x013B: ('Artist', ),
0091     0x013E: ('WhitePoint', ),
0092     0x013F: ('PrimaryChromaticities', ),
0093     0x0156: ('TransferRange', ),
0094     0x0200: ('JPEGProc', ),
0095     0x0201: ('JPEGInterchangeFormat', ),
0096     0x0202: ('JPEGInterchangeFormatLength', ),
0097     0x0211: ('YCbCrCoefficients', ),
0098     0x0212: ('YCbCrSubSampling', ),
0099     0x0213: ('YCbCrPositioning', ),
0100     0x0214: ('ReferenceBlackWhite', ),
0101     0x828D: ('CFARepeatPatternDim', ),
0102     0x828E: ('CFAPattern', ),
0103     0x828F: ('BatteryLevel', ),
0104     0x8298: ('Copyright', ),
0105     0x829A: ('ExposureTime', ),
0106     0x829D: ('FNumber', ),
0107     0x83BB: ('IPTC/NAA', ),
0108     0x8769: ('ExifOffset', ),
0109     0x8773: ('InterColorProfile', ),
0110     0x8822: ('ExposureProgram',
0111              {0: 'Unidentified',
0112               1: 'Manual',
0113               2: 'Program Normal',
0114               3: 'Aperture Priority',
0115               4: 'Shutter Priority',
0116               5: 'Program Creative',
0117               6: 'Program Action',
0118               7: 'Portrait Mode',
0119               8: 'Landscape Mode'}),
0120     0x8824: ('SpectralSensitivity', ),
0121     0x8825: ('GPSInfo', ),
0122     0x8827: ('ISOSpeedRatings', ),
0123     0x8828: ('OECF', ),
0124     0x9000: ('ExifVersion', ),
0125     0x9003: ('DateTimeOriginal', ),
0126     0x9004: ('DateTimeDigitized', ),
0127     0x9101: ('ComponentsConfiguration',
0128              {0: '',
0129               1: 'Y',
0130               2: 'Cb',
0131               3: 'Cr',
0132               4: 'Red',
0133               5: 'Green',
0134               6: 'Blue'}),
0135     0x9102: ('CompressedBitsPerPixel', ),
0136     0x9201: ('ShutterSpeedValue', ),
0137     0x9202: ('ApertureValue', ),
0138     0x9203: ('BrightnessValue', ),
0139     0x9204: ('ExposureBiasValue', ),
0140     0x9205: ('MaxApertureValue', ),
0141     0x9206: ('SubjectDistance', ),
0142     0x9207: ('MeteringMode',
0143              {0: 'Unidentified',
0144               1: 'Average',
0145               2: 'CenterWeightedAverage',
0146               3: 'Spot',
0147               4: 'MultiSpot'}),
0148     0x9208: ('LightSource',
0149              {0:   'Unknown',
0150               1:   'Daylight',
0151               2:   'Fluorescent',
0152               3:   'Tungsten',
0153               10:  'Flash',
0154               17:  'Standard Light A',
0155               18:  'Standard Light B',
0156               19:  'Standard Light C',
0157               20:  'D55',
0158               21:  'D65',
0159               22:  'D75',
0160               255: 'Other'}),
0161     0x9209: ('Flash', {0:  'No',
0162                        1:  'Fired',
0163                        5:  'Fired (?)', # no return sensed
0164                        7:  'Fired (!)', # return sensed
0165                        9:  'Fill Fired',
0166                        13: 'Fill Fired (?)',
0167                        15: 'Fill Fired (!)',
0168                        16: 'Off',
0169                        24: 'Auto Off',
0170                        25: 'Auto Fired',
0171                        29: 'Auto Fired (?)',
0172                        31: 'Auto Fired (!)',
0173                        32: 'Not Available'}),
0174     0x920A: ('FocalLength', ),
0175     0x927C: ('MakerNote', ),
0176     0x9286: ('UserComment', ),
0177     0x9290: ('SubSecTime', ),
0178     0x9291: ('SubSecTimeOriginal', ),
0179     0x9292: ('SubSecTimeDigitized', ),
0180     0xA000: ('FlashPixVersion', ),
0181     0xA001: ('ColorSpace', ),
0182     0xA002: ('ExifImageWidth', ),
0183     0xA003: ('ExifImageLength', ),
0184     0xA005: ('InteroperabilityOffset', ),
0185     0xA20B: ('FlashEnergy', ),               # 0x920B in TIFF/EP
0186     0xA20C: ('SpatialFrequencyResponse', ),  # 0x920C    -  -
0187     0xA20E: ('FocalPlaneXResolution', ),     # 0x920E    -  -
0188     0xA20F: ('FocalPlaneYResolution', ),     # 0x920F    -  -
0189     0xA210: ('FocalPlaneResolutionUnit', ),  # 0x9210    -  -
0190     0xA214: ('SubjectLocation', ),           # 0x9214    -  -
0191     0xA215: ('ExposureIndex', ),             # 0x9215    -  -
0192     0xA217: ('SensingMethod', ),             # 0x9217    -  -
0193     0xA300: ('FileSource',
0194              {3: 'Digital Camera'}),
0195     0xA301: ('SceneType',
0196              {1: 'Directly Photographed'}),
0197     }
0198 
0199 # interoperability tags
0200 INTR_TAGS={
0201     0x0001: ('InteroperabilityIndex', ),
0202     0x0002: ('InteroperabilityVersion', ),
0203     0x1000: ('RelatedImageFileFormat', ),
0204     0x1001: ('RelatedImageWidth', ),
0205     0x1002: ('RelatedImageLength', ),
0206     }
0207 
0208 # GPS tags (not used yet, haven't seen camera with GPS)
0209 GPS_TAGS={
0210     0x0000: ('GPSVersionID', ),
0211     0x0001: ('GPSLatitudeRef', ),
0212     0x0002: ('GPSLatitude', ),
0213     0x0003: ('GPSLongitudeRef', ),
0214     0x0004: ('GPSLongitude', ),
0215     0x0005: ('GPSAltitudeRef', ),
0216     0x0006: ('GPSAltitude', ),
0217     0x0007: ('GPSTimeStamp', ),
0218     0x0008: ('GPSSatellites', ),
0219     0x0009: ('GPSStatus', ),
0220     0x000A: ('GPSMeasureMode', ),
0221     0x000B: ('GPSDOP', ),
0222     0x000C: ('GPSSpeedRef', ),
0223     0x000D: ('GPSSpeed', ),
0224     0x000E: ('GPSTrackRef', ),
0225     0x000F: ('GPSTrack', ),
0226     0x0010: ('GPSImgDirectionRef', ),
0227     0x0011: ('GPSImgDirection', ),
0228     0x0012: ('GPSMapDatum', ),
0229     0x0013: ('GPSDestLatitudeRef', ),
0230     0x0014: ('GPSDestLatitude', ),
0231     0x0015: ('GPSDestLongitudeRef', ),
0232     0x0016: ('GPSDestLongitude', ),
0233     0x0017: ('GPSDestBearingRef', ),
0234     0x0018: ('GPSDestBearing', ),
0235     0x0019: ('GPSDestDistanceRef', ),
0236     0x001A: ('GPSDestDistance', )
0237     }
0238 
0239 # Nikon E99x MakerNote Tags
0240 # http://members.tripod.com/~tawba/990exif.htm
0241 MAKERNOTE_NIKON_NEWER_TAGS={
0242     0x0002: ('ISOSetting', ),
0243     0x0003: ('ColorMode', ),
0244     0x0004: ('Quality', ),
0245     0x0005: ('Whitebalance', ),
0246     0x0006: ('ImageSharpening', ),
0247     0x0007: ('FocusMode', ),
0248     0x0008: ('FlashSetting', ),
0249     0x000F: ('ISOSelection', ),
0250     0x0080: ('ImageAdjustment', ),
0251     0x0082: ('AuxiliaryLens', ),
0252     0x0085: ('ManualFocusDistance', ),
0253     0x0086: ('DigitalZoomFactor', ),
0254     0x0088: ('AFFocusPosition',
0255              {0x0000: 'Center',
0256               0x0100: 'Top',
0257               0x0200: 'Bottom',
0258               0x0300: 'Left',
0259               0x0400: 'Right'}),
0260     0x0094: ('Saturation',
0261              {-3: 'B&W',
0262               -2: '-2',
0263               -1: '-1',
0264               0:  '0',
0265               1:  '1',
0266               2:  '2'}),
0267     0x0095: ('NoiseReduction', ),
0268     0x0010: ('DataDump', )
0269     }
0270 
0271 MAKERNOTE_NIKON_OLDER_TAGS={
0272     0x0003: ('Quality',
0273              {1: 'VGA Basic',
0274               2: 'VGA Normal',
0275               3: 'VGA Fine',
0276               4: 'SXGA Basic',
0277               5: 'SXGA Normal',
0278               6: 'SXGA Fine'}),
0279     0x0004: ('ColorMode',
0280              {1: 'Color',
0281               2: 'Monochrome'}),
0282     0x0005: ('ImageAdjustment',
0283              {0: 'Normal',
0284               1: 'Bright+',
0285               2: 'Bright-',
0286               3: 'Contrast+',
0287               4: 'Contrast-'}),
0288     0x0006: ('CCDSpeed',
0289              {0: 'ISO 80',
0290               2: 'ISO 160',
0291               4: 'ISO 320',
0292               5: 'ISO 100'}),
0293     0x0007: ('WhiteBalance',
0294              {0: 'Auto',
0295               1: 'Preset',
0296               2: 'Daylight',
0297               3: 'Incandescent',
0298               4: 'Fluorescent',
0299               5: 'Cloudy',
0300               6: 'Speed Light'})
0301     }
0302 
0303 # decode Olympus SpecialMode tag in MakerNote
0304 def olympus_special_mode(v):
0305     a={
0306         0: 'Normal',
0307         1: 'Unknown',
0308         2: 'Fast',
0309         3: 'Panorama'}
0310     b={
0311         0: 'Non-panoramic',
0312         1: 'Left to right',
0313         2: 'Right to left',
0314         3: 'Bottom to top',
0315         4: 'Top to bottom'}
0316     return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]])
0317         
0318 MAKERNOTE_OLYMPUS_TAGS={
0319     # ah HAH! those sneeeeeaky bastids! this is how they get past the fact
0320     # that a JPEG thumbnail is not allowed in an uncompressed TIFF file
0321     0x0100: ('JPEGThumbnail', ),
0322     0x0200: ('SpecialMode', olympus_special_mode),
0323     0x0201: ('JPEGQual',
0324              {1: 'SQ',
0325               2: 'HQ',
0326               3: 'SHQ'}),
0327     0x0202: ('Macro',
0328              {0: 'Normal',
0329               1: 'Macro'}),
0330     0x0204: ('DigitalZoom', ),
0331     0x0207: ('SoftwareRelease',  ),
0332     0x0208: ('PictureInfo',  ),
0333     # print as string
0334     0x0209: ('CameraID', lambda x: ''.join(map(chr, x))), 
0335     0x0F00: ('DataDump',  )
0336     }
0337 
0338 MAKERNOTE_CASIO_TAGS={
0339     0x0001: ('RecordingMode',
0340              {1: 'Single Shutter',
0341               2: 'Panorama',
0342               3: 'Night Scene',
0343               4: 'Portrait',
0344               5: 'Landscape'}),
0345     0x0002: ('Quality',
0346              {1: 'Economy',
0347               2: 'Normal',
0348               3: 'Fine'}),
0349     0x0003: ('FocusingMode',
0350              {2: 'Macro',
0351               3: 'Auto Focus',
0352               4: 'Manual Focus',
0353               5: 'Infinity'}),
0354     0x0004: ('FlashMode',
0355              {1: 'Auto',
0356               2: 'On',
0357               3: 'Off',
0358               4: 'Red Eye Reduction'}),
0359     0x0005: ('FlashIntensity',
0360              {11: 'Weak',
0361               13: 'Normal',
0362               15: 'Strong'}),
0363     0x0006: ('Object Distance', ),
0364     0x0007: ('WhiteBalance',
0365              {1:   'Auto',
0366               2:   'Tungsten',
0367               3:   'Daylight',
0368               4:   'Fluorescent',
0369               5:   'Shade',
0370               129: 'Manual'}),
0371     0x000B: ('Sharpness',
0372              {0: 'Normal',
0373               1: 'Soft',
0374               2: 'Hard'}),
0375     0x000C: ('Contrast',
0376              {0: 'Normal',
0377               1: 'Low',
0378               2: 'High'}),
0379     0x000D: ('Saturation',
0380              {0: 'Normal',
0381               1: 'Low',
0382               2: 'High'}),
0383     0x0014: ('CCDSpeed',
0384              {64:  'Normal',
0385               80:  'Normal',
0386               100: 'High',
0387               125: '+1.0',
0388               244: '+3.0',
0389               250: '+2.0',})
0390     }
0391 
0392 MAKERNOTE_FUJIFILM_TAGS={
0393     0x0000: ('NoteVersion', lambda x: ''.join(map(chr, x))),
0394     0x1000: ('Quality', ),
0395     0x1001: ('Sharpness',
0396              {1: 'Soft',
0397               2: 'Soft',
0398               3: 'Normal',
0399               4: 'Hard',
0400               5: 'Hard'}),
0401     0x1002: ('WhiteBalance',
0402              {0:    'Auto',
0403               256:  'Daylight',
0404               512:  'Cloudy',
0405               768:  'DaylightColor-Fluorescent',
0406               769:  'DaywhiteColor-Fluorescent',
0407               770:  'White-Fluorescent',
0408               1024: 'Incandescent',
0409               3840: 'Custom'}),
0410     0x1003: ('Color',
0411              {0:   'Normal',
0412               256: 'High',
0413               512: 'Low'}),
0414     0x1004: ('Tone',
0415              {0:   'Normal',
0416               256: 'High',
0417               512: 'Low'}),
0418     0x1010: ('FlashMode',
0419              {0: 'Auto',
0420               1: 'On',
0421               2: 'Off',
0422               3: 'Red Eye Reduction'}),
0423     0x1011: ('FlashStrength', ),
0424     0x1020: ('Macro',
0425              {0: 'Off',
0426               1: 'On'}),
0427     0x1021: ('FocusMode',
0428              {0: 'Auto',
0429               1: 'Manual'}),
0430     0x1030: ('SlowSync',
0431              {0: 'Off',
0432               1: 'On'}),
0433     0x1031: ('PictureMode',
0434              {0:   'Auto',
0435               1:   'Portrait',
0436               2:   'Landscape',
0437               4:   'Sports',
0438               5:   'Night',
0439               6:   'Program AE',
0440               256: 'Aperture Priority AE',
0441               512: 'Shutter Priority AE',
0442               768: 'Manual Exposure'}),
0443     0x1100: ('MotorOrBracket',
0444              {0: 'Off',
0445               1: 'On'}),
0446     0x1300: ('BlurWarning',
0447              {0: 'Off',
0448               1: 'On'}),
0449     0x1301: ('FocusWarning',
0450              {0: 'Off',
0451               1: 'On'}),
0452     0x1302: ('AEWarning',
0453              {0: 'Off',
0454               1: 'On'})
0455     }
0456 
0457 MAKERNOTE_CANON_TAGS={
0458     0x0006: ('ImageType', ),
0459     0x0007: ('FirmwareVersion', ),
0460     0x0008: ('ImageNumber', ),
0461     0x0009: ('OwnerName', )
0462     }
0463 
0464 # see http://www.burren.cx/david/canon.html by David Burren
0465 # this is in element offset, name, optional value dictionary format
0466 MAKERNOTE_CANON_TAG_0x001={
0467     1: ('Macromode',
0468         {1: 'Macro',
0469          2: 'Normal'}),
0470     2: ('SelfTimer', ),
0471     3: ('Quality',
0472         {2: 'Normal',
0473          3: 'Fine',
0474          5: 'Superfine'}),
0475     4: ('FlashMode',
0476         {0: 'Flash Not Fired',
0477          1: 'Auto',
0478          2: 'On',
0479          3: 'Red-Eye Reduction',
0480          4: 'Slow Synchro',
0481          5: 'Auto + Red-Eye Reduction',
0482          6: 'On + Red-Eye Reduction',
0483          16: 'external flash'}),
0484     5: ('ContinuousDriveMode',
0485         {0: 'Single Or Timer',
0486          1: 'Continuous'}),
0487     7: ('FocusMode',
0488         {0: 'One-Shot',
0489          1: 'AI Servo',
0490          2: 'AI Focus',
0491          3: 'MF',
0492          4: 'Single',
0493          5: 'Continuous',
0494          6: 'MF'}),
0495     10: ('ImageSize',
0496          {0: 'Large',
0497           1: 'Medium',
0498           2: 'Small'}),
0499     11: ('EasyShootingMode',
0500          {0: 'Full Auto',
0501           1: 'Manual',
0502           2: 'Landscape',
0503           3: 'Fast Shutter',
0504           4: 'Slow Shutter',
0505           5: 'Night',
0506           6: 'B&W',
0507           7: 'Sepia',
0508           8: 'Portrait',
0509           9: 'Sports',
0510           10: 'Macro/Close-Up',
0511           11: 'Pan Focus'}),
0512     12: ('DigitalZoom',
0513          {0: 'None',
0514           1: '2x',
0515           2: '4x'}),
0516     13: ('Contrast',
0517          {0xFFFF: 'Low',
0518           0: 'Normal',
0519           1: 'High'}),
0520     14: ('Saturation',
0521          {0xFFFF: 'Low',
0522           0: 'Normal',
0523           1: 'High'}),
0524     15: ('Sharpness',
0525          {0xFFFF: 'Low',
0526           0: 'Normal',
0527           1: 'High'}),
0528     16: ('ISO',
0529          {0: 'See ISOSpeedRatings Tag',
0530           15: 'Auto',
0531           16: '50',
0532           17: '100',
0533           18: '200',
0534           19: '400'}),
0535     17: ('MeteringMode',
0536          {3: 'Evaluative',
0537           4: 'Partial',
0538           5: 'Center-weighted'}),
0539     18: ('FocusType',
0540          {0: 'Manual',
0541           1: 'Auto',
0542           3: 'Close-Up (Macro)',
0543           8: 'Locked (Pan Mode)'}),
0544     19: ('AFPointSelected',
0545          {0x3000: 'None (MF)',
0546           0x3001: 'Auto-Selected',
0547           0x3002: 'Right',
0548           0x3003: 'Center',
0549           0x3004: 'Left'}),
0550     20: ('ExposureMode',
0551          {0: 'Easy Shooting',
0552           1: 'Program',
0553           2: 'Tv-priority',
0554           3: 'Av-priority',
0555           4: 'Manual',
0556           5: 'A-DEP'}),
0557     23: ('LongFocalLengthOfLensInFocalUnits', ),
0558     24: ('ShortFocalLengthOfLensInFocalUnits', ),
0559     25: ('FocalUnitsPerMM', ),
0560     28: ('FlashActivity',
0561          {0: 'Did Not Fire',
0562           1: 'Fired'}),
0563     29: ('FlashDetails',
0564          {14: 'External E-TTL',
0565           13: 'Internal Flash',
0566           11: 'FP Sync Used',
0567           7: '2nd("Rear")-Curtain Sync Used',
0568           4: 'FP Sync Enabled'}),
0569     32: ('FocusMode',
0570          {0: 'Single',
0571           1: 'Continuous'})
0572     }
0573 
0574 MAKERNOTE_CANON_TAG_0x004={
0575     7: ('WhiteBalance',
0576         {0: 'Auto',
0577          1: 'Sunny',
0578          2: 'Cloudy',
0579          3: 'Tungsten',
0580          4: 'Fluorescent',
0581          5: 'Flash',
0582          6: 'Custom'}),
0583     9: ('SequenceNumber', ),
0584     14: ('AFPointUsed', ),
0585     15: ('FlashBias',
0586         {0XFFC0: '-2 EV',
0587          0XFFCC: '-1.67 EV',
0588          0XFFD0: '-1.50 EV',
0589          0XFFD4: '-1.33 EV',
0590          0XFFE0: '-1 EV',
0591          0XFFEC: '-0.67 EV',
0592          0XFFF0: '-0.50 EV',
0593          0XFFF4: '-0.33 EV',
0594          0X0000: '0 EV',
0595          0X000C: '0.33 EV',
0596          0X0010: '0.50 EV',
0597          0X0014: '0.67 EV',
0598          0X0020: '1 EV',
0599          0X002C: '1.33 EV',
0600          0X0030: '1.50 EV',
0601          0X0034: '1.67 EV',
0602          0X0040: '2 EV'}), 
0603     19: ('SubjectDistance', )
0604     }
0605 
0606 # extract multibyte integer in Motorola format (little endian)
0607 def s2n_motorola(str):
0608     x=0
0609     for c in str:
0610         x=(long(x) << 8) | ord(c)
0611     return x
0612 
0613 # extract multibyte integer in Intel format (big endian)
0614 def s2n_intel(str):
0615     x=0
0616     y=0
0617     for c in str:
0618         x=x | (ord(c) << y)
0619         y=y+8
0620     return x
0621 
0622 # ratio object that eventually will be able to reduce itself to lowest
0623 # common denominator for printing
0624 def gcd(a, b):
0625    if b == 0:
0626       return a
0627    else:
0628       return gcd(b, a % b)
0629 
0630 class Ratio:
0631     def __init__(self, num, den):
0632         self.num=num
0633         self.den=den
0634 
0635     def __repr__(self):
0636 #       self.reduce() # ugh, 259/250 worse 1036/1000
0637         if self.den == 1:
0638             return str(self.num)
0639         return '%d/%d' % (self.num, self.den)
0640 
0641     def reduce(self):
0642         div=gcd(self.num, self.den)
0643         if div > 1:
0644             self.num=self.num/div
0645             self.den=self.den/div
0646 
0647 # for ease of dealing with tags
0648 class IFD_Tag:
0649     def __init__(self, printable, tag, field_type, values, field_offset,
0650                  field_length):
0651         self.printable=printable
0652         self.tag=tag
0653         self.field_type=field_type
0654         self.field_offset=field_offset
0655         self.field_length=field_length
0656         self.values=values
0657         
0658     def __str__(self):
0659         return self.printable
0660     
0661     def __repr__(self):
0662         return '(0x%04X) %s=%s @ %d' % (self.tag,
0663                                         FIELD_TYPES[self.field_type][2],
0664                                         self.printable,
0665                                         self.field_offset)
0666 
0667 # class that handles an EXIF header
0668 class EXIF_header:
0669     def __init__(self, file, endian, offset, debug=0):
0670         self.file=file
0671         self.endian=endian
0672         self.offset=offset
0673         self.debug=debug
0674         self.tags={}
0675         
0676     # convert slice to integer, based on sign and endian flags
0677     def s2n(self, offset, length, signed=0):
0678         self.file.seek(self.offset+offset)
0679         slice=self.file.read(length)
0680         if self.endian == 'I':
0681             val=s2n_intel(slice)
0682         else:
0683             val=s2n_motorola(slice)
0684         # Sign extension ?
0685         if signed:
0686             msb=1L << (8*length-1)
0687             if val & msb:
0688                 val=val-(msb << 1)
0689         return val
0690 
0691     # convert offset to string
0692     def n2s(self, offset, length):
0693         s=''
0694         for i in range(length):
0695             if self.endian == 'I':
0696                 s=s+chr(offset & 0xFF)
0697             else:
0698                 s=chr(offset & 0xFF)+s
0699             offset=offset >> 8
0700         return s
0701     
0702     # return first IFD
0703     def first_IFD(self):
0704         return self.s2n(4, 4)
0705 
0706     # return pointer to next IFD
0707     def next_IFD(self, ifd):
0708         entries=self.s2n(ifd, 2)
0709         return self.s2n(ifd+2+12*entries, 4)
0710 
0711     # return list of IFDs in header
0712     def list_IFDs(self):
0713         i=self.first_IFD()
0714         a=[]
0715         while i:
0716             a.append(i)
0717             i=self.next_IFD(i)
0718         return a
0719 
0720     # return list of entries in this IFD
0721     def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS):
0722         entries=self.s2n(ifd, 2)
0723         for i in range(entries):
0724             entry=ifd+2+12*i
0725             tag=self.s2n(entry, 2)
0726             field_type=self.s2n(entry+2, 2)
0727             if not 0 < field_type < len(FIELD_TYPES):
0728                 # unknown field type
0729                 raise ValueError, \
0730                       'unknown type %d in tag 0x%04X' % (field_type, tag)
0731             typelen=FIELD_TYPES[field_type][0]
0732             count=self.s2n(entry+4, 4)
0733             offset=entry+8
0734             if count*typelen > 4:
0735                 # not the value, it's a pointer to the value
0736                 offset=self.s2n(offset, 4)
0737             field_offset=offset
0738             if field_type == 2:
0739                 # special case: null-terminated ASCII string
0740                 if count != 0:
0741                     self.file.seek(self.offset+offset)
0742                     values=self.file.read(count).strip().replace('\x00','')
0743                 else:
0744                     values=''
0745             elif tag == 0x927C or tag == 0x9286: # MakerNote or UserComment
0746 #            elif tag == 0x9286: # UserComment
0747                 values=[]
0748             else:
0749                 values=[]
0750                 signed=(field_type in [6, 8, 9, 10])
0751                 for j in range(count):
0752                     if field_type in (5, 10):
0753                         # a ratio
0754                         value_j=Ratio(self.s2n(offset,   4, signed),
0755                                       self.s2n(offset+4, 4, signed))
0756                     else:
0757                         value_j=self.s2n(offset, typelen, signed)
0758                     values.append(value_j)
0759                     offset=offset+typelen
0760             # now "values" is either a string or an array
0761             if count == 1 and field_type != 2:
0762                 printable=str(values[0])
0763             else:
0764                 printable=str(values)
0765             # figure out tag name
0766             tag_entry=dict.get(tag)
0767             if tag_entry:
0768                 tag_name=tag_entry[0]
0769                 if len(tag_entry) != 1:
0770                     # optional 2nd tag element is present
0771                     if callable(tag_entry[1]):
0772                         # call mapping function
0773                         printable=tag_entry[1](values)
0774                     else:
0775                         printable=''
0776                         for i in values:
0777                             # use LUT for this tag
0778                             printable+=tag_entry[1].get(i, repr(i))
0779             else:
0780                 tag_name='Tag 0x%04X' % tag
0781             self.tags[ifd_name+' '+tag_name]=IFD_Tag(printable, tag,
0782                                                      field_type,
0783                                                      values, field_offset,
0784                                                      count*typelen)
0785             if self.debug:
0786                 print '    %s: %s' % (tag_name,
0787                                       repr(self.tags[ifd_name+' '+tag_name]))
0788 
0789     # extract uncompressed TIFF thumbnail (like pulling teeth)
0790     # we take advantage of the pre-existing layout in the thumbnail IFD as
0791     # much as possible
0792     def extract_TIFF_thumbnail(self, thumb_ifd):
0793         entries=self.s2n(thumb_ifd, 2)
0794         # this is header plus offset to IFD ...
0795         if self.endian == 'M':
0796             tiff='MM\x00*\x00\x00\x00\x08'
0797         else:
0798             tiff='II*\x00\x08\x00\x00\x00'
0799         # ... plus thumbnail IFD data plus a null "next IFD" pointer
0800         self.file.seek(self.offset+thumb_ifd)
0801         tiff+=self.file.read(entries*12+2)+'\x00\x00\x00\x00'
0802         
0803         # fix up large value offset pointers into data area
0804         for i in range(entries):
0805             entry=thumb_ifd+2+12*i
0806             tag=self.s2n(entry, 2)
0807             field_type=self.s2n(entry+2, 2)
0808             typelen=FIELD_TYPES[field_type][0]
0809             count=self.s2n(entry+4, 4)
0810             oldoff=self.s2n(entry+8, 4)
0811             # start of the 4-byte pointer area in entry
0812             ptr=i*12+18
0813             # remember strip offsets location
0814             if tag == 0x0111:
0815                 strip_off=ptr
0816                 strip_len=count*typelen
0817             # is it in the data area?
0818             if count*typelen > 4:
0819                 # update offset pointer (nasty "strings are immutable" crap)
0820                 # should be able to say "tiff[ptr:ptr+4]=newoff"
0821                 newoff=len(tiff)
0822                 tiff=tiff[:ptr]+self.n2s(newoff, 4)+tiff[ptr+4:]
0823                 # remember strip offsets location
0824                 if tag == 0x0111:
0825                     strip_off=newoff
0826                     strip_len=4
0827                 # get original data and store it
0828                 self.file.seek(self.offset+oldoff)
0829                 tiff+=self.file.read(count*typelen)
0830                 
0831         # add pixel strips and update strip offset info
0832         old_offsets=self.tags['Thumbnail StripOffsets'].values
0833         old_counts=self.tags['Thumbnail StripByteCounts'].values
0834         for i in range(len(old_offsets)):
0835             # update offset pointer (more nasty "strings are immutable" crap)
0836             offset=self.n2s(len(tiff), strip_len)
0837             tiff=tiff[:strip_off]+offset+tiff[strip_off+strip_len:]
0838             strip_off+=strip_len
0839             # add pixel strip to end
0840             self.file.seek(self.offset+old_offsets[i])
0841             tiff+=self.file.read(old_counts[i])
0842             
0843         self.tags['TIFFThumbnail']=tiff
0844         
0845     # decode all the camera-specific MakerNote formats
0846     def decode_maker_note(self):
0847         note=self.tags['EXIF MakerNote']
0848         make=self.tags['Image Make'].printable
0849         model=self.tags['Image Model'].printable
0850 
0851         # Nikon
0852         if make == 'NIKON':
0853             if note.values[0:5] == [78, 105, 107, 111, 110]: # "Nikon"
0854                 # older model
0855                 self.dump_IFD(note.field_offset+8, 'MakerNote',
0856                               dict=MAKERNOTE_NIKON_OLDER_TAGS)
0857             else:
0858                 # newer model (E99x or D1)
0859                 self.dump_IFD(note.field_offset, 'MakerNote',
0860                               dict=MAKERNOTE_NIKON_NEWER_TAGS)
0861             return
0862 
0863         # Olympus
0864         if make[:7] == 'OLYMPUS':
0865             self.dump_IFD(note.field_offset+8, 'MakerNote',
0866                           dict=MAKERNOTE_OLYMPUS_TAGS)
0867             return
0868 
0869         # Casio
0870         if make == 'Casio':
0871             self.dump_IFD(note.field_offset, 'MakerNote',
0872                           dict=MAKERNOTE_CASIO_TAGS)
0873             return
0874         
0875         # Fujifilm
0876         if make == 'FUJIFILM':
0877             # bug: everything else is "Motorola" endian, but the MakerNote
0878             # is "Intel" endian 
0879             endian=self.endian
0880             self.endian='I'
0881             # bug: IFD offsets are from beginning of MakerNote, not
0882             # beginning of file header
0883             offset=self.offset
0884             self.offset+=note.field_offset
0885             # process note with bogus values (note is actually at offset 12)
0886             self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS)
0887             # reset to correct values
0888             self.endian=endian
0889             self.offset=offset
0890             return
0891         
0892         # Canon
0893         if make == 'Canon':
0894             self.dump_IFD(note.field_offset, 'MakerNote',
0895                           dict=MAKERNOTE_CANON_TAGS)
0896             for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
0897                       ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
0898                 if self.debug:
0899                   print ' SubMakerNote BitSet for ' +i[0]
0900                 self.canon_decode_tag(self.tags[i[0]].values, i[1])
0901             return
0902 
0903     # decode Canon MakerNote tag based on offset within tag
0904     # see http://www.burren.cx/david/canon.html by David Burren
0905     def canon_decode_tag(self, value, dict):
0906         for i in range(1, len(value)):
0907             x=dict.get(i, ('Unknown', ))
0908 #            if self.debug:
0909 #                print i, x
0910             name=x[0]
0911             if len(x) > 1:
0912                 val=x[1].get(value[i], 'Unknown')
0913             else:
0914                 val=value[i]
0915             if self.debug:
0916                 print '      '+name+':', val
0917             self.tags['MakerNote '+name]=val
0918 
0919 # process an image file (expects an open file object)
0920 # this is the function that has to deal with all the arbitrary nasty bits
0921 # of the EXIF standard
0922 def process_file(file, debug=0, noclose=0):
0923     # determine whether it's a JPEG or TIFF
0924     data=file.read(12)
0925     if data[0:4] in ['II*\x00', 'MM\x00*']:
0926         # it's a TIFF file
0927         file.seek(0)
0928         endian=file.read(1)
0929         file.read(1)
0930         offset=0
0931     elif data[0:2] == '\xFF\xD8':
0932         # it's a JPEG file
0933         # skip JFIF style header(s)
0934         while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM'):
0935             length=ord(data[4])*256+ord(data[5])
0936             file.read(length-8)
0937             # fake an EXIF beginning of file
0938             data='\xFF\x00'+file.read(10)
0939         if data[2] == '\xFF' and data[6:10] == 'Exif':
0940             # detected EXIF header
0941             offset=file.tell()
0942             endian=file.read(1)
0943         else:
0944             # no EXIF information
0945             return {}
0946     else:
0947         # file format not recognized
0948         return {}
0949 
0950     # deal with the EXIF info we found
0951     if debug:
0952         print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format'
0953     hdr=EXIF_header(file, endian, offset, debug)
0954     ifd_list=hdr.list_IFDs()
0955     ctr=0
0956     for i in ifd_list:
0957         if ctr == 0:
0958             IFD_name='Image'
0959         elif ctr == 1:
0960             IFD_name='Thumbnail'
0961             thumb_ifd=i
0962         else:
0963             IFD_name='IFD %d' % ctr
0964         if debug:
0965             print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i)
0966         hdr.tags['Exif Offset'] = offset
0967         hdr.tags['Exif Endian'] = endian
0968         hdr.tags[IFD_name+' IFDOffset'] = i
0969         hdr.dump_IFD(i, IFD_name)
0970         # EXIF IFD
0971         exif_off=hdr.tags.get(IFD_name+' ExifOffset')
0972         if exif_off:
0973             if debug:
0974                 print ' EXIF SubIFD at offset %d:' % exif_off.values[0]
0975             hdr.dump_IFD(exif_off.values[0], 'EXIF')
0976             # Interoperability IFD contained in EXIF IFD
0977             #intr_off=hdr.tags.get('EXIF SubIFD InteroperabilityOffset')
0978             intr_off=hdr.tags.get('EXIF InteroperabilityOffset')
0979             if intr_off:
0980                 if debug:
0981                     print ' EXIF Interoperability SubSubIFD at offset %d:' \
0982                           % intr_off.values[0]
0983                 hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
0984                              dict=INTR_TAGS)
0985             # deal with MakerNote contained in EXIF IFD
0986             if hdr.tags.has_key('EXIF MakerNote'):
0987                 if debug:
0988                     print ' EXIF MakerNote SubSubIFD at offset %d:' \
0989                           % intr_off.values[0]
0990                 hdr.decode_maker_note()
0991         # GPS IFD
0992         gps_off=hdr.tags.get(IFD_name+' GPSInfoOffset')
0993         if gps_off:
0994             if debug:
0995                 print ' GPS SubIFD at offset %d:' % gps_off.values[0]
0996             hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS)
0997         ctr+=1
0998 
0999 
1000     # extract uncompressed TIFF thumbnail
1001     thumb=hdr.tags.get('Thumbnail Compression')
1002     if thumb and thumb.printable == 'Uncompressed TIFF':
1003         hdr.extract_TIFF_thumbnail(thumb_ifd)
1004         
1005     # JPEG thumbnail (thankfully the JPEG data is stored as a unit)
1006     thumb_off=hdr.tags.get('Thumbnail JPEGInterchangeFormat')
1007     if thumb_off:
1008         file.seek(offset+thumb_off.values[0])
1009         size=hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0]
1010         hdr.tags['JPEGThumbnail']=file.read(size)
1011         
1012     # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
1013     # since it's not allowed in a uncompressed TIFF IFD
1014     if not hdr.tags.has_key('JPEGThumbnail'):
1015         thumb_off=hdr.tags.get('MakerNote JPEGThumbnail')
1016         if thumb_off:
1017             file.seek(offset+thumb_off.values[0])
1018             hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length)
1019             
1020     if noclose == 0:
1021       file.close()
1022     return hdr.tags
1023 
1024 # library test/debug function (dump given files)
1025 if __name__ == '__main__':
1026     import sys
1027     
1028     if len(sys.argv) < 2:
1029         print 'Usage: %s files...\n' % sys.argv[0]
1030         sys.exit(0)
1031         
1032     for filename in sys.argv[1:]:
1033         try:
1034             file=open(filename, 'rb')
1035         except:
1036             print filename, 'unreadable'
1037             print
1038             continue
1039         print filename+':'
1040         # data=process_file(file, 1) # with debug info
1041         data=process_file(file, 1)
1042         if not data:
1043             print 'No EXIF information found'
1044             continue
1045 
1046 #       x=data.keys()
1047 #       x.sort()
1048 #       for i in x:
1049 #           if i in ('JPEGThumbnail', 'TIFFThumbnail'):
1050 #               continue
1051 #           print '   %s (%s): %s' % \
1052 #                 (i, FIELD_TYPES[data[i].field_type][2], data[i].printable)
1053 #       if data.has_key('JPEGThumbnail'):
1054 #           print 'File has JPEG thumbnail'
1055 #       print