Coverage for /usr/lib/python3/dist-packages/PIL/ImagePalette.py: 18%
153 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
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
22from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
25class ImagePalette:
26 """
27 Color palette for palette mapped images
29 :param mode: The mode to use for the palette. See:
30 :ref:`concept-modes`. Defaults to "RGB"
31 :param palette: An optional palette. If given, it must be a bytearray,
32 an array or a list of ints between 0-255. The list must consist of
33 all channels for one color followed by the next color (e.g. RGBRGBRGB).
34 Defaults to an empty palette.
35 """
37 def __init__(self, mode="RGB", palette=None):
38 self.mode = mode
39 self.rawmode = None # if set, palette contains raw data
40 self.palette = palette or bytearray()
41 self.dirty = None
43 @property
44 def palette(self):
45 return self._palette
47 @palette.setter
48 def palette(self, palette):
49 self._colors = None
50 self._palette = palette
52 @property
53 def colors(self):
54 if self._colors is None:
55 mode_len = len(self.mode)
56 self._colors = {}
57 for i in range(0, len(self.palette), mode_len):
58 color = tuple(self.palette[i : i + mode_len])
59 if color in self._colors:
60 continue
61 self._colors[color] = i // mode_len
62 return self._colors
64 @colors.setter
65 def colors(self, colors):
66 self._colors = colors
68 def copy(self):
69 new = ImagePalette()
71 new.mode = self.mode
72 new.rawmode = self.rawmode
73 if self.palette is not None:
74 new.palette = self.palette[:]
75 new.dirty = self.dirty
77 return new
79 def getdata(self):
80 """
81 Get palette contents in format suitable for the low-level
82 ``im.putpalette`` primitive.
84 .. warning:: This method is experimental.
85 """
86 if self.rawmode:
87 return self.rawmode, self.palette
88 return self.mode, self.tobytes()
90 def tobytes(self):
91 """Convert palette to bytes.
93 .. warning:: This method is experimental.
94 """
95 if self.rawmode:
96 msg = "palette contains raw palette data"
97 raise ValueError(msg)
98 if isinstance(self.palette, bytes):
99 return self.palette
100 arr = array.array("B", self.palette)
101 return arr.tobytes()
103 # Declare tostring as an alias for tobytes
104 tostring = tobytes
106 def _new_color_index(self, image=None, e=None):
107 if not isinstance(self.palette, bytearray):
108 self._palette = bytearray(self.palette)
109 index = len(self.palette) // 3
110 special_colors = ()
111 if image:
112 special_colors = (
113 image.info.get("background"),
114 image.info.get("transparency"),
115 )
116 while index in special_colors:
117 index += 1
118 if index >= 256:
119 if image:
120 # Search for an unused index
121 for i, count in reversed(list(enumerate(image.histogram()))):
122 if count == 0 and i not in special_colors:
123 index = i
124 break
125 if index >= 256:
126 msg = "cannot allocate more than 256 colors"
127 raise ValueError(msg) from e
128 return index
130 def getcolor(self, color, image=None):
131 """Given an rgb tuple, allocate palette entry.
133 .. warning:: This method is experimental.
134 """
135 if self.rawmode:
136 msg = "palette contains raw palette data"
137 raise ValueError(msg)
138 if isinstance(color, tuple):
139 if self.mode == "RGB":
140 if len(color) == 4:
141 if color[3] != 255:
142 msg = "cannot add non-opaque RGBA color to RGB palette"
143 raise ValueError(msg)
144 color = color[:3]
145 elif self.mode == "RGBA":
146 if len(color) == 3:
147 color += (255,)
148 try:
149 return self.colors[color]
150 except KeyError as e:
151 # allocate new color slot
152 index = self._new_color_index(image, e)
153 self.colors[color] = index
154 if index * 3 < len(self.palette):
155 self._palette = (
156 self.palette[: index * 3]
157 + bytes(color)
158 + self.palette[index * 3 + 3 :]
159 )
160 else:
161 self._palette += bytes(color)
162 self.dirty = 1
163 return index
164 else:
165 msg = f"unknown color specifier: {repr(color)}"
166 raise ValueError(msg)
168 def save(self, fp):
169 """Save palette to text file.
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(fp, str):
177 fp = open(fp, "w")
178 fp.write("# Palette\n")
179 fp.write(f"# Mode: {self.mode}\n")
180 for i in range(256):
181 fp.write(f"{i}")
182 for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
183 try:
184 fp.write(f" {self.palette[j]}")
185 except IndexError:
186 fp.write(" 0")
187 fp.write("\n")
188 fp.close()
191# --------------------------------------------------------------------
192# Internal
195def raw(rawmode, data):
196 palette = ImagePalette()
197 palette.rawmode = rawmode
198 palette.palette = data
199 palette.dirty = 1
200 return palette
203# --------------------------------------------------------------------
204# Factories
207def make_linear_lut(black, white):
208 if black == 0:
209 return [white * i // 255 for i in range(256)]
211 msg = "unavailable when black is non-zero"
212 raise NotImplementedError(msg) # FIXME
215def make_gamma_lut(exp):
216 return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
219def negative(mode="RGB"):
220 palette = list(range(256 * len(mode)))
221 palette.reverse()
222 return ImagePalette(mode, [i // len(mode) for i in palette])
225def random(mode="RGB"):
226 from random import randint
228 palette = [randint(0, 255) for _ in range(256 * len(mode))]
229 return ImagePalette(mode, palette)
232def sepia(white="#fff0c0"):
233 bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
234 return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
237def wedge(mode="RGB"):
238 palette = list(range(256 * len(mode)))
239 return ImagePalette(mode, [i // len(mode) for i in palette])
242def load(filename):
243 # FIXME: supports GIMP gradients only
245 with open(filename, "rb") as fp:
246 for paletteHandler in [
247 GimpPaletteFile.GimpPaletteFile,
248 GimpGradientFile.GimpGradientFile,
249 PaletteFile.PaletteFile,
250 ]:
251 try:
252 fp.seek(0)
253 lut = paletteHandler(fp).getpalette()
254 if lut:
255 break
256 except (SyntaxError, ValueError):
257 pass
258 else:
259 msg = "cannot load palette"
260 raise OSError(msg)
262 return lut # data, rawmode