Coverage for pyguymer3/image/manuallyOptimisePng.py: 2%
51 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 manuallyOptimisePng(
5 pName,
6 /,
7 *,
8 calcAdaptive: bool = True,
9 calcAverage: bool = True,
10 calcNone: bool = True,
11 calcPaeth: bool = True,
12 calcSub: bool = True,
13 calcUp: bool = True,
14 debug: bool = __debug__,
15 dpi: None | int = None,
16 modTime = None,
17):
18 """Manually optimise a PNG image.
20 This function will load a PNG image and recreate it in RAM. If the stored
21 source in RAM is smaller than the source on disk then the PNG file will be
22 overwritten.
24 Parameters
25 ----------
26 pName : str
27 the PNG file name
28 calcAdaptive : bool, optional
29 See :py:func:`pyguymer3.image.makePng` for the documentation.
30 calcAverage : bool, optional
31 See :py:func:`pyguymer3.image.makePng` for the documentation.
32 calcNone : bool, optional
33 See :py:func:`pyguymer3.image.makePng` for the documentation.
34 calcPaeth : bool, optional
35 See :py:func:`pyguymer3.image.makePng` for the documentation.
36 calcSub : bool, optional
37 See :py:func:`pyguymer3.image.makePng` for the documentation.
38 calcUp : bool, optional
39 See :py:func:`pyguymer3.image.makePng` for the documentation.
40 debug : bool, optional
41 Print debug messages.
42 dpi : None or float or int, optional
43 See :py:func:`pyguymer3.image.makePng` for the documentation.
44 modTime : None or datetime.datetime, optional
45 See :py:func:`pyguymer3.image.makePng` for the documentation.
47 Notes
48 -----
49 Copyright 2017 Thomas Guymer [1]_
51 References
52 ----------
53 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3
54 """
56 # Import standard modules ...
57 import os
58 import sys
60 # Import special modules ...
61 try:
62 import numpy
63 except:
64 raise Exception("\"numpy\" is not installed; run \"pip install --user numpy\"") from None
65 try:
66 import PIL
67 import PIL.Image
68 PIL.Image.MAX_IMAGE_PIXELS = 1024 * 1024 * 1024 # [px]
69 except:
70 raise Exception("\"PIL\" is not installed; run \"pip install --user Pillow\"") from None
72 # Import sub-functions ...
73 from .makePng import makePng
75 # **************************************************************************
77 # Check system ...
78 assert sys.byteorder == "little", "the system is not little-endian"
80 # **************************************************************************
82 # Open image and make arrays ...
83 with PIL.Image.open(pName) as iObj:
84 match iObj.mode:
85 case "L":
86 arrUint8 = numpy.array(iObj).reshape((iObj.size[1], iObj.size[0], 1))
87 palUint8 = None
88 case "P":
89 arrUint8 = numpy.array(iObj).reshape((iObj.size[1], iObj.size[0], 1))
90 palUint8 = numpy.frombuffer(iObj.palette.tobytes(), dtype = numpy.uint8)
91 palUint8 = palUint8.reshape((palUint8.size // 3, 3))
92 case "RGB":
93 arrUint8 = numpy.array(iObj)
94 palUint8 = None
95 case _:
96 raise ValueError(f"the image has an unsupported mode (\"{iObj.mode}\")") from None
98 # Check soon-to-be input ...
99 assert arrUint8.dtype == "uint8", f"the NumPy array is not 8-bit (\"{arrUint8.dtype}\")"
100 assert arrUint8.ndim == 3, f"the NumPy array is not 3D (\"{arrUint8.ndim:d}\")"
101 match arrUint8.shape[2]:
102 case 1:
103 if palUint8 is None:
104 pass
105 else:
106 assert palUint8.dtype == "uint8", f"the NumPy palette is not 8-bit (\"{palUint8.dtype}\")"
107 assert palUint8.ndim == 2, f"the NumPy palette is not 2D (\"{palUint8.ndim:d}\")"
108 assert palUint8.shape[0] <= 256, f"the NumPy palette has more than 256 colours (\"{palUint8.shape[0]:,d}\")"
109 assert palUint8.shape[1] == 3, "the NumPy palette does not have 3 colour channels"
110 assert arrUint8.max() < palUint8.shape[0], f"the NumPy array references more colours than are in the NumPy palette (\"{arrUint8.max():d}\" -vs- \"{palUint8.shape[0]:d}\")"
111 case 3:
112 pass
113 case _:
114 raise ValueError(f"the NumPy array does not have either 1 or 3 colour channels (\"{arrUint8.shape[2]:d}\")") from None
116 # Make PNG source ...
117 src = makePng(
118 arrUint8,
119 calcAdaptive = calcAdaptive,
120 calcAverage = calcAverage,
121 calcNone = calcNone,
122 calcPaeth = calcPaeth,
123 calcSub = calcSub,
124 calcUp = calcUp,
125 choices = "all",
126 debug = debug,
127 dpi = dpi,
128 levels = [9,],
129 memLevels = [9,],
130 modTime = modTime,
131 palUint8 = palUint8,
132 strategies = None,
133 wbitss = [15,],
134 )
136 # Check if the new source is smaller than the old source ...
137 if len(src) >= os.path.getsize(pName):
138 return
140 if debug:
141 print(f"Overwriting \"{pName}\" with optimised version ({len(src):,d} bytes < {os.path.getsize(pName):,d} bytes) ...")
143 # Write PNG ...
144 with open(pName, "wb") as fObj:
145 fObj.write(src)