# HG changeset patch # User Tero Marttila # Date 1244985030 -10800 # Node ID f74d8cf678ce0b78d00bdff2c98aae49456560bb # Parent 2e2ef5c9998572e6aeda482ccde1c08e30446b1f relocate the new exif module to lib/, as it's not really part of degal as such diff -r 2e2ef5c99985 -r f74d8cf678ce degal/exif.py --- a/degal/exif.py Sun Jun 14 16:09:04 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,669 +0,0 @@ -""" - A custom EXIF parsing module, aimed at high performance. -""" - -import struct, mmap, os - -from utils import lazy_load, lazy_load_iter - -def read_struct (file, fmt) : - """ - Utility function to read data from the a file using struct - """ - - # length of data - fmt_size = struct.calcsize(fmt) - - # get data - file_data = file.read(fmt_size) - - # unpack single item, this should raise an error if file_data is too short - return struct.unpack(fmt, file_data) - -class Buffer (object) : - """ - Wraps a buffer object (anything that supports the python buffer protocol) for read-only access. - - Includes an offset for relative values, and an endianess for reading binary data. - """ - - def __init__ (self, obj, offset=None, size=None, struct_prefix='=') : - """ - Create a new Buffer object with a new underlying buffer, created from the given object, offset and size. - - The endiannes is given in the form of a struct-module prefix, which should be one of '<' or '>'. - Standard size/alignment are assumed. - """ - - # store - self.buf = buffer(obj, *(arg for arg in (offset, size) if arg is not None)) - self.offset = offset - self.size = size - self.prefix = struct_prefix - - def subregion (self, offset, length=None) : - """ - Create a new sub-Buffer referencing a view of this buffer, at the given offset, and with the given - length, if any, and the same struct_prefix. - """ - - return Buffer(self.buf, offset, length, struct_prefix=self.prefix) - - def pread (self, offset, length) : - """ - Read a random-access region of raw data - """ - - return self.buf[offset:offset + length] - - def pread_struct (self, offset, fmt) : - """ - Read structured data using the given struct format from the given offset. - """ - - return struct.unpack_from(self.prefix + fmt, self.buf, offset=offset) - - def pread_item (self, offset, fmt) : - """ - Read a single item of structured data from the given offset. - """ - - value, = self.pread_struct(offset, fmt) - - return value - - def iter_offsets (self, count, size, offset=0) : - """ - Yield a series of offsets for `count` items of `size` bytes, beginning at `offset`. - """ - - return xrange(offset, offset + count * size, size) - - def item_size (self, fmt) : - """ - Returns the size in bytes of the given item format - """ - - return struct.calcsize(self.prefix + fmt) - - def unpack_item (self, fmt, data) : - """ - Unpacks a single item from the given data - """ - - value, = struct.unpack(self.prefix + fmt, data) - - return value - -def mmap_buffer (file, size) : - """ - Create and return a new read-only mmap'd region - """ - - return mmap.mmap(file.fileno(), size, access=mmap.ACCESS_READ) - -import exif_data - -class Tag (object) : - """ - Represents a single Tag in an IFD - """ - - def __init__ (self, ifd, offset, tag, type, count, data_raw) : - """ - Build a Tag with the given binary items from the IFD entry - """ - - self.ifd = ifd - self.offset = offset - self.tag = tag - self.type = type - self.count = count - self.data_raw = data_raw - - # lookup the type for this tag - self.type_data = exif_data.FIELD_TYPES.get(type) - - # unpack it - if self.type_data : - self.type_format, self.type_name, self.type_func = self.type_data - - # lookup the tag data for this tag - self.tag_data = self.ifd.tag_dict.get(tag) - - @property - def name (self) : - """ - Lookup the name of this tag via its code, returns None if unknown. - """ - - if self.tag_data : - return self.tag_data.name - - 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. - """ - - if self.type_data and self.type_func : - # use the filter func - return self.type_func(raw_values) - - else : - # nada, just leave them - return raw_values - - def readable_value (self, values) : - """ - Convert the given values for this tag into a human-readable string. - - Returns the comma-separated values by default. - """ - - if self.tag_data : - # map it - return self.tag_data.map_values(values) - - else : - # default value-mapping - return ", ".join(str(value) for value in values) - -# size of an IFD entry in bytes -IFD_ENTRY_SIZE = 12 - -class IFD (Buffer) : - """ - Represents an IFD (Image file directory) region in EXIF data. - """ - - def __init__ (self, exif, buffer, tag_dict, **buffer_opts) : - """ - Access the IFD data from the given bufferable object with given buffer opts. - - This will read the `count` and `next_offset` values. - """ - - # init - super(IFD, self).__init__(buffer, **buffer_opts) - - # store - self.exif = exif - self.tag_dict = tag_dict - - # read header - self.count = self.pread_item(0, 'H') - - # read next-offset - self.next_offset = self.pread_item(0x02 + self.count * IFD_ENTRY_SIZE, 'I') - - @lazy_load_iter - def tags (self) : - """ - Iterate over all the Tag objects in this IFD - """ - - # read each tag - for offset in self.iter_offsets(self.count, IFD_ENTRY_SIZE, 0x02) : - # read the tag data - tag, type, count, data_raw = self.pread_struct(offset, 'HHI4s') - - # yield the new Tag - 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, **buffer_opts) : - """ - Access the EXIF data from the given bufferable object with the given buffer options. - """ - - # init Buffer - super(EXIF, self).__init__(buffer, **buffer_opts) - - # store - self.buffer = buffer - - @lazy_load_iter - def ifds (self) : - """ - Iterate over the primary IFDs in this EXIF. - """ - - # starting offset - offset = self.pread_item(0x04, 'I') - - while offset : - # create and read the IFD, operating on the right sub-buffer - ifd = IFD(self, self.buf, exif_data.EXIF_TAGS, offset=offset) - - # yield it - yield ifd - - # skip to next offset - offset = ifd.next_offset - - def _load_subifd (self, tag, tag_dict) : - """ - 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. - - Returns a (fmt, offset, size) tuple. - """ - # unknown tag? - if not tag.type_data : - return None - - # data format - if len(tag.type_format) == 1 : - # let struct handle the count - fmt = "%d%s" % (tag.count, tag.type_format) - - else : - # handle the count ourselves - fmt = tag.type_format * tag.count - - # size of the data - size = self.item_size(fmt) - - # inline or external? - if size > 0x04 : - # point at the external data - offset = self.unpack_item('I', tag.data_raw) - - else : - # point at the inline data - offset = tag.offset + 0x08 - - return fmt, offset, size - - def tag_values_raw (self, tag) : - """ - Get the raw values for the given tag as a tuple. - - Returns None if the tag could not be recognized. - """ - - # find the data - data_info = self.tag_data_info(tag) - - # not found? - if not data_info : - return None - - # unpack - data_fmt, data_offset, data_size = data_info - - # read values - return self.pread_struct(data_offset, data_fmt) - - def tag_values (self, tag) : - """ - Gets the processed values for the given tag as a list. - """ - - # read + process - return tag.process_values(self.tag_values_raw(tag)) - - def tag_value (self, tag) : - """ - Return the human-readable string value for the given tag. - """ - - # load the raw values - values = self.tag_values(tag) - - # unknown? - if not values : - return "" - - # 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 = { - 'II': '<', - 'MM': '>', -} - -# "An arbitrary but carefully chosen number (42) that further identifies the file as a TIFF file" -TIFF_BYTEORDER_MAGIC = 42 - -def tiff_load (file, length=0, **opts) : - """ - Load the Exif/TIFF data from the given file at its current position with optional length, using exif_load. - """ - - # all Exif data offsets are relative to the beginning of this TIFF header - offset = file.tell() - - # mmap the region for the EXIF data - buffer = mmap_buffer(file, length) - - # read byte-order header - byte_order = file.read(2) - - # map to struct prefix - struct_prefix = TIFF_BYTE_ORDER[byte_order] - - # validate - check_value, = read_struct(file, struct_prefix + 'H') - - if check_value != TIFF_BYTEORDER_MAGIC : - raise Exception("Invalid byte-order for TIFF: %2c -> %d" % (byte_order, check_value)) - - # build and return the EXIF object with the correct offset/size from the mmap region - return EXIF(buffer, offset=offset, size=length, **opts) - -# the JPEG markers that don't have any data -JPEG_NOSIZE_MARKERS = (0xD8, 0xD9) - -# the first marker in a JPEG File -JPEG_START_MARKER = 0xD8 - -# the JPEG APP1 marker used for EXIF -JPEG_EXIF_MARKER = 0xE1 - -# the JPEG APP1 Exif header -JPEG_EXIF_HEADER = "Exif\x00\x00" - -def jpeg_markers (file) : - """ - Iterate over the JPEG markers in the given file, yielding (type_byte, size) tuples. - - The size fields will be 0 for markers with no data. The file will be positioned at the beginning of the data - region, and may be seek'd around if needed. - - XXX: find a real implementation of this somewhere? - """ - - while True : - # read type - marker_byte, marker_type = read_struct(file, '!BB') - - # validate - if marker_byte != 0xff : - raise Exception("Not a JPEG marker: %x%x" % (marker_byte, marker_type)) - - # special cases for no data - if marker_type in JPEG_NOSIZE_MARKERS : - size = 0 - - else : - # read size field - size, = read_struct(file, '!H') - - # validate - if size < 0x02 : - raise Exception("Invalid size for marker %x%x: %x" % (marker_byte, marker_type, size)) - - else : - # do not count the size field itself - size = size - 2 - - # ok, data is at current position - offset = file.tell() - - # yield - yield marker_type, size - - # absolute seek to next marker - file.seek(offset + size) - -def jpeg_find_exif (file) : - """ - Find the Exif/TIFF section in the given JPEG file. - - If found, the file will be seek'd to the start of the Exif/TIFF header, and the size of the Exif/TIFF data will - be returned. - - Returns None if no EXIF section was found. - """ - - for count, (marker, size) in enumerate(jpeg_markers(file)) : - # verify that it's a JPEG file - if count == 0 : - # must start with the right marker - if marker != JPEG_START_MARKER : - raise Exception("JPEG file must start with 0xFF%02x marker" % (marker, )) - - # look for APP1 marker (0xE1) with EXIF signature - elif marker == JPEG_EXIF_MARKER and file.read(len(JPEG_EXIF_HEADER)) == JPEG_EXIF_HEADER: - # skipped the initial Exif marker signature - return size - len(JPEG_EXIF_HEADER) - - # nothing - return None - -def jpeg_load (file, **opts) : - """ - Loads the embedded Exif TIFF data from the given JPEG file using tiff_load. - - Returns None if no EXIF data could be found. - """ - - # look for the right section - size = jpeg_find_exif(file) - - # not found? - if not size : - # nothing - return - - else : - # load it as TIFF data - return tiff_load(file, size, **opts) - -def load_path (path, **opts) : - """ - Loads an EXIF object from the given filesystem path. - - Returns None if it could not be parsed. - """ - - # file extension - root, fext = os.path.splitext(path) - - # map - func = { - '.jpeg': jpeg_load, - '.jpg': jpeg_load, - '.tiff': tiff_load, # XXX: untested - }.get(fext.lower()) - - # not recognized? - if not func : - # XXX: sniff the file - return None - - # open it - file = open(path, 'rb') - - # 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 - """ - - print "EXIF offset=%#08x, size=%d:" % (exif.offset, exif.size) - - for i, ifd in enumerate(exif.ifds) : - # dump - dump_ifd(exif, i, ifd) - - -def list_tags (exif) : - """ - Print a neat listing of tags to stdout - """ - - for k, v in exif.get_main_tags().iteritems() : - print "%30s: %s" % (k, v) - -def main_path (path, dump) : - # dump path - print "%s: " % path - - # try and load it - exif = load_path(path) - - if not exif : - raise Exception("No EXIF data found") - - if dump : - # dump everything - dump_exif(exif) - - else : - # list them - list_tags(exif) - - -def main (paths, dump=False) : - """ - Load and dump EXIF data from the given path - """ - - # handle each one - for path in paths : - main_path(path, dump=dump) - -if __name__ == '__main__' : - import getopt - from sys import argv - - # defaults - dump = False - - # parse args - opts, args = getopt.getopt(argv[1:], "d", ["dump"]) - - for opt, val in opts : - if opt in ('-d', "--dump") : - dump = True - - main(args, dump=dump) - diff -r 2e2ef5c99985 -r f74d8cf678ce degal/exif_data.py --- a/degal/exif_data.py Sun Jun 14 16:09:04 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1304 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - EXIF file format data, including tag names, types, etc. - - Most of this was copied with modifications from EXIFpy: - # Library to extract EXIF information from digital camera image files - # http://sourceforge.net/projects/exif-py/ - # - # VERSION 1.1.0 - # - # Copyright (c) 2002-2007 Gene Cash All rights reserved - # Copyright (c) 2007-2008 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 - # are met: - # - # 1. Redistributions of source code must retain the above copyright - # notice, this list of conditions and the following disclaimer. - # - # 2. Redistributions in binary form must reproduce the above - # copyright notice, this list of conditions and the following - # disclaimer in the documentation and/or other materials provided - # with the distribution. - # - # 3. Neither the name of the authors nor the names of its contributors - # may be used to endorse or promote products derived from this - # software without specific prior written permission. - # - # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" - -import decimal, itertools - -def filter_ascii (values) : - """ - Default post-filter for ASCII values. - - This takes a single item of string data, splits it up into strings by ASCII-NUL. - - These sub-strings are then decoded into unicode as ASCII, and stripped. - """ - - return [string.decode('ascii', 'replace').rstrip() for string in values[0].split('\x00') if string] - -def build_ratio (num, denom) : - """ - Builds a Decimal ratio out of the given numerator and denominator - """ - - # XXX: this may be slow - return decimal.Decimal(num) / decimal.Decimal(denom) - -def filter_ratio (values) : - """ - Default post-filter for Ratio values. - - This takes the pairs of numerator/denominator values and builds Decimals out of them - """ - - return [build_ratio(values[i], values[i + 1]) for i in xrange(0, len(values), 2)] - - -# IFD Tag type information, indexed by code -# { type_code: (type_fmt, name, filter_func) } -# -# type_fmt's that are one char will be prefixed with the count for use with struct.unpack, those with more chars will -# be repeated as many times for use with struct.unpack. -FIELD_TYPES = { -# 0x0000: (None, 'Proprietary' ), # ??? no such type - 0x0001: ('B', 'Byte', None ), - 0x0002: ('s', 'ASCII', filter_ascii ), - 0x0003: ('H', 'Short', None ), - 0x0004: ('L', 'Long', None ), - 0x0005: ('LL', 'Ratio', filter_ratio ), - 0x0006: ('b', 'Signed Byte', None ), - 0x0007: ('s', 'Undefined', None ), - 0x0008: ('h', 'Signed Short', None ), - 0x0009: ('l', 'Signed Long', None ), - 0x000A: ('ll', 'Signed Ratio', filter_ratio ), -} - -# magic value to indicate sub-IFDs -SUB_IFD_MAGIC = object() - - -class Tag (object) : - """ - Represents an Exif Tag - """ - - def __init__ (self, name) : - """ - Build Exif tag with given name, and optional external values-filter function. - """ - - self.name = name - - def map_values (self, values) : - """ - Map the given tag value to a printable string using the given value spec. - """ - - # default value-mapping - return ", ".join(str(value) for value in values) - -class TagDict (Tag) : - """ - A tag with a dict mapping values to names - """ - - def __init__ (self, name, values_dict) : - super(TagDict, self).__init__(name) - - self.values_dict = values_dict - - def map_values (self, values) : - """ - Map the values through our dict, defaulting to the repr. - """ - - return ", ".join(self.values_dict.get(value, repr(value)) for value in values) - -class TagFunc (Tag) : - """ - A tag with a simple function mapping values to names - """ - - def __init__ (self, name, values_func) : - super(TagFunc, self).__init__(name) - - self.values_func = values_func - - def map_values (self, values) : - """ - Map the values through our func - """ - - 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' ), - - # XXX: WTF? What kind of charset is 'Unicode' supposed to be? - # UTF-16? Little-endian? Big-endian? - # Confusing reigns: http://www.cpanforum.com/threads/7329 - 'UNICODE': ('utf16', 'error' ), -} - - -def decode_UserComment (values) : - """ - A UserComment field starts with an eight-byte encoding designator. - """ - - # single binary string - value, = values - - # split up - charset, comment_raw = value[:8], value[8:] - - # strip NILs - charset = charset.rstrip('\x00') - - # map - encoding, replace = USER_COMMENT_CHARSETS.get(charset, ('ascii', 'replace')) - - # decode - return [comment_raw.decode(encoding, replace)] - -# Mappings of Exif tag codes to name and decoding information. -# { tag : (name, value_dict/value_func/None/SUB_IFD_MAGIC) } -# -# name is the official Exif tag name -# value_dict is a { value: value_name } mapping for human-readable values -# value_func is a `(values) -> values` mapping function which *overrides* the tag's type_func. -# 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'), - 0x0102: Tag('BitsPerSample'), - 0x0103: TagDict('Compression', - {1: 'Uncompressed', - 2: 'CCITT 1D', - 3: 'T4/Group 3 Fax', - 4: 'T6/Group 4 Fax', - 5: 'LZW', - 6: 'JPEG (old-style)', - 7: 'JPEG', - 8: 'Adobe Deflate', - 9: 'JBIG B&W', - 10: 'JBIG Color', - 32766: 'Next', - 32769: 'Epson ERF Compressed', - 32771: 'CCIRLEW', - 32773: 'PackBits', - 32809: 'Thunderscan', - 32895: 'IT8CTPAD', - 32896: 'IT8LW', - 32897: 'IT8MP', - 32898: 'IT8BL', - 32908: 'PixarFilm', - 32909: 'PixarLog', - 32946: 'Deflate', - 32947: 'DCS', - 34661: 'JBIG', - 34676: 'SGILog', - 34677: 'SGILog24', - 34712: 'JPEG 2000', - 34713: 'Nikon NEF Compressed', - 65000: 'Kodak DCR Compressed', - 65535: 'Pentax PEF Compressed'}), - 0x0106: Tag('PhotometricInterpretation'), - 0x0107: Tag('Thresholding'), - 0x010A: Tag('FillOrder'), - 0x010D: Tag('DocumentName'), - 0x010E: Tag('ImageDescription'), - 0x010F: Tag('Make'), - 0x0110: Tag('Model'), - 0x0111: Tag('StripOffsets'), - 0x0112: TagDict('Orientation', - {1: 'Horizontal (normal)', - 2: 'Mirrored horizontal', - 3: 'Rotated 180', - 4: 'Mirrored vertical', - 5: 'Mirrored horizontal then rotated 90 CCW', - 6: 'Rotated 90 CW', - 7: 'Mirrored horizontal then rotated 90 CW', - 8: 'Rotated 90 CCW'}), - 0x0115: Tag('SamplesPerPixel'), - 0x0116: Tag('RowsPerStrip'), - 0x0117: Tag('StripByteCounts'), - 0x011A: Tag('XResolution'), - 0x011B: Tag('YResolution'), - 0x011C: Tag('PlanarConfiguration'), - 0x011D: Tag('PageName'), - 0x0128: TagDict('ResolutionUnit', - {1: 'Not Absolute', - 2: 'Pixels/Inch', - 3: 'Pixels/Centimeter'}), - 0x012D: Tag('TransferFunction'), - 0x0131: Tag('Software'), - 0x0132: Tag('DateTime'), - 0x013B: Tag('Artist'), - 0x013E: Tag('WhitePoint'), - 0x013F: Tag('PrimaryChromaticities'), - 0x0156: Tag('TransferRange'), - 0x0200: Tag('JPEGProc'), - 0x0201: Tag('JPEGInterchangeFormat'), - 0x0202: Tag('JPEGInterchangeFormatLength'), - 0x0211: Tag('YCbCrCoefficients'), - 0x0212: Tag('YCbCrSubSampling'), - 0x0213: TagDict('YCbCrPositioning', - {1: 'Centered', - 2: 'Co-sited'}), - 0x0214: Tag('ReferenceBlackWhite'), - - 0x4746: Tag('Rating'), - - 0x828D: Tag('CFARepeatPatternDim'), - 0x828E: Tag('CFAPattern'), - 0x828F: Tag('BatteryLevel'), - 0x8298: Tag('Copyright'), - 0x829A: Tag('ExposureTime'), - 0x829D: Tag('FNumber'), - 0x83BB: Tag('IPTC/NAA'), - 0x8769: IFDTag('ExifOffset', None), - 0x8773: Tag('InterColorProfile'), - 0x8822: TagDict('ExposureProgram', - {0: 'Unidentified', - 1: 'Manual', - 2: 'Program Normal', - 3: 'Aperture Priority', - 4: 'Shutter Priority', - 5: 'Program Creative', - 6: 'Program Action', - 7: 'Portrait Mode', - 8: 'Landscape Mode'}), - 0x8824: Tag('SpectralSensitivity'), - 0x8825: IFDTag('GPSInfo', GPS_TAGS), - 0x8827: Tag('ISOSpeedRatings'), - 0x8828: Tag('OECF'), - 0x9000: Tag('ExifVersion'), - 0x9003: Tag('DateTimeOriginal'), - 0x9004: Tag('DateTimeDigitized'), - 0x9101: TagDict('ComponentsConfiguration', - {0: '', - 1: 'Y', - 2: 'Cb', - 3: 'Cr', - 4: 'Red', - 5: 'Green', - 6: 'Blue'}), - 0x9102: Tag('CompressedBitsPerPixel'), - 0x9201: Tag('ShutterSpeedValue'), - 0x9202: Tag('ApertureValue'), - 0x9203: Tag('BrightnessValue'), - 0x9204: Tag('ExposureBiasValue'), - 0x9205: Tag('MaxApertureValue'), - 0x9206: Tag('SubjectDistance'), - 0x9207: TagDict('MeteringMode', - {0: 'Unidentified', - 1: 'Average', - 2: 'CenterWeightedAverage', - 3: 'Spot', - 4: 'MultiSpot', - 5: 'Pattern'}), - 0x9208: TagDict('LightSource', - {0: 'Unknown', - 1: 'Daylight', - 2: 'Fluorescent', - 3: 'Tungsten', - 9: 'Fine Weather', - 10: 'Flash', - 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', - 20: 'D55', - 21: 'D65', - 22: 'D75', - 255: 'Other'}), - 0x9209: TagDict('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'}), - 0x920A: Tag('FocalLength'), - 0x9214: Tag('SubjectArea'), - 0x927C: Tag('MakerNote'), - 0x9286: TagFunc('UserComment', decode_UserComment), - 0x9290: Tag('SubSecTime'), - 0x9291: Tag('SubSecTimeOriginal'), - 0x9292: Tag('SubSecTimeDigitized'), - - # used by Windows Explorer - 0x9C9B: Tag('XPTitle'), - 0x9C9C: Tag('XPComment'), - 0x9C9D: Tag('XPAuthor'), #(ignored by Windows Explorer if Artist exists) - 0x9C9E: Tag('XPKeywords'), - 0x9C9F: Tag('XPSubject'), - - 0xA000: Tag('FlashPixVersion'), - 0xA001: TagDict('ColorSpace', - {1: 'sRGB', - 2: 'Adobe RGB', - 65535: 'Uncalibrated'}), - 0xA002: Tag('ExifImageWidth'), - 0xA003: Tag('ExifImageLength'), - 0xA005: IFDTag('InteroperabilityOffset', INTR_TAGS), - 0xA20B: Tag('FlashEnergy'), # 0x920B in TIFF/EP - 0xA20C: Tag('SpatialFrequencyResponse'), # 0x920C - 0xA20E: Tag('FocalPlaneXResolution'), # 0x920E - 0xA20F: Tag('FocalPlaneYResolution'), # 0x920F - 0xA210: Tag('FocalPlaneResolutionUnit'), # 0x9210 - 0xA214: Tag('SubjectLocation'), # 0x9214 - 0xA215: Tag('ExposureIndex'), # 0x9215 - 0xA217: TagDict('SensingMethod', # 0x9217 - {1: 'Not defined', - 2: 'One-chip color area', - 3: 'Two-chip color area', - 4: 'Three-chip color area', - 5: 'Color sequential area', - 7: 'Trilinear', - 8: 'Color sequential linear'}), - 0xA300: TagDict('FileSource', - {1: 'Film Scanner', - 2: 'Reflection Print Scanner', - 3: 'Digital Camera'}), - 0xA301: TagDict('SceneType', - {1: 'Directly Photographed'}), - 0xA302: Tag('CVAPattern'), - 0xA401: TagDict('CustomRendered', - {0: 'Normal', - 1: 'Custom'}), - 0xA402: TagDict('ExposureMode', - {0: 'Auto Exposure', - 1: 'Manual Exposure', - 2: 'Auto Bracket'}), - 0xA403: TagDict('WhiteBalance', - {0: 'Auto', - 1: 'Manual'}), - 0xA404: Tag('DigitalZoomRatio'), - 0xA405: ('FocalLengthIn35mmFilm', None), - 0xA406: TagDict('SceneCaptureType', - {0: 'Standard', - 1: 'Landscape', - 2: 'Portrait', - 3: 'Night)'}), - 0xA407: TagDict('GainControl', - {0: 'None', - 1: 'Low gain up', - 2: 'High gain up', - 3: 'Low gain down', - 4: 'High gain down'}), - 0xA408: TagDict('Contrast', - {0: 'Normal', - 1: 'Soft', - 2: 'Hard'}), - 0xA409: TagDict('Saturation', - {0: 'Normal', - 1: 'Soft', - 2: 'Hard'}), - 0xA40A: TagDict('Sharpness', - {0: 'Normal', - 1: 'Soft', - 2: 'Hard'}), - 0xA40B: Tag('DeviceSettingDescription'), - 0xA40C: Tag('SubjectDistanceRange'), - 0xA500: Tag('Gamma'), - 0xC4A5: Tag('PrintIM'), - 0xEA1C: ('Padding', None), - } - -# http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp -def nikon_ev_bias (seq) : - """ - # First digit seems to be in steps of 1/6 EV. - # Does the third value mean the step size? It is usually 6, - # but it is 12 for the ExposureDifference. - """ - - # check for an error condition that could cause a crash. - # this only happens if something has gone really wrong in - # reading the Nikon MakerNote. - if len(seq) < 4 : - return "" - - if seq == [252, 1, 6, 0]: - return "-2/3 EV" - - if seq == [253, 1, 6, 0]: - return "-1/2 EV" - - if seq == [254, 1, 6, 0]: - return "-1/3 EV" - - if seq == [0, 1, 6, 0]: - return "0 EV" - - if seq == [2, 1, 6, 0]: - return "+1/3 EV" - - if seq == [3, 1, 6, 0]: - return "+1/2 EV" - - if seq == [4, 1, 6, 0]: - return "+2/3 EV" - - # handle combinations not in the table. - a = seq[0] - - # causes headaches for the +/- logic, so special case it. - if a == 0: - return "0 EV" - - if a > 127: - a = 256 - a - ret_str = "-" - else: - ret_str = "+" - - b = seq[2] # assume third value means the step size - - whole = a / b - - a = a % b - - if whole != 0 : - ret_str = ret_str + str(whole) + " " - - if a == 0 : - ret_str = ret_str + "EV" - else : - r = Ratio(a, b) - ret_str = ret_str + r.__repr__() + " EV" - - return ret_str - -# Nikon E99x MakerNote Tags -MAKERNOTE_NIKON_NEWER_TAGS={ - 0x0001: Tag('MakernoteVersion'), # Sometimes binary - 0x0002: Tag('ISOSetting'), - 0x0003: Tag('ColorMode'), - 0x0004: Tag('Quality'), - 0x0005: Tag('Whitebalance'), - 0x0006: Tag('ImageSharpening'), - 0x0007: Tag('FocusMode'), - 0x0008: Tag('FlashSetting'), - 0x0009: Tag('AutoFlashMode'), - 0x000B: Tag('WhiteBalanceBias'), - 0x000C: Tag('WhiteBalanceRBCoeff'), - 0x000D: TagFunc('ProgramShift', nikon_ev_bias), - # Nearly the same as the other EV vals, but step size is 1/12 EV (?) - 0x000E: TagFunc('ExposureDifference', nikon_ev_bias), - 0x000F: Tag('ISOSelection'), - 0x0011: Tag('NikonPreview'), - 0x0012: TagFunc('FlashCompensation', nikon_ev_bias), - 0x0013: Tag('ISOSpeedRequested'), - 0x0016: Tag('PhotoCornerCoordinates'), - # 0x0017: Unknown, but most likely an EV value - 0x0018: TagFunc('FlashBracketCompensationApplied', nikon_ev_bias), - 0x0019: Tag('AEBracketCompensationApplied'), - 0x001A: Tag('ImageProcessing'), - 0x001B: Tag('CropHiSpeed'), - 0x001D: Tag('SerialNumber'), # Conflict with 0x00A0 ? - 0x001E: Tag('ColorSpace'), - 0x001F: Tag('VRInfo'), - 0x0020: Tag('ImageAuthentication'), - 0x0022: Tag('ActiveDLighting'), - 0x0023: Tag('PictureControl'), - 0x0024: Tag('WorldTime'), - 0x0025: Tag('ISOInfo'), - 0x0080: Tag('ImageAdjustment'), - 0x0081: Tag('ToneCompensation'), - 0x0082: Tag('AuxiliaryLens'), - 0x0083: Tag('LensType'), - 0x0084: Tag('LensMinMaxFocalMaxAperture'), - 0x0085: Tag('ManualFocusDistance'), - 0x0086: Tag('DigitalZoomFactor'), - 0x0087: TagDict('FlashMode', - {0x00: 'Did Not Fire', - 0x01: 'Fired, Manual', - 0x07: 'Fired, External', - 0x08: 'Fired, Commander Mode ', - 0x09: 'Fired, TTL Mode'}), - 0x0088: TagDict('AFFocusPosition', - {0x0000: 'Center', - 0x0100: 'Top', - 0x0200: 'Bottom', - 0x0300: 'Left', - 0x0400: 'Right'}), - 0x0089: TagDict('BracketingMode', - {0x00: 'Single frame, no bracketing', - 0x01: 'Continuous, no bracketing', - 0x02: 'Timer, no bracketing', - 0x10: 'Single frame, exposure bracketing', - 0x11: 'Continuous, exposure bracketing', - 0x12: 'Timer, exposure bracketing', - 0x40: 'Single frame, white balance bracketing', - 0x41: 'Continuous, white balance bracketing', - 0x42: 'Timer, white balance bracketing'}), - 0x008A: Tag('AutoBracketRelease'), - 0x008B: Tag('LensFStops'), - 0x008C: ('NEFCurve1', None), # ExifTool calls this 'ContrastCurve' - 0x008D: Tag('ColorMode'), - 0x008F: Tag('SceneMode'), - 0x0090: Tag('LightingType'), - 0x0091: Tag('ShotInfo'), # First 4 bytes are a version number in ASCII - 0x0092: Tag('HueAdjustment'), - # ExifTool calls this 'NEFCompression', should be 1-4 - 0x0093: Tag('Compression'), - 0x0094: TagDict('Saturation', - {-3: 'B&W', - -2: '-2', - -1: '-1', - 0: '0', - 1: '1', - 2: '2'}), - 0x0095: Tag('NoiseReduction'), - 0x0096: ('NEFCurve2', None), # ExifTool calls this 'LinearizationTable' - 0x0097: Tag('ColorBalance'), # First 4 bytes are a version number in ASCII - 0x0098: Tag('LensData'), # First 4 bytes are a version number in ASCII - 0x0099: Tag('RawImageCenter'), - 0x009A: Tag('SensorPixelSize'), - 0x009C: Tag('Scene Assist'), - 0x009E: Tag('RetouchHistory'), - 0x00A0: Tag('SerialNumber'), - 0x00A2: Tag('ImageDataSize'), - # 00A3: unknown - a single byte 0 - # 00A4: In NEF, looks like a 4 byte ASCII version number ('0200') - 0x00A5: Tag('ImageCount'), - 0x00A6: Tag('DeletedImageCount'), - 0x00A7: Tag('TotalShutterReleases'), - # First 4 bytes are a version number in ASCII, with version specific - # info to follow. Its hard to treat it as a string due to embedded nulls. - 0x00A8: Tag('FlashInfo'), - 0x00A9: Tag('ImageOptimization'), - 0x00AA: Tag('Saturation'), - 0x00AB: Tag('DigitalVariProgram'), - 0x00AC: Tag('ImageStabilization'), - 0x00AD: Tag('Responsive AF'), # 'AFResponse' - 0x00B0: Tag('MultiExposure'), - 0x00B1: Tag('HighISONoiseReduction'), - 0x00B7: Tag('AFInfo'), - 0x00B8: Tag('FileInfo'), - # 00B9: unknown - 0x0100: Tag('DigitalICE'), - 0x0103: TagDict('PreviewCompression', - {1: 'Uncompressed', - 2: 'CCITT 1D', - 3: 'T4/Group 3 Fax', - 4: 'T6/Group 4 Fax', - 5: 'LZW', - 6: 'JPEG (old-style)', - 7: 'JPEG', - 8: 'Adobe Deflate', - 9: 'JBIG B&W', - 10: 'JBIG Color', - 32766: 'Next', - 32769: 'Epson ERF Compressed', - 32771: 'CCIRLEW', - 32773: 'PackBits', - 32809: 'Thunderscan', - 32895: 'IT8CTPAD', - 32896: 'IT8LW', - 32897: 'IT8MP', - 32898: 'IT8BL', - 32908: 'PixarFilm', - 32909: 'PixarLog', - 32946: 'Deflate', - 32947: 'DCS', - 34661: 'JBIG', - 34676: 'SGILog', - 34677: 'SGILog24', - 34712: 'JPEG 2000', - 34713: 'Nikon NEF Compressed', - 65000: 'Kodak DCR Compressed', - 65535: 'Pentax PEF Compressed',}), - 0x0201: Tag('PreviewImageStart'), - 0x0202: Tag('PreviewImageLength'), - 0x0213: TagDict('PreviewYCbCrPositioning', - {1: 'Centered', - 2: 'Co-sited'}), - 0x0010: Tag('DataDump'), - } - -MAKERNOTE_NIKON_OLDER_TAGS = { - 0x0003: TagDict('Quality', - {1: 'VGA Basic', - 2: 'VGA Normal', - 3: 'VGA Fine', - 4: 'SXGA Basic', - 5: 'SXGA Normal', - 6: 'SXGA Fine'}), - 0x0004: TagDict('ColorMode', - {1: 'Color', - 2: 'Monochrome'}), - 0x0005: TagDict('ImageAdjustment', - {0: 'Normal', - 1: 'Bright+', - 2: 'Bright-', - 3: 'Contrast+', - 4: 'Contrast-'}), - 0x0006: TagDict('CCDSpeed', - {0: 'ISO 80', - 2: 'ISO 160', - 4: 'ISO 320', - 5: 'ISO 100'}), - 0x0007: TagDict('WhiteBalance', - {0: 'Auto', - 1: 'Preset', - 2: 'Daylight', - 3: 'Incandescent', - 4: 'Fluorescent', - 5: 'Cloudy', - 6: 'Speed Light'}), - } - -def olympus_special_mode (values) : - """ - Decode Olympus SpecialMode tag in MakerNote - """ - - a = { - 0: 'Normal', - 1: 'Unknown', - 2: 'Fast', - 3: 'Panorama' - } - - b = { - 0: 'Non-panoramic', - 1: 'Left to right', - 2: 'Right to left', - 3: 'Bottom to top', - 4: 'Top to bottom' - } - - if v[0] not in a or v[2] not in b: - return values - - return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]]) - -MAKERNOTE_OLYMPUS_TAGS={ - # ah HAH! those sneeeeeaky bastids! this is how they get past the fact - # that a JPEG thumbnail is not allowed in an uncompressed TIFF file - 0x0100: Tag('JPEGThumbnail'), - 0x0200: TagFunc('SpecialMode', olympus_special_mode), - 0x0201: TagDict('JPEGQual', - {1: 'SQ', - 2: 'HQ', - 3: 'SHQ'}), - 0x0202: TagDict('Macro', - {0: 'Normal', - 1: 'Macro', - 2: 'SuperMacro'}), - 0x0203: TagDict('BWMode', - {0: 'Off', - 1: 'On'}), - 0x0204: Tag('DigitalZoom'), - 0x0205: Tag('FocalPlaneDiagonal'), - 0x0206: Tag('LensDistortionParams'), - 0x0207: Tag('SoftwareRelease'), - 0x0208: Tag('PictureInfo'), - 0x0209: Tag('CameraID'), # print as string - 0x0F00: Tag('DataDump'), - 0x0300: Tag('PreCaptureFrames'), - 0x0404: Tag('SerialNumber'), - 0x1000: Tag('ShutterSpeedValue'), - 0x1001: Tag('ISOValue'), - 0x1002: Tag('ApertureValue'), - 0x1003: Tag('BrightnessValue'), - 0x1004: Tag('FlashMode'), - 0x1004: TagDict('FlashMode', - {2: 'On', - 3: 'Off'}), - 0x1005: TagDict('FlashDevice', - {0: 'None', - 1: 'Internal', - 4: 'External', - 5: 'Internal + External'}), - 0x1006: Tag('ExposureCompensation'), - 0x1007: Tag('SensorTemperature'), - 0x1008: Tag('LensTemperature'), - 0x100b: TagDict('FocusMode', - {0: 'Auto', - 1: 'Manual'}), - 0x1017: Tag('RedBalance'), - 0x1018: Tag('BlueBalance'), - 0x101a: Tag('SerialNumber'), - 0x1023: Tag('FlashExposureComp'), - 0x1026: TagDict('ExternalFlashBounce', - {0: 'No', - 1: 'Yes'}), - 0x1027: Tag('ExternalFlashZoom'), - 0x1028: Tag('ExternalFlashMode'), - 0x1029: ('Contrast int16u', - {0: 'High', - 1: 'Normal', - 2: 'Low'}), - 0x102a: Tag('SharpnessFactor'), - 0x102b: Tag('ColorControl'), - 0x102c: Tag('ValidBits'), - 0x102d: Tag('CoringFilter'), - 0x102e: Tag('OlympusImageWidth'), - 0x102f: Tag('OlympusImageHeight'), - 0x1034: Tag('CompressionRatio'), - 0x1035: TagDict('PreviewImageValid', - {0: 'No', - 1: 'Yes'}), - 0x1036: Tag('PreviewImageStart'), - 0x1037: Tag('PreviewImageLength'), - 0x1039: TagDict('CCDScanMode', - {0: 'Interlaced', - 1: 'Progressive'}), - 0x103a: TagDict('NoiseReduction', - {0: 'Off', - 1: 'On'}), - 0x103b: Tag('InfinityLensStep'), - 0x103c: Tag('NearLensStep'), - - # TODO - these need extra definitions - # http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html - 0x2010: Tag('Equipment'), - 0x2020: Tag('CameraSettings'), - 0x2030: Tag('RawDevelopment'), - 0x2040: Tag('ImageProcessing'), - 0x2050: Tag('FocusInfo'), - 0x3000: Tag('RawInfo '), - } - -# 0x2020 CameraSettings -MAKERNOTE_OLYMPUS_TAG_0x2020={ - 0x0100: TagDict('PreviewImageValid', - {0: 'No', - 1: 'Yes'}), - 0x0101: Tag('PreviewImageStart'), - 0x0102: Tag('PreviewImageLength'), - 0x0200: TagDict('ExposureMode', - {1: 'Manual', - 2: 'Program', - 3: 'Aperture-priority AE', - 4: 'Shutter speed priority AE', - 5: 'Program-shift'}), - 0x0201: TagDict('AELock', - {0: 'Off', - 1: 'On'}), - 0x0202: TagDict('MeteringMode', - {2: 'Center Weighted', - 3: 'Spot', - 5: 'ESP', - 261: 'Pattern+AF', - 515: 'Spot+Highlight control', - 1027: 'Spot+Shadow control'}), - 0x0300: TagDict('MacroMode', - {0: 'Off', - 1: 'On'}), - 0x0301: TagDict('FocusMode', - {0: 'Single AF', - 1: 'Sequential shooting AF', - 2: 'Continuous AF', - 3: 'Multi AF', - 10: 'MF'}), - 0x0302: TagDict('FocusProcess', - {0: 'AF Not Used', - 1: 'AF Used'}), - 0x0303: TagDict('AFSearch', - {0: 'Not Ready', - 1: 'Ready'}), - 0x0304: Tag('AFAreas'), - 0x0401: Tag('FlashExposureCompensation'), - 0x0500: ('WhiteBalance2', - {0: 'Auto', - 16: '7500K (Fine Weather with Shade)', - 17: '6000K (Cloudy)', - 18: '5300K (Fine Weather)', - 20: '3000K (Tungsten light)', - 21: '3600K (Tungsten light-like)', - 33: '6600K (Daylight fluorescent)', - 34: '4500K (Neutral white fluorescent)', - 35: '4000K (Cool white fluorescent)', - 48: '3600K (Tungsten light-like)', - 256: 'Custom WB 1', - 257: 'Custom WB 2', - 258: 'Custom WB 3', - 259: 'Custom WB 4', - 512: 'Custom WB 5400K', - 513: 'Custom WB 2900K', - 514: 'Custom WB 8000K', }), - 0x0501: Tag('WhiteBalanceTemperature'), - 0x0502: Tag('WhiteBalanceBracket'), - 0x0503: Tag('CustomSaturation'), # (3 numbers: 1. CS Value, 2. Min, 3. Max) - 0x0504: TagDict('ModifiedSaturation', - {0: 'Off', - 1: 'CM1 (Red Enhance)', - 2: 'CM2 (Green Enhance)', - 3: 'CM3 (Blue Enhance)', - 4: 'CM4 (Skin Tones)'}), - 0x0505: Tag('ContrastSetting'), # (3 numbers: 1. Contrast, 2. Min, 3. Max) - 0x0506: Tag('SharpnessSetting'), # (3 numbers: 1. Sharpness, 2. Min, 3. Max) - 0x0507: TagDict('ColorSpace', - {0: 'sRGB', - 1: 'Adobe RGB', - 2: 'Pro Photo RGB'}), - 0x0509: TagDict('SceneMode', - {0: 'Standard', - 6: 'Auto', - 7: 'Sport', - 8: 'Portrait', - 9: 'Landscape+Portrait', - 10: 'Landscape', - 11: 'Night scene', - 13: 'Panorama', - 16: 'Landscape+Portrait', - 17: 'Night+Portrait', - 19: 'Fireworks', - 20: 'Sunset', - 22: 'Macro', - 25: 'Documents', - 26: 'Museum', - 28: 'Beach&Snow', - 30: 'Candle', - 35: 'Underwater Wide1', - 36: 'Underwater Macro', - 39: 'High Key', - 40: 'Digital Image Stabilization', - 44: 'Underwater Wide2', - 45: 'Low Key', - 46: 'Children', - 48: 'Nature Macro'}), - 0x050a: TagDict('NoiseReduction', - {0: 'Off', - 1: 'Noise Reduction', - 2: 'Noise Filter', - 3: 'Noise Reduction + Noise Filter', - 4: 'Noise Filter (ISO Boost)', - 5: 'Noise Reduction + Noise Filter (ISO Boost)'}), - 0x050b: TagDict('DistortionCorrection', - {0: 'Off', - 1: 'On'}), - 0x050c: TagDict('ShadingCompensation', - {0: 'Off', - 1: 'On'}), - 0x050d: Tag('CompressionFactor'), - 0x050f: TagDict('Gradation', - {'-1 -1 1': 'Low Key', - '0 -1 1': 'Normal', - '1 -1 1': 'High Key'}), - 0x0520: TagDict('PictureMode', - {1: 'Vivid', - 2: 'Natural', - 3: 'Muted', - 256: 'Monotone', - 512: 'Sepia'}), - 0x0521: Tag('PictureModeSaturation'), - 0x0522: Tag('PictureModeHue?'), - 0x0523: Tag('PictureModeContrast'), - 0x0524: Tag('PictureModeSharpness'), - 0x0525: TagDict('PictureModeBWFilter', - {0: 'n/a', - 1: 'Neutral', - 2: 'Yellow', - 3: 'Orange', - 4: 'Red', - 5: 'Green'}), - 0x0526: TagDict('PictureModeTone', - {0: 'n/a', - 1: 'Neutral', - 2: 'Sepia', - 3: 'Blue', - 4: 'Purple', - 5: 'Green'}), - 0x0600: Tag('Sequence'), # 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits - 0x0601: Tag('PanoramaMode'), # (2 numbers: 1. Mode, 2. Shot number) - 0x0603: ('ImageQuality2', - {1: 'SQ', - 2: 'HQ', - 3: 'SHQ', - 4: 'RAW'}), - 0x0901: Tag('ManometerReading'), - } - - -MAKERNOTE_CASIO_TAGS={ - 0x0001: TagDict('RecordingMode', - {1: 'Single Shutter', - 2: 'Panorama', - 3: 'Night Scene', - 4: 'Portrait', - 5: 'Landscape'}), - 0x0002: TagDict('Quality', - {1: 'Economy', - 2: 'Normal', - 3: 'Fine'}), - 0x0003: TagDict('FocusingMode', - {2: 'Macro', - 3: 'Auto Focus', - 4: 'Manual Focus', - 5: 'Infinity'}), - 0x0004: TagDict('FlashMode', - {1: 'Auto', - 2: 'On', - 3: 'Off', - 4: 'Red Eye Reduction'}), - 0x0005: TagDict('FlashIntensity', - {11: 'Weak', - 13: 'Normal', - 15: 'Strong'}), - 0x0006: Tag('Object Distance'), - 0x0007: TagDict('WhiteBalance', - {1: 'Auto', - 2: 'Tungsten', - 3: 'Daylight', - 4: 'Fluorescent', - 5: 'Shade', - 129: 'Manual'}), - 0x000B: TagDict('Sharpness', - {0: 'Normal', - 1: 'Soft', - 2: 'Hard'}), - 0x000C: TagDict('Contrast', - {0: 'Normal', - 1: 'Low', - 2: 'High'}), - 0x000D: TagDict('Saturation', - {0: 'Normal', - 1: 'Low', - 2: 'High'}), - 0x0014: TagDict('CCDSpeed', - {64: 'Normal', - 80: 'Normal', - 100: 'High', - 125: '+1.0', - 244: '+3.0', - 250: '+2.0'}), - } - -MAKERNOTE_FUJIFILM_TAGS={ - 0x0000: Tag('NoteVersion'), - 0x1000: Tag('Quality'), - 0x1001: TagDict('Sharpness', - {1: 'Soft', - 2: 'Soft', - 3: 'Normal', - 4: 'Hard', - 5: 'Hard'}), - 0x1002: TagDict('WhiteBalance', - {0: 'Auto', - 256: 'Daylight', - 512: 'Cloudy', - 768: 'DaylightColor-Fluorescent', - 769: 'DaywhiteColor-Fluorescent', - 770: 'White-Fluorescent', - 1024: 'Incandescent', - 3840: 'Custom'}), - 0x1003: TagDict('Color', - {0: 'Normal', - 256: 'High', - 512: 'Low'}), - 0x1004: TagDict('Tone', - {0: 'Normal', - 256: 'High', - 512: 'Low'}), - 0x1010: TagDict('FlashMode', - {0: 'Auto', - 1: 'On', - 2: 'Off', - 3: 'Red Eye Reduction'}), - 0x1011: Tag('FlashStrength'), - 0x1020: TagDict('Macro', - {0: 'Off', - 1: 'On'}), - 0x1021: TagDict('FocusMode', - {0: 'Auto', - 1: 'Manual'}), - 0x1030: TagDict('SlowSync', - {0: 'Off', - 1: 'On'}), - 0x1031: TagDict('PictureMode', - {0: 'Auto', - 1: 'Portrait', - 2: 'Landscape', - 4: 'Sports', - 5: 'Night', - 6: 'Program AE', - 256: 'Aperture Priority AE', - 512: 'Shutter Priority AE', - 768: 'Manual Exposure'}), - 0x1100: TagDict('MotorOrBracket', - {0: 'Off', - 1: 'On'}), - 0x1300: TagDict('BlurWarning', - {0: 'Off', - 1: 'On'}), - 0x1301: TagDict('FocusWarning', - {0: 'Off', - 1: 'On'}), - 0x1302: TagDict('AEWarning', - {0: 'Off', - 1: 'On'}), - } - -MAKERNOTE_CANON_TAGS = { - 0x0006: Tag('ImageType'), - 0x0007: Tag('FirmwareVersion'), - 0x0008: Tag('ImageNumber'), - 0x0009: Tag('OwnerName'), - } - -# this is in element offset, name, optional value dictionary format -MAKERNOTE_CANON_TAG_0x001 = { - 1: TagDict('Macromode', - {1: 'Macro', - 2: 'Normal'}), - 2: Tag('SelfTimer'), - 3: TagDict('Quality', - {2: 'Normal', - 3: 'Fine', - 5: 'Superfine'}), - 4: TagDict('FlashMode', - {0: 'Flash Not Fired', - 1: 'Auto', - 2: 'On', - 3: 'Red-Eye Reduction', - 4: 'Slow Synchro', - 5: 'Auto + Red-Eye Reduction', - 6: 'On + Red-Eye Reduction', - 16: 'external flash'}), - 5: TagDict('ContinuousDriveMode', - {0: 'Single Or Timer', - 1: 'Continuous'}), - 7: TagDict('FocusMode', - {0: 'One-Shot', - 1: 'AI Servo', - 2: 'AI Focus', - 3: 'MF', - 4: 'Single', - 5: 'Continuous', - 6: 'MF'}), - 10: TagDict('ImageSize', - {0: 'Large', - 1: 'Medium', - 2: 'Small'}), - 11: TagDict('EasyShootingMode', - {0: 'Full Auto', - 1: 'Manual', - 2: 'Landscape', - 3: 'Fast Shutter', - 4: 'Slow Shutter', - 5: 'Night', - 6: 'B&W', - 7: 'Sepia', - 8: 'Portrait', - 9: 'Sports', - 10: 'Macro/Close-Up', - 11: 'Pan Focus'}), - 12: TagDict('DigitalZoom', - {0: 'None', - 1: '2x', - 2: '4x'}), - 13: TagDict('Contrast', - {0xFFFF: 'Low', - 0: 'Normal', - 1: 'High'}), - 14: TagDict('Saturation', - {0xFFFF: 'Low', - 0: 'Normal', - 1: 'High'}), - 15: TagDict('Sharpness', - {0xFFFF: 'Low', - 0: 'Normal', - 1: 'High'}), - 16: TagDict('ISO', - {0: 'See ISOSpeedRatings Tag', - 15: 'Auto', - 16: '50', - 17: '100', - 18: '200', - 19: '400'}), - 17: TagDict('MeteringMode', - {3: 'Evaluative', - 4: 'Partial', - 5: 'Center-weighted'}), - 18: TagDict('FocusType', - {0: 'Manual', - 1: 'Auto', - 3: 'Close-Up (Macro)', - 8: 'Locked (Pan Mode)'}), - 19: TagDict('AFPointSelected', - {0x3000: 'None (MF)', - 0x3001: 'Auto-Selected', - 0x3002: 'Right', - 0x3003: 'Center', - 0x3004: 'Left'}), - 20: TagDict('ExposureMode', - {0: 'Easy Shooting', - 1: 'Program', - 2: 'Tv-priority', - 3: 'Av-priority', - 4: 'Manual', - 5: 'A-DEP'}), - 23: Tag('LongFocalLengthOfLensInFocalUnits'), - 24: Tag('ShortFocalLengthOfLensInFocalUnits'), - 25: Tag('FocalUnitsPerMM'), - 28: TagDict('FlashActivity', - {0: 'Did Not Fire', - 1: 'Fired'}), - 29: TagDict('FlashDetails', - {14: 'External E-TTL', - 13: 'Internal Flash', - 11: 'FP Sync Used', - 7: '2nd("Rear")-Curtain Sync Used', - 4: 'FP Sync Enabled'}), - 32: TagDict('FocusMode', - {0: 'Single', - 1: 'Continuous'}), - } - -MAKERNOTE_CANON_TAG_0x004 = { - 7: TagDict('WhiteBalance', - {0: 'Auto', - 1: 'Sunny', - 2: 'Cloudy', - 3: 'Tungsten', - 4: 'Fluorescent', - 5: 'Flash', - 6: 'Custom'}), - 9: Tag('SequenceNumber'), - 14: Tag('AFPointUsed'), - 15: TagDict('FlashBias', - {0xFFC0: '-2 EV', - 0xFFCC: '-1.67 EV', - 0xFFD0: '-1.50 EV', - 0xFFD4: '-1.33 EV', - 0xFFE0: '-1 EV', - 0xFFEC: '-0.67 EV', - 0xFFF0: '-0.50 EV', - 0xFFF4: '-0.33 EV', - 0x0000: '0 EV', - 0x000C: '0.33 EV', - 0x0010: '0.50 EV', - 0x0014: '0.67 EV', - 0x0020: '1 EV', - 0x002C: '1.33 EV', - 0x0030: '1.50 EV', - 0x0034: '1.67 EV', - 0x0040: '2 EV'}), - 19: Tag('SubjectDistance'), - } - -# ratio object that eventually will be able to reduce itself to lowest -# common denominator for printing -# XXX: unused -def gcd(a, b): - if b == 0: - return a - else: - return gcd(b, a % b) - -class Ratio: - def __init__(self, num, den): - self.num = num - self.den = den - - def __repr__(self): - self.reduce() - if self.den == 1: - return str(self.num) - return '%d/%d' % (self.num, self.den) - - def reduce(self): - div = gcd(self.num, self.den) - if div > 1: - self.num = self.num / div - self.den = self.den / div - - diff -r 2e2ef5c99985 -r f74d8cf678ce degal/lib/exif.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/degal/lib/exif.py Sun Jun 14 16:10:30 2009 +0300 @@ -0,0 +1,669 @@ +""" + A custom EXIF parsing module, aimed at high performance. +""" + +import struct, mmap, os + +from utils import lazy_load, lazy_load_iter + +def read_struct (file, fmt) : + """ + Utility function to read data from the a file using struct + """ + + # length of data + fmt_size = struct.calcsize(fmt) + + # get data + file_data = file.read(fmt_size) + + # unpack single item, this should raise an error if file_data is too short + return struct.unpack(fmt, file_data) + +class Buffer (object) : + """ + Wraps a buffer object (anything that supports the python buffer protocol) for read-only access. + + Includes an offset for relative values, and an endianess for reading binary data. + """ + + def __init__ (self, obj, offset=None, size=None, struct_prefix='=') : + """ + Create a new Buffer object with a new underlying buffer, created from the given object, offset and size. + + The endiannes is given in the form of a struct-module prefix, which should be one of '<' or '>'. + Standard size/alignment are assumed. + """ + + # store + self.buf = buffer(obj, *(arg for arg in (offset, size) if arg is not None)) + self.offset = offset + self.size = size + self.prefix = struct_prefix + + def subregion (self, offset, length=None) : + """ + Create a new sub-Buffer referencing a view of this buffer, at the given offset, and with the given + length, if any, and the same struct_prefix. + """ + + return Buffer(self.buf, offset, length, struct_prefix=self.prefix) + + def pread (self, offset, length) : + """ + Read a random-access region of raw data + """ + + return self.buf[offset:offset + length] + + def pread_struct (self, offset, fmt) : + """ + Read structured data using the given struct format from the given offset. + """ + + return struct.unpack_from(self.prefix + fmt, self.buf, offset=offset) + + def pread_item (self, offset, fmt) : + """ + Read a single item of structured data from the given offset. + """ + + value, = self.pread_struct(offset, fmt) + + return value + + def iter_offsets (self, count, size, offset=0) : + """ + Yield a series of offsets for `count` items of `size` bytes, beginning at `offset`. + """ + + return xrange(offset, offset + count * size, size) + + def item_size (self, fmt) : + """ + Returns the size in bytes of the given item format + """ + + return struct.calcsize(self.prefix + fmt) + + def unpack_item (self, fmt, data) : + """ + Unpacks a single item from the given data + """ + + value, = struct.unpack(self.prefix + fmt, data) + + return value + +def mmap_buffer (file, size) : + """ + Create and return a new read-only mmap'd region + """ + + return mmap.mmap(file.fileno(), size, access=mmap.ACCESS_READ) + +import exif_data + +class Tag (object) : + """ + Represents a single Tag in an IFD + """ + + def __init__ (self, ifd, offset, tag, type, count, data_raw) : + """ + Build a Tag with the given binary items from the IFD entry + """ + + self.ifd = ifd + self.offset = offset + self.tag = tag + self.type = type + self.count = count + self.data_raw = data_raw + + # lookup the type for this tag + self.type_data = exif_data.FIELD_TYPES.get(type) + + # unpack it + if self.type_data : + self.type_format, self.type_name, self.type_func = self.type_data + + # lookup the tag data for this tag + self.tag_data = self.ifd.tag_dict.get(tag) + + @property + def name (self) : + """ + Lookup the name of this tag via its code, returns None if unknown. + """ + + if self.tag_data : + return self.tag_data.name + + 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. + """ + + if self.type_data and self.type_func : + # use the filter func + return self.type_func(raw_values) + + else : + # nada, just leave them + return raw_values + + def readable_value (self, values) : + """ + Convert the given values for this tag into a human-readable string. + + Returns the comma-separated values by default. + """ + + if self.tag_data : + # map it + return self.tag_data.map_values(values) + + else : + # default value-mapping + return ", ".join(str(value) for value in values) + +# size of an IFD entry in bytes +IFD_ENTRY_SIZE = 12 + +class IFD (Buffer) : + """ + Represents an IFD (Image file directory) region in EXIF data. + """ + + def __init__ (self, exif, buffer, tag_dict, **buffer_opts) : + """ + Access the IFD data from the given bufferable object with given buffer opts. + + This will read the `count` and `next_offset` values. + """ + + # init + super(IFD, self).__init__(buffer, **buffer_opts) + + # store + self.exif = exif + self.tag_dict = tag_dict + + # read header + self.count = self.pread_item(0, 'H') + + # read next-offset + self.next_offset = self.pread_item(0x02 + self.count * IFD_ENTRY_SIZE, 'I') + + @lazy_load_iter + def tags (self) : + """ + Iterate over all the Tag objects in this IFD + """ + + # read each tag + for offset in self.iter_offsets(self.count, IFD_ENTRY_SIZE, 0x02) : + # read the tag data + tag, type, count, data_raw = self.pread_struct(offset, 'HHI4s') + + # yield the new Tag + 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, **buffer_opts) : + """ + Access the EXIF data from the given bufferable object with the given buffer options. + """ + + # init Buffer + super(EXIF, self).__init__(buffer, **buffer_opts) + + # store + self.buffer = buffer + + @lazy_load_iter + def ifds (self) : + """ + Iterate over the primary IFDs in this EXIF. + """ + + # starting offset + offset = self.pread_item(0x04, 'I') + + while offset : + # create and read the IFD, operating on the right sub-buffer + ifd = IFD(self, self.buf, exif_data.EXIF_TAGS, offset=offset) + + # yield it + yield ifd + + # skip to next offset + offset = ifd.next_offset + + def _load_subifd (self, tag, tag_dict) : + """ + 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. + + Returns a (fmt, offset, size) tuple. + """ + # unknown tag? + if not tag.type_data : + return None + + # data format + if len(tag.type_format) == 1 : + # let struct handle the count + fmt = "%d%s" % (tag.count, tag.type_format) + + else : + # handle the count ourselves + fmt = tag.type_format * tag.count + + # size of the data + size = self.item_size(fmt) + + # inline or external? + if size > 0x04 : + # point at the external data + offset = self.unpack_item('I', tag.data_raw) + + else : + # point at the inline data + offset = tag.offset + 0x08 + + return fmt, offset, size + + def tag_values_raw (self, tag) : + """ + Get the raw values for the given tag as a tuple. + + Returns None if the tag could not be recognized. + """ + + # find the data + data_info = self.tag_data_info(tag) + + # not found? + if not data_info : + return None + + # unpack + data_fmt, data_offset, data_size = data_info + + # read values + return self.pread_struct(data_offset, data_fmt) + + def tag_values (self, tag) : + """ + Gets the processed values for the given tag as a list. + """ + + # read + process + return tag.process_values(self.tag_values_raw(tag)) + + def tag_value (self, tag) : + """ + Return the human-readable string value for the given tag. + """ + + # load the raw values + values = self.tag_values(tag) + + # unknown? + if not values : + return "" + + # 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 = { + 'II': '<', + 'MM': '>', +} + +# "An arbitrary but carefully chosen number (42) that further identifies the file as a TIFF file" +TIFF_BYTEORDER_MAGIC = 42 + +def tiff_load (file, length=0, **opts) : + """ + Load the Exif/TIFF data from the given file at its current position with optional length, using exif_load. + """ + + # all Exif data offsets are relative to the beginning of this TIFF header + offset = file.tell() + + # mmap the region for the EXIF data + buffer = mmap_buffer(file, length) + + # read byte-order header + byte_order = file.read(2) + + # map to struct prefix + struct_prefix = TIFF_BYTE_ORDER[byte_order] + + # validate + check_value, = read_struct(file, struct_prefix + 'H') + + if check_value != TIFF_BYTEORDER_MAGIC : + raise Exception("Invalid byte-order for TIFF: %2c -> %d" % (byte_order, check_value)) + + # build and return the EXIF object with the correct offset/size from the mmap region + return EXIF(buffer, offset=offset, size=length, **opts) + +# the JPEG markers that don't have any data +JPEG_NOSIZE_MARKERS = (0xD8, 0xD9) + +# the first marker in a JPEG File +JPEG_START_MARKER = 0xD8 + +# the JPEG APP1 marker used for EXIF +JPEG_EXIF_MARKER = 0xE1 + +# the JPEG APP1 Exif header +JPEG_EXIF_HEADER = "Exif\x00\x00" + +def jpeg_markers (file) : + """ + Iterate over the JPEG markers in the given file, yielding (type_byte, size) tuples. + + The size fields will be 0 for markers with no data. The file will be positioned at the beginning of the data + region, and may be seek'd around if needed. + + XXX: find a real implementation of this somewhere? + """ + + while True : + # read type + marker_byte, marker_type = read_struct(file, '!BB') + + # validate + if marker_byte != 0xff : + raise Exception("Not a JPEG marker: %x%x" % (marker_byte, marker_type)) + + # special cases for no data + if marker_type in JPEG_NOSIZE_MARKERS : + size = 0 + + else : + # read size field + size, = read_struct(file, '!H') + + # validate + if size < 0x02 : + raise Exception("Invalid size for marker %x%x: %x" % (marker_byte, marker_type, size)) + + else : + # do not count the size field itself + size = size - 2 + + # ok, data is at current position + offset = file.tell() + + # yield + yield marker_type, size + + # absolute seek to next marker + file.seek(offset + size) + +def jpeg_find_exif (file) : + """ + Find the Exif/TIFF section in the given JPEG file. + + If found, the file will be seek'd to the start of the Exif/TIFF header, and the size of the Exif/TIFF data will + be returned. + + Returns None if no EXIF section was found. + """ + + for count, (marker, size) in enumerate(jpeg_markers(file)) : + # verify that it's a JPEG file + if count == 0 : + # must start with the right marker + if marker != JPEG_START_MARKER : + raise Exception("JPEG file must start with 0xFF%02x marker" % (marker, )) + + # look for APP1 marker (0xE1) with EXIF signature + elif marker == JPEG_EXIF_MARKER and file.read(len(JPEG_EXIF_HEADER)) == JPEG_EXIF_HEADER: + # skipped the initial Exif marker signature + return size - len(JPEG_EXIF_HEADER) + + # nothing + return None + +def jpeg_load (file, **opts) : + """ + Loads the embedded Exif TIFF data from the given JPEG file using tiff_load. + + Returns None if no EXIF data could be found. + """ + + # look for the right section + size = jpeg_find_exif(file) + + # not found? + if not size : + # nothing + return + + else : + # load it as TIFF data + return tiff_load(file, size, **opts) + +def load_path (path, **opts) : + """ + Loads an EXIF object from the given filesystem path. + + Returns None if it could not be parsed. + """ + + # file extension + root, fext = os.path.splitext(path) + + # map + func = { + '.jpeg': jpeg_load, + '.jpg': jpeg_load, + '.tiff': tiff_load, # XXX: untested + }.get(fext.lower()) + + # not recognized? + if not func : + # XXX: sniff the file + return None + + # open it + file = open(path, 'rb') + + # 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 + """ + + print "EXIF offset=%#08x, size=%d:" % (exif.offset, exif.size) + + for i, ifd in enumerate(exif.ifds) : + # dump + dump_ifd(exif, i, ifd) + + +def list_tags (exif) : + """ + Print a neat listing of tags to stdout + """ + + for k, v in exif.get_main_tags().iteritems() : + print "%30s: %s" % (k, v) + +def main_path (path, dump) : + # dump path + print "%s: " % path + + # try and load it + exif = load_path(path) + + if not exif : + raise Exception("No EXIF data found") + + if dump : + # dump everything + dump_exif(exif) + + else : + # list them + list_tags(exif) + + +def main (paths, dump=False) : + """ + Load and dump EXIF data from the given path + """ + + # handle each one + for path in paths : + main_path(path, dump=dump) + +if __name__ == '__main__' : + import getopt + from sys import argv + + # defaults + dump = False + + # parse args + opts, args = getopt.getopt(argv[1:], "d", ["dump"]) + + for opt, val in opts : + if opt in ('-d', "--dump") : + dump = True + + main(args, dump=dump) + diff -r 2e2ef5c99985 -r f74d8cf678ce degal/lib/exif_data.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/degal/lib/exif_data.py Sun Jun 14 16:10:30 2009 +0300 @@ -0,0 +1,1304 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + EXIF file format data, including tag names, types, etc. + + Most of this was copied with modifications from EXIFpy: + # Library to extract EXIF information from digital camera image files + # http://sourceforge.net/projects/exif-py/ + # + # VERSION 1.1.0 + # + # Copyright (c) 2002-2007 Gene Cash All rights reserved + # Copyright (c) 2007-2008 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 + # are met: + # + # 1. Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above + # copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided + # with the distribution. + # + # 3. Neither the name of the authors nor the names of its contributors + # may be used to endorse or promote products derived from this + # software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" + +import decimal, itertools + +def filter_ascii (values) : + """ + Default post-filter for ASCII values. + + This takes a single item of string data, splits it up into strings by ASCII-NUL. + + These sub-strings are then decoded into unicode as ASCII, and stripped. + """ + + return [string.decode('ascii', 'replace').rstrip() for string in values[0].split('\x00') if string] + +def build_ratio (num, denom) : + """ + Builds a Decimal ratio out of the given numerator and denominator + """ + + # XXX: this may be slow + return decimal.Decimal(num) / decimal.Decimal(denom) + +def filter_ratio (values) : + """ + Default post-filter for Ratio values. + + This takes the pairs of numerator/denominator values and builds Decimals out of them + """ + + return [build_ratio(values[i], values[i + 1]) for i in xrange(0, len(values), 2)] + + +# IFD Tag type information, indexed by code +# { type_code: (type_fmt, name, filter_func) } +# +# type_fmt's that are one char will be prefixed with the count for use with struct.unpack, those with more chars will +# be repeated as many times for use with struct.unpack. +FIELD_TYPES = { +# 0x0000: (None, 'Proprietary' ), # ??? no such type + 0x0001: ('B', 'Byte', None ), + 0x0002: ('s', 'ASCII', filter_ascii ), + 0x0003: ('H', 'Short', None ), + 0x0004: ('L', 'Long', None ), + 0x0005: ('LL', 'Ratio', filter_ratio ), + 0x0006: ('b', 'Signed Byte', None ), + 0x0007: ('s', 'Undefined', None ), + 0x0008: ('h', 'Signed Short', None ), + 0x0009: ('l', 'Signed Long', None ), + 0x000A: ('ll', 'Signed Ratio', filter_ratio ), +} + +# magic value to indicate sub-IFDs +SUB_IFD_MAGIC = object() + + +class Tag (object) : + """ + Represents an Exif Tag + """ + + def __init__ (self, name) : + """ + Build Exif tag with given name, and optional external values-filter function. + """ + + self.name = name + + def map_values (self, values) : + """ + Map the given tag value to a printable string using the given value spec. + """ + + # default value-mapping + return ", ".join(str(value) for value in values) + +class TagDict (Tag) : + """ + A tag with a dict mapping values to names + """ + + def __init__ (self, name, values_dict) : + super(TagDict, self).__init__(name) + + self.values_dict = values_dict + + def map_values (self, values) : + """ + Map the values through our dict, defaulting to the repr. + """ + + return ", ".join(self.values_dict.get(value, repr(value)) for value in values) + +class TagFunc (Tag) : + """ + A tag with a simple function mapping values to names + """ + + def __init__ (self, name, values_func) : + super(TagFunc, self).__init__(name) + + self.values_func = values_func + + def map_values (self, values) : + """ + Map the values through our func + """ + + 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' ), + + # XXX: WTF? What kind of charset is 'Unicode' supposed to be? + # UTF-16? Little-endian? Big-endian? + # Confusing reigns: http://www.cpanforum.com/threads/7329 + 'UNICODE': ('utf16', 'error' ), +} + + +def decode_UserComment (values) : + """ + A UserComment field starts with an eight-byte encoding designator. + """ + + # single binary string + value, = values + + # split up + charset, comment_raw = value[:8], value[8:] + + # strip NILs + charset = charset.rstrip('\x00') + + # map + encoding, replace = USER_COMMENT_CHARSETS.get(charset, ('ascii', 'replace')) + + # decode + return [comment_raw.decode(encoding, replace)] + +# Mappings of Exif tag codes to name and decoding information. +# { tag : (name, value_dict/value_func/None/SUB_IFD_MAGIC) } +# +# name is the official Exif tag name +# value_dict is a { value: value_name } mapping for human-readable values +# value_func is a `(values) -> values` mapping function which *overrides* the tag's type_func. +# 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'), + 0x0102: Tag('BitsPerSample'), + 0x0103: TagDict('Compression', + {1: 'Uncompressed', + 2: 'CCITT 1D', + 3: 'T4/Group 3 Fax', + 4: 'T6/Group 4 Fax', + 5: 'LZW', + 6: 'JPEG (old-style)', + 7: 'JPEG', + 8: 'Adobe Deflate', + 9: 'JBIG B&W', + 10: 'JBIG Color', + 32766: 'Next', + 32769: 'Epson ERF Compressed', + 32771: 'CCIRLEW', + 32773: 'PackBits', + 32809: 'Thunderscan', + 32895: 'IT8CTPAD', + 32896: 'IT8LW', + 32897: 'IT8MP', + 32898: 'IT8BL', + 32908: 'PixarFilm', + 32909: 'PixarLog', + 32946: 'Deflate', + 32947: 'DCS', + 34661: 'JBIG', + 34676: 'SGILog', + 34677: 'SGILog24', + 34712: 'JPEG 2000', + 34713: 'Nikon NEF Compressed', + 65000: 'Kodak DCR Compressed', + 65535: 'Pentax PEF Compressed'}), + 0x0106: Tag('PhotometricInterpretation'), + 0x0107: Tag('Thresholding'), + 0x010A: Tag('FillOrder'), + 0x010D: Tag('DocumentName'), + 0x010E: Tag('ImageDescription'), + 0x010F: Tag('Make'), + 0x0110: Tag('Model'), + 0x0111: Tag('StripOffsets'), + 0x0112: TagDict('Orientation', + {1: 'Horizontal (normal)', + 2: 'Mirrored horizontal', + 3: 'Rotated 180', + 4: 'Mirrored vertical', + 5: 'Mirrored horizontal then rotated 90 CCW', + 6: 'Rotated 90 CW', + 7: 'Mirrored horizontal then rotated 90 CW', + 8: 'Rotated 90 CCW'}), + 0x0115: Tag('SamplesPerPixel'), + 0x0116: Tag('RowsPerStrip'), + 0x0117: Tag('StripByteCounts'), + 0x011A: Tag('XResolution'), + 0x011B: Tag('YResolution'), + 0x011C: Tag('PlanarConfiguration'), + 0x011D: Tag('PageName'), + 0x0128: TagDict('ResolutionUnit', + {1: 'Not Absolute', + 2: 'Pixels/Inch', + 3: 'Pixels/Centimeter'}), + 0x012D: Tag('TransferFunction'), + 0x0131: Tag('Software'), + 0x0132: Tag('DateTime'), + 0x013B: Tag('Artist'), + 0x013E: Tag('WhitePoint'), + 0x013F: Tag('PrimaryChromaticities'), + 0x0156: Tag('TransferRange'), + 0x0200: Tag('JPEGProc'), + 0x0201: Tag('JPEGInterchangeFormat'), + 0x0202: Tag('JPEGInterchangeFormatLength'), + 0x0211: Tag('YCbCrCoefficients'), + 0x0212: Tag('YCbCrSubSampling'), + 0x0213: TagDict('YCbCrPositioning', + {1: 'Centered', + 2: 'Co-sited'}), + 0x0214: Tag('ReferenceBlackWhite'), + + 0x4746: Tag('Rating'), + + 0x828D: Tag('CFARepeatPatternDim'), + 0x828E: Tag('CFAPattern'), + 0x828F: Tag('BatteryLevel'), + 0x8298: Tag('Copyright'), + 0x829A: Tag('ExposureTime'), + 0x829D: Tag('FNumber'), + 0x83BB: Tag('IPTC/NAA'), + 0x8769: IFDTag('ExifOffset', None), + 0x8773: Tag('InterColorProfile'), + 0x8822: TagDict('ExposureProgram', + {0: 'Unidentified', + 1: 'Manual', + 2: 'Program Normal', + 3: 'Aperture Priority', + 4: 'Shutter Priority', + 5: 'Program Creative', + 6: 'Program Action', + 7: 'Portrait Mode', + 8: 'Landscape Mode'}), + 0x8824: Tag('SpectralSensitivity'), + 0x8825: IFDTag('GPSInfo', GPS_TAGS), + 0x8827: Tag('ISOSpeedRatings'), + 0x8828: Tag('OECF'), + 0x9000: Tag('ExifVersion'), + 0x9003: Tag('DateTimeOriginal'), + 0x9004: Tag('DateTimeDigitized'), + 0x9101: TagDict('ComponentsConfiguration', + {0: '', + 1: 'Y', + 2: 'Cb', + 3: 'Cr', + 4: 'Red', + 5: 'Green', + 6: 'Blue'}), + 0x9102: Tag('CompressedBitsPerPixel'), + 0x9201: Tag('ShutterSpeedValue'), + 0x9202: Tag('ApertureValue'), + 0x9203: Tag('BrightnessValue'), + 0x9204: Tag('ExposureBiasValue'), + 0x9205: Tag('MaxApertureValue'), + 0x9206: Tag('SubjectDistance'), + 0x9207: TagDict('MeteringMode', + {0: 'Unidentified', + 1: 'Average', + 2: 'CenterWeightedAverage', + 3: 'Spot', + 4: 'MultiSpot', + 5: 'Pattern'}), + 0x9208: TagDict('LightSource', + {0: 'Unknown', + 1: 'Daylight', + 2: 'Fluorescent', + 3: 'Tungsten', + 9: 'Fine Weather', + 10: 'Flash', + 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', + 20: 'D55', + 21: 'D65', + 22: 'D75', + 255: 'Other'}), + 0x9209: TagDict('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'}), + 0x920A: Tag('FocalLength'), + 0x9214: Tag('SubjectArea'), + 0x927C: Tag('MakerNote'), + 0x9286: TagFunc('UserComment', decode_UserComment), + 0x9290: Tag('SubSecTime'), + 0x9291: Tag('SubSecTimeOriginal'), + 0x9292: Tag('SubSecTimeDigitized'), + + # used by Windows Explorer + 0x9C9B: Tag('XPTitle'), + 0x9C9C: Tag('XPComment'), + 0x9C9D: Tag('XPAuthor'), #(ignored by Windows Explorer if Artist exists) + 0x9C9E: Tag('XPKeywords'), + 0x9C9F: Tag('XPSubject'), + + 0xA000: Tag('FlashPixVersion'), + 0xA001: TagDict('ColorSpace', + {1: 'sRGB', + 2: 'Adobe RGB', + 65535: 'Uncalibrated'}), + 0xA002: Tag('ExifImageWidth'), + 0xA003: Tag('ExifImageLength'), + 0xA005: IFDTag('InteroperabilityOffset', INTR_TAGS), + 0xA20B: Tag('FlashEnergy'), # 0x920B in TIFF/EP + 0xA20C: Tag('SpatialFrequencyResponse'), # 0x920C + 0xA20E: Tag('FocalPlaneXResolution'), # 0x920E + 0xA20F: Tag('FocalPlaneYResolution'), # 0x920F + 0xA210: Tag('FocalPlaneResolutionUnit'), # 0x9210 + 0xA214: Tag('SubjectLocation'), # 0x9214 + 0xA215: Tag('ExposureIndex'), # 0x9215 + 0xA217: TagDict('SensingMethod', # 0x9217 + {1: 'Not defined', + 2: 'One-chip color area', + 3: 'Two-chip color area', + 4: 'Three-chip color area', + 5: 'Color sequential area', + 7: 'Trilinear', + 8: 'Color sequential linear'}), + 0xA300: TagDict('FileSource', + {1: 'Film Scanner', + 2: 'Reflection Print Scanner', + 3: 'Digital Camera'}), + 0xA301: TagDict('SceneType', + {1: 'Directly Photographed'}), + 0xA302: Tag('CVAPattern'), + 0xA401: TagDict('CustomRendered', + {0: 'Normal', + 1: 'Custom'}), + 0xA402: TagDict('ExposureMode', + {0: 'Auto Exposure', + 1: 'Manual Exposure', + 2: 'Auto Bracket'}), + 0xA403: TagDict('WhiteBalance', + {0: 'Auto', + 1: 'Manual'}), + 0xA404: Tag('DigitalZoomRatio'), + 0xA405: ('FocalLengthIn35mmFilm', None), + 0xA406: TagDict('SceneCaptureType', + {0: 'Standard', + 1: 'Landscape', + 2: 'Portrait', + 3: 'Night)'}), + 0xA407: TagDict('GainControl', + {0: 'None', + 1: 'Low gain up', + 2: 'High gain up', + 3: 'Low gain down', + 4: 'High gain down'}), + 0xA408: TagDict('Contrast', + {0: 'Normal', + 1: 'Soft', + 2: 'Hard'}), + 0xA409: TagDict('Saturation', + {0: 'Normal', + 1: 'Soft', + 2: 'Hard'}), + 0xA40A: TagDict('Sharpness', + {0: 'Normal', + 1: 'Soft', + 2: 'Hard'}), + 0xA40B: Tag('DeviceSettingDescription'), + 0xA40C: Tag('SubjectDistanceRange'), + 0xA500: Tag('Gamma'), + 0xC4A5: Tag('PrintIM'), + 0xEA1C: ('Padding', None), + } + +# http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp +def nikon_ev_bias (seq) : + """ + # First digit seems to be in steps of 1/6 EV. + # Does the third value mean the step size? It is usually 6, + # but it is 12 for the ExposureDifference. + """ + + # check for an error condition that could cause a crash. + # this only happens if something has gone really wrong in + # reading the Nikon MakerNote. + if len(seq) < 4 : + return "" + + if seq == [252, 1, 6, 0]: + return "-2/3 EV" + + if seq == [253, 1, 6, 0]: + return "-1/2 EV" + + if seq == [254, 1, 6, 0]: + return "-1/3 EV" + + if seq == [0, 1, 6, 0]: + return "0 EV" + + if seq == [2, 1, 6, 0]: + return "+1/3 EV" + + if seq == [3, 1, 6, 0]: + return "+1/2 EV" + + if seq == [4, 1, 6, 0]: + return "+2/3 EV" + + # handle combinations not in the table. + a = seq[0] + + # causes headaches for the +/- logic, so special case it. + if a == 0: + return "0 EV" + + if a > 127: + a = 256 - a + ret_str = "-" + else: + ret_str = "+" + + b = seq[2] # assume third value means the step size + + whole = a / b + + a = a % b + + if whole != 0 : + ret_str = ret_str + str(whole) + " " + + if a == 0 : + ret_str = ret_str + "EV" + else : + r = Ratio(a, b) + ret_str = ret_str + r.__repr__() + " EV" + + return ret_str + +# Nikon E99x MakerNote Tags +MAKERNOTE_NIKON_NEWER_TAGS={ + 0x0001: Tag('MakernoteVersion'), # Sometimes binary + 0x0002: Tag('ISOSetting'), + 0x0003: Tag('ColorMode'), + 0x0004: Tag('Quality'), + 0x0005: Tag('Whitebalance'), + 0x0006: Tag('ImageSharpening'), + 0x0007: Tag('FocusMode'), + 0x0008: Tag('FlashSetting'), + 0x0009: Tag('AutoFlashMode'), + 0x000B: Tag('WhiteBalanceBias'), + 0x000C: Tag('WhiteBalanceRBCoeff'), + 0x000D: TagFunc('ProgramShift', nikon_ev_bias), + # Nearly the same as the other EV vals, but step size is 1/12 EV (?) + 0x000E: TagFunc('ExposureDifference', nikon_ev_bias), + 0x000F: Tag('ISOSelection'), + 0x0011: Tag('NikonPreview'), + 0x0012: TagFunc('FlashCompensation', nikon_ev_bias), + 0x0013: Tag('ISOSpeedRequested'), + 0x0016: Tag('PhotoCornerCoordinates'), + # 0x0017: Unknown, but most likely an EV value + 0x0018: TagFunc('FlashBracketCompensationApplied', nikon_ev_bias), + 0x0019: Tag('AEBracketCompensationApplied'), + 0x001A: Tag('ImageProcessing'), + 0x001B: Tag('CropHiSpeed'), + 0x001D: Tag('SerialNumber'), # Conflict with 0x00A0 ? + 0x001E: Tag('ColorSpace'), + 0x001F: Tag('VRInfo'), + 0x0020: Tag('ImageAuthentication'), + 0x0022: Tag('ActiveDLighting'), + 0x0023: Tag('PictureControl'), + 0x0024: Tag('WorldTime'), + 0x0025: Tag('ISOInfo'), + 0x0080: Tag('ImageAdjustment'), + 0x0081: Tag('ToneCompensation'), + 0x0082: Tag('AuxiliaryLens'), + 0x0083: Tag('LensType'), + 0x0084: Tag('LensMinMaxFocalMaxAperture'), + 0x0085: Tag('ManualFocusDistance'), + 0x0086: Tag('DigitalZoomFactor'), + 0x0087: TagDict('FlashMode', + {0x00: 'Did Not Fire', + 0x01: 'Fired, Manual', + 0x07: 'Fired, External', + 0x08: 'Fired, Commander Mode ', + 0x09: 'Fired, TTL Mode'}), + 0x0088: TagDict('AFFocusPosition', + {0x0000: 'Center', + 0x0100: 'Top', + 0x0200: 'Bottom', + 0x0300: 'Left', + 0x0400: 'Right'}), + 0x0089: TagDict('BracketingMode', + {0x00: 'Single frame, no bracketing', + 0x01: 'Continuous, no bracketing', + 0x02: 'Timer, no bracketing', + 0x10: 'Single frame, exposure bracketing', + 0x11: 'Continuous, exposure bracketing', + 0x12: 'Timer, exposure bracketing', + 0x40: 'Single frame, white balance bracketing', + 0x41: 'Continuous, white balance bracketing', + 0x42: 'Timer, white balance bracketing'}), + 0x008A: Tag('AutoBracketRelease'), + 0x008B: Tag('LensFStops'), + 0x008C: ('NEFCurve1', None), # ExifTool calls this 'ContrastCurve' + 0x008D: Tag('ColorMode'), + 0x008F: Tag('SceneMode'), + 0x0090: Tag('LightingType'), + 0x0091: Tag('ShotInfo'), # First 4 bytes are a version number in ASCII + 0x0092: Tag('HueAdjustment'), + # ExifTool calls this 'NEFCompression', should be 1-4 + 0x0093: Tag('Compression'), + 0x0094: TagDict('Saturation', + {-3: 'B&W', + -2: '-2', + -1: '-1', + 0: '0', + 1: '1', + 2: '2'}), + 0x0095: Tag('NoiseReduction'), + 0x0096: ('NEFCurve2', None), # ExifTool calls this 'LinearizationTable' + 0x0097: Tag('ColorBalance'), # First 4 bytes are a version number in ASCII + 0x0098: Tag('LensData'), # First 4 bytes are a version number in ASCII + 0x0099: Tag('RawImageCenter'), + 0x009A: Tag('SensorPixelSize'), + 0x009C: Tag('Scene Assist'), + 0x009E: Tag('RetouchHistory'), + 0x00A0: Tag('SerialNumber'), + 0x00A2: Tag('ImageDataSize'), + # 00A3: unknown - a single byte 0 + # 00A4: In NEF, looks like a 4 byte ASCII version number ('0200') + 0x00A5: Tag('ImageCount'), + 0x00A6: Tag('DeletedImageCount'), + 0x00A7: Tag('TotalShutterReleases'), + # First 4 bytes are a version number in ASCII, with version specific + # info to follow. Its hard to treat it as a string due to embedded nulls. + 0x00A8: Tag('FlashInfo'), + 0x00A9: Tag('ImageOptimization'), + 0x00AA: Tag('Saturation'), + 0x00AB: Tag('DigitalVariProgram'), + 0x00AC: Tag('ImageStabilization'), + 0x00AD: Tag('Responsive AF'), # 'AFResponse' + 0x00B0: Tag('MultiExposure'), + 0x00B1: Tag('HighISONoiseReduction'), + 0x00B7: Tag('AFInfo'), + 0x00B8: Tag('FileInfo'), + # 00B9: unknown + 0x0100: Tag('DigitalICE'), + 0x0103: TagDict('PreviewCompression', + {1: 'Uncompressed', + 2: 'CCITT 1D', + 3: 'T4/Group 3 Fax', + 4: 'T6/Group 4 Fax', + 5: 'LZW', + 6: 'JPEG (old-style)', + 7: 'JPEG', + 8: 'Adobe Deflate', + 9: 'JBIG B&W', + 10: 'JBIG Color', + 32766: 'Next', + 32769: 'Epson ERF Compressed', + 32771: 'CCIRLEW', + 32773: 'PackBits', + 32809: 'Thunderscan', + 32895: 'IT8CTPAD', + 32896: 'IT8LW', + 32897: 'IT8MP', + 32898: 'IT8BL', + 32908: 'PixarFilm', + 32909: 'PixarLog', + 32946: 'Deflate', + 32947: 'DCS', + 34661: 'JBIG', + 34676: 'SGILog', + 34677: 'SGILog24', + 34712: 'JPEG 2000', + 34713: 'Nikon NEF Compressed', + 65000: 'Kodak DCR Compressed', + 65535: 'Pentax PEF Compressed',}), + 0x0201: Tag('PreviewImageStart'), + 0x0202: Tag('PreviewImageLength'), + 0x0213: TagDict('PreviewYCbCrPositioning', + {1: 'Centered', + 2: 'Co-sited'}), + 0x0010: Tag('DataDump'), + } + +MAKERNOTE_NIKON_OLDER_TAGS = { + 0x0003: TagDict('Quality', + {1: 'VGA Basic', + 2: 'VGA Normal', + 3: 'VGA Fine', + 4: 'SXGA Basic', + 5: 'SXGA Normal', + 6: 'SXGA Fine'}), + 0x0004: TagDict('ColorMode', + {1: 'Color', + 2: 'Monochrome'}), + 0x0005: TagDict('ImageAdjustment', + {0: 'Normal', + 1: 'Bright+', + 2: 'Bright-', + 3: 'Contrast+', + 4: 'Contrast-'}), + 0x0006: TagDict('CCDSpeed', + {0: 'ISO 80', + 2: 'ISO 160', + 4: 'ISO 320', + 5: 'ISO 100'}), + 0x0007: TagDict('WhiteBalance', + {0: 'Auto', + 1: 'Preset', + 2: 'Daylight', + 3: 'Incandescent', + 4: 'Fluorescent', + 5: 'Cloudy', + 6: 'Speed Light'}), + } + +def olympus_special_mode (values) : + """ + Decode Olympus SpecialMode tag in MakerNote + """ + + a = { + 0: 'Normal', + 1: 'Unknown', + 2: 'Fast', + 3: 'Panorama' + } + + b = { + 0: 'Non-panoramic', + 1: 'Left to right', + 2: 'Right to left', + 3: 'Bottom to top', + 4: 'Top to bottom' + } + + if v[0] not in a or v[2] not in b: + return values + + return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]]) + +MAKERNOTE_OLYMPUS_TAGS={ + # ah HAH! those sneeeeeaky bastids! this is how they get past the fact + # that a JPEG thumbnail is not allowed in an uncompressed TIFF file + 0x0100: Tag('JPEGThumbnail'), + 0x0200: TagFunc('SpecialMode', olympus_special_mode), + 0x0201: TagDict('JPEGQual', + {1: 'SQ', + 2: 'HQ', + 3: 'SHQ'}), + 0x0202: TagDict('Macro', + {0: 'Normal', + 1: 'Macro', + 2: 'SuperMacro'}), + 0x0203: TagDict('BWMode', + {0: 'Off', + 1: 'On'}), + 0x0204: Tag('DigitalZoom'), + 0x0205: Tag('FocalPlaneDiagonal'), + 0x0206: Tag('LensDistortionParams'), + 0x0207: Tag('SoftwareRelease'), + 0x0208: Tag('PictureInfo'), + 0x0209: Tag('CameraID'), # print as string + 0x0F00: Tag('DataDump'), + 0x0300: Tag('PreCaptureFrames'), + 0x0404: Tag('SerialNumber'), + 0x1000: Tag('ShutterSpeedValue'), + 0x1001: Tag('ISOValue'), + 0x1002: Tag('ApertureValue'), + 0x1003: Tag('BrightnessValue'), + 0x1004: Tag('FlashMode'), + 0x1004: TagDict('FlashMode', + {2: 'On', + 3: 'Off'}), + 0x1005: TagDict('FlashDevice', + {0: 'None', + 1: 'Internal', + 4: 'External', + 5: 'Internal + External'}), + 0x1006: Tag('ExposureCompensation'), + 0x1007: Tag('SensorTemperature'), + 0x1008: Tag('LensTemperature'), + 0x100b: TagDict('FocusMode', + {0: 'Auto', + 1: 'Manual'}), + 0x1017: Tag('RedBalance'), + 0x1018: Tag('BlueBalance'), + 0x101a: Tag('SerialNumber'), + 0x1023: Tag('FlashExposureComp'), + 0x1026: TagDict('ExternalFlashBounce', + {0: 'No', + 1: 'Yes'}), + 0x1027: Tag('ExternalFlashZoom'), + 0x1028: Tag('ExternalFlashMode'), + 0x1029: ('Contrast int16u', + {0: 'High', + 1: 'Normal', + 2: 'Low'}), + 0x102a: Tag('SharpnessFactor'), + 0x102b: Tag('ColorControl'), + 0x102c: Tag('ValidBits'), + 0x102d: Tag('CoringFilter'), + 0x102e: Tag('OlympusImageWidth'), + 0x102f: Tag('OlympusImageHeight'), + 0x1034: Tag('CompressionRatio'), + 0x1035: TagDict('PreviewImageValid', + {0: 'No', + 1: 'Yes'}), + 0x1036: Tag('PreviewImageStart'), + 0x1037: Tag('PreviewImageLength'), + 0x1039: TagDict('CCDScanMode', + {0: 'Interlaced', + 1: 'Progressive'}), + 0x103a: TagDict('NoiseReduction', + {0: 'Off', + 1: 'On'}), + 0x103b: Tag('InfinityLensStep'), + 0x103c: Tag('NearLensStep'), + + # TODO - these need extra definitions + # http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html + 0x2010: Tag('Equipment'), + 0x2020: Tag('CameraSettings'), + 0x2030: Tag('RawDevelopment'), + 0x2040: Tag('ImageProcessing'), + 0x2050: Tag('FocusInfo'), + 0x3000: Tag('RawInfo '), + } + +# 0x2020 CameraSettings +MAKERNOTE_OLYMPUS_TAG_0x2020={ + 0x0100: TagDict('PreviewImageValid', + {0: 'No', + 1: 'Yes'}), + 0x0101: Tag('PreviewImageStart'), + 0x0102: Tag('PreviewImageLength'), + 0x0200: TagDict('ExposureMode', + {1: 'Manual', + 2: 'Program', + 3: 'Aperture-priority AE', + 4: 'Shutter speed priority AE', + 5: 'Program-shift'}), + 0x0201: TagDict('AELock', + {0: 'Off', + 1: 'On'}), + 0x0202: TagDict('MeteringMode', + {2: 'Center Weighted', + 3: 'Spot', + 5: 'ESP', + 261: 'Pattern+AF', + 515: 'Spot+Highlight control', + 1027: 'Spot+Shadow control'}), + 0x0300: TagDict('MacroMode', + {0: 'Off', + 1: 'On'}), + 0x0301: TagDict('FocusMode', + {0: 'Single AF', + 1: 'Sequential shooting AF', + 2: 'Continuous AF', + 3: 'Multi AF', + 10: 'MF'}), + 0x0302: TagDict('FocusProcess', + {0: 'AF Not Used', + 1: 'AF Used'}), + 0x0303: TagDict('AFSearch', + {0: 'Not Ready', + 1: 'Ready'}), + 0x0304: Tag('AFAreas'), + 0x0401: Tag('FlashExposureCompensation'), + 0x0500: ('WhiteBalance2', + {0: 'Auto', + 16: '7500K (Fine Weather with Shade)', + 17: '6000K (Cloudy)', + 18: '5300K (Fine Weather)', + 20: '3000K (Tungsten light)', + 21: '3600K (Tungsten light-like)', + 33: '6600K (Daylight fluorescent)', + 34: '4500K (Neutral white fluorescent)', + 35: '4000K (Cool white fluorescent)', + 48: '3600K (Tungsten light-like)', + 256: 'Custom WB 1', + 257: 'Custom WB 2', + 258: 'Custom WB 3', + 259: 'Custom WB 4', + 512: 'Custom WB 5400K', + 513: 'Custom WB 2900K', + 514: 'Custom WB 8000K', }), + 0x0501: Tag('WhiteBalanceTemperature'), + 0x0502: Tag('WhiteBalanceBracket'), + 0x0503: Tag('CustomSaturation'), # (3 numbers: 1. CS Value, 2. Min, 3. Max) + 0x0504: TagDict('ModifiedSaturation', + {0: 'Off', + 1: 'CM1 (Red Enhance)', + 2: 'CM2 (Green Enhance)', + 3: 'CM3 (Blue Enhance)', + 4: 'CM4 (Skin Tones)'}), + 0x0505: Tag('ContrastSetting'), # (3 numbers: 1. Contrast, 2. Min, 3. Max) + 0x0506: Tag('SharpnessSetting'), # (3 numbers: 1. Sharpness, 2. Min, 3. Max) + 0x0507: TagDict('ColorSpace', + {0: 'sRGB', + 1: 'Adobe RGB', + 2: 'Pro Photo RGB'}), + 0x0509: TagDict('SceneMode', + {0: 'Standard', + 6: 'Auto', + 7: 'Sport', + 8: 'Portrait', + 9: 'Landscape+Portrait', + 10: 'Landscape', + 11: 'Night scene', + 13: 'Panorama', + 16: 'Landscape+Portrait', + 17: 'Night+Portrait', + 19: 'Fireworks', + 20: 'Sunset', + 22: 'Macro', + 25: 'Documents', + 26: 'Museum', + 28: 'Beach&Snow', + 30: 'Candle', + 35: 'Underwater Wide1', + 36: 'Underwater Macro', + 39: 'High Key', + 40: 'Digital Image Stabilization', + 44: 'Underwater Wide2', + 45: 'Low Key', + 46: 'Children', + 48: 'Nature Macro'}), + 0x050a: TagDict('NoiseReduction', + {0: 'Off', + 1: 'Noise Reduction', + 2: 'Noise Filter', + 3: 'Noise Reduction + Noise Filter', + 4: 'Noise Filter (ISO Boost)', + 5: 'Noise Reduction + Noise Filter (ISO Boost)'}), + 0x050b: TagDict('DistortionCorrection', + {0: 'Off', + 1: 'On'}), + 0x050c: TagDict('ShadingCompensation', + {0: 'Off', + 1: 'On'}), + 0x050d: Tag('CompressionFactor'), + 0x050f: TagDict('Gradation', + {'-1 -1 1': 'Low Key', + '0 -1 1': 'Normal', + '1 -1 1': 'High Key'}), + 0x0520: TagDict('PictureMode', + {1: 'Vivid', + 2: 'Natural', + 3: 'Muted', + 256: 'Monotone', + 512: 'Sepia'}), + 0x0521: Tag('PictureModeSaturation'), + 0x0522: Tag('PictureModeHue?'), + 0x0523: Tag('PictureModeContrast'), + 0x0524: Tag('PictureModeSharpness'), + 0x0525: TagDict('PictureModeBWFilter', + {0: 'n/a', + 1: 'Neutral', + 2: 'Yellow', + 3: 'Orange', + 4: 'Red', + 5: 'Green'}), + 0x0526: TagDict('PictureModeTone', + {0: 'n/a', + 1: 'Neutral', + 2: 'Sepia', + 3: 'Blue', + 4: 'Purple', + 5: 'Green'}), + 0x0600: Tag('Sequence'), # 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits + 0x0601: Tag('PanoramaMode'), # (2 numbers: 1. Mode, 2. Shot number) + 0x0603: ('ImageQuality2', + {1: 'SQ', + 2: 'HQ', + 3: 'SHQ', + 4: 'RAW'}), + 0x0901: Tag('ManometerReading'), + } + + +MAKERNOTE_CASIO_TAGS={ + 0x0001: TagDict('RecordingMode', + {1: 'Single Shutter', + 2: 'Panorama', + 3: 'Night Scene', + 4: 'Portrait', + 5: 'Landscape'}), + 0x0002: TagDict('Quality', + {1: 'Economy', + 2: 'Normal', + 3: 'Fine'}), + 0x0003: TagDict('FocusingMode', + {2: 'Macro', + 3: 'Auto Focus', + 4: 'Manual Focus', + 5: 'Infinity'}), + 0x0004: TagDict('FlashMode', + {1: 'Auto', + 2: 'On', + 3: 'Off', + 4: 'Red Eye Reduction'}), + 0x0005: TagDict('FlashIntensity', + {11: 'Weak', + 13: 'Normal', + 15: 'Strong'}), + 0x0006: Tag('Object Distance'), + 0x0007: TagDict('WhiteBalance', + {1: 'Auto', + 2: 'Tungsten', + 3: 'Daylight', + 4: 'Fluorescent', + 5: 'Shade', + 129: 'Manual'}), + 0x000B: TagDict('Sharpness', + {0: 'Normal', + 1: 'Soft', + 2: 'Hard'}), + 0x000C: TagDict('Contrast', + {0: 'Normal', + 1: 'Low', + 2: 'High'}), + 0x000D: TagDict('Saturation', + {0: 'Normal', + 1: 'Low', + 2: 'High'}), + 0x0014: TagDict('CCDSpeed', + {64: 'Normal', + 80: 'Normal', + 100: 'High', + 125: '+1.0', + 244: '+3.0', + 250: '+2.0'}), + } + +MAKERNOTE_FUJIFILM_TAGS={ + 0x0000: Tag('NoteVersion'), + 0x1000: Tag('Quality'), + 0x1001: TagDict('Sharpness', + {1: 'Soft', + 2: 'Soft', + 3: 'Normal', + 4: 'Hard', + 5: 'Hard'}), + 0x1002: TagDict('WhiteBalance', + {0: 'Auto', + 256: 'Daylight', + 512: 'Cloudy', + 768: 'DaylightColor-Fluorescent', + 769: 'DaywhiteColor-Fluorescent', + 770: 'White-Fluorescent', + 1024: 'Incandescent', + 3840: 'Custom'}), + 0x1003: TagDict('Color', + {0: 'Normal', + 256: 'High', + 512: 'Low'}), + 0x1004: TagDict('Tone', + {0: 'Normal', + 256: 'High', + 512: 'Low'}), + 0x1010: TagDict('FlashMode', + {0: 'Auto', + 1: 'On', + 2: 'Off', + 3: 'Red Eye Reduction'}), + 0x1011: Tag('FlashStrength'), + 0x1020: TagDict('Macro', + {0: 'Off', + 1: 'On'}), + 0x1021: TagDict('FocusMode', + {0: 'Auto', + 1: 'Manual'}), + 0x1030: TagDict('SlowSync', + {0: 'Off', + 1: 'On'}), + 0x1031: TagDict('PictureMode', + {0: 'Auto', + 1: 'Portrait', + 2: 'Landscape', + 4: 'Sports', + 5: 'Night', + 6: 'Program AE', + 256: 'Aperture Priority AE', + 512: 'Shutter Priority AE', + 768: 'Manual Exposure'}), + 0x1100: TagDict('MotorOrBracket', + {0: 'Off', + 1: 'On'}), + 0x1300: TagDict('BlurWarning', + {0: 'Off', + 1: 'On'}), + 0x1301: TagDict('FocusWarning', + {0: 'Off', + 1: 'On'}), + 0x1302: TagDict('AEWarning', + {0: 'Off', + 1: 'On'}), + } + +MAKERNOTE_CANON_TAGS = { + 0x0006: Tag('ImageType'), + 0x0007: Tag('FirmwareVersion'), + 0x0008: Tag('ImageNumber'), + 0x0009: Tag('OwnerName'), + } + +# this is in element offset, name, optional value dictionary format +MAKERNOTE_CANON_TAG_0x001 = { + 1: TagDict('Macromode', + {1: 'Macro', + 2: 'Normal'}), + 2: Tag('SelfTimer'), + 3: TagDict('Quality', + {2: 'Normal', + 3: 'Fine', + 5: 'Superfine'}), + 4: TagDict('FlashMode', + {0: 'Flash Not Fired', + 1: 'Auto', + 2: 'On', + 3: 'Red-Eye Reduction', + 4: 'Slow Synchro', + 5: 'Auto + Red-Eye Reduction', + 6: 'On + Red-Eye Reduction', + 16: 'external flash'}), + 5: TagDict('ContinuousDriveMode', + {0: 'Single Or Timer', + 1: 'Continuous'}), + 7: TagDict('FocusMode', + {0: 'One-Shot', + 1: 'AI Servo', + 2: 'AI Focus', + 3: 'MF', + 4: 'Single', + 5: 'Continuous', + 6: 'MF'}), + 10: TagDict('ImageSize', + {0: 'Large', + 1: 'Medium', + 2: 'Small'}), + 11: TagDict('EasyShootingMode', + {0: 'Full Auto', + 1: 'Manual', + 2: 'Landscape', + 3: 'Fast Shutter', + 4: 'Slow Shutter', + 5: 'Night', + 6: 'B&W', + 7: 'Sepia', + 8: 'Portrait', + 9: 'Sports', + 10: 'Macro/Close-Up', + 11: 'Pan Focus'}), + 12: TagDict('DigitalZoom', + {0: 'None', + 1: '2x', + 2: '4x'}), + 13: TagDict('Contrast', + {0xFFFF: 'Low', + 0: 'Normal', + 1: 'High'}), + 14: TagDict('Saturation', + {0xFFFF: 'Low', + 0: 'Normal', + 1: 'High'}), + 15: TagDict('Sharpness', + {0xFFFF: 'Low', + 0: 'Normal', + 1: 'High'}), + 16: TagDict('ISO', + {0: 'See ISOSpeedRatings Tag', + 15: 'Auto', + 16: '50', + 17: '100', + 18: '200', + 19: '400'}), + 17: TagDict('MeteringMode', + {3: 'Evaluative', + 4: 'Partial', + 5: 'Center-weighted'}), + 18: TagDict('FocusType', + {0: 'Manual', + 1: 'Auto', + 3: 'Close-Up (Macro)', + 8: 'Locked (Pan Mode)'}), + 19: TagDict('AFPointSelected', + {0x3000: 'None (MF)', + 0x3001: 'Auto-Selected', + 0x3002: 'Right', + 0x3003: 'Center', + 0x3004: 'Left'}), + 20: TagDict('ExposureMode', + {0: 'Easy Shooting', + 1: 'Program', + 2: 'Tv-priority', + 3: 'Av-priority', + 4: 'Manual', + 5: 'A-DEP'}), + 23: Tag('LongFocalLengthOfLensInFocalUnits'), + 24: Tag('ShortFocalLengthOfLensInFocalUnits'), + 25: Tag('FocalUnitsPerMM'), + 28: TagDict('FlashActivity', + {0: 'Did Not Fire', + 1: 'Fired'}), + 29: TagDict('FlashDetails', + {14: 'External E-TTL', + 13: 'Internal Flash', + 11: 'FP Sync Used', + 7: '2nd("Rear")-Curtain Sync Used', + 4: 'FP Sync Enabled'}), + 32: TagDict('FocusMode', + {0: 'Single', + 1: 'Continuous'}), + } + +MAKERNOTE_CANON_TAG_0x004 = { + 7: TagDict('WhiteBalance', + {0: 'Auto', + 1: 'Sunny', + 2: 'Cloudy', + 3: 'Tungsten', + 4: 'Fluorescent', + 5: 'Flash', + 6: 'Custom'}), + 9: Tag('SequenceNumber'), + 14: Tag('AFPointUsed'), + 15: TagDict('FlashBias', + {0xFFC0: '-2 EV', + 0xFFCC: '-1.67 EV', + 0xFFD0: '-1.50 EV', + 0xFFD4: '-1.33 EV', + 0xFFE0: '-1 EV', + 0xFFEC: '-0.67 EV', + 0xFFF0: '-0.50 EV', + 0xFFF4: '-0.33 EV', + 0x0000: '0 EV', + 0x000C: '0.33 EV', + 0x0010: '0.50 EV', + 0x0014: '0.67 EV', + 0x0020: '1 EV', + 0x002C: '1.33 EV', + 0x0030: '1.50 EV', + 0x0034: '1.67 EV', + 0x0040: '2 EV'}), + 19: Tag('SubjectDistance'), + } + +# ratio object that eventually will be able to reduce itself to lowest +# common denominator for printing +# XXX: unused +def gcd(a, b): + if b == 0: + return a + else: + return gcd(b, a % b) + +class Ratio: + def __init__(self, num, den): + self.num = num + self.den = den + + def __repr__(self): + self.reduce() + if self.den == 1: + return str(self.num) + return '%d/%d' % (self.num, self.den) + + def reduce(self): + div = gcd(self.num, self.den) + if div > 1: + self.num = self.num / div + self.den = self.den / div + +