Merge pull request #5552 from radarhere/palette

This commit is contained in:
Hugo van Kemenade 2021-06-28 19:01:54 +03:00 committed by GitHub
commit 50302231ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 169 additions and 75 deletions

View File

@ -249,8 +249,8 @@ def test_apng_mode():
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGBA")
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
assert im.getpixel((0, 0)) == (255, 0, 0, 0)
assert im.getpixel((64, 32)) == (255, 0, 0, 0)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert im.mode == "P"

View File

@ -766,10 +766,10 @@ def test_rgb_transparency(tmp_path):
# Single frame
im = Image.new("RGB", (1, 1))
im.info["transparency"] = (255, 0, 0)
pytest.warns(UserWarning, im.save, out)
im.save(out)
with Image.open(out) as reloaded:
assert "transparency" not in reloaded.info
assert "transparency" in reloaded.info
# Multiple frames
im = Image.new("RGB", (1, 1))

View File

@ -582,6 +582,10 @@ class TestImage:
assert ext_individual == ext_multiple
def test_remap_palette(self):
# Test identity transform
with Image.open("Tests/images/hopper.gif") as im:
assert_image_equal(im, im.remap_palette(list(range(256))))
# Test illegal image mode
with hopper() as im:
with pytest.raises(ValueError):
@ -606,7 +610,7 @@ class TestImage:
else:
assert new_im.palette is None
_make_new(im, im_p, im_p.palette)
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
_make_new(im_p, im, None)
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())

View File

@ -93,7 +93,7 @@ def test_trns_p(tmp_path):
im_l.save(f)
im_rgb = im.convert("RGB")
assert im_rgb.info["transparency"] == (0, 0, 0) # undone
assert im_rgb.info["transparency"] == (0, 1, 2) # undone
im_rgb.save(f)
@ -128,8 +128,8 @@ def test_trns_l(tmp_path):
assert "transparency" in im_p.info
im_p.save(f)
im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE)
assert "transparency" not in im_p.info
im_p = im.convert("P", palette=Image.ADAPTIVE)
assert "transparency" in im_p.info
im_p.save(f)
@ -155,13 +155,19 @@ def test_trns_RGB(tmp_path):
assert "transparency" not in im_p.info
im_p.save(f)
im = Image.new("RGB", (1, 1))
im.info["transparency"] = im.getpixel((0, 0))
im_p = im.convert("P", palette=Image.ADAPTIVE)
assert im_p.info["transparency"] == im_p.getpixel((0, 0))
im_p.save(f)
def test_gif_with_rgba_palette_to_p():
# See https://github.com/python-pillow/Pillow/issues/2433
with Image.open("Tests/images/hopper.gif") as im:
im.info["transparency"] = 255
im.load()
assert im.palette.mode == "RGBA"
assert im.palette.mode == "RGB"
im_p = im.convert("P")
# Should not raise ValueError: unrecognized raw mode

View File

@ -157,9 +157,16 @@ def test_scale():
def test_expand_palette():
im = Image.open("Tests/images/hopper.gif")
im_expanded = ImageOps.expand(im)
assert_image_equal(im_expanded.convert("RGB"), im.convert("RGB"))
im = Image.open("Tests/images/p_16.tga")
im_expanded = ImageOps.expand(im, 10, (255, 0, 0))
px = im_expanded.convert("RGB").load()
assert px[0, 0] == (255, 0, 0)
im_cropped = im_expanded.crop(
(10, 10, im_expanded.width - 10, im_expanded.height - 10)
)
assert_image_equal(im_cropped, im)
def test_colorize_2color():

View File

