Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
ImagePalette.py291 linesDownload Raw Back to PIL
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
23
24from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
25
26TYPE_CHECKING = False
27if TYPE_CHECKING:
28    from . import Image
29
30
31class ImagePalette:
32    """
33    Color palette for palette mapped images
34
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    """
42
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
52
53    @property
54    def palette(self) -> Sequence[int] | bytes | bytearray:
55        return self._palette
56
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
61
62    @property
63    def colors(self) -> dict[tuple[int, ...], int]:
64        if self._colors is None:
65            mode_len = len(self.mode)
66            self._colors = {}
67            for i in range(0, len(self.palette), mode_len):
68                color = tuple(self.palette[i : i + mode_len])
69                if color in self._colors:
70                    continue
71                self._colors[color] = i // mode_len
72        return self._colors
73
74    @colors.setter
75    def colors(self, colors: dict[tuple[int, ...], int]) -> None:
76        self._colors = colors
77
78    def copy(self) -> ImagePalette:
79        new = ImagePalette()
80
81        new.mode = self.mode
82        new.rawmode = self.rawmode
83        if self.palette is not None:
84            new.palette = self.palette[:]
85        new.dirty = self.dirty
86
87        return new
88
89    def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]:
90        """
91        Get palette contents in format suitable for the low-level
92        ``im.putpalette`` primitive.
93
94        .. warning:: This method is experimental.
95        """
96        if self.rawmode:
97            return self.rawmode, self.palette
98        return self.mode, self.tobytes()
99
100    def tobytes(self) -> bytes:
101        """Convert palette to bytes.
102
103        .. warning:: This method is experimental.
104        """
105        if self.rawmode:
106            msg = "palette contains raw palette data"
107            raise ValueError(msg)
108        if isinstance(self.palette, bytes):
109            return self.palette
110        arr = array.array("B", self.palette)
111        return arr.tobytes()
112
113    # Declare tostring as an alias for tobytes
114    tostring = tobytes
115
116    def _new_color_index(
117        self, image: Image.Image | None = None, e: Exception | None = None
118    ) -> int:
119        if not isinstance(self.palette, bytearray):
120            self._palette = bytearray(self.palette)
121        index = len(self.palette) // len(self.mode)
122        special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
123        if image:
124            special_colors = (
125                image.info.get("background"),
126                image.info.get("transparency"),
127            )
128            while index in special_colors:
129                index += 1
130        if index >= 256:
131            if image:
132                # Search for an unused index
133                for i, count in reversed(list(enumerate(image.histogram()))):
134                    if count == 0 and i not in special_colors:
135                        index = i
136                        break
137            if index >= 256:
138                msg = "cannot allocate more than 256 colors"
139                raise ValueError(msg) from e
140        return index
141
142    def getcolor(
143        self,
144        color: tuple[int, ...],
145        image: Image.Image | None = None,
146    ) -> int:
147        """Given an rgb tuple, allocate palette entry.
148
149        .. warning:: This method is experimental.
150        """
151        if self.rawmode:
152            msg = "palette contains raw palette data"
153            raise ValueError(msg)
154        if isinstance(color, tuple):
155            if self.mode == "RGB":
156                if len(color) == 4:
157                    if color[3] != 255:
158                        msg = "cannot add non-opaque RGBA color to RGB palette"
159                        raise ValueError(msg)
160                    color = color[:3]
161            elif self.mode == "RGBA":
162                if len(color) == 3:
163                    color += (255,)
164            try:
165                return self.colors[color]
166            except KeyError as e:
167                # allocate new color slot
168                index = self._new_color_index(image, e)
169                assert isinstance(self._palette, bytearray)
170                self.colors[color] = index
171                mode_len = len(self.mode)
172                if index * mode_len < len(self.palette):
173                    self._palette = (
174                        self._palette[: index * mode_len]
175                        + bytes(color)
176                        + self._palette[index * mode_len + mode_len :]
177                    )
178                else:
179                    self._palette += bytes(color)
180                self.dirty = 1
181                return index
182        else:
183            msg = f"unknown color specifier: {repr(color)}"  # type: ignore[unreachable]
184            raise ValueError(msg)
185
186    def save(self, fp: str | IO[str]) -> None:
187        """Save palette to text file.
188
189        .. warning:: This method is experimental.
190        """
191        if self.rawmode:
192            msg = "palette contains raw palette data"
193            raise ValueError(msg)
194        open_fp = False
195        if isinstance(fp, str):
196            fp = open(fp, "w")
197            open_fp = True
198        try:
199            fp.write("# Palette\n")
200            fp.write(f"# Mode: {self.mode}\n")
201            palette_len = len(self.palette)
202            for i in range(256):
203                fp.write(f"{i}")
204                for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
205                    fp.write(f" {self.palette[j] if j < palette_len else 0}")
206                fp.write("\n")
207        finally:
208            if open_fp:
209                fp.close()
210
211
212# --------------------------------------------------------------------
213# Internal
214
215
216def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
217    palette = ImagePalette()
218    palette.rawmode = rawmode
219    palette.palette = data
220    palette.dirty = 1
221    return palette
222
223
224# --------------------------------------------------------------------
225# Factories
226
227
228def make_linear_lut(black: int, white: float) -> list[int]:
229    if black == 0:
230        return [int(white * i // 255) for i in range(256)]
231
232    msg = "unavailable when black is non-zero"
233    raise NotImplementedError(msg)  # FIXME
234
235
236def make_gamma_lut(exp: float) -> list[int]:
237    return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
238
239
240def negative(mode: str = "RGB") -> ImagePalette:
241    palette = list(range(256 * len(mode)))
242    palette.reverse()
243    return ImagePalette(mode, [i // len(mode) for i in palette])
244
245
246def random(mode: str = "RGB") -> ImagePalette:
247    from random import randint
248
249    palette = [randint(0, 255) for _ in range(256 * len(mode))]
250    return ImagePalette(mode, palette)
251
252
253def sepia(white: str = "#fff0c0") -> ImagePalette:
254    bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
255    return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
256
257
258def wedge(mode: str = "RGB") -> ImagePalette:
259    palette = list(range(256 * len(mode)))
260    return ImagePalette(mode, [i // len(mode) for i in palette])
261
262
263def load(filename: str) -> tuple[bytes, str]:
264    # FIXME: supports GIMP gradients only
265
266    with open(filename, "rb") as fp:
267        paletteHandlers: list[
268            type[
269                GimpPaletteFile.GimpPaletteFile
270                | GimpGradientFile.GimpGradientFile
271                | PaletteFile.PaletteFile
272            ]
273        ] = [
274            GimpPaletteFile.GimpPaletteFile,
275            GimpGradientFile.GimpGradientFile,
276            PaletteFile.PaletteFile,
277        ]
278        for paletteHandler in paletteHandlers:
279            try:
280                fp.seek(0)
281                lut = paletteHandler(fp).getpalette()
282                if lut:
283                    break
284            except (SyntaxError, ValueError):
285                pass
286        else:
287            msg = "cannot load palette"
288            raise OSError(msg)
289
290    return lut  # data, rawmode
291 
codekingpro/portable-devtools · Team Ai