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

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

172 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 

23 

24from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile 

25 

26TYPE_CHECKING = False 

27if TYPE_CHECKING: 

28 from . import Image 

29 

30 

31class ImagePalette: 

32 """ 

33 Color palette for palette mapped images 

34 

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

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

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

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

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

40 Defaults to an empty palette. 

41 """ 

42 

43 def __init__( 

44 self, 

45 mode: str = "RGB", 

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

47 ) -> None: 

48 self.mode = mode 

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

50 self.palette = palette or bytearray() 

51 self.dirty: int | None = None 

52 

53 @property 

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

55 return self._palette 

56 

57 @palette.setter 

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

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

60 self._palette = palette 

61 

62 @property 

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

64 if self._colors is None: 

65 palette = self.palette 

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

67 from . import Image 

68 

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

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

71 palette = im.getpalette(self.mode) 

72 

73 mode_len = len(self.mode) 

74 self._colors = {} 

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

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

77 if color in self._colors: 

78 continue 

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

80 return self._colors 

81 

82 @colors.setter 

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

84 self._colors = colors 

85 

86 def copy(self) -> ImagePalette: 

87 new = ImagePalette() 

88 

89 new.mode = self.mode 

90 new.rawmode = self.rawmode 

91 if self.palette is not None: 

92 new.palette = self.palette[:] 

93 new.dirty = self.dirty 

94 

95 return new 

96 

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

98 """ 

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

100 ``im.putpalette`` primitive. 

101 

102 .. warning:: This method is experimental. 

103 """ 

104 if self.rawmode: 

105 return self.rawmode, self.palette 

106 return self.mode, self.tobytes() 

107 

108 def tobytes(self) -> bytes: 

109 """Convert palette to bytes. 

110 

111 .. warning:: This method is experimental. 

112 """ 

113 if self.rawmode: 

114 msg = "palette contains raw palette data" 

115 raise ValueError(msg) 

116 if isinstance(self.palette, bytes): 

117 return self.palette 

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

119 return arr.tobytes() 

120 

121 # Declare tostring as an alias for tobytes 

122 tostring = tobytes 

123 

124 def _new_color_index( 

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

126 ) -> int: 

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

128 self._palette = bytearray(self.palette) 

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

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

131 if image: 

132 special_colors = ( 

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

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

135 ) 

136 assert isinstance(self._palette, bytearray) 

137 while index in special_colors: 

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

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

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

141 index += 1 

142 if index >= 256: 

143 if image: 

144 # Search for an unused index 

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

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

147 index = i 

148 break 

149 if index >= 256: 

150 msg = "cannot allocate more than 256 colors" 

151 raise ValueError(msg) from e 

152 return index 

153 

154 def getcolor( 

155 self, 

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

157 image: Image.Image | None = None, 

158 ) -> int: 

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

160 

161 .. warning:: This method is experimental. 

162 """ 

163 if self.rawmode: 

164 msg = "palette contains raw palette data" 

165 raise ValueError(msg) 

166 if isinstance(color, tuple): 

167 if self.mode == "RGB": 

168 if len(color) == 4: 

169 if color[3] != 255: 

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

171 raise ValueError(msg) 

172 color = color[:3] 

173 elif self.mode == "RGBA": 

174 if len(color) == 3: 

175 color += (255,) 

176 try: 

177 return self.colors[color] 

178 except KeyError as e: 

179 # allocate new color slot 

180 index = self._new_color_index(image, e) 

181 assert isinstance(self._palette, bytearray) 

182 self.colors[color] = index 

183 mode_len = len(self.mode) 

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

185 self._palette = ( 

186 self._palette[: index * mode_len] 

187 + bytes(color) 

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

189 ) 

190 else: 

191 self._palette += bytes(color) 

192 self.dirty = 1 

193 return index 

194 else: 

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

196 raise ValueError(msg) 

197 

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

199 """Save palette to text file. 

200 

201 .. warning:: This method is experimental. 

202 """ 

203 if self.rawmode: 

204 msg = "palette contains raw palette data" 

205 raise ValueError(msg) 

206 open_fp = False 

207 if isinstance(fp, str): 

208 fp = open(fp, "w") 

209 open_fp = True 

210 try: 

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

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

213 palette_len = len(self.palette) 

214 for i in range(256): 

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

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

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

218 fp.write("\n") 

219 finally: 

220 if open_fp: 

221 fp.close() 

222 

223 

224# -------------------------------------------------------------------- 

225# Internal 

226 

227 

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

229 palette = ImagePalette() 

230 palette.rawmode = rawmode 

231 palette.palette = data 

232 palette.dirty = 1 

233 return palette 

234 

235 

236# -------------------------------------------------------------------- 

237# Factories 

238 

239 

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

241 if black == 0: 

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

243 

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

245 raise NotImplementedError(msg) # FIXME 

246 

247 

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

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

250 

251 

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

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

254 palette.reverse() 

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

256 

257 

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

259 from random import randint 

260 

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

262 return ImagePalette(mode, palette) 

263 

264 

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

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

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

268 

269 

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

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

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

273 

274 

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

276 # FIXME: supports GIMP gradients only 

277 

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

279 paletteHandlers: list[ 

280 type[ 

281 GimpPaletteFile.GimpPaletteFile 

282 | GimpGradientFile.GimpGradientFile 

283 | PaletteFile.PaletteFile 

284 ] 

285 ] = [ 

286 GimpPaletteFile.GimpPaletteFile, 

287 GimpGradientFile.GimpGradientFile, 

288 PaletteFile.PaletteFile, 

289 ] 

290 for paletteHandler in paletteHandlers: 

291 try: 

292 fp.seek(0) 

293 lut = paletteHandler(fp).getpalette() 

294 if lut: 

295 break 

296 except (SyntaxError, ValueError): 

297 pass 

298 else: 

299 msg = "cannot load palette" 

300 raise OSError(msg) 

301 

302 return lut # data, rawmode