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