# HG changeset patch # User Tero Marttila # Date 1244920924 -10800 # Node ID a4f605bd122c856565a6c84ff93bd7238b338e41 # Parent effae6f387490107b0029a285feb7f89b454e52c add support for sub-IFDs, kind of hacky diff -r effae6f38749 -r a4f605bd122c degal/exif.py --- a/degal/exif.py Sat Jun 13 20:59:53 2009 +0300 +++ b/degal/exif.py Sat Jun 13 22:22:04 2009 +0300 @@ -143,6 +143,25 @@ else : return None + def is_subifd (self) : + """ + Tests if this Tag is of a IFDTag type + """ + + return self.tag_data and isinstance(self.tag_data, exif_data.IFDTag) + + @lazy_load + def subifd (self) : + """ + Load the sub-IFD for this tag + """ + + # the tag_dict to use + tag_dict = self.tag_data.ifd_tags or self.ifd.tag_dict + + # construct, return + return self.ifd.exif._load_subifd(self, tag_dict) + def process_values (self, raw_values) : """ Process the given raw values unpacked from the file. @@ -179,7 +198,7 @@ Represents an IFD (Image file directory) region in EXIF data. """ - def __init__ (self, buffer, tag_dict, **buffer_opts) : + def __init__ (self, exif, buffer, tag_dict, **buffer_opts) : """ Access the IFD data from the given bufferable object with given buffer opts. @@ -190,6 +209,7 @@ super(IFD, self).__init__(buffer, **buffer_opts) # store + self.exif = exif self.tag_dict = tag_dict # read header @@ -210,18 +230,31 @@ tag, type, count, data_raw = self.pread_struct(offset, 'HHI4s') # yield the new Tag - yield Tag(self, offset, tag, type, count, data_raw) + yield Tag(self, self.offset + offset, tag, type, count, data_raw) + + def get_tags (self, filter=None) : + """ + Yield a series of tag objects for this IFD and all sub-IFDs. + """ + + for tag in self.tags : + if tag.is_subifd() : + # recurse + for subtag in tag.subifd.get_tags(filter=filter) : + yield subtag + + else : + # normal tag + yield tag class EXIF (Buffer) : """ Represents the EXIF data embedded in some image file in the form of a Region. """ - def __init__ (self, buffer, tags=None, **buffer_opts) : + def __init__ (self, buffer, **buffer_opts) : """ Access the EXIF data from the given bufferable object with the given buffer options. - - `tags`, if given, specifies that only the given named tags should be loaded. """ # init Buffer @@ -241,7 +274,7 @@ while offset : # create and read the IFD, operating on the right sub-buffer - ifd = IFD(self.buf, exif_data.EXIF_TAGS, offset=offset) + ifd = IFD(self, self.buf, exif_data.EXIF_TAGS, offset=offset) # yield it yield ifd @@ -249,11 +282,17 @@ # skip to next offset offset = ifd.next_offset - def iter_all_ifds (self) : + def _load_subifd (self, tag, tag_dict) : """ - Iterate over all of the IFDs contained within this EXIF, or within other IFDs. + Creates and returns a sub-IFD for the given tag. """ - + + # locate it + offset, = self.tag_values_raw(tag) + + # construct the new IFD + return IFD(self, self.buf, tag_dict, offset=offset) + def tag_data_info (self, tag) : """ Calculate the location, format and size of the given tag's data. @@ -329,6 +368,21 @@ # return as comma-separated formatted string, yes return tag.readable_value(values) + + def get_main_tags (self, **opts) : + """ + Get the tags for the main image's IFD as a dict. + """ + + if not self.ifds : + # weird case + raise Exception("No IFD for main image found") + + # the main IFD is always the first one + main_ifd = self.ifds[0] + + # do it + return dict((tag.name, self.tag_value(tag)) for tag in main_ifd.get_tags(**opts)) # mapping from two-byte TIFF byte order marker to struct prefix TIFF_BYTE_ORDER = { @@ -492,6 +546,63 @@ # try and load it return func(file, **opts) +def dump_tag (exif, i, tag, indent=2) : + """ + Dump the given tag + """ + + data_info = exif.tag_data_info(tag) + + if data_info : + data_fmt, data_offset, data_size = data_info + + else : + data_fmt = data_offset = data_size = None + + print "%sTag:%d offset=%#04x(%#08x), tag=%d/%s, type=%d/%s, count=%d, fmt=%s, offset=%#04x, size=%s, is_subifd=%s:" % ( + '\t'*indent, + i, + tag.offset, tag.offset + exif.offset, + tag.tag, tag.name or '???', + tag.type, tag.type_name if tag.type_data else '???', + tag.count, + data_fmt, data_offset, data_size, + tag.is_subifd(), + ) + + if tag.is_subifd() : + # recurse + dump_ifd(exif, 0, tag.subifd, indent + 1) + + else : + # dump each value + values = exif.tag_values(tag) + + for i, value in enumerate(values) : + print "%s\t%02d: %.120r" % ('\t'*indent, i, value) + + # and then the readable one + print "%s\t-> %.120s" % ('\t'*indent, tag.readable_value(values), ) + + +def dump_ifd (exif, i, ifd, indent=1) : + """ + Dump the given IFD, recursively + """ + + print "%sIFD:%d offset=%#04x(%#08x), count=%d, next=%d:" % ( + '\t'*indent, + i, + ifd.offset, ifd.offset + exif.offset, + ifd.count, + ifd.next_offset + ) + + for i, tag in enumerate(ifd.tags) : + # dump + dump_tag(exif, i, tag, indent + 1) + + def dump_exif (exif) : """ Dump all tags from the given EXIF object to stdout @@ -500,39 +611,19 @@ print "EXIF offset=%#08x, size=%d:" % (exif.offset, exif.size) for i, ifd in enumerate(exif.ifds) : - print "\tIFD:%d offset=%#04x(%#08x), count=%d, next=%d:" % ( - i, - ifd.offset, ifd.offset + exif.offset, - ifd.count, - ifd.next_offset - ) - - for i, tag in enumerate(ifd.tags) : - data_info = exif.tag_data_info(tag) - - if data_info : - data_fmt, data_offset, data_size = data_info - - else : - data_fmt = data_offset = data_size = None + # dump + dump_ifd(exif, i, ifd) - print "\t\tTag:%d offset=%#04x(%#08x), tag=%d/%s, type=%d/%s, count=%d, fmt=%s, offset=%#04x, size=%s:" % ( - i, - tag.offset, tag.offset + exif.offset, - tag.tag, tag.name or '???', - tag.type, tag.type_name if tag.type_data else '???', - tag.count, - data_fmt, data_offset, data_size, - ) - values = exif.tag_values(tag) - - for i, value in enumerate(values) : - print "\t\t\t%02d: %r" % (i, value) +def list_tags (exif) : + """ + Print a neat listing of tags to stdout + """ - print "\t\t\t-> %s" % (tag.readable_value(values), ) + for k, v in exif.get_main_tags().iteritems() : + print "%30s: %s" % (k, v) -def main (path, quiet=False) : +def main (path, debug=False) : """ Load and dump EXIF data from the given path """ @@ -543,15 +634,19 @@ if not exif : raise Exception("No EXIF data found") - if not quiet : + if debug : # dump it print "%s: " % path print dump_exif(exif) + + else : + # list them + list_tags(exif) if __name__ == '__main__' : from sys import argv - main(argv[1], '-q' in argv) + main(argv[1], '-d' in argv) diff -r effae6f38749 -r a4f605bd122c degal/exif_data.py --- a/degal/exif_data.py Sat Jun 13 20:59:53 2009 +0300 +++ b/degal/exif_data.py Sat Jun 13 22:22:04 2009 +0300 @@ -151,6 +151,21 @@ return self.values_func(values) +class IFDTag (Tag) : + """ + A tag that references another IFD + """ + + def __init__ (self, name, ifd_tags=None) : + """ + A tag that points to another IFD block. `ifd_tags`, if given, lists the tags for that block, otherwise, + the same tags as for the current block are used. + """ + + super(IFDTag, self).__init__(name) + + self.ifd_tags = ifd_tags + USER_COMMENT_CHARSETS = { 'ASCII': ('ascii', 'replace' ), 'JIS': ('jis', 'error' ), @@ -191,6 +206,48 @@ # XXX: or does it? # SUB_IFD_MAGIC signifies that this IFD points to # otherwise, the value is left as-is. +# interoperability tags +INTR_TAGS = { + 0x0001: Tag('InteroperabilityIndex'), + 0x0002: Tag('InteroperabilityVersion'), + 0x1000: Tag('RelatedImageFileFormat'), + 0x1001: Tag('RelatedImageWidth'), + 0x1002: Tag('RelatedImageLength'), + } + +# GPS tags (not used yet, haven't seen camera with GPS) +GPS_TAGS = { + 0x0000: Tag('GPSVersionID'), + 0x0001: Tag('GPSLatitudeRef'), + 0x0002: Tag('GPSLatitude'), + 0x0003: Tag('GPSLongitudeRef'), + 0x0004: Tag('GPSLongitude'), + 0x0005: Tag('GPSAltitudeRef'), + 0x0006: Tag('GPSAltitude'), + 0x0007: Tag('GPSTimeStamp'), + 0x0008: Tag('GPSSatellites'), + 0x0009: Tag('GPSStatus'), + 0x000A: Tag('GPSMeasureMode'), + 0x000B: Tag('GPSDOP'), + 0x000C: Tag('GPSSpeedRef'), + 0x000D: Tag('GPSSpeed'), + 0x000E: Tag('GPSTrackRef'), + 0x000F: Tag('GPSTrack'), + 0x0010: Tag('GPSImgDirectionRef'), + 0x0011: Tag('GPSImgDirection'), + 0x0012: Tag('GPSMapDatum'), + 0x0013: Tag('GPSDestLatitudeRef'), + 0x0014: Tag('GPSDestLatitude'), + 0x0015: Tag('GPSDestLongitudeRef'), + 0x0016: Tag('GPSDestLongitude'), + 0x0017: Tag('GPSDestBearingRef'), + 0x0018: Tag('GPSDestBearing'), + 0x0019: Tag('GPSDestDistanceRef'), + 0x001A: Tag('GPSDestDistance'), + 0x001D: Tag('GPSDate'), + } + + EXIF_TAGS = { 0x0100: Tag('ImageWidth'), 0x0101: Tag('ImageLength'), @@ -280,7 +337,7 @@ 0x829A: Tag('ExposureTime'), 0x829D: Tag('FNumber'), 0x83BB: Tag('IPTC/NAA'), - 0x8769: Tag('ExifOffset'), + 0x8769: IFDTag('ExifOffset', None), 0x8773: Tag('InterColorProfile'), 0x8822: TagDict('ExposureProgram', {0: 'Unidentified', @@ -293,7 +350,7 @@ 7: 'Portrait Mode', 8: 'Landscape Mode'}), 0x8824: Tag('SpectralSensitivity'), - 0x8825: Tag('GPSInfo'), + 0x8825: IFDTag('GPSInfo', GPS_TAGS), 0x8827: Tag('ISOSpeedRatings'), 0x8828: Tag('OECF'), 0x9000: Tag('ExifVersion'), @@ -376,7 +433,7 @@ 65535: 'Uncalibrated'}), 0xA002: Tag('ExifImageWidth'), 0xA003: Tag('ExifImageLength'), - 0xA005: Tag('InteroperabilityOffset'), + 0xA005: IFDTag('InteroperabilityOffset', INTR_TAGS), 0xA20B: Tag('FlashEnergy'), # 0x920B in TIFF/EP 0xA20C: Tag('SpatialFrequencyResponse'), # 0x920C 0xA20E: Tag('FocalPlaneXResolution'), # 0x920E @@ -441,47 +498,6 @@ 0xEA1C: ('Padding', None), } -# interoperability tags -INTR_TAGS = { - 0x0001: Tag('InteroperabilityIndex'), - 0x0002: Tag('InteroperabilityVersion'), - 0x1000: Tag('RelatedImageFileFormat'), - 0x1001: Tag('RelatedImageWidth'), - 0x1002: Tag('RelatedImageLength'), - } - -# GPS tags (not used yet, haven't seen camera with GPS) -GPS_TAGS = { - 0x0000: Tag('GPSVersionID'), - 0x0001: Tag('GPSLatitudeRef'), - 0x0002: Tag('GPSLatitude'), - 0x0003: Tag('GPSLongitudeRef'), - 0x0004: Tag('GPSLongitude'), - 0x0005: Tag('GPSAltitudeRef'), - 0x0006: Tag('GPSAltitude'), - 0x0007: Tag('GPSTimeStamp'), - 0x0008: Tag('GPSSatellites'), - 0x0009: Tag('GPSStatus'), - 0x000A: Tag('GPSMeasureMode'), - 0x000B: Tag('GPSDOP'), - 0x000C: Tag('GPSSpeedRef'), - 0x000D: Tag('GPSSpeed'), - 0x000E: Tag('GPSTrackRef'), - 0x000F: Tag('GPSTrack'), - 0x0010: Tag('GPSImgDirectionRef'), - 0x0011: Tag('GPSImgDirection'), - 0x0012: Tag('GPSMapDatum'), - 0x0013: Tag('GPSDestLatitudeRef'), - 0x0014: Tag('GPSDestLatitude'), - 0x0015: Tag('GPSDestLongitudeRef'), - 0x0016: Tag('GPSDestLongitude'), - 0x0017: Tag('GPSDestBearingRef'), - 0x0018: Tag('GPSDestBearing'), - 0x0019: Tag('GPSDestDistanceRef'), - 0x001A: Tag('GPSDestDistance'), - 0x001D: Tag('GPSDate'), - } - # http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp def nikon_ev_bias (seq) : """