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