Skip to content

Commit 5030223

Browse files
authored
Merge pull request #5552 from radarhere/palette
2 parents d0394d4 + 1ee30de commit 5030223

13 files changed

+169
-75
lines changed

Tests/test_file_apng.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ def test_apng_mode():
249249
assert im.mode == "P"
250250
im.seek(im.n_frames - 1)
251251
im = im.convert("RGBA")
252-
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
253-
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
252+
assert im.getpixel((0, 0)) == (255, 0, 0, 0)
253+
assert im.getpixel((64, 32)) == (255, 0, 0, 0)
254254

255255
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
256256
assert im.mode == "P"

Tests/test_file_gif.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -766,10 +766,10 @@ def test_rgb_transparency(tmp_path):
766766
# Single frame
767767
im = Image.new("RGB", (1, 1))
768768
im.info["transparency"] = (255, 0, 0)
769-
pytest.warns(UserWarning, im.save, out)
769+
im.save(out)
770770

771771
with Image.open(out) as reloaded:
772-
assert "transparency" not in reloaded.info
772+
assert "transparency" in reloaded.info
773773

774774
# Multiple frames
775775
im = Image.new("RGB", (1, 1))

Tests/test_image.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,10 @@ def test_register_extensions(self):
582582
assert ext_individual == ext_multiple
583583

584584
def test_remap_palette(self):
585+
# Test identity transform
586+
with Image.open("Tests/images/hopper.gif") as im:
587+
assert_image_equal(im, im.remap_palette(list(range(256))))
588+
585589
# Test illegal image mode
586590
with hopper() as im:
587591
with pytest.raises(ValueError):
@@ -606,7 +610,7 @@ def _make_new(base_image, im, palette_result=None):
606610
else:
607611
assert new_im.palette is None
608612

609-
_make_new(im, im_p, im_p.palette)
613+
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
610614
_make_new(im_p, im, None)
611615
_make_new(im, blank_p, ImagePalette.ImagePalette())
612616
_make_new(im, blank_pa, ImagePalette.ImagePalette())

Tests/test_image_convert.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_trns_p(tmp_path):
9393
im_l.save(f)
9494

9595
im_rgb = im.convert("RGB")
96-
assert im_rgb.info["transparency"] == (0, 0, 0) # undone
96+
assert im_rgb.info["transparency"] == (0, 1, 2) # undone
9797
im_rgb.save(f)
9898

9999

@@ -128,8 +128,8 @@ def test_trns_l(tmp_path):
128128
assert "transparency" in im_p.info
129129
im_p.save(f)
130130

131-
im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE)
132-
assert "transparency" not in im_p.info
131+
im_p = im.convert("P", palette=Image.ADAPTIVE)
132+
assert "transparency" in im_p.info
133133
im_p.save(f)
134134

135135

@@ -155,13 +155,19 @@ def test_trns_RGB(tmp_path):
155155
assert "transparency" not in im_p.info
156156
im_p.save(f)
157157

158+
im = Image.new("RGB", (1, 1))
159+
im.info["transparency"] = im.getpixel((0, 0))
160+
im_p = im.convert("P", palette=Image.ADAPTIVE)
161+
assert im_p.info["transparency"] == im_p.getpixel((0, 0))
162+
im_p.save(f)
163+
158164

159165
def test_gif_with_rgba_palette_to_p():
160166
# See https://github.com/python-pillow/Pillow/issues/2433
161167
with Image.open("Tests/images/hopper.gif") as im:
162168
im.info["transparency"] = 255
163169
im.load()
164-
assert im.palette.mode == "RGBA"
170+
assert im.palette.mode == "RGB"
165171
im_p = im.convert("P")
166172

167173
# Should not raise ValueError: unrecognized raw mode

Tests/test_imageops.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,16 @@ def test_scale():
157157

158158

