Coverage for /usr/lib/python3/dist-packages/PIL/ImagePalette.py: 18%

153 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# image palette object 

6# 

7# History: 

8# 1996-03-11 fl Rewritten. 

9# 1997-01-03 fl Up and running. 

10# 1997-08-23 fl Added load hack 

11# 2001-04-16 fl Fixed randint shadow bug in random() 

12# 

13# Copyright (c) 1997-2001 by Secret Labs AB 

14# Copyright (c) 1996-1997 by Fredrik Lundh 

15# 

16# See the README file for information on usage and redistribution. 

17# 

18from __future__ import annotations 

19 

20import array 

21 

22from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile 

23 

24 

25class ImagePalette: 

26 """ 

27 Color palette for palette mapped images 

28 

29 :param mode: The mode to use for the palette. See: 

30 :ref:`concept-modes`. Defaults to "RGB" 

31 :param palette: An optional palette. If given, it must be a bytearray, 

32 an array or a list of ints between 0-255. The list must consist of 

33 all channels for one color followed by the next color (e.g. RGBRGBRGB). 

34 Defaults to an empty palette. 

35 """ 

36 

37 def __init__(self, mode="RGB", palette=None): 

38 self.mode = mode 

39 self.rawmode = None # if set, palette contains raw data 

40 self.palette = palette or bytearray() 

41 self.dirty = None 

42 

43 @property 

44 def palette(self): 

45 return self._palette 

46 

47 @palette.setter 

48 def palette(self, palette): 

49 self._colors = None 

50 self._palette = palette 

51 

52 @property 

53 def colors(self): 

54 if self._colors is None: 

55 mode_len = len(self.mode) 

56 self._colors = {} 

57 for i in range(0, len(self.palette), mode_len): 

58 color = tuple(self.palette[i : i + mode_len]) 

59 if color in self._colors: 

60 continue 

61 self._colors[color] = i // mode_len 

62 return self._colors 

63 

64 @colors.setter 

65 def colors(self, colors): 

66 self._colors = colors 

67 

68 def copy(self): 

69 new = ImagePalette() 

70 

71 new.mode = self.mode 

72 new.rawmode = self.rawmode 

73 if self.palette is not None: 

74 new.palette = self.palette[:] 

75 new.dirty = self.dirty 

76 

77 return new 

78 

79 def getdata(self): 

80 """ 

81 Get palette contents in format suitable for the low-level 

82 ``im.putpalette`` primitive. 

83 

84 .. warning:: This method is experimental. 

85 """ 

86 if self.rawmode: 

87 return self.rawmode, self.palette 

88 return self.mode, self.tobytes() 

89 

90 def tobytes(self): 

91 """Convert palette to bytes. 

92 

93 .. warning:: This method is experimental. 

94 """ 

95 if self.rawmode: 

96 msg = "palette contains raw palette data" 

97 raise ValueError(msg) 

98 if isinstance(self.palette, bytes): 

99 return self.palette 

100 arr = array.array("B", self.palette) 

101 return arr.tobytes() 

102 

103 # Declare tostring as an alias for tobytes 

104 tostring = tobytes 

105 

106 def _new_color_index(self, image=None, e=None): 

107 if not isinstance(self.palette, bytearray): 

108 self._palette = bytearray(self.palette) 

109 index = len(self.palette) // 3 

110 special_colors = () 

111 if image: 

112 special_colors = ( 

113 image.info.get("background"), 

114 image.info.get("transparency"), 

115 ) 

116 while index in special_colors: 

117 index += 1 

118 if index >= 256: 

119 if image: 

120 # Search for an unused index 

121 for i, count in reversed(list(enumerate(image.histogram()))): 

122 if count == 0 and i not in special_colors: 

123 index = i 

124 break 

125 if index >= 256: 

126 msg = "cannot allocate more than 256 colors" 

127 raise ValueError(msg) from e 

128 return index 

129 

130 def getcolor(self, color, image=None): 

