Spaces:
Paused
Paused
| # | |
| # The Python Imaging Library. | |
| # $Id$ | |
| # | |
| # image palette object | |
| # | |
| # History: | |
| # 1996-03-11 fl Rewritten. | |
| # 1997-01-03 fl Up and running. | |
| # 1997-08-23 fl Added load hack | |
| # 2001-04-16 fl Fixed randint shadow bug in random() | |
| # | |
| # Copyright (c) 1997-2001 by Secret Labs AB | |
| # Copyright (c) 1996-1997 by Fredrik Lundh | |
| # | |
| # See the README file for information on usage and redistribution. | |
| # | |
| from __future__ import annotations | |
| import array | |
| from typing import IO, TYPE_CHECKING, Sequence | |
| from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile | |
| if TYPE_CHECKING: | |
| from . import Image | |
| class ImagePalette: | |
| """ | |
| Color palette for palette mapped images | |
| :param mode: The mode to use for the palette. See: | |
| :ref:`concept-modes`. Defaults to "RGB" | |
| :param palette: An optional palette. If given, it must be a bytearray, | |
| an array or a list of ints between 0-255. The list must consist of | |
| all channels for one color followed by the next color (e.g. RGBRGBRGB). | |
| Defaults to an empty palette. | |
| """ | |
| def __init__( | |
| self, | |
| mode: str = "RGB", | |
| palette: Sequence[int] | bytes | bytearray | None = None, | |
| ) -> None: | |
| self.mode = mode | |
| self.rawmode: str | None = None # if set, palette contains raw data | |
| self.palette = palette or bytearray() | |
| self.dirty: int | None = None | |
| def palette(self) -> Sequence[int] | bytes | bytearray: | |
| return self._palette | |
| def palette(self, palette: Sequence[int] | bytes | bytearray) -> None: | |
| self._colors: dict[tuple[int, ...], int] | None = None | |
| self._palette = palette | |
| def colors(self) -> dict[tuple[int, ...], int]: | |
| if self._colors is None: | |
| mode_len = len(self.mode) | |
| self._colors = {} | |
| for i in range(0, len(self.palette), mode_len): | |
| color = tuple(self.palette[i : i + mode_len]) | |
| if color in self._colors: | |
| continue | |
| self._colors[color] = i // mode_len | |
| return self._colors | |
| def colors(self, colors: dict[tuple[int, ...], int]) -> None: | |
| self._colors = colors | |
| def copy(self) -> ImagePalette: | |
| new = ImagePalette() | |
| new.mode = self.mode | |
| new.rawmode = self.rawmode | |
| if self.palette is not None: | |
| new.palette = self.palette[:] | |
| new.dirty = self.dirty | |
| return new | |
| def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]: | |
| """ | |
| Get palette contents in format suitable for the low-level | |
| ``im.putpalette`` primitive. | |
| .. warning:: This method is experimental. | |
| """ | |
| if self.rawmode: | |
| return self.rawmode, self.palette | |
| return self.mode, self.tobytes() | |
| def tobytes(self) -> bytes: | |
| """Convert palette to bytes. | |
| .. warning:: This method is experimental. | |
| """ | |
| if self.rawmode: | |
| msg = "palette contains raw palette data" | |
| raise ValueError(msg) | |
| if isinstance(self.palette, bytes): | |
| return self.palette | |
| arr = array.array("B", self.palette) | |
| return arr.tobytes() | |
| # Declare tostring as an alias for tobytes | |
| tostring = tobytes | |
| def _new_color_index( | |
| self, image: Image.Image | None = None, e: Exception | None = None | |
| ) -> int: | |
| if not isinstance(self.palette, bytearray): | |
| self._palette = bytearray(self.palette) | |
| index = len(self.palette) // 3 | |
| special_colors: tuple[int | tuple[int, ...] | None, ...] = () | |
| if image: | |
| special_colors = ( | |
| image.info.get("background"), | |
| image.info.get("transparency"), | |
| ) | |
| while index in special_colors: | |
| index += 1 | |
| if index >= 256: | |
| if image: | |
| # Search for an unused index | |
| for i, count in reversed(list(enumerate(image.histogram()))): | |
| if count == 0 and i not in special_colors: | |
| index = i | |
| break | |
| if index >= 256: | |
| msg = "cannot allocate more than 256 colors" | |
| raise ValueError(msg) from e | |
| return index | |
| def getcolor( | |
| self, | |
| color: tuple[int, ...], | |
| image: Image.Image | None = None, | |
| ) -> int: | |
| """Given an rgb tuple, allocate palette entry. | |
| .. warning:: This method is experimental. | |
| """ | |
| if self.rawmode: | |
| msg = "palette contains raw palette data" | |
| raise ValueError(msg) | |
| if isinstance(color, tuple): | |
| if self.mode == "RGB": | |
| if len(color) == 4: | |
| if color[3] != 255: | |
| msg = "cannot add non-opaque RGBA color to RGB palette" | |
| raise ValueError(msg) | |
| color = color[:3] | |
| elif self.mode == "RGBA": | |
| if len(color) == 3: | |
| color += (255,) | |
| try: | |
| return self.colors[color] | |
| except KeyError as e: | |
| # allocate new color slot | |
| index = self._new_color_index(image, e) | |
| assert isinstance(self._palette, bytearray) | |
| self.colors[color] = index | |
| if index * 3 < len(self.palette): | |
| self._palette = ( | |
| self._palette[: index * 3] | |
| + bytes(color) | |
| + self._palette[index * 3 + 3 :] | |
| ) | |
| else: | |
| self._palette += bytes(color) | |
| self.dirty = 1 | |
| return index | |
| else: | |
| msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable] | |
| raise ValueError(msg) | |
| def save(self, fp: str | IO[str]) -> None: | |
| """Save palette to text file. | |
| .. warning:: This method is experimental. | |
| """ | |
| if self.rawmode: | |
| msg = "palette contains raw palette data" | |
| raise ValueError(msg) | |
| if isinstance(fp, str): | |
| fp = open(fp, "w") | |
| fp.write("# Palette\n") | |
| fp.write(f"# Mode: {self.mode}\n") | |
| for i in range(256): | |
| fp.write(f"{i}") | |
| for j in range(i * len(self.mode), (i + 1) * len(self.mode)): | |
| try: | |
| fp.write(f" {self.palette[j]}") | |
| except IndexError: | |
| fp.write(" 0") | |
| fp.write("\n") | |
| fp.close() | |
| # -------------------------------------------------------------------- | |
| # Internal | |
| def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: | |
| palette = ImagePalette() | |
| palette.rawmode = rawmode | |
| palette.palette = data | |
| palette.dirty = 1 | |
| return palette | |
| # -------------------------------------------------------------------- | |
| # Factories | |
| def make_linear_lut(black: int, white: float) -> list[int]: | |
| if black == 0: | |
| return [int(white * i // 255) for i in range(256)] | |
| msg = "unavailable when black is non-zero" | |
| raise NotImplementedError(msg) # FIXME | |
| def make_gamma_lut(exp: float) -> list[int]: | |
| return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] | |
| def negative(mode: str = "RGB") -> ImagePalette: | |
| palette = list(range(256 * len(mode))) | |
| palette.reverse() | |
| return ImagePalette(mode, [i // len(mode) for i in palette]) | |
| def random(mode: str = "RGB") -> ImagePalette: | |
| from random import randint | |
| palette = [randint(0, 255) for _ in range(256 * len(mode))] | |
| return ImagePalette(mode, palette) | |
| def sepia(white: str = "#fff0c0") -> ImagePalette: | |
| bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] | |
| return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) | |
| def wedge(mode: str = "RGB") -> ImagePalette: | |
| palette = list(range(256 * len(mode))) | |
| return ImagePalette(mode, [i // len(mode) for i in palette]) | |
| def load(filename: str) -> tuple[bytes, str]: | |
| # FIXME: supports GIMP gradients only | |
| with open(filename, "rb") as fp: | |
| paletteHandlers: list[ | |
| type[ | |
| GimpPaletteFile.GimpPaletteFile | |
| | GimpGradientFile.GimpGradientFile | |
| | PaletteFile.PaletteFile | |
| ] | |
| ] = [ | |
| GimpPaletteFile.GimpPaletteFile, | |
| GimpGradientFile.GimpGradientFile, | |
| PaletteFile.PaletteFile, | |
| ] | |
| for paletteHandler in paletteHandlers: | |
| try: | |
| fp.seek(0) | |
| lut = paletteHandler(fp).getpalette() | |
| if lut: | |
| break | |
| except (SyntaxError, ValueError): | |
| pass | |
| else: | |
| msg = "cannot load palette" | |
| raise OSError(msg) | |
| return lut # data, rawmode | |