Skip to content

Add Jarvis-Judice-Ninke dithering - #10265

Open
hiyouwhatsapp wants to merge 14 commits into
python-pillow:mainfrom
hiyouwhatsapp:feature/jjn-dithering-9619
Open

hiyouwhatsapp wants to merge 14 commits into
python-pillow:mainfrom
hiyouwhatsapp:feature/jjn-dithering-9619

Conversation

@hiyouwhatsapp

@hiyouwhatsapp hiyouwhatsapp commented Oct 20, 2026 •

Copy link
Copy Markdown

Adds Jarvis-Judice-Ninke dithering for bilevel and palette images, keeping Floyd-Steinberg as the default.

Uses the shared ImagingDitherType enum for the C mode constant and documents the three-row error buffer and boundary handling.

The tests compare against the checked-in JJN-dithered Hopper fixture. The duplicate Python diffusion implementation has been removed; small solid-image cases retain edge-size coverage.

Merged current main and resolved the conversion-code conflicts. Fork Linux/macOS tests, Windows tests, MSYS2 tests, Docker tests and lint pass on commit 3b7346d. All 668 benchmark cases also passed. The benchmark workflow only failed when CodSpeed rejected the fork results upload with HTTP 401 (repository not configured or authorized). Upstream Actions is awaiting maintainer approval.

The requested prerequisite #9829 remains open.

Fixes #9619

@radarhere radarhere changed the title Feature/jjn dithering 9619 Add Jarvis-Judice-Ninke dithering Oct 21, 2026
@akx

akx commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

It would be nice to get #9829 merged first, as it refactors the palette quantization code into a separate module (and fixes bugs in it).

Comment thread src/libImaging/Convert.c Outdated
}

if (dither) {
if (dither == 4) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably shouldn't use the magic number 4. (Also see #10122, related)

Comment thread Tests/test_image_dither_jjn.py Outdated
Comment on lines +9 to +48
# The JJN kernel has twelve forward neighbors and is normalized by 48.
TAPS = (
(1, 0, 7),
(2, 0, 5),
(-2, 1, 3),
(-1, 1, 5),
(0, 1, 7),
(1, 1, 5),
(2, 1, 3),
(-2, 2, 1),
(-1, 2, 3),
(0, 2, 5),
(1, 2, 3),
(2, 2, 1),
)


def _expected_bilevel(values: list[int], size: tuple[int, int]) -> list[int]:
width, height = size
errors = [[0] * width for _ in range(3)]
output: list[int] = []

for y in range(height):
for x in range(width):
weighted_error = errors[y % 3][x]
correction = abs(weighted_error) // 48
if weighted_error < 0:
correction = -correction
value = max(0, min(255, values[y * width + x] + correction))
color = 255 if value > 128 else 0
output.append(color)

for dx, dy, weight in TAPS:
xx, yy = x + dx, y + dy
if 0 <= xx < width and yy < height:
errors[yy % 3][xx] += (value - color) * weight

errors[y % 3] = [0] * width

return output

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just a reimplementation of JJN in Python? I think house style would be to add a JJN-dithered Hopper test fixture image to compare against?

Comment thread src/libImaging/Convert.c Outdated
}

/* Jarvis-Judice-Ninke error diffusion, with a 5x3 kernel (divisor 48).
* Keep three rows of weighted errors; every contribution stays in bounds. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this comment mean?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Jarvis-judice-ninke dithering

3 participants