Files
018-hirst-painting/tests/test_hirst_painting.py
T
Tirsvad b7119ef907 Add the colour palette: white-shade filter and palette extraction
MIL-001 tasks 1 to 6.

- Pin the toolchain and tool configuration in pyproject.toml: Python 3.13,
  colorgram.py 1.2.0, and ruff, mypy (strict) and pytest as dev tools.
- Add is_white_shade and remove_white_shades (threshold 240) with tests that
  pin the boundary at 239 and 240.
- Add extract_palette, which reads the reference image with colorgram and drops
  white shades, with tests on synthetic images and on the reference image.
- Add the reference image assets/20260524_132700.jpg and a note on it.
- Record the code review against QC-PY-001 as RC-005 (verdict Go).

Closes #1
Closes #2
Closes #3
Closes #4
Closes #5
Closes #6

Task: MIL-001#1
Task: MIL-001#2
Task: MIL-001#3
Task: MIL-001#4
Task: MIL-001#5
Task: MIL-001#6
2026-10-07 23:49:38 +08:00

121 lines
3.2 KiB
Python

"""Tests for the colour palette helpers of the Hirst painting program."""
from pathlib import Path
import pytest
from PIL import Image
from hirst_painting import (
REFERENCE_IMAGE_PATH,
Colour,
extract_palette,
is_white_shade,
remove_white_shades,
)
STRIPE_WIDTH = 20
def _write_stripes(path: Path, colours: list[Colour]) -> None:
"""Save an image of vertical stripes, one flat stripe per colour."""
image = Image.new("RGB", (STRIPE_WIDTH * len(colours), STRIPE_WIDTH))
for index, colour in enumerate(colours):
left = index * STRIPE_WIDTH
image.paste(colour, (left, 0, left + STRIPE_WIDTH, STRIPE_WIDTH))
image.save(path)
@pytest.mark.parametrize(
"colour",
[
(255, 255, 255),
(240, 240, 240),
(245, 250, 241),
],
)
def test_is_white_shade_when_all_channels_at_or_above_240(colour: Colour) -> None:
assert is_white_shade(colour)
@pytest.mark.parametrize(
"colour",
[
(239, 239, 239),
(255, 255, 239),
(239, 255, 255),
(255, 239, 255),
(200, 30, 40),
(0, 0, 0),
],
)
def test_is_not_white_shade_when_any_channel_below_240(colour: Colour) -> None:
assert not is_white_shade(colour)
def test_remove_white_shades_keeps_order_and_drops_white() -> None:
colours: list[Colour] = [
(255, 255, 255),
(200, 30, 40),
(245, 245, 245),
(30, 90, 160),
(240, 240, 240),
]
assert remove_white_shades(colours) == [(200, 30, 40), (30, 90, 160)]
def test_remove_white_shades_returns_empty_list_when_all_white() -> None:
assert remove_white_shades([(255, 255, 255), (240, 240, 240)]) == []
def test_remove_white_shades_returns_empty_list_when_no_colours() -> None:
assert remove_white_shades([]) == []
def test_remove_white_shades_accepts_any_iterable() -> None:
colours = ((colour, 30, 40) for colour in (200, 255))
assert remove_white_shades(colours) == [(200, 30, 40), (255, 30, 40)]
def test_remove_white_shades_leaves_input_unchanged() -> None:
colours: list[Colour] = [(255, 255, 255), (200, 30, 40)]
remove_white_shades(colours)
assert colours == [(255, 255, 255), (200, 30, 40)]
def test_extract_palette_returns_colours_without_white_shades(tmp_path: Path) -> None:
image_path = tmp_path / "stripes.png"
_write_stripes(
image_path,
[(200, 30, 40), (255, 255, 255), (30, 90, 160), (240, 240, 240)],
)
assert set(extract_palette(image_path)) == {(200, 30, 40), (30, 90, 160)}
def test_extract_palette_returns_requested_count_when_image_has_more_colours(
tmp_path: Path,
) -> None:
image_path = tmp_path / "stripes.png"
_write_stripes(
image_path,
[(200, 30, 40), (30, 90, 160), (20, 120, 60), (90, 40, 130)],
)
assert len(extract_palette(image_path, colour_count=2)) == 2
def test_extract_palette_raises_when_image_is_missing(tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
extract_palette(tmp_path / "missing.png")
def test_reference_image_palette_has_two_colours_and_no_white_shade() -> None:
palette = extract_palette(REFERENCE_IMAGE_PATH)
assert len(palette) >= 2
assert not any(is_white_shade(colour) for colour in palette)