add support for sub-IFDs, kind of hacky new-exif
authorTero Marttila <terom@fixme.fi>
Sat, 13 Jun 2009 22:22:04 +0300
branchnew-exif
changeset 106 a4f605bd122c
parent 105 effae6f38749
child 107 2e2ef5c99985
add support for sub-IFDs, kind of hacky
degal/exif.py
degal/exif_data.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)
 
--- 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) :
     """