#!/usr/bin/env python """ Show the PNG 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 argparse import io import re import sys import zlib from bytepack import unpack_uint32be, unpack_uint8 from exifinfo import print_raw_exif_info _PNG_SIGNATURE = b"\x89PNG\x0d\x0a\x1a\x0a" _PNG_CHUNK_SIZE_MAX = 0x7fffffff _READ_DATA_SIZE_MAX = 0x3ffff def print_error(msg): """Print an error message to stderr.""" sys.stderr.write("%s: error: %s\n" % (sys.argv[0], msg)) def print_debug(msg): """Print a debug message to stderr.""" sys.stderr.write("%s: debug: %s\n" % (sys.argv[0], msg)) def _check_png(condition, chunk_sig=None): """Check a PNG-specific assertion.""" if condition: return if chunk_sig is None: raise RuntimeError("bad PNG data") raise RuntimeError("bad PNG data in '%s'" % chunk_sig) def _check_png_crc(data, checksum, chunk_sig): """Check a CRC32 value inside a PNG stream.""" if unpack_uint32be(data) == (checksum & 0xffffffff): return raise RuntimeError("bad PNG checksum in '%s'" % chunk_sig) def _extract_png_exif(data, **kwargs): """Extract the EXIF header and data from a PNG chunk.""" debug = kwargs.get("debug", False) if unpack_uint8(data, 0) == 0: if debug: print_debug("found compressed EXIF, compression method 0") if (unpack_uint8(data, 1) & 0x0f) == 0x08: data = zlib.decompress(data[1:]) elif unpack_uint8(data, 1) == 0 \ and (unpack_uint8(data, 5) & 0x0f) == 0x08: if debug: print_debug("found uncompressed-length EXIF field") data_len = unpack_uint32be(data, 1) data = zlib.decompress(data[5:]) if data_len != len(data): raise RuntimeError( "incorrect uncompressed-length field in PNG EXIF") else: raise RuntimeError("invalid compression method in PNG EXIF") if data.startswith(b"MM\x00\x2a") or data.startswith(b"II\x2a\x00"): return data raise RuntimeError("invalid TIFF/EXIF header in PNG EXIF") def print_png_exif_info(instream, **kwargs): """Print the EXIF information found in the given PNG datastream.""" debug = kwargs.get("debug", False) has_exif = False while True: chunk_hdr = instream.read(8) _check_png(len(chunk_hdr) == 8) chunk_len = unpack_uint32be(chunk_hdr, offset=0) chunk_sig = chunk_hdr[4:8].decode("latin_1", errors="ignore") _check_png(re.search(r"^[A-Za-z]{4}$", chunk_sig), chunk_sig=chunk_sig) _check_png(chunk_len < _PNG_CHUNK_SIZE_MAX, chunk_sig=chunk_sig) if debug: print_debug("processing chunk: %s" % chunk_sig) if chunk_len <= _READ_DATA_SIZE_MAX: # The chunk size does not exceed an arbitrary, reasonable limit. chunk_data = instream.read(chunk_len) chunk_crc = instream.read(4) _check_png(len(chunk_data) == chunk_len and len(chunk_crc) == 4, chunk_sig=chunk_sig) checksum = zlib.crc32(chunk_hdr[4:8]) checksum = zlib.crc32(chunk_data, checksum) _check_png_crc(chunk_crc, checksum, chunk_sig=chunk_sig) else: # The chunk is too big. Skip it. instream.seek(chunk_len + 4, io.SEEK_CUR) continue if chunk_sig == "IEND": _check_png(chunk_len == 0, chunk_sig=chunk_sig) break if chunk_sig.lower() in ["exif", "zxif"] and chunk_len > 8: has_exif = True exif_data = _extract_png_exif(chunk_data, **kwargs) print_raw_exif_info(exif_data, **kwargs) if not has_exif: raise RuntimeError("no EXIF data in PNG stream") def print_exif_info(file, **kwargs): """Print the EXIF information found in the given file.""" with open(file, "rb") as stream: header = stream.read(4) if header == _PNG_SIGNATURE[0:4]: if stream.read(4) != _PNG_SIGNATURE[4:8]: raise RuntimeError("corrupted PNG file") print_png_exif_info(instream=stream, **kwargs) elif header == b"II\x2a\x00" or header == b"MM\x00\x2a": data = header + stream.read(_READ_DATA_SIZE_MAX) print_raw_exif_info(data, **kwargs) else: raise RuntimeError("not a PNG file") def main(): """The main function.""" parser = argparse.ArgumentParser( prog="pngexifinfo", usage="%(prog)s [options] [--] files...", description="Show the PNG EXIF information.") parser.add_argument("files", metavar="file", nargs="*", help="a PNG file or a raw EXIF blob") parser.add_argument("-x", "--hex", dest="hex", action="store_true", help="show EXIF tags in base 16") parser.add_argument("-v", "--verbose", dest="verbose", action="store_true", help="run in verbose mode") parser.add_argument("--debug", dest="debug", action="store_true", help="run in debug mode") args = parser.parse_args() if not args.files: parser.error("missing file operand") result = 0 for file in args.files: try: print_exif_info(file, hex=args.hex, debug=args.debug, verbose=args.verbose) except (IOError, OSError) as err: print_error(str(err)) result = 66 # os.EX_NOINPUT except RuntimeError as err: print_error("%s: %s" % (file, str(err))) result = 69 # os.EX_UNAVAILABLE parser.exit(result) if __name__ == "__main__": try: main() except KeyboardInterrupt: sys.stderr.write("INTERRUPTED\n") sys.exit(130) # SIGINT