Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pillow-10.4.0-py3.8-linux-x86_64.egg/PIL/ImagePalette.py: 28%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

158 statements  

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 

21from typing import IO, TYPE_CHECKING, Sequence 

22 

23from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile 

24 

25if TYPE_CHECKING: 

26 from . import Image 

27 

28 

29class ImagePalette: 

30 """ 

31 Color palette for palette mapped images 

32 

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

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

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

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

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

38 Defaults to an empty palette. 

39 """ 

40 

41 def __init__( 

42 self, 

43 mode: str = "RGB", 

44 palette: Sequence[int] | bytes | bytearray | None = None, 

45 ) -> None: 

46 self.mode = mode 

47 self.rawmode: str | None = None # if set, palette contains raw data 

48 self.palette = palette or bytearray() 

49 self.dirty: int | None = None 

50 

51 @property 

52 def palette(self) -> Sequence[int] | bytes | bytearray: 

53 return self._palette 

54 

55 @palette.setter 

56 def palette(self, palette: Sequence[int] | bytes | bytearray) -> None: 

57 self._colors: dict[tuple[int, ...], int] | None = None 

58 self._palette = palette 

59 

60 @property 

61 def colors(self) -> dict[tuple[int, ...], int]: 

62 if self._colors is None: 

63 mode_len = len(self.mode) 

64 self._colors = {} 

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

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

67 if color in self._colors: 

68 continue 

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

70 return self._colors 

71 

72 @colors.setter 

73 def colors(self, colors: dict[tuple[int, ...], int]) -> None: 

74 self._colors = colors 

75 

76 def copy(self) -> ImagePalette: 

77 new = ImagePalette() 

78 

79 new.mode = self.mode 

80 new.rawmode = self.rawmode 

81 if self.palette is not None: 

82 new.palette = self.palette[:] 

83 new.dirty = self.dirty 

84 

85 return new 

86 

87 def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]: 

88 """ 

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

90 ``im.putpalette`` primitive. 

91 

92 .. warning:: This method is experimental. 

93 """ 

94 if self.rawmode: 

95 return self.rawmode, self.palette 

96 return self.mode, self.tobytes() 

97 

98 def tobytes(self) -> bytes: 

99 """Convert palette to bytes. 

100 

101 .. warning:: This method is experimental. 

102 """ 

103 if self.rawmode: 

104 msg = "palette contains raw palette data" 

105 raise ValueError(msg) 

106 if isinstance(self.palette, bytes): 

107 return self.palette 

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

109 return arr.tobytes() 

110 

111 # Declare tostring as an alias for tobytes 

112 tostring = tobytes 

113 

114 def _new_color_index( 

115 self, image: Image.Image | None = None, e: Exception | None = None 

116 ) -> int: 

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

118 self._palette = bytearray(self.palette) 

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

120 special_colors: tuple[int | tuple[int, ...] | None, ...] = () 

121 if image: 

122 special_colors = ( 

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

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

125 ) 

126 while index in special_colors: 

127 index += 1 

128 if index >= 256: 

129 if image: 

130 # Search for an unused index 

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

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

133 index = i 

134 break 

135 if index >= 256: 

136 msg = "cannot allocate more than 256 colors" 

137 raise ValueError(msg) from e 

138 return index 

139 

140 def getcolor( 

141 self, 

142 color: tuple[int, ...], 

143 image: Image.Image | None = None, 

144 ) -> int: 

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

146 

147 .. warning:: This method is experimental. 

148 """ 

149 if self.rawmode: 

150 msg = "palette contains raw palette data" 

151 raise ValueError(msg) 

152 if isinstance(color, tuple): 

153 if self.mode == "RGB": 

154 if len(color) == 4: 

155 if color[3] != 255: 

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

157 raise ValueError(msg) 

158 color = color[:3] 

159 elif self.mode == "RGBA": 

160 if len(color) == 3: 

161 color += (255,) 

162 try: 

163 return self.colors[color] 

164 except KeyError as e: 

165 # allocate new color slot 

166 index = self._new_color_index(image, e) 

167 assert isinstance(self._palette, bytearray) 

168 self.colors[color] = index 

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

170 self._palette = ( 

171 self._palette[: index * 3] 

172 + bytes(color) 

173 + self._palette[index * 3 + 3 :] 

174 ) 

175 else: 

176 self._palette += bytes(color) 

177 self.dirty = 1 

178 return index 

179 else: 

180 msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable] 

181 raise ValueError(msg) 

182 

183 def save(self, fp: str | IO[str]) -> None: 

184 """Save palette to text file. 

185 

186 .. warning:: This method is experimental. 

187 """ 

188 if self.rawmode: 

189 msg = "palette contains raw palette data" 

190 raise ValueError(msg) 

191 if isinstance(fp, str): 

192 fp = open(fp, "w") 

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

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

195 for i in range(256): 

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

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

198 try: 

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

200 except IndexError: 

201 fp.write(" 0") 

202 fp.write("\n") 

203 fp.close() 

204 

205 

206# -------------------------------------------------------------------- 

207# Internal 

208 

209 

210def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: 

211 palette = ImagePalette() 

212 palette.rawmode = rawmode 

213 palette.palette = data 

214 palette.dirty = 1 

215 return palette 

216 

217 

218# -------------------------------------------------------------------- 

219# Factories 

220 

221 

222def make_linear_lut(black: int, white: float) -> list[int]: 

223 if black == 0: 

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

225 

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

227 raise NotImplementedError(msg) # FIXME 

228 

229 

230def make_gamma_lut(exp: float) -> list[int]: 

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

232 

233 

234def negative(mode: str = "RGB") -> ImagePalette: 

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

236 palette.reverse() 

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

238 

239 

240def random(mode: str = "RGB") -> ImagePalette: 

241 from random import randint 

242 

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

244 return ImagePalette(mode, palette) 

245 

246 

247def sepia(white: str = "#fff0c0") -> ImagePalette: 

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

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

250 

251 

252def wedge(mode: str = "RGB") -> ImagePalette: 

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

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

255 

256 

257def load(filename: str) -> tuple[bytes, str]: 

258 # FIXME: supports GIMP gradients only 

259 

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

261 paletteHandlers: list[ 

262 type[ 

263 GimpPaletteFile.GimpPaletteFile 

264 | GimpGradientFile.GimpGradientFile 

265 | PaletteFile.PaletteFile 

266 ] 

267 ] = [ 

268 GimpPaletteFile.GimpPaletteFile, 

269 GimpGradientFile.GimpGradientFile, 

270 PaletteFile.PaletteFile, 

271 ] 

272 for paletteHandler in paletteHandlers: 

273 try: 

274 fp.seek(0) 

275 lut = paletteHandler(fp).getpalette() 

276 if lut: 

277 break 

278 except (SyntaxError, ValueError): 

279 pass 

280 else: 

281 msg = "cannot load palette" 

282 raise OSError(msg) 

283 

284 return lut # data, rawmode