#!/usr/bin/env python """ Show the EXIF information. Copyright (C) 2017-2020 Cosmin Truta. Use, modification and distribution are subject to the MIT License. Please see the accompanying file LICENSE_MIT.txt """ from __future__ import absolute_import, division, print_function import sys from bytepack import (unpack_uint32be, unpack_uint32le, unpack_uint16be, unpack_uint16le, unpack_uint8) # Generously allow the TIFF file to occupy up to a quarter-gigabyte. # TODO: Reduce this limit to 64K and use file seeking for anything larger. _READ_DATA_SIZE_MAX = 256 * 1024 * 1024 _TIFF_TAG_TYPES = { 1: "byte", 2: "ascii", 3: "short", 4: "long", 5: "rational", 6: "sbyte", 7: "undefined", 8: "sshort", 9: "slong", 10: "srational", 11: "float", 12: "double", } # See http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml _TIFF_TAGS = { 0x00fe: "Subfile Type", 0x0100: "Width", 0x0101: "Height", 0x0102: "Bits per Sample", 0x0103: "Compression", 0x0106: "Photometric", 0x010d: "Document Name", 0x010e: "Image Description", 0x010f: "Make", 0x0110: "Model", 0x0111: "Strip Offsets", 0x0112: "Orientation", 0x0115: "Samples per Pixel", 0x0116: "Rows per Strip", 0x0117: "Strip Byte Counts", 0x0118: "Min Sample Value", 0x0119: "Max Sample Value", 0x011a: "X Resolution", 0x011b: "Y Resolution", 0x011c: "Planar Configuration", 0x011d: "Page Name", 0x011e: "X Position", 0x011f: "Y Position", 0x0128: "Resolution Unit", 0x0129: "Page Number", 0x0131: "Software", 0x0132: "Date Time", 0x013b: "Artist", 0x013c: "Host Computer", 0x013d: "Predictor", 0x013e: "White Point", 0x013f: "Primary Chromaticities", 0x0140: "Color Map", 0x0141: "Half-Tone Hints", 0x0142: "Tile Width", 0x0143: "Tile Length", 0x0144: "Tile Offsets", 0x0145: "Tile Byte Counts", 0x0211: "YCbCr Coefficients", 0x0212: "YCbCr Subsampling", 0x0213: "YCbCr Positioning", 0x0214: "Reference Black White", 0x022f: "Strip Row Counts", 0x02bc: "XMP", 0x8298: "Copyright", 0x83bb: "IPTC", 0x8769: "EXIF IFD", 0x8773: "ICC Profile", 0x8825: "GPS IFD", 0xa005: "Interoperability IFD", 0xc4a5: "Print IM", # EXIF IFD tags 0x829a: "Exposure Time", 0x829d: "F-Number", 0x8822: "Exposure Program", 0x8824: "Spectral Sensitivity", 0x8827: "ISO Speed Ratings", 0x8828: "OECF", 0x9000: "EXIF Version", 0x9003: "DateTime Original", 0x9004: "DateTime Digitized", 0x9101: "Components Configuration", 0x9102: "Compressed Bits Per Pixel", 0x9201: "Shutter Speed Value", 0x9202: "Aperture Value", 0x9203: "Brightness Value", 0x9204: "Exposure Bias Value", 0x9205: "Max Aperture Value", 0x9206: "Subject Distance", 0x9207: "Metering Mode", 0x9208: "Light Source", 0x9209: "Flash", 0x920a: "Focal Length", 0x9214: "Subject Area", 0x927c: "Maker Note", 0x9286: "User Comment", # ... TODO 0xa000: "Flashpix Version", 0xa001: "Color Space", 0xa002: "Pixel X Dimension", 0xa003: "Pixel Y Dimension", 0xa004: "Related Sound File", # ... TODO # GPS IFD tags # ... TODO } _TIFF_EXIF_IFD = 0x8769 _GPS_IFD = 0x8825 _INTEROPERABILITY_IFD = 0xa005 class ExifInfo: """EXIF reader and information lister.""" _endian = None _buffer = None _offset = 0 _global_ifd_offset = 0 _exif_ifd_offset = 0 _gps_ifd_offset = 0 _interoperability_ifd_offset = 0 _hex = False def __init__(self, buffer, **kwargs): """Initialize the EXIF data reader.""" self._hex = kwargs.get("hex", False) self._verbose = kwargs.get("verbose", False) if not isinstance(buffer, bytes): raise RuntimeError("invalid EXIF data type") if buffer.startswith(b"MM\x00\x2a"): self._endian = "MM" elif buffer.startswith(b"II\x2a\x00"): self._endian = "II" else: raise RuntimeError("invalid EXIF header") self._buffer = buffer self._offset = 4 self._global_ifd_offset = self._ui32() def endian(self): """Return the endianness of the EXIF data.""" return self._endian def _tags_for_ifd(self, ifd_offset): """Yield the tags found at the given TIFF IFD offset.""" if ifd_offset < 8: raise RuntimeError("invalid TIFF IFD offset") self._offset = ifd_offset ifd_size = self._ui16() for _ in range(0, ifd_size): tag_id = self._ui16() tag_type = self._ui16() count = self._ui32() value_or_offset = self._ui32() if self._endian == "MM": # FIXME: # value_or_offset requires a fixup under big-endian encoding. if tag_type == 2: # 2 --> "ascii" value_or_offset >>= 24 elif tag_type == 3: # 3 --> "short" value_or_offset >>= 16 else: # ... FIXME pass if count == 0: raise RuntimeError("unsupported count=0 in tag 0x%x" % tag_id) if tag_id == _TIFF_EXIF_IFD: if tag_type != 4: raise RuntimeError("incorrect tag type for EXIF IFD") self._exif_ifd_offset = value_or_offset elif tag_id == _GPS_IFD: if tag_type != 4: raise RuntimeError("incorrect tag type for GPS IFD") self._gps_ifd_offset = value_or_offset elif tag_id == _INTEROPERABILITY_IFD: if tag_type != 4: raise RuntimeError("incorrect tag type for Interop IFD") self._interoperability_ifd_offset = value_or_offset yield (tag_id, tag_type, count, value_or_offset) def tags(self): """Yield all TIFF/EXIF tags.""" if self._verbose: print("TIFF IFD : 0x%08x" % self._global_ifd_offset) for tag in self._tags_for_ifd(self._global_ifd_offset): yield tag if self._exif_ifd_offset > 0: if self._verbose: print("EXIF IFD : 0x%08x" % self._exif_ifd_offset) for tag in self._tags_for_ifd(self._exif_ifd_offset): yield tag if self._gps_ifd_offset > 0: if self._verbose: print("GPS IFD : 0x%08x" % self._gps_ifd_offset) for tag in self._tags_for_ifd(self._gps_ifd_offset): yield tag if self._interoperability_ifd_offset > 0: if self._verbose: print("Interoperability IFD : 0x%08x" % self._interoperability_ifd_offset) for tag in self._tags_for_ifd(self._interoperability_ifd_offset): yield tag def tagid2str(self, tag_id): """Return an informative string representation of a TIFF tag id.""" idstr = _TIFF_TAGS.get(tag_id, "[Unknown]") if self._hex: idnum = "0x%04x" % tag_id else: idnum = "%d" % tag_id return "%s (%s)" % (idstr, idnum) @staticmethod def tagtype2str(tag_type): """Return an informative string representation of a TIFF tag type.""" typestr = _TIFF_TAG_TYPES.get(tag_type, "[unknown]") return "%d:%s" % (tag_type, typestr) def tag2str(self, tag_id, tag_type, count, value_or_offset): """Return an informative string representation of a TIFF tag tuple.""" return "%s (type=%s) (count=%d) : 0x%08x" \ % (self.tagid2str(tag_id), self.tagtype2str(tag_type), count, value_or_offset) def _ui32(self): """Decode a 32-bit unsigned int found at the current offset; advance the offset by 4. """ if self._offset + 4 > len(self._buffer): raise RuntimeError("out-of-bounds uint32 access in EXIF") if self._endian == "MM": result = unpack_uint32be(self._buffer, self._offset) else: result = unpack_uint32le(self._buffer, self._offset) self._offset += 4 return result def _ui16(self): """Decode a 16-bit unsigned int found at the current offset; advance the offset by 2. """ if self._offset + 2 > len(self._buffer): raise RuntimeError("out-of-bounds uint16 access in EXIF") if self._endian == "MM": result = unpack_uint16be(self._buffer, self._offset) else: result = unpack_uint16le(self._buffer, self._offset) self._offset += 2 return result def _ui8(self): """Decode an 8-bit unsigned int found at the current offset; advance the offset by 1. """ if self._offset + 1 > len(self._buffer): raise RuntimeError("out-of-bounds uint8 access in EXIF") result = unpack_uint8(self._buffer, self._offset) self._offset += 1 return result def print_raw_exif_info(buffer, **kwargs): """Print the EXIF information found in a raw byte stream.""" lister = ExifInfo(buffer, **kwargs) print("EXIF (endian=%s)" % lister.endian()) for (tag_id, tag_type, count, value_or_offset) in lister.tags(): print(lister.tag2str(tag_id=tag_id, tag_type=tag_type, count=count, value_or_offset=value_or_offset)) if __name__ == "__main__": # For testing only. for arg in sys.argv[1:]: with open(arg, "rb") as test_stream: test_buffer = test_stream.read(_READ_DATA_SIZE_MAX) print_raw_exif_info(test_buffer, hex=True, verbose=True)