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 mode_len = len(self.mode)
66 self._colors = {}
67 for i in range(0, len(self.palette), mode_len):
68 color = tuple(self.palette[i : i + mode_len])
69 if color in self._colors:
70 continue
71 self._colors[color] = i // mode_len
72 return self._colors
73
74 @colors.setter
75 def colors(self, colors: dict[tuple[int, ...], int]) -> None:
76 self._colors = colors
77
78 def copy(self) -> ImagePalette:
79 new = ImagePalette()
80
81 new.mode = self.mode
82 new.rawmode = self.rawmode
83 if self.palette is not None:
84 new.palette = self.palette[:]
85 new.dirty = self.dirty
86
87 return new
88
89 def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]:
90 """
91 Get palette contents in format suitable for the low-level
92 ``im.putpalette`` primitive.
93
94 .. warning:: This method is experimental.
95 """
96 if self.rawmode:
97 return self.rawmode, self.palette
98 return self.mode, self.tobytes()
99
100 def tobytes(self) -> bytes:
101 """Convert palette to bytes.
102
103 .. warning:: This method is experimental.
104 """
105 if self.rawmode:
106 msg = "palette contains raw palette data"
107 raise ValueError(msg)
108 if isinstance(self.palette, bytes):
109 return self.palette
110 arr = array.array("B", self.palette)
111 return arr.tobytes()
112
113 # Declare tostring as an alias for tobytes
114 tostring = tobytes
115
116 def _new_color_index(
117 self, image: Image.Image | None = None, e: Exception | None = None
118 ) -> int:
119 if not isinstance(self.palette, bytearray):
120 self._palette = bytearray(self.palette)
121 index = len(self.palette) // 3
122 special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
123 if image:
124 special_colors = (
125 image.info.get("background"),
126 image.info.get("transparency"),
127 )
128 while index in special_colors:
129 index += 1
130 if index >= 256:
131 if image:
132 # Search for an unused index
133 for i, count in reversed(list(enumerate(image.histogram()))):
134 if count == 0 and i not in special_colors:
135 index = i
136 break
137 if index >= 256:
138 msg = "cannot allocate more than 256 colors"
139 raise ValueError(msg) from e
140 return index
141
142 def getcolor(
143 self,
144 color: tuple[int, ...],
145 image: Image.Image | None = None,
146 ) -> int:
147 """Given an rgb tuple, allocate palette entry.
148
149 .. warning:: This method is experimental.
150 """
151 if self.rawmode:
152 msg = "palette contains raw palette data"
153 raise ValueError(msg)
154 if isinstance(color, tuple):
155 if self.mode == "RGB":
156 if len(color) == 4:
157 if color[3] != 255:
158 msg = "cannot add non-opaque RGBA color to RGB palette"
159 raise ValueError(msg)
160 color = color[:3]
161 elif self.mode == "RGBA":
162 if len(color) == 3:
163 color += (255,)
164 try:
165 return self.colors[color]
166 except KeyError as e:
167 # allocate new color slot
168 index = self._new_color_index(image, e)
169 assert isinstance(self._palette, bytearray)
170 self.colors[color] = index
171 if index * 3 < len(self.palette):
172 self._palette = (
173 self._palette[: index * 3]
174 + bytes(color)
175 + self._palette[index * 3 + 3 :]
176 )
177 else:
178 self._palette += bytes(color)
179 self.dirty = 1
180 return index
181 else:
182 msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable]
183 raise ValueError(msg)
184
185 def save(self, fp: str | IO[str]) -> None:
186 """Save palette to text file.
187
188 .. warning:: This method is experimental.
189 """
190 if self.rawmode:
191 msg = "palette contains raw palette data"
192 raise ValueError(msg)
193 if isinstance(fp, str):
194 fp = open(fp, "w")
195 fp.write("# Palette\n")
196 fp.write(f"# Mode: {self.mode}\n")
197 for i in range(256):
198 fp.write(f"{i}")
199 for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
200 try:
201 fp.write(f" {self.palette[j]}")
202 except IndexError:
203 fp.write(" 0")
204 fp.write("\n")
205 fp.close()
206
207
208# --------------------------------------------------------------------
209# Internal
210
211
212def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
213 palette = ImagePalette()
214 palette.rawmode = rawmode
215 palette.palette = data
216 palette.dirty = 1
217 return palette
218
219
220# --------------------------------------------------------------------
221# Factories
222
223
224def make_linear_lut(black: int, white: float) -> list[int]:
225 if black == 0:
226 return [int(white * i // 255) for i in range(256)]
227
228 msg = "unavailable when black is non-zero"
229 raise NotImplementedError(msg) # FIXME
230
231
232def make_gamma_lut(exp: float) -> list[int]:
233 return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
234
235
236def negative(mode: str = "RGB") -> ImagePalette:
237 palette = list(range(256 * len(mode)))
238 palette.reverse()
239 return ImagePalette(mode, [i // len(mode) for i in palette])
240
241
242def random(mode: str = "RGB") -> ImagePalette:
243 from random import randint
244
245 palette = [randint(0, 255) for _ in range(256 * len(mode))]
246 return ImagePalette(mode, palette)
247
248
249def sepia(white: str = "#fff0c0") -> ImagePalette:
250 bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
251 return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
252
253
254def wedge(mode: str = "RGB") -> ImagePalette:
255 palette = list(range(256 * len(mode)))
256 return ImagePalette(mode, [i // len(mode) for i in palette])
257
258
259def load(filename: str) -> tuple[bytes, str]:
260 # FIXME: supports GIMP gradients only
261
262 with open(filename, "rb") as fp:
263 paletteHandlers: list[
264 type[
265 GimpPaletteFile.GimpPaletteFile
266 | GimpGradientFile.GimpGradientFile
267 | PaletteFile.PaletteFile
268 ]
269 ] = [
270 GimpPaletteFile.GimpPaletteFile,
271 GimpGradientFile.GimpGradientFile,
272 PaletteFile.PaletteFile,
273 ]
274 for paletteHandler in paletteHandlers:
275 try:
276 fp.seek(0)
277 lut = paletteHandler(fp).getpalette()
278 if lut:
279 break
280 except (SyntaxError, ValueError):
281 pass
282 else:
283 msg = "cannot load palette"
284 raise OSError(msg)
285
286 return lut # data, rawmode