@ -2,27 +2,47 @@ import pytest
from PIL import Image, ImagePalette
from .helper import assert_image_equal_tofile
from .helper import assert_image_equal, assert_image_equal_tofile
def test_sanity():
ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
assert len(palette.colors) == 256
with pytest.raises(ValueError):
ImagePalette.ImagePalette("RGB", list(range(256)) * 2)
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
def test_reload():
im = Image.open("Tests/images/hopper.gif")
original = im.copy()
im.palette.dirty = 1
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
def test_getcolor():
palette = ImagePalette.ImagePalette()
assert len(palette.palette) == 0
assert len(palette.colors) == 0
test_map = {}
for i in range(256):
test_map[palette.getcolor((i, i, i))] = i
assert len(test_map) == 256
# Colors can be converted between RGB and RGBA
rgba_palette = ImagePalette.ImagePalette("RGBA")
assert rgba_palette.getcolor((0, 0, 0)) == rgba_palette.getcolor((0, 0, 0, 255))
assert palette.getcolor((0, 0, 0)) == palette.getcolor((0, 0, 0, 255))
# An error is raised when the palette is full
with pytest.raises(ValueError):
palette.getcolor((1, 2, 3))
# But not if the image is not using one of the palette entries
palette.getcolor((1, 2, 3), image=Image.new("P", (1, 1)))
# Test unknown color specifier
with pytest.raises(ValueError):
@ -116,7 +136,7 @@ def test_getdata():
mode, data_out = palette.getdata()
# Assert
assert mode == "RGB;L"
assert mode == "RGB"
def test_rawmode_getdata():

View File

@ -472,10 +472,10 @@ def _write_multiple_frames(im, fp, palette):
previous = im_frames[-1]
if encoderinfo.get("disposal") == 2:
if background_im is None:
background = _get_background(
im,
im.encoderinfo.get("background", im.info.get("background")),
color = im.encoderinfo.get(
"transparency", im.info.get("transparency", (0, 0, 0))
)
background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background)
background_im.putpalette(im_frames[0]["im"].palette)
base_im = background_im
@ -771,7 +771,15 @@ def _get_background(im, infoBackground):
# WebPImagePlugin stores an RGBA value in info["background"]
# So it must be converted to the same format as GifImagePlugin's
# info["background"] - a global color table index
background = im.palette.getcolor(background)
try:
background = im.palette.getcolor(background, im)
except ValueError as e:
if str(e) == "cannot allocate more than 256 colors":
# If all 256 colors are in use,
# then there is no need for the background color
return 0
else:
raise
return background

View File

