diff options
Diffstat (limited to 'extlib/exif/EXIF.py')
-rwxr-xr-x | extlib/exif/EXIF.py | 219 |
1 files changed, 174 insertions, 45 deletions
diff --git a/extlib/exif/EXIF.py b/extlib/exif/EXIF.py index ed4192af..a188154e 100755 --- a/extlib/exif/EXIF.py +++ b/extlib/exif/EXIF.py @@ -1,8 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Library to extract EXIF information from digital camera image files -# http://sourceforge.net/projects/exif-py/ +# +# Library to extract EXIF information from digital camera image files. +# https://github.com/ianare/exif-py +# # # VERSION 1.1.0 # @@ -22,7 +24,6 @@ # # These 2 are useful when you are retrieving a large list of images # -# # To return an error on invalid tags, # pass the -s or --strict argument, or as # tags = EXIF.process_file(f, strict=True) @@ -48,7 +49,7 @@ # 'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode' # # Copyright (c) 2002-2007 Gene Cash All rights reserved -# Copyright (c) 2007-2008 Ianaré Sévi All rights reserved +# Copyright (c) 2007-2012 Ianaré Sévi All rights reserved # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -102,7 +103,7 @@ def make_string_uc(seq): seq = seq[8:] # Of course, this is only correct if ASCII, and the standard explicitly # allows JIS and Unicode. - return make_string(seq) + return make_string( make_string(seq) ) # field type descriptions as (length, abbreviation, full name) tuples FIELD_TYPES = ( @@ -171,9 +172,9 @@ EXIF_TAGS = { 3: 'Rotated 180', 4: 'Mirrored vertical', 5: 'Mirrored horizontal then rotated 90 CCW', - 6: 'Rotated 90 CW', + 6: 'Rotated 90 CCW', 7: 'Mirrored horizontal then rotated 90 CW', - 8: 'Rotated 90 CCW'}), + 8: 'Rotated 90 CW'}), 0x0115: ('SamplesPerPixel', ), 0x0116: ('RowsPerStrip', ), 0x0117: ('StripByteCounts', ), @@ -251,40 +252,54 @@ EXIF_TAGS = { 2: 'CenterWeightedAverage', 3: 'Spot', 4: 'MultiSpot', - 5: 'Pattern'}), + 5: 'Pattern', + 6: 'Partial', + 255: 'other'}), 0x9208: ('LightSource', {0: 'Unknown', 1: 'Daylight', 2: 'Fluorescent', - 3: 'Tungsten', - 9: 'Fine Weather', - 10: 'Flash', + 3: 'Tungsten (incandescent light)', + 4: 'Flash', + 9: 'Fine weather', + 10: 'Cloudy weather', 11: 'Shade', - 12: 'Daylight Fluorescent', - 13: 'Day White Fluorescent', - 14: 'Cool White Fluorescent', - 15: 'White Fluorescent', - 17: 'Standard Light A', - 18: 'Standard Light B', - 19: 'Standard Light C', + 12: 'Daylight fluorescent (D 5700 - 7100K)', + 13: 'Day white fluorescent (N 4600 - 5400K)', + 14: 'Cool white fluorescent (W 3900 - 4500K)', + 15: 'White fluorescent (WW 3200 - 3700K)', + 17: 'Standard light A', + 18: 'Standard light B', + 19: 'Standard light C', 20: 'D55', 21: 'D65', 22: 'D75', - 255: 'Other'}), + 23: 'D50', + 24: 'ISO studio tungsten', + 255: 'other light source',}), 0x9209: ('Flash', - {0: 'No', - 1: 'Fired', - 5: 'Fired (?)', # no return sensed - 7: 'Fired (!)', # return sensed - 9: 'Fill Fired', - 13: 'Fill Fired (?)', - 15: 'Fill Fired (!)', - 16: 'Off', - 24: 'Auto Off', - 25: 'Auto Fired', - 29: 'Auto Fired (?)', - 31: 'Auto Fired (!)', - 32: 'Not Available'}), + {0: 'Flash did not fire', + 1: 'Flash fired', + 5: 'Strobe return light not detected', + 7: 'Strobe return light detected', + 9: 'Flash fired, compulsory flash mode', + 13: 'Flash fired, compulsory flash mode, return light not detected', + 15: 'Flash fired, compulsory flash mode, return light detected', + 16: 'Flash did not fire, compulsory flash mode', + 24: 'Flash did not fire, auto mode', + 25: 'Flash fired, auto mode', + 29: 'Flash fired, auto mode, return light not detected', + 31: 'Flash fired, auto mode, return light detected', + 32: 'No flash function', + 65: 'Flash fired, red-eye reduction mode', + 69: 'Flash fired, red-eye reduction mode, return light not detected', + 71: 'Flash fired, red-eye reduction mode, return light detected', + 73: 'Flash fired, compulsory flash mode, red-eye reduction mode', + 77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', + 79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected', + 89: 'Flash fired, auto mode, red-eye reduction mode', + 93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode', + 95: 'Flash fired, auto mode, return light detected, red-eye reduction mode'}), 0x920A: ('FocalLength', ), 0x9214: ('SubjectArea', ), 0x927C: ('MakerNote', ), @@ -410,7 +425,10 @@ GPS_TAGS = { 0x0018: ('GPSDestBearing', ), 0x0019: ('GPSDestDistanceRef', ), 0x001A: ('GPSDestDistance', ), + 0x001B: ('GPSProcessingMethod', ), + 0x001C: ('GPSAreaInformation', ), 0x001D: ('GPSDate', ), + 0x001E: ('GPSDifferential', ), } # Ignore these tags when quick processing @@ -1231,10 +1249,17 @@ class IFD_Tag: return self.printable def __repr__(self): - return '(0x%04X) %s=%s @ %d' % (self.tag, + try: + s= '(0x%04X) %s=%s @ %d' % (self.tag, FIELD_TYPES[self.field_type][2], self.printable, self.field_offset) + except: + s= '(%s) %s=%s @ %s' % (str(self.tag), + FIELD_TYPES[self.field_type][2], + self.printable, + str(self.field_offset)) + return s # class that handles an EXIF header class EXIF_header: @@ -1283,7 +1308,11 @@ class EXIF_header: # return pointer to next IFD def next_IFD(self, ifd): entries=self.s2n(ifd, 2) - return self.s2n(ifd+2+12*entries, 4) + next_ifd = self.s2n(ifd+2+12*entries, 4) + if next_ifd == ifd: + return 0 + else: + return next_ifd # return list of IFDs in header def list_IFDs(self): @@ -1348,14 +1377,15 @@ class EXIF_header: # special case: null-terminated ASCII string # XXX investigate # sometimes gets too big to fit in int value - if count != 0 and count < (2**31): - self.file.seek(self.offset + offset) - values = self.file.read(count) - #print values - # Drop any garbage after a null. - values = values.split('\x00', 1)[0] - else: - values = '' + if count != 0: # and count < (2**31): # 2E31 is hardware dependant. --gd + try: + self.file.seek(self.offset + offset) + values = self.file.read(count) + #print values + # Drop any garbage after a null. + values = values.split('\x00', 1)[0] + except OverflowError: + values = '' else: values = [] signed = (field_type in [6, 8, 9, 10]) @@ -1567,7 +1597,8 @@ class EXIF_header: dict=MAKERNOTE_CANON_TAGS) for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001), ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)): - self.canon_decode_tag(self.tags[i[0]].values, i[1]) + if i[0] in self.tags: + self.canon_decode_tag(self.tags[i[0]].values, i[1]) return @@ -1613,26 +1644,124 @@ def process_file(f, stop_tag='UNDEF', details=True, strict=False, debug=False): offset = 0 elif data[0:2] == '\xFF\xD8': # it's a JPEG file + if debug: print "JPEG format recognized data[0:2] == '0xFFD8'." + base = 2 while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM', 'Phot'): + if debug: print "data[2] == 0xxFF data[3]==%x and data[6:10] = %s"%(ord(data[3]),data[6:10]) length = ord(data[4])*256+ord(data[5]) + if debug: print "Length offset is",length f.read(length-8) # fake an EXIF beginning of file + # I don't think this is used. --gd data = '\xFF\x00'+f.read(10) fake_exif = 1 - if data[2] == '\xFF' and data[6:10] == 'Exif': + if base>2: + if debug: print "added to base " + base = base + length + 4 -2 + else: + if debug: print "added to zero " + base = length + 4 + if debug: print "Set segment base to",base + + # Big ugly patch to deal with APP2 (or other) data coming before APP1 + f.seek(0) + data = f.read(base+4000) # in theory, this could be insufficient since 64K is the maximum size--gd + # base = 2 + while 1: + if debug: print "Segment base 0x%X" % base + if data[base:base+2]=='\xFF\xE1': + # APP1 + if debug: print "APP1 at base",hex(base) + if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3])) + if debug: print "Code",data[base+4:base+8] + if data[base+4:base+8] == "Exif": + if debug: print "Decrement base by",2,"to get to pre-segment header (for compatibility with later code)" + base = base-2 + break + if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2 + base=base+ord(data[base+2])*256+ord(data[base+3])+2 + elif data[base:base+2]=='\xFF\xE0': + # APP0 + if debug: print "APP0 at base",hex(base) + if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3])) + if debug: print "Code",data[base+4:base+8] + if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2 + base=base+ord(data[base+2])*256+ord(data[base+3])+2 + elif data[base:base+2]=='\xFF\xE2': + # APP2 + if debug: print "APP2 at base",hex(base) + if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3])) + if debug: print "Code",data[base+4:base+8] + if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2 + base=base+ord(data[base+2])*256+ord(data[base+3])+2 + elif data[base:base+2]=='\xFF\xEE': + # APP14 + if debug: print "APP14 Adobe segment at base",hex(base) + if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3])) + if debug: print "Code",data[base+4:base+8] + if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2 + print "There is useful EXIF-like data here, but we have no parser for it." + base=base+ord(data[base+2])*256+ord(data[base+3])+2 + elif data[base:base+2]=='\xFF\xDB': + if debug: print "JPEG image data at base",hex(base),"No more segments are expected." + # sys.exit(0) + break + elif data[base:base+2]=='\xFF\xD8': + # APP12 + if debug: print "FFD8 segment at base",hex(base) + if debug: print "Got",hex(ord(data[base])), hex(ord(data[base+1])),"and", data[4+base:10+base], "instead." + if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3])) + if debug: print "Code",data[base+4:base+8] + if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2 + base=base+ord(data[base+2])*256+ord(data[base+3])+2 + elif data[base:base+2]=='\xFF\xEC': + # APP12 + if debug: print "APP12 XMP (Ducky) or Pictureinfo segment at base",hex(base) + if debug: print "Got",hex(ord(data[base])), hex(ord(data[base+1])),"and", data[4+base:10+base], "instead." + if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3])) + if debug: print "Code",data[base+4:base+8] + if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2 + print "There is useful EXIF-like data here (quality, comment, copyright), but we have no parser for it." + base=base+ord(data[base+2])*256+ord(data[base+3])+2 + else: + try: + if debug: print "Unexpected/unhandled segment type or file content." + if debug: print "Got",hex(ord(data[base])), hex(ord(data[base+1])),"and", data[4+base:10+base], "instead." + if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2 + except: pass + try: base=base+ord(data[base+2])*256+ord(data[base+3])+2 + except: pass + + f.seek(base+12) + if data[2+base] == '\xFF' and data[6+base:10+base] == 'Exif': # detected EXIF header offset = f.tell() endian = f.read(1) + #HACK TEST: endian = 'M' + elif data[2+base] == '\xFF' and data[6+base:10+base+1] == 'Ducky': + # detected Ducky header. + if debug: print "EXIF-like header (normally 0xFF and code):",hex(ord(data[2+base])) , "and", data[6+base:10+base+1] + offset = f.tell() + endian = f.read(1) + elif data[2+base] == '\xFF' and data[6+base:10+base+1] == 'Adobe': + # detected APP14 (Adobe) + if debug: print "EXIF-like header (normally 0xFF and code):",hex(ord(data[2+base])) , "and", data[6+base:10+base+1] + offset = f.tell() + endian = f.read(1) else: # no EXIF information + if debug: print "No EXIF header expected data[2+base]==0xFF and data[6+base:10+base]===Exif (or Duck)" + if debug: print " but got",hex(ord(data[2+base])) , "and", data[6+base:10+base+1] return {} else: # file format not recognized + if debug: print "file format not recognized" return {} # deal with the EXIF info we found if debug: - print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format' + print "Endian format is ",endian + print {'I': 'Intel', 'M': 'Motorola', '\x01':'Adobe Ducky', 'd':'XMP/Adobe unknown' }[endian], 'format' hdr = EXIF_header(f, endian, offset, fake_exif, strict, debug) ifd_list = hdr.list_IFDs() ctr = 0 |