Coverage for pyguymer3/image/makePngSrc/createStream.py: 56%

96 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-08 18:47 +0000

1#!/usr/bin/env python3 

2 

3# Define function ... 

4def createStream( 

5 arrUint8, 

6 arrInt16, 

7 /, 

8 *, 

9 calcAdaptive: bool = True, 

10 calcAverage: bool = True, 

11 calcNone: bool = True, 

12 calcPaeth: bool = True, 

13 calcSub: bool = True, 

14 calcUp: bool = True, 

15 choices: str = "all", 

16 debug: bool = __debug__, 

17 levels: None | list[int] = None, 

18 memLevels: None | list[int] = None, 

19 strategies: None | list[int] = None, 

20 wbitss: None | list[int] = None, 

21) -> bytearray: 

22 """Compress the PNG image data stream 

23 

24 This function loops over sets of settings and returns the smallest 

25 compressed PNG image data stream. See :py:func:`pyguymer3.image.makePng` for 

26 a discussion on how it does that. 

27 

28 Parameters 

29 ---------- 

30 arrUint8 : numpy.ndarray 

31 A "height * width * colour" unsigned 8-bit integer NumPy array. 

32 arrInt16 : numpy.ndarray 

33 A signed 16-bit integer NumPy array copy of ``arrUint8``. 

34 calcAdaptive : bool, optional 

35 See :py:func:`pyguymer3.image.makePng` for the documentation. 

36 calcAverage : bool, optional 

37 See :py:func:`pyguymer3.image.makePng` for the documentation. 

38 calcNone : bool, optional 

39 See :py:func:`pyguymer3.image.makePng` for the documentation. 

40 calcPaeth : bool, optional 

41 See :py:func:`pyguymer3.image.makePng` for the documentation. 

42 calcSub : bool, optional 

43 See :py:func:`pyguymer3.image.makePng` for the documentation. 

44 calcUp : bool, optional 

45 See :py:func:`pyguymer3.image.makePng` for the documentation. 

46 choices : str, optional 

47 See :py:func:`pyguymer3.image.makePng` for the documentation. 

48 debug : bool, optional 

49 Print debug messages. 

50 levels : None or list of int, optional 

51 See :py:func:`pyguymer3.image.makePng` for the documentation. 

52 memLevels : None or list of int, optional 

53 See :py:func:`pyguymer3.image.makePng` for the documentation. 

54 strategies : None or list of int, optional 

55 See :py:func:`pyguymer3.image.makePng` for the documentation. 

56 wbitss : None or list of int, optional 

57 See :py:func:`pyguymer3.image.makePng` for the documentation. 

58 

59 Returns 

60 ------- 

61 stream : bytearray 

62 The compressed PNG image data stream. 

63 

64 Notes 

65 ----- 

66 

67 Copyright 2017 Thomas Guymer [1]_ 

68 

69 References 

70 ---------- 

71 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3 

72 """ 

73 

74 # Import standard modules ... 

75 import zlib 

76 

77 # Import special modules ... 

78 try: 

79 import numpy 

80 except: 

81 raise Exception("\"numpy\" is not installed; run \"pip install --user numpy\"") from None 

82 

83 # Import sub-functions ... 

84 from .createStreamAdaptive import createStreamAdaptive 

85 from .createStreamAverage import createStreamAverage 

86 from .createStreamNone import createStreamNone 

87 from .createStreamPaeth import createStreamPaeth 

88 from .createStreamSub import createStreamSub 

89 from .createStreamUp import createStreamUp 

90 

91 # ************************************************************************** 

92 

93 # Check input ... 

94 assert arrUint8.dtype == "uint8", f"the NumPy array is not 8-bit (\"{arrUint8.dtype}\")" 

95 assert arrInt16.dtype == "int16", f"the NumPy array is not 16-bit (\"{arrInt16.dtype}\")" 

96 assert arrUint8.ndim == 3, f"the NumPy array is not 3D (\"{arrUint8.ndim:d}\")" 

97 match arrUint8.shape[2]: 

98 case 1: 

99 pass 

100 case 3: 

101 pass 

102 case _: 

103 raise ValueError(f"the NumPy array does not have either 1 or 3 colour channels (\"{arrUint8.shape[2]:d}\")") from None 

104 assert arrUint8.shape == arrInt16.shape, "the NumPy arrays do not have the same shape" 

105 

106 # ************************************************************************** 

107 

108 # Populate compression levels if the user has not ... 

109 if levels is None: 

110 match choices: 

111 case "fastest": 

112 levels = [0,] 

113 case "best": 

114 levels = [9,] 

115 case "all": 

116 levels = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,] 

117 case _: 

118 raise ValueError(f"\"choices\" was an unexpected value (\"{choices}\")") from None 

119 

120 # Populate memory levels if the user has not ... 

121 if memLevels is None: 

