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

1#!/usr/bin/env python3 

2 

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. 

19 

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. 

23 

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. 

46 

47 Notes 

48 ----- 

49 Copyright 2017 Thomas Guymer [1]_ 

50 

51 References 

52 ---------- 

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

54 """ 

55 

56 # Import standard modules ... 

57 import os 

58 import sys 

59 

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 

71 

72 # Import sub-functions ... 

73 from .makePng import makePng 

74 

75 # ************************************************************************** 

76 

77 # Check system ... 

78 assert sys.byteorder == "little", "the system is not little-endian" 

79 

80 # ************************************************************************** 

81 

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 

97 

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 

115 

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 ) 

135 

136 # Check if the new source is smaller than the old source ... 

137 if len(src) >= os.path.getsize(pName): 

138 return 

139 

140 if debug: 

141 print(f"Overwriting \"{pName}\" with optimised version ({len(src):,d} bytes < {os.path.getsize(pName):,d} bytes) ...") 

142 

143 # Write PNG ... 

144 with open(pName, "wb") as fObj: 

145 fObj.write(src)