blob: 1bf754cb4f7866c7c33ff0d988b11e7b406ce41b [file] [log] [blame]
#!/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)