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
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
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
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.
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.
59 Returns
60 -------
61 stream : bytearray
62 The compressed PNG image data stream.
64 Notes
65 -----
67 Copyright 2017 Thomas Guymer [1]_
69 References
70 ----------
71 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3
72 """
74 # Import standard modules ...
75 import zlib
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
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
91 # **************************************************************************
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"
106 # **************************************************************************
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
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
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
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
156 # **************************************************************************
158 # Initialize best answer and figure-of-merit ...
159 bestStream = bytearray()
160 minSize = numpy.iinfo("uint64").max # [B]
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 )
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}"
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)
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.")
260 # Overwrite the best ...
261 bestStream = bytearray()
262 bestStream += possibleStream
263 minSize = len(bestStream) # [B]
265 # Check that a best stream was found ...
266 assert len(bestStream) > 0, "no best stream was found"
268 # Return answer ...
269 return bestStream