159159
def test_expand_palette():
160-
im = Image.open("Tests/images/hopper.gif")
161-
im_expanded = ImageOps.expand(im)
162-
assert_image_equal(im_expanded.convert("RGB"), im.convert("RGB"))
160+
im = Image.open("Tests/images/p_16.tga")
161+
im_expanded = ImageOps.expand(im, 10, (255, 0, 0))
162+
163+
px = im_expanded.convert("RGB").load()
164+
assert px[0, 0] == (255, 0, 0)
165+
166+
im_cropped = im_expanded.crop(
167+
(10, 10, im_expanded.width - 10, im_expanded.height - 10)
168+
)
169+
assert_image_equal(im_cropped, im)
163170

164171

165172
def test_colorize_2color():

Tests/test_imagepalette.py

+25-5
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,47 @@
22

33
from PIL import Image, ImagePalette
44

5-
from .helper import assert_image_equal_tofile
5+
from .helper import assert_image_equal, assert_image_equal_tofile
66

77

88
def test_sanity():
99

10-
ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
10+
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
11+
assert len(palette.colors) == 256
12+
1113
with pytest.raises(ValueError):
12-
ImagePalette.ImagePalette("RGB", list(range(256)) * 2)
14+
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
15+
16+
17+
def test_reload():
18+
im = Image.open("Tests/images/hopper.gif")
19+
original = im.copy()
20+
im.palette.dirty = 1
21+
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
1322

1423

1524
def test_getcolor():
1625

1726
palette = ImagePalette.ImagePalette()
27+
assert len(palette.palette) == 0
28+
assert len(palette.colors) == 0
1829

1930
test_map = {}
2031
for i in range(256):
2132
test_map[palette.getcolor((i, i, i))] = i
22-
2333
assert len(test_map) == 256
34+
35+
# Colors can be converted between RGB and RGBA
36+
rgba_palette = ImagePalette.ImagePalette("RGBA")
37+
assert rgba_palette.getcolor((0, 0, 0)) == rgba_palette.getcolor((0, 0, 0, 255))
38+
39+
assert palette.getcolor((0, 0, 0)) == palette.getcolor((0, 0, 0, 255))
40+
41+
# An error is raised when the palette is full
2442
with pytest.raises(ValueError):
2543
palette.getcolor((1, 2, 3))
44+
# But not if the image is not using one of the palette entries
45+
palette.getcolor((1, 2, 3), image=Image.new("P", (1, 1)))
2646

2747
# Test unknown color specifier
2848
with pytest.raises(ValueError):
@@ -116,7 +136,7 @@ def test_getdata():
116136
mode, data_out = palette.getdata()
117137

118138
# Assert
119-
assert mode == "RGB;L"
139+
assert mode == "RGB"
120140

121141

122142
def test_rawmode_getdata():

