Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/ImagePalette.py: 53%
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
20__lazy_modules__ = {"array"}
22import array
24from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
26TYPE_CHECKING = False
27if TYPE_CHECKING:
28 from collections.abc import Sequence
29 from typing import IO
31 from . import Image
34class ImagePalette:
35 """
36 Color palette for palette mapped images
38 :param mode: The mode to use for the palette. See:
39 :ref:`concept-modes`. Must be "RGB", "RGBA" or "CMYK". Defaults to "RGB"
40 :param palette: An optional palette. If given, it must be a bytearray,
41 an array or a list of ints between 0-255. The list must consist of
42 all channels for one color followed by the next color (e.g. RGBRGBRGB).
43 Defaults to an empty palette.
44 """
46 def __init__(
47 self,
48 mode: str = "RGB",
49 palette: Sequence[int] | bytes | bytearray | None = None,
50 ) -> None:
51 if mode not in {"RGB", "RGBA", "CMYK"}:
52 msg = "unsupported palette mode"
53 raise ValueError(msg)
55 self.mode = mode
56 self.rawmode: str | None = None # if set, palette contains raw data
57 self.palette = palette or bytearray()
58 self.dirty: int | None = None
60 @property
61 def palette(self) -> Sequence[int] | bytes | bytearray:
62 return self._palette
64 @palette.setter
65 def palette(self, palette: Sequence[int] | bytes | bytearray) -> None:
66 self._colors: dict[tuple[int, ...], int] | None = None
67 self._palette = palette
69 @property
70 def colors(self) -> dict[tuple[int, ...], int]:
71 if self._colors is None:
72 palette = self.palette
73 if self.rawmode and self.rawmode != self.mode:
74 from . import Image
76 im = Image.core.new("P", (0, 0))
77 im.putpalette(self.mode, self.rawmode, bytes(palette))
78 palette = im.getpalette(self.mode)
80 mode_len = len(self.mode)
81 self._colors = {}
82 for i in range(0, len(palette), mode_len):
83 color = tuple(palette[i : i + mode_len])
84 if color in self._colors:
85 continue
86 self._colors[color] = i // mode_len
87 return self._colors
89 @colors.setter
90 def colors(self, colors: dict[tuple[int, ...], int]) -> None:
91 self._colors = colors
93 def copy(self) -> ImagePalette:
94 new = ImagePalette()
96 new.mode = self.mode
97 new.rawmode = self.rawmode
98 if self.palette is not None:
99 new.palette = self.palette[:]
100 new.dirty = self.dirty
102 return new
104 def _tobytes(self) -> bytes:
105 if isinstance(self.palette, bytes):
106 return self.palette
107 arr = array.array("B", self.palette)
108 return arr.tobytes()
110 def getdata(self) -> tuple[str, bytes]:
111 """
112 Get palette contents in format suitable for the low-level
113 ``im.putpalette`` primitive.
115 .. warning:: This method is experimental.
116 """
117 if self.rawmode:
118 return self.rawmode, self._tobytes()
119 return self.mode, self.tobytes()
121 def tobytes(self) -> bytes:
122 """Convert palette to bytes.
124 .. warning:: This method is experimental.
125 """
126 if self.rawmode:
127 msg = "palette contains raw palette data"
128 raise ValueError(msg)
129 return self._tobytes()
131 # Declare tostring as an alias for tobytes
132 tostring = tobytes
134 def _new_color_index(
135 self, image: Image.Image | None = None, e: Exception | None = None
136 ) -> int:
137 if not isinstance(self.palette, bytearray):
138 self._palette = bytearray(self.palette)
139 index = len(self.palette) // len(self.mode)
140 special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
141 if image:
142 special_colors = (
143 image.info.get("background"),
144 image.info.get("transparency"),
145 )
146 assert isinstance(self._palette, bytearray)
147 while index in special_colors:
148 # Background or transparency index points past the end of the palette.
149 # Set it to black, so that the new color can be written afterwards.
150 self._palette += bytearray(len(self.mode))
151 index += 1
152 if index >= 256:
153 if image:
154 # Search for an unused index
155 for i, count in reversed(list(enumerate(image.histogram()[:256]))):
156 if count == 0 and i not in special_colors:
157 index = i
158 break
159 if index >= 256:
160 msg = "cannot allocate more than 256 colors"
161 raise ValueError(msg) from e
162 return index
164 def getcolor(
165 self,
166 color: tuple[int, ...],
167 image: Image.Image | None = None,
168 ) -> int:
169 """Given an rgb tuple, allocate palette entry.
171 .. warning:: This method is experimental.
172 """
173 if self.rawmode:
174 msg = "palette contains raw palette data"
175 raise ValueError(msg)
176 if isinstance(color, tuple):
177 if self.mode == "RGB":
178 if len(color) == 4:
179 if color[3] != 255:
180 msg = "cannot add non-opaque RGBA color to RGB palette"
181 raise ValueError(msg)
182 color = color[:3]
183 elif self.mode == "RGBA":
184 if len(color) == 3:
185 color += (255,)
186 try:
187 return self.colors[color]
188 except KeyError as e:
189 # allocate new color slot
190 index = self._new_color_index(image, e)
191 assert isinstance(self._palette, bytearray)
192 mode_len = len(self.mode)
193 if index * mode_len < len(self.palette):
194 self._palette = (
195 self._palette[: index * mode_len]
196 + bytes(color)
197 + self._palette[index * mode_len + mode_len :]
198 )
199 self._colors = None
200 else:
201 self._palette += bytes(color)
202 self.colors[color] = index
203 self.dirty = 1
204 return index
205 else:
206 msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable]
207 raise ValueError(msg)
209 def save(self, fp: str | IO[str]) -> None:
210 """Save palette to text file.
212 .. warning:: This method is experimental.
213 """
214 if self.rawmode:
215 msg = "palette contains raw palette data"
216 raise ValueError(msg)
217 open_fp = False
218 if isinstance(fp, str):
219 fp = open(fp, "w")
220 open_fp = True
221 try:
222 fp.write("# Palette\n")
223 fp.write(f"# Mode: {self.mode}\n")
224 palette_len = len(self.palette)
225 for i in range(256):
226 fp.write(f"{i}")
227 for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
228 fp.write(f" {self.palette[j] if j < palette_len else 0}")
229 fp.write("\n")
230 finally:
231 if open_fp:
232 fp.close()
235# --------------------------------------------------------------------
236# Internal
239def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
240 palette = ImagePalette()
241 if rawmode != "RGB":
242 palette.rawmode = rawmode
243 palette.palette = data
244 palette.dirty = 1
245 return palette
248# --------------------------------------------------------------------
249# Factories
252def make_linear_lut(black: int, white: float) -> list[int]:
253 if black == 0:
254 return [int(white * i // 255) for i in range(256)]
256 msg = "unavailable when black is non-zero"
257 raise NotImplementedError(msg) # FIXME
260def make_gamma_lut(exp: float) -> list[int]:
261 return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
264def negative(mode: str = "RGB") -> ImagePalette:
265 palette = list(range(256 * len(mode)))
266 palette.reverse()
267 return ImagePalette(mode, [i // len(mode) for i in palette])
270def random(mode: str = "RGB") -> ImagePalette:
271 from random import randint
273 palette = [randint(0, 255) for _ in range(256 * len(mode))]
274 return ImagePalette(mode, palette)
277def sepia(white: str = "#fff0c0") -> ImagePalette:
278 bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
279 return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
282def wedge(mode: str = "RGB") -> ImagePalette:
283 palette = list(range(256 * len(mode)))
284 return ImagePalette(mode, [i // len(mode) for i in palette])
287def load(filename: str) -> tuple[bytes, str]:
288 # FIXME: supports GIMP gradients only
290 with open(filename, "rb") as fp:
291 paletteHandlers: list[
292 type[
293 GimpPaletteFile.GimpPaletteFile
294 | GimpGradientFile.GimpGradientFile
295 | PaletteFile.PaletteFile
296 ]
297 ] = [
298 GimpPaletteFile.GimpPaletteFile,
299 GimpGradientFile.GimpGradientFile,
300 PaletteFile.PaletteFile,
301 ]
302 for paletteHandler in paletteHandlers:
303 try:
304 fp.seek(0)
305 lut = paletteHandler(fp).getpalette()
306 if lut:
307 break
308 except (SyntaxError, ValueError):
309 pass
310 else:
311 msg = "cannot load palette"
312 raise OSError(msg)
314 return lut # data, rawmode