Coverage for pyguymer3/image/returnPngInfo.py: 1%
80 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-16 08:31 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-16 08:31 +0000
1#!/usr/bin/env python3
3# Define function ...
4def returnPngInfo(
5 pName,
6 /,
7 *,
8 debug = __debug__,
9):
10 """Return information about a PNG
12 This function returns information, such as filter type and compression
13 method, about a PNG.
15 Parameters
16 ----------
17 pName : str
18 The input PNG.
19 debug : bool, optional
20 Print debug messages.
22 Returns
23 -------
24 nx : int
25 The width of the input PNG.
26 ny : int
27 The height of the input PNG.
28 ct : str
29 The colour type of the input PNG (as defined in the PNG specification
30 [2]_).
31 ft : str
32 The filter type of the input PNG (as defined in the PNG specification
33 [2]_)
34 wbits : int
35 The compression window size of the input PNG (as defined in the ZLIB
36 compressed data format specification [3]_).
37 fdict : bool
38 Whether the compressor was preconditioned with data.
39 flevel : str
40 The compression level of the input PNG (as defined in the ZLIB
41 compressed data format specification [3]_).
43 Notes
44 -----
45 This function only supports 8-bit images (either greyscale, paletted or
46 truecolour), without interlacing.
48 Copyright 2017 Thomas Guymer [1]_
50 References
51 ----------
52 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3
53 .. [2] PNG Specification (Third Edition), https://www.w3.org/TR/png-3/
54 .. [3] ZLIB Compressed Data Format Specification (Version 3.3), https://datatracker.ietf.org/doc/html/rfc1950
55 """
57 # Import standard modules ...
58 import binascii
59 import os
60 import struct
61 import zlib
63 # **************************************************************************
65 # Create short-hands ...
66 chkLen = None # [B]
67 chkSrc = bytearray()
68 cts = {
69 0 : "greyscale",
70 2 : "truecolor",
71 3 : "indexed-color",
72 4 : "greyscale with alpha",
73 6 : "truecolor with alpha",
74 }
75 fts = {
76 0 : "none",
77 1 : "sub",
78 2 : "up",
79 3 : "average",
80 4 : "paeth",
81 }
82 nc = None # [B/px]
83 nx = None # [px]
84 ny = None # [px]
85 pSize = os.path.getsize(pName) # [B]
86 zlib_flevels = {
87 0 : "compressor used fastest algorithm",
88 1 : "compressor used fast algorithm",
89 2 : "compressor used default algorithm",
90 3 : "compressor used maximum compression, slowest algorithm",
91 }
93 # Open file ...
94 with open(pName, "rb") as fObj:
95 # Read file signature ...
96 pngSig = fObj.read(8)
97 assert pngSig == binascii.unhexlify("89504E470D0A1A0A"), f"\"{pName}\" is not a PNG file"
99 # Loop over chunks ...
100 while fObj.tell() < pSize:
101 # Read chunk header ...
102 chkLen, = struct.unpack(">I", fObj.read(4)) # [B]
103 chkTyp = fObj.read(4).decode("ascii")
105 # Populate PNG metadata if this is the IHDR chunk and skip ahead to
106 # the next chunk ...
107 if chkTyp == "IHDR":
108 assert chkLen == 13, f"the \"IHDR\" chunk is {chkLen:,d} bytes long"
109 nx, = struct.unpack(">I", fObj.read(4)) # [px]
110 ny, = struct.unpack(">I", fObj.read(4)) # [px]
111 bd, = struct.unpack("B", fObj.read(1)) # [b]
112 ct, = struct.unpack("B", fObj.read(1))
113 cm, = struct.unpack("B", fObj.read(1))
114 fm, = struct.unpack("B", fObj.read(1))
115 im, = struct.unpack("B", fObj.read(1))
116 if bd != 8:
117 return f"un-supported bit depth ({bd:,d} bits)"
118 assert ct in cts, f"the colour type is {ct:,d}"
119 if ct not in [0, 2, 3,]:
120 return f"un-supported colour type ({ct:,d}; {cts[ct]})"
121 assert cm == 0, f"the compression method is {cm:,d}"
122 assert fm == 0, f"the filter method is {fm:,d}"
123 if im != 0:
124 return f"un-supported interlace method ({im:,d})"
125 if ct in [0, 3,]:
126 nc = 1 # [B/px]
127 else:
128 nc = 3 # [B/px]
129 fObj.seek(4, os.SEEK_CUR)
130 continue
132 # Concatenate image data if this is a IDAT chunk and skip ahead to
133 # the next chunk ...
134 if chkTyp == "IDAT":
135 chkSrc += fObj.read(chkLen)
136 fObj.seek(4, os.SEEK_CUR)
137 continue
139 # Skip ahead to the next chunk ...
140 fObj.seek(chkLen, os.SEEK_CUR)
141 fObj.seek(4, os.SEEK_CUR)
142 assert chkLen is not None, "\"chkLen\" has not been determined"
143 assert nc is not None, "\"nc\" has not been determined"
144 assert nx is not None, "\"nx\" has not been determined"
145 assert ny is not None, "\"ny\" has not been determined"
147 # Populate ZLIB metadata ...
148 zlib_cmf = chkSrc[0]
149 zlib_cm = zlib_cmf % 16
150 zlib_cinfo = zlib_cmf // 16
151 assert zlib_cm == 8, f"the ZLIB compression method is {zlib_cm:,d}"
152 assert zlib_cinfo <= 7, f"the ZLIB window size minus 8 is {zlib_cinfo:,d}"
153 wbits = zlib_cinfo + 8
155 # Populate more ZLIB metadata ...
156 zlib_flg = chkSrc[1]
157 zlib_fcheck = zlib_flg % 32
158 zlib_fdict = (zlib_flg % 64) // 32
159 zlib_flevel = zlib_flg // 64
160 assert (zlib_cmf * 256 + zlib_flg) % 31 == 0, f"the ZLIB flag check is {zlib_fcheck:,d}"
161 assert zlib_fdict in [0, 1,], f"the ZLIB preset dictionary is {zlib_fdict:,d}"
162 assert zlib_flevel in zlib_flevels, f"the ZLIB compression level is {zlib_flevel:,d}"
164 # Decompress the image data ...
165 chkSrc = zlib.decompress(chkSrc)
166 assert len(chkSrc) == ny * (nx * nc + 1), f"the decompressed image data is {len(chkSrc):,d} bytes"
168 if debug:
169 print(f"DEBUG: \"{pName}\" has a compression ratio of {float(chkLen) / float(len(chkSrc)):.3f}×.")
171 # Initialize histogram ...
172 hist = {}
173 for ft, name in fts.items():
174 hist[name] = 0 # [#]
176 # Populate histogram ...
177 for iy in range(ny):
178 ft = chkSrc[iy * (nx * nc + 1)]
179 hist[fts[ft]] += 1 # [#]
181 # Return answer ...
182 for name, n in hist.items():
183 if n == ny:
184 return nx, ny, cts[ct], name, wbits, bool(zlib_fdict), zlib_flevels[zlib_flevel], None
185 return nx, ny, cts[ct], "adaptive", wbits, bool(zlib_fdict), zlib_flevels[zlib_flevel], hist