1#!/usr/bin/env python 2 3""" 4Show the PNG 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 argparse 15import io 16import re 17import sys 18import zlib 19 20from bytepack import unpack_uint32be, unpack_uint8 21from exifinfo import print_raw_exif_info 22 23_PNG_SIGNATURE = b"\x89PNG\x0d\x0a\x1a\x0a" 24_PNG_CHUNK_SIZE_MAX = 0x7fffffff 25_READ_DATA_SIZE_MAX = 0x3ffff 26 27 28def print_error(msg): 29 """Print an error message to stderr.""" 30 sys.stderr.write("%s: error: %s\n" % (sys.argv[0], msg)) 31 32 33def print_debug(msg): 34 """Print a debug message to stderr.""" 35 sys.stderr.write("%s: debug: %s\n" % (sys.argv[0], msg)) 36 37 38def _check_png(condition, chunk_sig=None): 39 """Check a PNG-specific assertion.""" 40 if condition: 41 return 42 if chunk_sig is None: 43 raise RuntimeError("bad PNG data") 44 raise RuntimeError("bad PNG data in '%s'" % chunk_sig) 45 46 47def _check_png_crc(data, checksum, chunk_sig): 48 """Check a CRC32 value inside a PNG stream.""" 49 if unpack_uint32be(data) == (checksum & 0xffffffff): 50 return 51 raise RuntimeError("bad PNG checksum in '%s'" % chunk_sig) 52 53 54def _extract_png_exif(data, **kwargs): 55 """Extract the EXIF header and data from a PNG chunk.""" 56 debug = kwargs.get("debug", False) 57 if unpack_uint8(data, 0) == 0: 58 if debug: 59 print_debug("found compressed EXIF, compression method 0") 60 if (unpack_uint8(data, 1) & 0x0f) == 0x08: 61 data = zlib.decompress(data[1:]) 62 elif unpack_uint8(data, 1) == 0 \ 63 and (unpack_uint8(data, 5) & 0x0f) == 0x08: 64 if debug: 65 print_debug("found uncompressed-length EXIF field") 66 data_len = unpack_uint32be(data, 1) 67 data = zlib.decompress(data[5:]) 68 if data_len != len(data): 69 raise RuntimeError( 70 "incorrect uncompressed-length field in PNG EXIF") 71 else: 72 raise RuntimeError("invalid compression method in PNG EXIF") 73 if data.startswith(b"MM\x00\x2a") or data.startswith(b"II\x2a\x00"): 74 return data 75 raise RuntimeError("invalid TIFF/EXIF header in PNG EXIF") 76 77 78def print_png_exif_info(instream, **kwargs): 79 """Print the EXIF information found in the given PNG datastream.""" 80 debug = kwargs.get("debug", False) 81 has_exif = False 82 while True: 83 chunk_hdr = instream.read(8) 84 _check_png(len(chunk_hdr) == 8) 85 chunk_len = unpack_uint32be(chunk_hdr, offset=0) 86 chunk_sig = chunk_hdr[4:8].decode("latin_1", errors="ignore") 87 _check_png(re.search(r"^[A-Za-z]{4}$", chunk_sig), chunk_sig=chunk_sig) 88 _check_png(chunk_len < _PNG_CHUNK_SIZE_MAX, chunk_sig=chunk_sig) 89 if debug: 90 print_debug("processing chunk: %s" % chunk_sig) 91 if chunk_len <= _READ_DATA_SIZE_MAX: 92 # The chunk size does not exceed an arbitrary, reasonable limit. 93 chunk_data = instream.read(chunk_len) 94 chunk_crc = instream.read(4) 95 _check_png(len(chunk_data) == chunk_len and len(chunk_crc) == 4, 96 chunk_sig=chunk_sig) 97 checksum = zlib.crc32(chunk_hdr[4:8]) 98 checksum = zlib.crc32(chunk_data, checksum) 99 _check_png_crc(chunk_crc, checksum, chunk_sig=chunk_sig) 100 else: 101 # The chunk is too big. Skip it. 102 instream.seek(chunk_len + 4, io.SEEK_CUR) 103 continue 104 if chunk_sig == "IEND": 105 _check_png(chunk_len == 0, chunk_sig=chunk_sig) 106 break 107 if chunk_sig.lower() in ["exif", "zxif"] and chunk_len > 8: 108 has_exif = True 109 exif_data = _extract_png_exif(chunk_data, **kwargs) 110 print_raw_exif_info(exif_data, **kwargs) 111 if not has_exif: 112 raise RuntimeError("no EXIF data in PNG stream") 113 114 115def print_exif_info(file, **kwargs): 116 """Print the EXIF information found in the given file.""" 117 with open(file, "rb") as stream: 118 header = stream.read(4) 119 if header == _PNG_SIGNATURE[0:4]: 120 if stream.read(4) != _PNG_SIGNATURE[4:8]: 121 raise RuntimeError("corrupted PNG file") 122 print_png_exif_info(instream=stream, **kwargs) 123 elif header == b"II\x2a\x00" or header == b"MM\x00\x2a": 124 data = header + stream.read(_READ_DATA_SIZE_MAX) 125 print_raw_exif_info(data, **kwargs) 126 else: 127 raise RuntimeError("not a PNG file") 128 129 130def main(): 131 """The main function.""" 132 parser = argparse.ArgumentParser( 133 prog="pngexifinfo", 134 usage="%(prog)s [options] [--] files...", 135 description="Show the PNG EXIF information.") 136 parser.add_argument("files", 137 metavar="file", 138 nargs="*", 139 help="a PNG file or a raw EXIF blob") 140 parser.add_argument("-x", 141 "--hex", 142 dest="hex", 143 action="store_true", 144 help="show EXIF tags in base 16") 145 parser.add_argument("-v", 146 "--verbose", 147 dest="verbose", 148 action="store_true", 149 help="run in verbose mode") 150 parser.add_argument("--debug", 151 dest="debug", 152 action="store_true", 153 help="run in debug mode") 154 args = parser.parse_args() 155 if not args.files: 156 parser.error("missing file operand") 157 result = 0 158 for file in args.files: 159 try: 160 print_exif_info(file, 161 hex=args.hex, 162 debug=args.debug, 163 verbose=args.verbose) 164 except (IOError, OSError) as err: 165 print_error(str(err)) 166 result = 66 # os.EX_NOINPUT 167 except RuntimeError as err: 168 print_error("%s: %s" % (file, str(err))) 169 result = 69 # os.EX_UNAVAILABLE 170 parser.exit(result) 171 172 173if __name__ == "__main__": 174 try: 175 main() 176 except KeyboardInterrupt: 177 sys.stderr.write("INTERRUPTED\n") 178 sys.exit(130) # SIGINT 179