From 3310c84b96fe31b7ef3340a2b622725c4d5247b9 Mon Sep 17 00:00:00 2001 From: gabriel Date: Mon, 6 Jul 2026 16:44:02 +0200 Subject: [PATCH] fix(python): ship the core tone-map NaN fix in the wheel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto tone-map NaN collapse (bilevel bright images rendered as an all-black frame) was fixed in epaper-dithering-core 4.0.1, but the Python package pulls core by path and has had no release since — the wheel on PyPI still carries the bug. This commit adds the Python-level regression test (fails against the pre-4.0.1 core, passes now) and triggers a python package release. Found in the field: Home Assistant drawcustom with the default tone=auto uploaded 69-byte all-black frames for any crisp text-on-white payload rendered by odl-renderer. --- packages/python/tests/test_dithering.py | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/python/tests/test_dithering.py b/packages/python/tests/test_dithering.py index 1a17ba7..bd2d5f4 100644 --- a/packages/python/tests/test_dithering.py +++ b/packages/python/tests/test_dithering.py @@ -262,3 +262,37 @@ def test_tone_compression_changes_measured_output(self): assert not np.array_equal(np.array(result_off), np.array(result_on)), ( "Tone compression should produce different output than no compression" ) + + +class TestToneAutoRegression: + """Regression: auto tone mapping must not NaN-collapse bilevel images. + + A crisp bilevel frame (pure-white background, a few percent pure-black + pixels, no antialiasing — exactly what a UI renderer produces) drove the + log-skew strength epsilon-negative; (-x).powf(1.4) is NaN, which poisoned + every pixel and made the nearest-palette search return index 0 for the + whole frame: an all-black display instead of white with text. + """ + + def test_bilevel_white_image_survives_auto_tone(self): + from epaper_dithering import ColorPalette + + # Measured-style palette whose paper white is well below sRGB white, so + # the auto tone compression actually engages for a pure-white image. + palette = ColorPalette( + colors={"black": (30, 30, 30), "white": (200, 200, 200)}, + accent="black", + scheme=ColorScheme.MONO, + ) + img = Image.new("RGB", (100, 100), (255, 255, 255)) + # 3% pure black — enough that the 2nd-percentile luminance is 0.0. + for x in range(100): + for y in range(3): + img.putpixel((x, y), (0, 0, 0)) + + result = dither_image(img, palette, mode=DitherMode.NONE, tone="auto") + + white_pixels = list(result.getdata()).count(1) + assert white_pixels > 9000, ( + f"expected a mostly-white result, got {white_pixels} white pixels — auto tone collapsed the frame" + )