1#!/usr/bin/env python 2 3""" 4Show the EXIF information. 5 6Copyright (C) 2017-2020 Cosmin Truta. 7 8Use, modification and distribution are subject to the MIT License. 9Please see the accompanying file LICENSE_MIT.txt 10""" 11 12from __future__ import absolute_import, division, print_function 13 14import sys 15 16from bytepack import (unpack_uint32be, 17 unpack_uint32le, 18 unpack_uint16be, 19 unpack_uint16le, 20 unpack_uint8) 21 22 23# Generously allow the TIFF file to occupy up to a quarter-gigabyte. 24# TODO: Reduce this limit to 64K and use file seeking for anything larger. 25_READ_DATA_SIZE_MAX = 256 * 1024 * 1024 26 27_TIFF_TAG_TYPES = { 28 1: "byte", 29 2: "ascii", 30 3: "short", 31 4: "long", 32 5: "rational", 33 6: "sbyte", 34 7: "undefined", 35 8: "sshort", 36 9: "slong", 37 10: "srational", 38 11: "float", 39 12: "double", 40} 41 42# See http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml 43_TIFF_TAGS = { 44 0x00fe: "Subfile Type", 45 0x0100: "Width", 46 0x0101: "Height", 47 0x0102: "Bits per Sample", 48 0x0103: "Compression", 49 0x0106: "Photometric", 50 0x010d: "Document Name", 51 0x010e: "Image Description", 52 0x010f: "Make", 53 0x0110: "Model", 54 0x0111: "Strip Offsets", 55 0x0112: "Orientation", 56 0x0115: "Samples per Pixel", 57 0x0116: "Rows per Strip", 58 0x0117: "Strip Byte Counts", 59 0x0118: "Min Sample Value", 60 0x0119: "Max Sample Value", 61 0x011a: "X Resolution", 62 0x011b: "Y Resolution", 63 0x011c: "Planar Configuration", 64 0x011d: "Page Name", 65 0x011e: "X Position", 66 0x011f: "Y Position", 67 0x0128: "Resolution Unit", 68 0x0129: "Page Number", 69 0x0131: "Software", 70 0x0132: "Date Time", 71 0x013b: "Artist", 72 0x013c: "Host Computer", 73 0x013d: "Predictor", 74 0x013e: "White Point", 75 0x013f: "Primary Chromaticities", 76 0x0140: "Color Map", 77 0x0141: "Half-Tone Hints", 78 0x0142: "Tile Width", 79 0x0143: "Tile Length", 80 0x0144: "Tile Offsets", 81 0x0145: "Tile Byte Counts", 82 0x0211: "YCbCr Coefficients", 83 0x0212: "YCbCr Subsampling", 84 0x0213: "YCbCr Positioning", 85 0x0214: "Reference Black White", 86 0x022f: "Strip Row Counts", 87 0x02bc: "XMP", 88 0x8298: "Copyright", 89 0x83bb: "IPTC", 90 0x8769: "EXIF IFD", 91 0x8773: "ICC Profile", 92 0x8825: "GPS IFD", 93 0xa005: "Interoperability IFD", 94 0xc4a5: "Print IM", 95 96 # EXIF IFD tags 97 0x829a: "Exposure Time", 98 0x829d: "F-Number", 99 0x8822: "Exposure Program", 100 0x8824: "Spectral Sensitivity", 101 0x8827: "ISO Speed Ratings", 102 0x8828: "OECF", 103 0x9000: "EXIF Version", 104 0x9003: "DateTime Original", 105 0x9004: "DateTime Digitized", 106 0x9101: "Components Configuration", 107 0x9102: "Compressed Bits Per Pixel", 108 0x9201: "Shutter Speed Value", 109 0x9202: "Aperture Value", 110 0x9203: "Brightness Value", 111 0x9204: "Exposure Bias Value", 112 0x9205: "Max Aperture Value", 113 0x9206: "Subject Distance", 114 0x9207: "Metering Mode", 115 0x9208: "Light Source", 116 0x9209: "Flash", 117 0x920a: "Focal Length", 118 0x9214: "Subject Area", 119 0x927c: "Maker Note", 120 0x9286: "User Comment", 121 # ... TODO 122 0xa000: "Flashpix Version", 123 0xa001: "Color Space", 124 0xa002: "Pixel X Dimension", 125 0xa003: "Pixel Y Dimension", 126 0xa004: "Related Sound File", 127 # ... TODO 128 129 # GPS IFD tags 130 # ... TODO 131} 132 133_TIFF_EXIF_IFD = 0x8769 134_GPS_IFD = 0x8825 135_INTEROPERABILITY_IFD = 0xa005 136 137 138class ExifInfo: 139 """EXIF reader and information lister.""" 140 141 _endian = None 142 _buffer = None 143 _offset = 0 144 _global_ifd_offset = 0 145 _exif_ifd_offset = 0 146 _gps_ifd_offset = 0 147 _interoperability_ifd_offset = 0 148 _hex = False 149 150 def __init__(self, buffer, **kwargs): 151 """Initialize the EXIF data reader.""" 152 self._hex = kwargs.get("hex", False) 153 self._verbose = kwargs.get("verbose", False) 154 if not isinstance(buffer, bytes): 155 raise RuntimeError("invalid EXIF data type") 156 if buffer.startswith(b"MM\x00\x2a"): 157 self._endian = "MM" 158 elif buffer.startswith(b"II\x2a\x00"): 159 self._endian = "II" 160 else: 161 raise RuntimeError("invalid EXIF header") 162 self._buffer = buffer 163 self._offset = 4 164 self._global_ifd_offset = self._ui32() 165 166 def endian(self): 167 """Return the endianness of the EXIF data.""" 168 return self._endian 169 170 def _tags_for_ifd(self, ifd_offset): 171 """Yield the tags found at the given TIFF IFD offset.""" 172 if ifd_offset < 8: 173 raise RuntimeError("invalid TIFF IFD offset") 174 self._offset = ifd_offset 175 ifd_size = self._ui16() 176 for _ in range(0, ifd_size): 177 tag_id = self._ui16() 178 tag_type = self._ui16() 179 count = self._ui32() 180 value_or_offset = self._ui32() 181 if self._endian == "MM": 182 # FIXME: 183 # value_or_offset requires a fixup under big-endian encoding. 184 if tag_type == 2: 185 # 2 --> "ascii" 186 value_or_offset >>= 24 187 elif tag_type == 3: 188 # 3 --> "short" 189 value_or_offset >>= 16 190 else: 191 # ... FIXME 192 pass 193 if count == 0: 194 raise RuntimeError("unsupported count=0 in tag 0x%x" % tag_id) 195 if tag_id == _TIFF_EXIF_IFD: 196 if tag_type != 4: 197 raise RuntimeError("incorrect tag type for EXIF IFD") 198 self._exif_ifd_offset = value_or_offset 199 elif tag_id == _GPS_IFD: 200 if tag_type != 4: 201 raise RuntimeError("incorrect tag type for GPS IFD") 202 self._gps_ifd_offset = value_or_offset 203 elif tag_id == _INTEROPERABILITY_IFD: 204 if tag_type != 4: 205 raise RuntimeError("incorrect tag type for Interop IFD") 206 self._interoperability_ifd_offset = value_or_offset 207 yield (tag_id, tag_type, count, value_or_offset) 208 209 def tags(self): 210 """Yield all TIFF/EXIF tags.""" 211 if self._verbose: 212 print("TIFF IFD : 0x%08x" % self._global_ifd_offset) 213 for tag in self._tags_for_ifd(self._global_ifd_offset): 214 yield tag 215 if self._exif_ifd_offset > 0: 216 if self._verbose: 217 print("EXIF IFD : 0x%08x" % self._exif_ifd_offset) 218 for tag in self._tags_for_ifd(self._exif_ifd_offset): 219 yield tag 220 if self._gps_ifd_offset > 0: 221 if self._verbose: 222 print("GPS IFD : 0x%08x" % self._gps_ifd_offset) 223 for tag in self._tags_for_ifd(self._gps_ifd_offset): 224 yield tag 225 if self._interoperability_ifd_offset > 0: 226 if self._verbose: 227 print("Interoperability IFD : 0x%08x" % 228 self._interoperability_ifd_offset) 229 for tag in self._tags_for_ifd(self._interoperability_ifd_offset): 230 yield tag 231 232 def tagid2str(self, tag_id): 233 """Return an informative string representation of a TIFF tag id.""" 234 idstr = _TIFF_TAGS.get(tag_id, "[Unknown]") 235 if self._hex: 236 idnum = "0x%04x" % tag_id 237 else: 238 idnum = "%d" % tag_id 239 return "%s (%s)" % (idstr, idnum) 240 241 @staticmethod 242 def tagtype2str(tag_type): 243 """Return an informative string representation of a TIFF tag type.""" 244 typestr = _TIFF_TAG_TYPES.get(tag_type, "[unknown]") 245 return "%d:%s" % (tag_type, typestr) 246 247 def tag2str(self, tag_id, tag_type, count, value_or_offset): 248 """Return an informative string representation of a TIFF tag tuple.""" 249 return "%s (type=%s) (count=%d) : 0x%08x" \ 250 % (self.tagid2str(tag_id), self.tagtype2str(tag_type), count, 251 value_or_offset) 252 253 def _ui32(self): 254 """Decode a 32-bit unsigned int found at the current offset; 255 advance the offset by 4. 256 """ 257 if self._offset + 4 > len(self._buffer): 258 raise RuntimeError("out-of-bounds uint32 access in EXIF") 259 if self._endian == "MM": 260 result = unpack_uint32be(self._buffer, self._offset) 261 else: 262 result = unpack_uint32le(self._buffer, self._offset) 263 self._offset += 4 264 return result 265 266 def _ui16(self): 267 """Decode a 16-bit unsigned int found at the current offset; 268 advance the offset by 2. 269 """ 270 if self._offset + 2 > len(self._buffer): 271 raise RuntimeError("out-of-bounds uint16 access in EXIF") 272 if self._endian == "MM": 273 result = unpack_uint16be(self._buffer, self._offset) 274 else: 275 result = unpack_uint16le(self._buffer, self._offset) 276 self._offset += 2 277 return result 278 279 def _ui8(self): 280 """Decode an 8-bit unsigned int found at the current offset; 281 advance the offset by 1. 282 """ 283 if self._offset + 1 > len(self._buffer): 284 raise RuntimeError("out-of-bounds uint8 access in EXIF") 285 result = unpack_uint8(self._buffer, self._offset) 286 self._offset += 1 287 return result 288 289 290def print_raw_exif_info(buffer, **kwargs): 291 """Print the EXIF information found in a raw byte stream.""" 292 lister = ExifInfo(buffer, **kwargs) 293 print("EXIF (endian=%s)" % lister.endian()) 294 for (tag_id, tag_type, count, value_or_offset) in lister.tags(): 295 print(lister.tag2str(tag_id=tag_id, 296 tag_type=tag_type, 297 count=count, 298 value_or_offset=value_or_offset)) 299 300 301if __name__ == "__main__": 302 # For testing only. 303 for arg in sys.argv[1:]: 304 with open(arg, "rb") as test_stream: 305 test_buffer = test_stream.read(_READ_DATA_SIZE_MAX) 306 print_raw_exif_info(test_buffer, hex=True, verbose=True) 307