src/PIL/GifImagePlugin.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -472,10 +472,10 @@ def _write_multiple_frames(im, fp, palette):
472472
previous = im_frames[-1]
473473
if encoderinfo.get("disposal") == 2:
474474
if background_im is None:
475-
background = _get_background(
476-
im,
477-
im.encoderinfo.get("background", im.info.get("background")),
475+
color = im.encoderinfo.get(
476+
"transparency", im.info.get("transparency", (0, 0, 0))
478477
)
478+
background = _get_background(im_frame, color)
479479
background_im = Image.new("P", im_frame.size, background)
480480
background_im.putpalette(im_frames[0]["im"].palette)
481481
base_im = background_im
@@ -771,7 +771,15 @@ def _get_background(im, infoBackground):
771771
# WebPImagePlugin stores an RGBA value in info["background"]
772772
# So it must be converted to the same format as GifImagePlugin's
773773
# info["background"] - a global color table index
774-
background = im.palette.getcolor(background)
774+
try:
775+
background = im.palette.getcolor(background, im)
776+
except ValueError as e:
777+
if str(e) == "cannot allocate more than 256 colors":
778+
# If all 256 colors are in use,
779+
# then there is no need for the background color
780+
return 0
781+
else:
782+
raise
775783
return background
776784

777785

src/PIL/Image.py

+41-29
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ def load(self):
833833
palette_length = self.im.putpalette(mode, arr)
834834
self.palette.dirty = 0
835835
self.palette.rawmode = None
836-
if "transparency" in self.info:
836+
if "transparency" in self.info and mode in ("RGBA", "LA", "PA"):
837837
if isinstance(self.info["transparency"], int):
838838
self.im.putpalettealpha(self.info["transparency"], 0)
839839
else:
@@ -977,21 +977,28 @@ def convert_transparency(m, v):
977977
if self.mode == "P":
978978
trns_im.putpalette(self.palette)
979979
if isinstance(t, tuple):
980+
err = "Couldn't allocate a palette color for transparency"
980981
try:
981-
t = trns_im.palette.getcolor(t)
982-
except Exception as e:
983-
raise ValueError(
984-
"Couldn't allocate a palette color for transparency"
985-
) from e
986-
trns_im.putpixel((0, 0), t)
987-
988-
if mode in ("L", "RGB"):
989-
trns_im = trns_im.convert(mode)
982+
t = trns_im.palette.getcolor(t, self)
983+
except ValueError as e:
984+
if str(e) == "cannot allocate more than 256 colors":
985+
# If all 256 colors are in use,
986+
# then there is no need for transparency
987+
t = None
988+
else:
989+
raise ValueError(err) from e
990+
if t is None:
991+
trns = None
990992
else:
991-
# can't just retrieve the palette number, got to do it
992-
# after quantization.
993-
trns_im = trns_im.convert("RGB")
994-
trns = trns_im.getpixel((0, 0))
993+
trns_im.putpixel((0, 0), t)
994+
995+
if mode in ("L", "RGB"):
996+
trns_im = trns_im.convert(mode)
997+
else:
998+
# can't just retrieve the palette number, got to do it
999+
# after quantization.
1000+
trns_im = trns_im.convert("RGB")
1001+
trns = trns_im.getpixel((0, 0))
9951002

9961003
elif self.mode == "P" and mode == "RGBA":
9971004
t = self.info["transparency"]
@@ -1009,14 +1016,14 @@ def convert_transparency(m, v):
10091016
new = self._new(im)
10101017
from . import ImagePalette
10111018

1012-
new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB"))
1019+
new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB"))
10131020
if delete_trns:
10141021
# This could possibly happen if we requantize to fewer colors.
10151022
# The transparency would be totally off in that case.
10161023
del new.info["transparency"]
10171024
if trns is not None:
10181025
try:
1019-
new.info["transparency"] = new.palette.getcolor(trns)
1026+
new.info["transparency"] = new.palette.getcolor(trns, new)
10201027
except Exception:
10211028
# if we can't make a transparent color, don't leave the old
10221029
# transparency hanging around to mess us up.
@@ -1039,16 +1046,25 @@ def convert_transparency(m, v):
10391046
raise ValueError("illegal conversion") from e
10401047

10411048
new_im = self._new(im)
1049+
if mode == "P" and palette != ADAPTIVE:
1050+
from . import ImagePalette
1051+
1052+
new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
10421053
if delete_trns:
10431054
# crash fail if we leave a bytes transparency in an rgb/l mode.
10441055
del new_im.info["transparency"]
10451056
if trns is not None:
10461057
if new_im.mode == "P":
10471058
try:
1048-
new_im.info["transparency"] = new_im.palette.getcolor(trns)
1049-
except Exception:
1059+
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
1060+
except ValueError as e:
10501061
del new_im.info["transparency"]
1051-
warnings.warn("Couldn't allocate palette entry for transparency")
1062+
if str(e) != "cannot allocate more than 256 colors":
1063+
# If all 256 colors are in use,
1064+
# then there is no need for transparency
1065+
warnings.warn(
1066+
"Couldn't allocate palette entry for transparency"
1067+
)
10521068
else:
10531069
new_im.info["transparency"] = trns
10541070
return new_im
@@ -1732,7 +1748,7 @@ def putpalette(self, data, rawmode="RGB"):
17321748
Attaches a palette to this image. The image must be a "P", "PA", "L"
17331749
or "LA" image.
17341750
1735-
The palette sequence must contain either 768 integer values, or 1024
1751+
The palette sequence must contain at most 768 integer values, or 1024
17361752
integer values if alpha is included. Each group of values represents
17371753
the red, green, blue (and alpha if included) values for the
17381754
corresponding pixel index. Instead of an integer sequence, you can use
@@ -1745,7 +1761,6 @@ def putpalette(self, data, rawmode="RGB"):
17451761

17461762
if self.mode not in ("L", "LA", "P", "PA"):
17471763
raise ValueError("illegal image mode")
1748-
self.load()
17491764
if isinstance(data, ImagePalette.ImagePalette):
17501765
palette = ImagePalette.raw(data.rawmode, data.palette)
17511766
else:
@@ -1792,7 +1807,7 @@ def putpixel(self, xy, value):
17921807
and len(value) in [3, 4]
17931808
):
17941809
# RGB or RGBA value for a P image
1795-
value = self.palette.getcolor(value)
1810+
value = self.palette.getcolor(value, self)
17961811
return self.im.putpixel(xy, value)
17971812

