mirror of
https://git.code.sf.net/p/libpng/code.git
synced 2025-07-10 18:04:09 +02:00

We used this experimental project in the development of the PNG-EXIF ("eXIf") specification, back in 2017. The project evolved together with the draft specification, which was finalized on 2017-Jun-15 and approved by the PNG Group on 2017-Jul-13. The EXIF specification, outside of the scope of PNG and libpng, is quite complex. The libpng implementation cannot grow too much beyond performing basic integrity checks on top of serialization. In order to create and manipulate PNG-EXIF image files, the use of external libraries and tools such as ExifTool is necessary. Now, with the addition of contrib/pngexif to the libpng repository, offline tasks like metadata inspection and linting can be performed without importing external dependencies.
179 lines
6.2 KiB
Python
Executable File
179 lines
6.2 KiB
Python
Executable File
#!/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
|