[4] | 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 |
---|
| 93 | FIELD_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 |
---|
| 110 | EXIF_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 |
---|
| 260 | INTR_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) |
---|
| 269 | GPS_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 |
---|
| 301 | MAKERNOTE_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 | |
---|
| 360 | MAKERNOTE_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 |
---|
| 393 | def 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 | |
---|
| 407 | MAKERNOTE_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 | |
---|
| 427 | MAKERNOTE_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 | |
---|
| 481 | MAKERNOTE_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 | |
---|
| 546 | MAKERNOTE_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 |
---|
| 555 | MAKERNOTE_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 | |
---|
| 663 | MAKERNOTE_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) |
---|
| 696 | def 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) |
---|
| 703 | def 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 |
---|
| 713 | def gcd(a, b): |
---|
| 714 | if b == 0: |
---|
| 715 | return a |
---|
| 716 | else: |
---|
| 717 | return gcd(b, a % b) |
---|
| 718 | |
---|
| 719 | class 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 |
---|
| 737 | class 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 |
---|
| 763 | class 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 |
---|
| 1065 | def 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) |
---|
| 1161 | if __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 |
---|