17981813
def remap_palette(self, dest_map, source_palette=None):
@@ -1813,6 +1828,7 @@ def remap_palette(self, dest_map, source_palette=None):
18131828

18141829
if source_palette is None:
18151830
if self.mode == "P":
1831+
self.load()
18161832
real_source_palette = self.im.getpalette("RGB")[:768]
18171833
else: # L-mode
18181834
real_source_palette = bytearray(i // 3 for i in range(768))
@@ -1850,23 +1866,19 @@ def remap_palette(self, dest_map, source_palette=None):
18501866
m_im = self.copy()
18511867
m_im.mode = "P"
18521868

1853-
m_im.palette = ImagePalette.ImagePalette(
1854-
"RGB", palette=mapping_palette * 3, size=768
1855-
)
1869+
m_im.palette = ImagePalette.ImagePalette("RGB", palette=mapping_palette * 3)
18561870
# possibly set palette dirty, then
18571871
# m_im.putpalette(mapping_palette, 'L') # converts to 'P'
18581872
# or just force it.
18591873
# UNDONE -- this is part of the general issue with palettes
1860-
m_im.im.putpalette(*m_im.palette.getdata())
1874+
m_im.im.putpalette("RGB;L", m_im.palette.tobytes())
18611875

18621876
m_im = m_im.convert("L")
18631877

18641878
# Internally, we require 768 bytes for a palette.
18651879
new_palette_bytes = palette_bytes + (768 - len(palette_bytes)) * b"\x00"
18661880
m_im.putpalette(new_palette_bytes)
1867-
m_im.palette = ImagePalette.ImagePalette(
1868-
"RGB", palette=palette_bytes, size=len(palette_bytes)
1869-
)
1881+
m_im.palette = ImagePalette.ImagePalette("RGB", palette=palette_bytes)
18701882

18711883
return m_im
18721884

src/PIL/ImageDraw.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def __init__(self, im, mode=None):
7070
self.palette = im.palette
7171
else:
7272
self.palette = None
73+
self._image = im
7374
self.im = im.im
7475
self.draw = Image.core.draw(self.im, blend)
7576
self.mode = mode
@@ -108,13 +109,13 @@ def _getink(self, ink, fill=None):
108109
if isinstance(ink, str):
109110
ink = ImageColor.getcolor(ink, self.mode)
110111
if self.palette and not isinstance(ink, numbers.Number):
111-
ink = self.palette.getcolor(ink)
112+
ink = self.palette.getcolor(ink, self._image)
112113
ink = self.draw.draw_ink(ink)
113114
if fill is not None:
114115
if isinstance(fill, str):
115116
fill = ImageColor.getcolor(fill, self.mode)
116117
if self.palette and not isinstance(fill, numbers.Number):
117-
fill = self.palette.getcolor(fill)
118+
fill = self.palette.getcolor(fill, self._image)
118119
fill = self.draw.draw_ink(fill)
119120
return ink, fill
120121

0 commit comments

Comments
 (0)