Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Tests/test_imageops.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
hopper,
)

TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable


class Deformer(ImageOps.SupportsGetMesh):
def getmesh(
Expand Down Expand Up @@ -627,3 +631,36 @@ def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None:
img, cutoff=10, preserve_tone=True
) # single color 10 cutoff
assert_image_equal(img, out)


@pytest.mark.parametrize(
"operation",
(
ImageOps.invert,
lambda im: ImageOps.posterize(im, 3),
ImageOps.solarize,
),
)
def test_palette_operation(operation: Callable[[Image.Image], Image.Image]) -> None:
im = hopper("P")
result = operation(im)

assert result.mode == "P"
assert_image_equal(result.convert("RGB"), operation(im.convert("RGB")))


def test_palette_operation_keeps_alpha() -> None:
im = Image.new("P", (2, 1))
im.putpalette((0, 0, 0, 0, 255, 0, 0, 128), "RGBA")
im.putpixel((1, 0), 1)

result = ImageOps.invert(im)

palette = result.getpalette("RGBA")
assert palette is not None
assert palette[:8] == [255, 255, 255, 0, 0, 255, 255, 128]


def test_palette_histogram_operation_not_supported() -> None:
with pytest.raises(NotImplementedError):
ImageOps.autocontrast(hopper("P"))
31 changes: 24 additions & 7 deletions src/PIL/ImageOps.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,28 @@ def _color(
return color


def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
def _lut(image: Image.Image, lut: list[int], palette: bool = False) -> Image.Image:
if image.mode == "P":
# FIXME: apply to lookup table, not image data
msg = "mode P support coming soon"
raise NotImplementedError(msg)
if not palette:
# The lookup table was built from the palette indexes, not colors
msg = "mode P support coming soon"
raise NotImplementedError(msg)
# A per-pixel operation can be applied to the palette colors
colors = image.getpalette(None)
assert colors is not None
im = image.copy()

assert image.palette is not None
palette_mode = image.palette.mode
channels = len(palette_mode)
im.putpalette(
[
v if palette_mode[i % channels] == "A" else lut[v]
for i, v in enumerate(colors)
],
palette_mode,
)
return im
elif image.mode in ("L", "RGB"):
if image.mode == "RGB" and len(lut) == 256:
lut = lut + lut + lut
Expand Down Expand Up @@ -655,7 +672,7 @@ def invert(image: Image.Image) -> Image.Image:
:return: An image.
"""
lut = list(range(255, -1, -1))
return image.point(lut) if image.mode == "1" else _lut(image, lut)
return image.point(lut) if image.mode == "1" else _lut(image, lut, palette=True)


def mirror(image: Image.Image) -> Image.Image:
Expand All @@ -678,7 +695,7 @@ def posterize(image: Image.Image, bits: int) -> Image.Image:
"""
mask = ~(2 ** (8 - bits) - 1)
lut = [i & mask for i in range(256)]
return _lut(image, lut)
return _lut(image, lut, palette=True)


def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
Expand All @@ -695,7 +712,7 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
lut.append(i)
else:
lut.append(255 - i)
return _lut(image, lut)
return _lut(image, lut, palette=True)


@overload
Expand Down
Loading