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
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
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
20import array
21from collections.abc import Sequence
22from typing import IO
24from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
26TYPE_CHECKING = False
27if TYPE_CHECKING:
28 from . import Image
31class ImagePalette:
32 """
33 Color palette for palette mapped images
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 """
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
53 @property
54 def palette(self) -> Sequence[int] | bytes | bytearray:
55 return self._palette
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
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
69 im = Image.core.new("P", (0, 0))
70 im.putpalette(self.mode, self.rawmode, bytes(palette))
71 palette = im.getpalette(self.mode)
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
82 @colors.setter
83 def colors(self, colors: dict[tuple[int, ...], int]) -> None:
84 self._colors = colors
86 def copy(self) -> ImagePalette:
87 new = ImagePalette()
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
95 return new
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.
102 .. warning:: This method is experimental.
103 """
104 if self.rawmode:
105 return self.rawmode, self.palette
106 return self.mode, self.tobytes()
108 def tobytes(self) -> bytes:
109 """Convert palette to bytes.
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()
121 # Declare tostring as an alias for tobytes
122 tostring = tobytes
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
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.
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)
198 def save(self, fp: str | IO[str]) -> None:
199 """Save palette to text file.
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()
224# --------------------------------------------------------------------
225# Internal
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
236# --------------------------------------------------------------------
237# Factories
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)]
244 msg = "unavailable when black is non-zero"
245 raise NotImplementedError(msg) # FIXME
248def make_gamma_lut(exp: float) -> list[int]:
249 return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
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])
258def random(mode: str = "RGB") -> ImagePalette:
259 from random import randint
261 palette = [randint(0, 255) for _ in range(256 * len(mode))]
262 return ImagePalette(mode, palette)
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)])
270def wedge(mode: str = "RGB") -> ImagePalette:
271 palette = list(range(256 * len(mode)))
272 return ImagePalette(mode, [i // len(mode) for i in palette])
275def load(filename: str) -> tuple[bytes, str]:
276 # FIXME: supports GIMP gradients only
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)
302 return lut # data, rawmode