xref: /aosp_15_r20/external/libpng/contrib/pngexif/pngexifinfo.py (revision a67afe4df73cf47866eedc69947994b8ff839aba)
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