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