122 match choices: 

123 case "fastest": 

124 memLevels = [9,] 

125 case "best": 

126 memLevels = [9,] 

127 case "all": 

128 memLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9,] 

129 case _: 

130 raise ValueError(f"\"choices\" was an unexpected value (\"{choices}\")") from None 

131 

132 # Populate strategies if the user has not ... 

133 if strategies is None: 

134 match choices: 

135 case "fastest": 

136 strategies = [zlib.Z_DEFAULT_STRATEGY,] 

137 case "best": 

138 strategies = [zlib.Z_DEFAULT_STRATEGY,] 

139 case "all": 

140 strategies = [zlib.Z_DEFAULT_STRATEGY, zlib.Z_FILTERED, zlib.Z_HUFFMAN_ONLY, zlib.Z_RLE, zlib.Z_FIXED,] 

141 case _: 

142 raise ValueError(f"\"choices\" was an unexpected value (\"{choices}\")") from None 

143 

144 # Populate window sizes if the user has not ... 

145 if wbitss is None: 

146 match choices: 

147 case "fastest": 

148 wbitss = [15,] 

149 case "best": 

150 wbitss = [15,] 

151 case "all": 

152 wbitss = [9, 10, 11, 12, 13, 14, 15,] 

153 case _: 

154 raise ValueError(f"\"choices\" was an unexpected value (\"{choices}\")") from None 

155 

156 # ************************************************************************** 

157 

158 # Initialize best answer and figure-of-merit ... 

159 bestStream = bytearray() 

160 minSize = numpy.iinfo("uint64").max # [B] 

161 

162 # Calculate streams ... 

163 streams = [] 

164 if calcNone: 

165 streams.append( 

166 ( 

167 0, 

168 createStreamNone( 

169 arrUint8, 

170 arrInt16, 

171 ), 

172 ) 

173 ) 

174 if calcSub: 

175 streams.append( 

176 ( 

177 1, 

178 createStreamSub( 

179 arrUint8, 

180 arrInt16, 

181 ), 

182 ) 

183 ) 

184 if calcUp: 

185 streams.append( 

186 ( 

187 2, 

188 createStreamUp( 

189 arrUint8, 

190 arrInt16, 

191 ), 

192 ) 

193 ) 

194 if calcAverage: 

195 streams.append( 

196 ( 

197 3, 

198 createStreamAverage( 

199 arrUint8, 

200 arrInt16, 

201 ), 

202 ) 

203 ) 

204 if calcPaeth: 

205 streams.append( 

206 ( 

207 4, 

208 createStreamPaeth( 

209 arrUint8, 

210 arrInt16, 

211 ), 

212 ) 

213 ) 

214 if calcAdaptive: 

215 streams.append( 

216 ( 

217 5, 

218 createStreamAdaptive( 

219 arrUint8, 

220 arrInt16, 

221 ), 

222 ) 

223 ) 

224 

225 # Loop over streams ... 

226 for (filtType, stream,) in streams: 

227 # Loop over compression levels ... 

228 for level in levels: 

229 # Loop over window sizes ... 

230 for wbits in wbitss: 

231 # Check window size ... 

232 assert pow(2, wbits) <= 32768, f"the PNG specification only allows window sizes up to 32,768; you have asked for 2 ** {wbits:d}" 

233 

234 # Loop over memory levels ... 

235 for memLevel in memLevels: 

236 # Loop over strategies ... 

237 for strategy in strategies: 

238 # Make a compression object and compress the stream ... 

239 # NOTE: On 28/Jun/2025, I replaced zlib.compressobj(...) 

240 # with zlib.compress(..., level = 9) and confirmed 

241 # that all five filters, and adaptive filtering, 

242 # produced binary identical PNG files to the (soon 

243 # to be legacy) function save_array_as_PNG(). 

244 zObj = zlib.compressobj( 

245 level = level, 

246 memLevel = memLevel, 

247 method = zlib.DEFLATED, 

248 strategy = strategy, 

249 wbits = wbits, 

250 ) 

251 possibleStream = bytearray() 

252 possibleStream += zObj.compress(stream) 

253 possibleStream += zObj.flush(zlib.Z_FINISH) 

254 

255 # Check if this compressed stream is the best ... 

256 if len(possibleStream) < minSize: 

257 if debug: 

258 print(f"DEBUG: filter = {filtType:d}; compression level = {level:d}; window size = {wbits:2d}; memory level = {memLevel:d}; strategy = {strategy:d} --> {len(possibleStream):,d} bytes.") 

259 

260 # Overwrite the best ... 

261 bestStream = bytearray() 

262 bestStream += possibleStream 

263 minSize = len(bestStream) # [B] 

264 

265 # Check that a best stream was found ... 

266 assert len(bestStream) > 0, "no best stream was found" 

267 

268 # Return answer ... 

269 return bestStream