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