Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/ImagePalette.py: 53%

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

180 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 

20__lazy_modules__ = {"array"} 

21 

22import array 

23 

24from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile 

25 

26TYPE_CHECKING = False 

27if TYPE_CHECKING: 

28 from collections.abc import Sequence 

29 from typing import IO 

30 

31 from . import Image 

32 

33 

34class ImagePalette: 

35 """ 

36 Color palette for palette mapped images 

37 

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

39 :ref:`concept-modes`. Must be "RGB", "RGBA" or "CMYK". Defaults to "RGB" 

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

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

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

43 Defaults to an empty palette. 

44 """ 

45 

46 def __init__( 

47 self, 

48 mode: str = "RGB", 

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

50 ) -> None: 

51 if mode not in {"RGB", "RGBA", "CMYK"}: 

52 msg = "unsupported palette mode" 

53 raise ValueError(msg) 

54 

55 self.mode = mode 

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

57 self.palette = palette or bytearray() 

58 self.dirty: int | None = None 

59 

60 @property 

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

62 return self._palette 

63 

64 @palette.setter 

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

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

67 self._palette = palette 

68 

69 @property 

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

71 if self._colors is None: 

72 palette = self.palette 

73 if self.rawmode and self.rawmode != self.mode: 

74 from . import Image 

75 

76 im = Image.core.new("P", (0, 0)) 

77 im.putpalette(self.mode, self.rawmode, bytes(palette)) 

78 palette = im.getpalette(self.mode) 

79 

80 mode_len = len(self.mode) 

81 self._colors = {} 

82 for i in range(0, len(palette), mode_len): 

83 color = tuple(palette[i : i + mode_len]) 

84 if color in self._colors: 

85 continue 

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

87 return self._colors 

88 

89 @colors.setter 

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

91 self._colors = colors 

92 

93 def copy(self) -> ImagePalette: 

94 new = ImagePalette() 

95 

96 new.mode = self.mode 

97 new.rawmode = self.rawmode 

98 if self.palette is not None: 

99 new.palette = self.palette[:] 

100 new.dirty = self.dirty 

101 

102 return new 

103 

104 def _tobytes(self) -> bytes: 

105 if isinstance(self.palette, bytes): 

106 return self.palette 

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

108 return arr.tobytes() 

109 

110 def getdata(self) -> tuple[str, bytes]: 

111 """ 

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

113 ``im.putpalette`` primitive. 

114 

115 .. warning:: This method is experimental. 

116 """ 

117 if self.rawmode: 

118 return self.rawmode, self._tobytes() 

119 return self.mode, self.tobytes() 

120 

121 def tobytes(self) -> bytes: 

122 """Convert palette to bytes. 

123 

124 .. warning:: This method is experimental. 

125 """ 

126 if self.rawmode: 

127 msg = "palette contains raw palette data" 

128 raise ValueError(msg) 

129 return self._tobytes() 

130 

131 # Declare tostring as an alias for tobytes 

132 tostring = tobytes 

133 

134 def _new_color_index( 

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

136 ) -> int: 

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

138 self._palette = bytearray(self.palette) 

139 index = len(self.palette) // len(self.mode) 

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

141 if image: 

142 special_colors = ( 

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

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

145 ) 

146 assert isinstance(self._palette, bytearray) 

147 while index in special_colors: 

148 # Background or transparency index points past the end of the palette. 

149 # Set it to black, so that the new color can be written afterwards. 

150 self._palette += bytearray(len(self.mode)) 

151 index += 1 

152 if index >= 256: 

153 if image: 

154 # Search for an unused index 

155 for i, count in reversed(list(enumerate(image.histogram()[:256]))): 

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

157 index = i 

158 break 

159 if index >= 256: 

160 msg = "cannot allocate more than 256 colors" 

161 raise ValueError(msg) from e 

162 return index 

163 

164 def getcolor( 

165 self, 

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

167 image: Image.Image | None = None, 

168 ) -> int: 

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

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(color, tuple): 

177 if self.mode == "RGB": 

178 if len(color) == 4: 

179 if color[3] != 255: 

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

181 raise ValueError(msg) 

182 color = color[:3] 

183 elif self.mode == "RGBA": 

184 if len(color) == 3: 

185 color += (255,) 

186 try: 

187 return self.colors[color] 

188 except KeyError as e: 

189 # allocate new color slot 

190 index = self._new_color_index(image, e) 

191 assert isinstance(self._palette, bytearray) 

192 mode_len = len(self.mode) 

193 if index * mode_len < len(self.palette): 

194 self._palette = ( 

195 self._palette[: index * mode_len] 

196 + bytes(color) 

197 + self._palette[index * mode_len + mode_len :] 

198 ) 

199 self._colors = None 

200 else: 

201 self._palette += bytes(color) 

202 self.colors[color] = index 

203 self.dirty = 1 

204 return index 

205 else: 

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

207 raise ValueError(msg) 

208 

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

210 """Save palette to text file. 

211 

212 .. warning:: This method is experimental. 

213 """ 

214 if self.rawmode: 

215 msg = "palette contains raw palette data" 

216 raise ValueError(msg) 

217 open_fp = False 

218 if isinstance(fp, str): 

219 fp = open(fp, "w") 

220 open_fp = True 

221 try: 

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

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

224 palette_len = len(self.palette) 

225 for i in range(256): 

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

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

228 fp.write(f" {self.palette[j] if j < palette_len else 0}") 

229 fp.write("\n") 

230 finally: 

231 if open_fp: 

232 fp.close() 

233 

234 

235# -------------------------------------------------------------------- 

236# Internal 

237 

238 

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

240 palette = ImagePalette() 

241 if rawmode != "RGB": 

242 palette.rawmode = rawmode 

243 palette.palette = data 

244 palette.dirty = 1 

245 return palette 

246 

247 

248# -------------------------------------------------------------------- 

249# Factories 

250 

251 

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

253 if black == 0: 

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

255 

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

257 raise NotImplementedError(msg) # FIXME 

258 

259 

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

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

262 

263 

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

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

266 palette.reverse() 

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

268 

269 

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

271 from random import randint 

272 

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

274 return ImagePalette(mode, palette) 

275 

276 

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

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

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

280 

281 

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

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

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

285 

286 

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

288 # FIXME: supports GIMP gradients only 

289 

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

291 paletteHandlers: list[ 

292 type[ 

293 GimpPaletteFile.GimpPaletteFile 

294 | GimpGradientFile.GimpGradientFile 

295 | PaletteFile.PaletteFile 

296 ] 

297 ] = [ 

298 GimpPaletteFile.GimpPaletteFile, 

299 GimpGradientFile.GimpGradientFile, 

300 PaletteFile.PaletteFile, 

301 ] 

302 for paletteHandler in paletteHandlers: 

303 try: 

304 fp.seek(0) 

305 lut = paletteHandler(fp).getpalette() 

306 if lut: 

307 break 

308 except (SyntaxError, ValueError): 

309 pass 

310 else: 

311 msg = "cannot load palette" 

312 raise OSError(msg) 

313 

314 return lut # data, rawmode