131 """Given an rgb tuple, allocate palette entry. 

132 

133 .. warning:: This method is experimental. 

134 """ 

135 if self.rawmode: 

136 msg = "palette contains raw palette data" 

137 raise ValueError(msg) 

138 if isinstance(color, tuple): 

139 if self.mode == "RGB": 

140 if len(color) == 4: 

141 if color[3] != 255: 

142 msg = "cannot add non-opaque RGBA color to RGB palette" 

143 raise ValueError(msg) 

144 color = color[:3] 

145 elif self.mode == "RGBA": 

146 if len(color) == 3: 

147 color += (255,) 

148 try: 

149 return self.colors[color] 

150 except KeyError as e: 

151 # allocate new color slot 

152 index = self._new_color_index(image, e) 

153 self.colors[color] = index 

154 if index * 3 < len(self.palette): 

155 self._palette = ( 

156 self.palette[: index * 3] 

157 + bytes(color) 

158 + self.palette[index * 3 + 3 :] 

159 ) 

160 else: 

161 self._palette += bytes(color) 

162 self.dirty = 1 

163 return index 

164 else: 

165 msg = f"unknown color specifier: {repr(color)}" 

166 raise ValueError(msg) 

167 

168 def save(self, fp): 

169 """Save palette to text file. 

170 

171 .. warning:: This method is experimental. 

172 """ 

173 if self.rawmode: 

174 msg = "palette contains raw palette data" 

175 raise ValueError(msg) 

176 if isinstance(fp, str): 

177 fp = open(fp, "w") 

178 fp.write("# Palette\n") 

179 fp.write(f"# Mode: {self.mode}\n") 

180 for i in range(256): 

181 fp.write(f"{i}") 

182 for j in range(i * len(self.mode), (i + 1) * len(self.mode)): 

183 try: 

184 fp.write(f" {self.palette[j]}") 

185 except IndexError: 

186 fp.write(" 0") 

187 fp.write("\n") 

188 fp.close() 

189 

190 

191# -------------------------------------------------------------------- 

192# Internal 

193 

194 

195def raw(rawmode, data): 

196 palette = ImagePalette() 

197 palette.rawmode = rawmode 

198 palette.palette = data 

199 palette.dirty = 1 

200 return palette 

201 

202 

203# -------------------------------------------------------------------- 

204# Factories 

205 

206 

207def make_linear_lut(black, white): 

208 if black == 0: 

209 return [white * i // 255 for i in range(256)] 

210 

211 msg = "unavailable when black is non-zero" 

212 raise NotImplementedError(msg) # FIXME 

213 

214 

215def make_gamma_lut(exp): 

216 return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] 

217 

218 

219def negative(mode="RGB"): 

220 palette = list(range(256 * len(mode))) 

221 palette.reverse() 

222 return ImagePalette(mode, [i // len(mode) for i in palette]) 

223 

224 

225def random(mode="RGB"): 

226 from random import randint 

227 

228 palette = [randint(0, 255) for _ in range(256 * len(mode))] 

229 return ImagePalette(mode, palette) 

230 

231 

232def sepia(white="#fff0c0"): 

233 bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] 

234 return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) 

235 

236 

237def wedge(mode="RGB"): 

238 palette = list(range(256 * len(mode))) 

239 return ImagePalette(mode, [i // len(mode) for i in palette]) 

240 

241 

242def load(filename): 

243 # FIXME: supports GIMP gradients only 

244 

245 with open(filename, "rb") as fp: 

246 for paletteHandler in [ 

247 GimpPaletteFile.GimpPaletteFile, 

248 GimpGradientFile.GimpGradientFile, 

249 PaletteFile.PaletteFile, 

250 ]: 

251 try: 

252 fp.seek(0) 

253 lut = paletteHandler(fp).getpalette() 

254 if lut: 

255 break 

256 except (SyntaxError, ValueError): 

257 pass 

258 else: 

259 msg = "cannot load palette" 

260 raise OSError(msg) 

261 

262 return lut # data, rawmode