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

1#!/usr/bin/env python3 

2 

3# Define function ... 

4def returnPngInfo( 

5 pName, 

6 /, 

7 *, 

8 debug = __debug__, 

9): 

10 """Return information about a PNG 

11 

12 This function returns information, such as filter type and compression 

13 method, about a PNG. 

14 

15 Parameters 

16 ---------- 

17 pName : str 

18 The input PNG. 

19 debug : bool, optional 

20 Print debug messages. 

21 

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]_). 

42 

43 Notes 

44 ----- 

45 This function only supports 8-bit images (either greyscale, paletted or 

46 truecolour), without interlacing. 

47 

48 Copyright 2017 Thomas Guymer [1]_ 

49 

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 """ 

56 

57 # Import standard modules ... 

58 import binascii 

59 import os 

60 import struct 

61 import zlib 

62 

63 # ************************************************************************** 

64 

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 } 

92 

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" 

98 

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") 

104 

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 

131 

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 

138 

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" 

146 

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 

154 

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}" 

163 

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" 

167 

168 if debug: 

169 print(f"DEBUG: \"{pName}\" has a compression ratio of {float(chkLen) / float(len(chkSrc)):.3f}×.") 

170 

171 # Initialize histogram ... 

172 hist = {} 

173 for ft, name in fts.items(): 

174 hist[name] = 0 # [#] 

175 

176 # Populate histogram ... 

177 for iy in range(ny): 

178 ft = chkSrc[iy * (nx * nc + 1)] 

179 hist[fts[ft]] += 1 # [#] 

180 

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