@ -833,7 +833,7 @@ class Image:
palette_length = self.im.putpalette(mode, arr)
self.palette.dirty = 0
self.palette.rawmode = None
if "transparency" in self.info:
if "transparency" in self.info and mode in ("RGBA", "LA", "PA"):
if isinstance(self.info["transparency"], int):
self.im.putpalettealpha(self.info["transparency"], 0)
else:
@ -977,21 +977,28 @@ class Image:
if self.mode == "P":
trns_im.putpalette(self.palette)
if isinstance(t, tuple):
err = "Couldn't allocate a palette color for transparency"
try:
t = trns_im.palette.getcolor(t)
except Exception as e:
raise ValueError(
"Couldn't allocate a palette color for transparency"
) from e
trns_im.putpixel((0, 0), t)
if mode in ("L", "RGB"):
trns_im = trns_im.convert(mode)
t = trns_im.palette.getcolor(t, self)
except ValueError as e:
if str(e) == "cannot allocate more than 256 colors":
# If all 256 colors are in use,
# then there is no need for transparency
t = None
else:
raise ValueError(err) from e
if t is None:
trns = None
else:
# can't just retrieve the palette number, got to do it
# after quantization.
trns_im = trns_im.convert("RGB")
trns = trns_im.getpixel((0, 0))
trns_im.putpixel((0, 0), t)
if mode in ("L", "RGB"):
trns_im = trns_im.convert(mode)
else:
# can't just retrieve the palette number, got to do it
# after quantization.
trns_im = trns_im.convert("RGB")
trns = trns_im.getpixel((0, 0))
elif self.mode == "P" and mode == "RGBA":
t = self.info["transparency"]
@ -1009,14 +1016,14 @@ class Image:
new = self._new(im)
from . import ImagePalette
new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB"))
new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB"))
if delete_trns:
# This could possibly happen if we requantize to fewer colors.
# The transparency would be totally off in that case.
del new.info["transparency"]
if trns is not None:
try:
new.info["transparency"] = new.palette.getcolor(trns)
new.info["transparency"] = new.palette.getcolor(trns, new)
except Exception:
# if we can't make a transparent color, don't leave the old
# transparency hanging around to mess us up.
@ -1039,16 +1046,25 @@ class Image:
raise ValueError("illegal conversion") from e
new_im = self._new(im)
if mode == "P" and palette != ADAPTIVE:
from . import ImagePalette
new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
if delete_trns:
# crash fail if we leave a bytes transparency in an rgb/l mode.
del new_im.info["transparency"]
if trns is not None:
if new_im.mode == "P":
try:
new_im.info["transparency"] = new_im.palette.getcolor(trns)
except Exception:
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
except ValueError as e:
del new_im.info["transparency"]
warnings.warn("Couldn't allocate palette entry for transparency")
if str(e) != "cannot allocate more than 256 colors":
# If all 256 colors are in use,
# then there is no need for transparency
warnings.warn(
"Couldn't allocate palette entry for transparency"
)
else:
new_im.info["transparency"] = trns
return new_im
@ -1732,7 +1748,7 @@ class Image:
Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image.
The palette sequence must contain either 768 integer values, or 1024
The palette sequence must contain at most 768 integer values, or 1024
integer values if alpha is included. Each group of values represents
the red, green, blue (and alpha if included) values for the
corresponding pixel index. Instead of an integer sequence, you can use
@ -1745,7 +1761,6 @@ class Image:
if self.mode not in ("L", "LA", "P", "PA"):
raise ValueError("illegal image mode")
self.load()
if isinstance(data, ImagePalette.ImagePalette):
palette = ImagePalette.raw(data.rawmode, data.palette)
else:
@ -1792,7 +1807,7 @@ class Image:
and len(value) in [3, 4]
):
# RGB or RGBA value for a P image
value = self.palette.getcolor(value)
value = self.palette.getcolor(value, self)
return self.im.putpixel(xy, value)
def remap_palette(self, dest_map, source_palette=None):
@ -1813,6 +1828,7 @@ class Image:
if source_palette is None:
if self.mode == "P":
self.load()
real_source_palette = self.im.getpalette("RGB")[:768]
else: # L-mode
real_source_palette = bytearray(i // 3 for i in range(768))
@ -1850,23 +1866,19 @@ class Image:
m_im = self.copy()
m_im.mode = "P"
m_im.palette = ImagePalette.ImagePalette(
"RGB", palette=mapping_palette * 3, size=768
)
m_im.palette = ImagePalette.ImagePalette("RGB", palette=mapping_palette * 3)
# possibly set palette dirty, then
# m_im.putpalette(mapping_palette, 'L') # converts to 'P'
# or just force it.
# UNDONE -- this is part of the general issue with palettes
m_im.im.putpalette(*m_im.palette.getdata())
m_im.im.putpalette("RGB;L", m_im.palette.tobytes())
m_im = m_im.convert("L")
# Internally, we require 768 bytes for a palette.
new_palette_bytes = palette_bytes + (768 - len(palette_bytes)) * b"\x00"
m_im.putpalette(new_palette_bytes)
m_im.palette = ImagePalette.ImagePalette(
"RGB", palette=palette_bytes, size=len(palette_bytes)
)
m_im.palette = ImagePalette.ImagePalette("RGB", palette=palette_bytes)
return m_im

View File

@ -70,6 +70,7 @@ class ImageDraw:
self.palette = im.palette
else:
self.palette = None
self._image = im
self.im = im.im
self.draw = Image.core.draw(self.im, blend)
self.mode = mode
@ -108,13 +109,13 @@ class ImageDraw:
if isinstance(ink, str):
ink = ImageColor.getcolor(ink, self.mode)
if self.palette and not isinstance(ink, numbers.Number):
ink = self.palette.getcolor(ink)
ink = self.palette.getcolor(ink, self._image)
ink = self.draw.draw_ink(ink)
if fill is not None:
if isinstance(fill, str):
fill = ImageColor.getcolor(fill, self.mode)
if self.palette and not isinstance(fill, numbers.Number):
fill = self.palette.getcolor(fill)
fill = self.palette.getcolor(fill, self._image)
fill = self.draw.draw_ink(fill)
return ink, fill

View File

@ -20,7 +20,7 @@
import functools
import operator
from . import Image
from . import Image, ImageDraw
#
# helpers
@ -392,10 +392,17 @@ def expand(image, border=0, fill=0):
left, top, right, bottom = _border(border)
width = left + image.size[0] + right
height = top + image.size[1] + bottom
out = Image.new(image.mode, (width, height), _color(fill, image.mode))
color = _color(fill, image.mode)
if image.mode == "P" and image.palette:
out = Image.new(image.mode, (width, height))
out.putpalette(image.palette)
out.paste(image, (left, top))
out.paste(image, (left, top))
draw = ImageDraw.Draw(out)
draw.rectangle((0, 0, width, height), outline=color, width=border)
else:
out = Image.new(image.mode, (width, height), color)
out.paste(image, (left, top))
return out

View File

@ -39,14 +39,27 @@ class ImagePalette:
def __init__(self, mode="RGB", palette=None, size=0):
self.mode = mode
self.rawmode = None # if set, palette contains raw data
self.palette = palette or bytearray(range(256)) * len(self.mode)
self.colors = {}
self.palette = palette or bytearray()
self.dirty = None
if (size == 0 and len(self.mode) * 256 != len(self.palette)) or (
size != 0 and size != len(self.palette)
):
if size != 0 and size != len(self.palette):
raise ValueError("wrong palette size")
@property
def palette(self):
return self._palette
@palette.setter
def palette(self, palette):
self._palette = palette
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
def copy(self):
new = ImagePalette()
@ -54,7 +67,6 @@ class ImagePalette:
new.rawmode = self.rawmode
if self.palette is not None:
new.palette = self.palette[:]
new.colors = self.colors.copy()
new.dirty = self.dirty
return new
@ -68,7 +80,7 @@ class ImagePalette:
"""
if self.rawmode:
return self.rawmode, self.palette
return self.mode + ";L", self.tobytes()
return self.mode, self.tobytes()
def tobytes(self):
"""Convert palette to bytes.
@ -80,14 +92,12 @@ class ImagePalette:
if isinstance(self.palette, bytes):
return self.palette
arr = array.array("B", self.palette)
if hasattr(arr, "tobytes"):
return arr.tobytes()
return arr.tostring()
return arr.tobytes()
# Declare tostring as an alias for tobytes
tostring = tobytes
def getcolor(self, color):
def getcolor(self, color, image=None):
"""Given an rgb tuple, allocate palette entry.
.. warning:: This method is experimental.
@ -95,19 +105,37 @@ class ImagePalette:
if self.rawmode:
raise ValueError("palette contains raw palette data")
if isinstance(color, tuple):
if self.mode == "RGB":
if len(color) == 4 and color[3] == 255:
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
if isinstance(self.palette, bytes):
self.palette = bytearray(self.palette)
index = len(self.colors)
if not isinstance(self.palette, bytearray):
self._palette = bytearray(self.palette)
index = len(self.palette) // 3
if index >= 256:
raise ValueError("cannot allocate more than 256 colors") from e
if image:
# Search for an unused index
for i, count in reversed(list(enumerate(image.histogram()))):
if count == 0:
index = i
break
if index >= 256:
raise ValueError("cannot allocate more than 256 colors") from e
self.colors[color] = index
self.palette[index] = color[0]
self.palette[index + 256] = color[1]
self.palette[index + 512] = color[2]
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:

View File

@ -54,6 +54,7 @@ class PyAccess:
self.image32 = ffi.cast("int **", vals["image32"])
self.image = ffi.cast("unsigned char **", vals["image"])
self.xsize, self.ysize = img.im.size
self._img = img
# Keep pointer to im object to prevent dereferencing.
self._im = img.im
@ -93,7 +94,7 @@ class PyAccess:
and len(color) in [3, 4]
):
# RGB or RGBA value for a P image
color = self._palette.getcolor(color)
color = self._palette.getcolor(color, self._img)
return self.set_pixel(x, y, color)

View File

@ -320,7 +320,7 @@ def _save(im, fp, filename):
alpha = (
"A" in im.mode
or "a" in im.mode
or (im.mode == "P" and "A" in im.im.getpalettemode())
or (im.mode == "P" and "transparency" in im.info)
)
im = im.convert("RGBA" if alpha else "RGB")