Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/pillow-11.0.0-py3.10-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

159 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 collections.abc import Sequence 

22from typing import IO, TYPE_CHECKING 

23 

24from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile 

25 

26if TYPE_CHECKING: 

27 from . import Image 

28 

29 

30class ImagePalette: 

31 """ 

32 Color palette for palette mapped images 

33 

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

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

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

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

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

39 Defaults to an empty palette. 

40 """ 

41 

42 def __init__( 

43 self, 

44 mode: str = "RGB", 

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

46 ) -> None: 

47 self.mode = mode 

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

49 self.palette = palette or bytearray() 

50 self.dirty: int | None = None 

51 

52 @property 

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

54 return self._palette 

55 

56 @palette.setter 

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

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

59 self._palette = palette 

60 

61 @property 

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

63 if self._colors is None: 

64 mode_len = len(self.mode) 

65 self._colors = {} 

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

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

68 if color in self._colors: 

69 continue 

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

71 return self._colors 

72 

73 @colors.setter 

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

75 self._colors = colors 

76 

77 def copy(self) -> ImagePalette: 

78 new = ImagePalette() 

79 

80 new.mode = self.mode 

81 new.rawmode = self.rawmode 

82 if self.palette is not None: 

83 new.palette = self.palette[:] 

84 new.dirty = self.dirty 

85 

86 return new 

87 

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

89 """ 

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

91 ``im.putpalette`` primitive. 

92 

93 .. warning:: This method is experimental. 

94 """ 

95 if self.rawmode: 

96 return self.rawmode, self.palette 

97 return self.mode, self.tobytes() 

98 

99 def tobytes(self) -> bytes: 

100 """Convert palette to bytes. 

101 

102 .. warning:: This method is experimental. 

103 """ 

104 if self.rawmode: 

105 msg = "palette contains raw palette data" 

106 raise ValueError(msg) 

107 if isinstance(self.palette, bytes): 

108 return self.palette 

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

110 return arr.tobytes() 

111 

112 # Declare tostring as an alias for tobytes 

113 tostring = tobytes 

114 

115 def _new_color_index( 

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

117 ) -> int: 

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

119 self._palette = bytearray(self.palette) 

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

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

122 if image: 

123 special_colors = ( 

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

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

126 ) 

127 while index in special_colors: 

128 index += 1 

129 if index >= 256: 

130 if image: 

131 # Search for an unused index 

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

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

134 index = i 

135 break 

136 if index >= 256: 

137 msg = "cannot allocate more than 256 colors" 

138 raise ValueError(msg) from e 

139 return index 

140 

141 def getcolor( 

142 self, 

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

144 image: Image.Image | None = None, 

145 ) -> int: 

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

147 

148 .. warning:: This method is experimental. 

149 """ 

150 if self.rawmode: 

151 msg = "palette contains raw palette data" 

152 raise ValueError(msg) 

153 if isinstance(color, tuple): 

154 if self.mode == "RGB": 

155 if len(color) == 4: 

156 if color[3] != 255: 

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

158 raise ValueError(msg) 

159 color = color[:3] 

160 elif self.mode == "RGBA": 

161 if len(color) == 3: 

162 color += (255,) 

163 try: 

164 return self.colors[color] 

165 except KeyError as e: 

166 # allocate new color slot 

167 index = self._new_color_index(image, e) 

168 assert isinstance(self._palette, bytearray) 

169 self.colors[color] = index 

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

171 self._palette = ( 

172 self._palette[: index * 3] 

173 + bytes(color) 

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

175 ) 

176 else: 

177 self._palette += bytes(color) 

178 self.dirty = 1 

179 return index 

180 else: 

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

182 raise ValueError(msg) 

183 

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

185 """Save palette to text file. 

186 

187 .. warning:: This method is experimental. 

188 """ 

189 if self.rawmode: 

190 msg = "palette contains raw palette data" 

191 raise ValueError(msg) 

192 if isinstance(fp, str): 

193 fp = open(fp, "w") 

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

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

196 for i in range(256): 

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

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

199 try: 

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

201 except IndexError: 

202 fp.write(" 0") 

203 fp.write("\n") 

204 fp.close() 

205 

206 

207# -------------------------------------------------------------------- 

208# Internal 

209 

210 

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

212 palette = ImagePalette() 

213 palette.rawmode = rawmode 

214 palette.palette = data 

215 palette.dirty = 1 

216 return palette 

217 

218 

219# -------------------------------------------------------------------- 

220# Factories 

221 

222 

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

224 if black == 0: 

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

226 

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

228 raise NotImplementedError(msg) # FIXME 

229 

230 

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

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

233 

234 

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

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

237 palette.reverse() 

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

239 

240 

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

242 from random import randint 

243 

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

245 return ImagePalette(mode, palette) 

246 

247 

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

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

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

251 

252 

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

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

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

256 

257 

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

259 # FIXME: supports GIMP gradients only 

260 

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

262 paletteHandlers: list[ 

263 type[ 

264 GimpPaletteFile.GimpPaletteFile 

265 | GimpGradientFile.GimpGradientFile 

266 | PaletteFile.PaletteFile 

267 ] 

268 ] = [ 

269 GimpPaletteFile.GimpPaletteFile, 

270 GimpGradientFile.GimpGradientFile, 

271 PaletteFile.PaletteFile, 

272 ] 

273 for paletteHandler in paletteHandlers: 

274 try: 

275 fp.seek(0) 

276 lut = paletteHandler(fp).getpalette() 

277 if lut: 

278 break 

279 except (SyntaxError, ValueError): 

280 pass 

281 else: 

282 msg = "cannot load palette" 

283 raise OSError(msg) 

284 

285 return lut # data, rawmode