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