From 69f73df558bddafed22c9d8b74bd3abd46573cf9 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 29 Jul 2026 10:29:13 +0300 Subject: [PATCH 01/13] Improve benchmark test image The old image was (e.g. in the case of palette optimization) pessimal in some cases. --- Tests/benchmarks.py | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/Tests/benchmarks.py b/Tests/benchmarks.py index 0c67d298dcb..99670d4e6b1 100644 --- a/Tests/benchmarks.py +++ b/Tests/benchmarks.py @@ -4,6 +4,7 @@ from __future__ import annotations +import hashlib import pathlib from importlib.util import find_spec from io import BytesIO @@ -78,12 +79,40 @@ def bench( def make_pillow_image( mode: str, size: tuple[int, int], - pattern_offset: int = 0, + seed: int = 0, ) -> Image.Image: - im = Image.new("RGB", size) - n = im.width * im.height * 3 - period = bytes((i + pattern_offset) % 256 for i in range(256)) - im.frombytes((period * (n // 256 + 1))[:n]) + """ + Generate a synthetic test image with the given mode and size. + Different seeds give different final images. + """ + width, height = size + vertical = Image.linear_gradient("L") + horizontal = vertical.transpose(Transpose.ROTATE_90) + radial = Image.radial_gradient("L") + base = Image.merge("RGB", (horizontal, vertical, radial)).resize( + size, Resampling.BILINEAR + ) + # SHAKE128 gives us a predictable noise pattern. + noise_bytes = hashlib.shake_128(f"pillow-benchmark-{seed}".encode()).digest( + width * height * 3 + ) + noise = Image.frombytes("RGB", size, noise_bytes) + + def centered_box(area_fraction: float) -> tuple[int, int, int, int]: + inset = (1 - area_fraction**0.5) / 2 + return ( + round(width * inset), + round(height * inset), + round(width * (1 - inset)), + round(height * (1 - inset)), + ) + + im = base + noise_box = centered_box(1 / 2) + im.paste(noise.crop(noise_box), noise_box) # Noise in the middle + im.paste(tuple(noise_bytes[:3]), centered_box(1 / 6)) # Solid center + if seed: + im = ImageChops.offset(im, seed * 383, seed * 271) return im.convert(mode) @@ -96,7 +125,7 @@ def test_blend( size: tuple[int, int], ) -> None: im1 = make_pillow_image(mode, size) - im2 = make_pillow_image(mode, size, pattern_offset=1024) + im2 = make_pillow_image(mode, size, seed=1) result = bench(Image.blend, im1, im2, 0.5) assert result.size == im1.size @@ -145,7 +174,7 @@ def test_alpha_composite( alpha: str, ) -> None: im1 = make_pillow_image(mode, size) - im2 = make_pillow_image(mode, size, pattern_offset=1024) + im2 = make_pillow_image(mode, size, seed=1) if alpha == "opaque": im2.putalpha(255) elif alpha == "transparent": @@ -407,7 +436,7 @@ def test_chops( op: Callable[[Image.Image, Image.Image], Image.Image], ) -> None: im1 = make_pillow_image(mode, size) - im2 = make_pillow_image(mode, size, pattern_offset=1024) + im2 = make_pillow_image(mode, size, seed=1) bench.extra_info["label"] = [op.__name__] result = bench(op, im1, im2) assert result.size == im1.size From d3ed7651ab84d85f0be22f2beb8d028d9ec0408d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 29 Jul 2026 09:25:49 +0300 Subject: [PATCH 02/13] Improve docs for Image.quantize() --- src/PIL/Image.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ddbaec20a05..42e3e5e87ba 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1292,8 +1292,9 @@ def quantize( and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so :data:`Quantize.FASTOCTREE` is used by default instead. :param kmeans: Integer greater than or equal to zero. - :param palette: Quantize to the palette of given - :py:class:`PIL.Image.Image`. + :param palette: Quantize to the palette of given :py:class:`PIL.Image.Image`. + The `colors`, `method` and `kmeans` parameters are ignored + if a reference palette is used. :param dither: Dithering method, used when converting from mode "RGB" to "P" or from "RGB" or "L" to "1". Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` From e97b8701945ae63a03d14f962ae6e7953fcf4e5d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 29 Jul 2026 09:54:51 +0300 Subject: [PATCH 03/13] Add benchmark_save fixture --- Tests/benchmarks.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Tests/benchmarks.py b/Tests/benchmarks.py index 99670d4e6b1..eacb825ff12 100644 --- a/Tests/benchmarks.py +++ b/Tests/benchmarks.py @@ -5,7 +5,10 @@ from __future__ import annotations import hashlib +import os import pathlib +import re +import warnings from importlib.util import find_spec from io import BytesIO @@ -18,6 +21,8 @@ if TYPE_CHECKING: from collections.abc import Callable + BenchmarkSave = Callable[[Image.Image], None] + from pytest_benchmark.fixture import ( # type: ignore[import-not-found] BenchmarkFixture, ) @@ -25,6 +30,9 @@ if not (find_spec("pytest_benchmark") or find_spec("pytest_codspeed")): pytest.skip("pytest-benchmark or pytest-codspeed required", allow_module_level=True) +_save_results = os.environ.get("PILLOW_BENCHMARK_SAVE_RESULTS_PATH") +SAVE_RESULTS_PATH = pathlib.Path(_save_results) if _save_results else None + # These can be adjusted to add more modes to benchmark # (however all features benchmarked might not support all PIL modes). MODES = ["RGB", "RGBA", "L", "LA"] @@ -76,6 +84,28 @@ def bench( return benchmark +@pytest.fixture +def benchmark_save(request: pytest.FixtureRequest) -> BenchmarkSave: + """ + Fixture to save a benchmark image, if so configured. + """ + + def save(im: Image.Image) -> None: + if SAVE_RESULTS_PATH: + safe_name = re.sub("[^-a-zA-Z0-9]", "_", str(request.node.name)) + name = (SAVE_RESULTS_PATH / safe_name).with_suffix(".png") + try: + SAVE_RESULTS_PATH.mkdir(parents=True, exist_ok=True) + im.save(name) + except Exception as e: + warnings.warn( + f"Failed to save benchmark result to {name}: {e}", + stacklevel=2, + ) + + return save + + def make_pillow_image( mode: str, size: tuple[int, int], From ea925e5a07de9779a53e67063eeada01bf5ee4b3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 28 Jul 2026 15:40:48 +0300 Subject: [PATCH 04/13] Add benchmark for quantize-to-palette --- Tests/benchmarks.py | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/Tests/benchmarks.py b/Tests/benchmarks.py index eacb825ff12..92fd59d2974 100644 --- a/Tests/benchmarks.py +++ b/Tests/benchmarks.py @@ -565,3 +565,74 @@ def test_quantize(bench: BenchmarkFixture, mode: str, size: tuple[int, int]) -> bench.extra_info["label"] = [f"quantize {mode}"] result = bench(im.quantize, 256) assert result.mode == "P" + + +@pytest.mark.benchmark(group="quantize") +@pytest.mark.parametrize("output_mode", ["P", "PA"]) +@pytest.mark.parametrize("size", SIZES, ids=_format_size) +def test_quantize_grayscale_to_palette( + bench: BenchmarkFixture, + output_mode: str, + size: tuple[int, int], +) -> None: + im = make_pillow_image("L", size) + bench.extra_info["label"] = [f"quantize L to {output_mode}"] + result = bench(im.convert, output_mode) + assert result.mode == output_mode + assert result.convert("L").tobytes() == im.tobytes() + + +@pytest.mark.benchmark(group="quantize") +@pytest.mark.parametrize( + "dither", + [Image.Dither.NONE, Image.Dither.FLOYDSTEINBERG], + ids=["none", "floyd-steinberg"], +) +@pytest.mark.parametrize("output_mode", ["P", "PA"]) +@pytest.mark.parametrize( + "source_type", + [ + "synthetic", + *(pytest.param(image, id=f"{image.stem}") for image in PATHS), + ], +) +@pytest.mark.parametrize("palette_type", ["exact", "grayscale", "web"]) +@pytest.mark.parametrize("size", SIZES, ids=_format_size) +def test_quantize_to_palette( + bench: BenchmarkFixture, + benchmark_save: BenchmarkSave, + dither: Image.Dither, + output_mode: str, + source_type: str | pathlib.Path, + palette_type: str, + size: tuple[int, int], +) -> None: + if isinstance(source_type, pathlib.Path): + im = Image.open(source_type).convert("RGB").resize(size) + elif source_type == "synthetic": + im = make_pillow_image("RGB", size) + if palette_type == "exact": + palette = im.quantize(256) + im = palette.convert("RGB") + elif palette_type == "web": + palette = Image.new("RGB", (1, 1)).convert( + "P", + palette=Image.Palette.WEB, + dither=Image.Dither.NONE, + ) + else: + palette = Image.new("P", (1, 1)) + palette.putpalette(tuple(channel for i in range(256) for channel in (i, i, i))) + + bench.extra_info["label"] = [ + f"{source_type} RGB to {output_mode}, " + f"{palette_type} palette, " + f"{dither.name.lower()} dither", + ] + if output_mode == "P": + result = bench(im.quantize, palette=palette, dither=dither) + else: + result = bench(lambda: im._new(im.im.convert(output_mode, dither, palette.im))) + assert result.mode == output_mode + + benchmark_save(result) From 188c0c5591853f444d11359103ebdc9a2ed6c1fd Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 28 Jul 2026 14:56:19 +0300 Subject: [PATCH 05/13] Add documentation for ImagingError_MemoryError --- src/_imaging.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index 9bdb6328782..8da4d297911 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -364,6 +364,10 @@ static const char *no_palette = "image has no palette"; static const char *readonly = "image is readonly"; /* static const char* no_content = "image has no content"; */ +/** + * Set a MemoryError exception and return NULL. + * @return Always NULL. + */ void * ImagingError_MemoryError(void) { return PyErr_NoMemory(); From a187e1d667e48afcc1988cb3c39d17b158cb36c5 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 28 Jul 2026 11:42:33 +0300 Subject: [PATCH 06/13] Document palette constraints better; use constants --- src/libImaging/Imaging.h | 9 +++++---- src/libImaging/Palette.c | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 472bda5d0fd..34e32a821f3 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -151,16 +151,17 @@ struct ImagingHistogramInstance { long *histogram; /* Histogram (bands*256 longs) */ }; +#define IMAGING_PALETTE_MAX_ENTRIES 256 + struct ImagingPaletteInstance { /* Format */ - ModeID mode; + ModeID mode; /* RGB/RGBA/CMYK */ /* Data */ int size; - UINT8 palette[1024]; /* Palette data (same format as image data) */ + UINT8 palette[IMAGING_PALETTE_MAX_ENTRIES * 4]; /* Palette data (see mode) */ - INT16 *cache; /* Palette cache (used for predefined palettes) */ - int keep_cache; /* This palette will be reused; keep cache */ + INT16 *cache; /* Palette cache (used for predefined palettes) */ }; typedef struct ImagingMemoryArena { diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index b2dacf656b5..95702af78ac 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -40,7 +40,7 @@ ImagingPaletteNew(const ModeID mode) { palette->mode = mode; palette->size = 0; - for (i = 0; i < 256; i++) { + for (i = 0; i < IMAGING_PALETTE_MAX_ENTRIES; i++) { palette->palette[i * 4 + 3] = 255; /* opaque */ } @@ -153,7 +153,7 @@ ImagingPaletteDelete(ImagingPalette palette) { void ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { int i, j; - unsigned int dmin[256], dmax; + unsigned int dmin[IMAGING_PALETTE_MAX_ENTRIES], dmax; int r0, g0, b0; int r1, g1, b1; int rc, gc, bc; From a4bbdcbbf1a253d4bd6c77e0c71bd902dadf1d9a Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 28 Jul 2026 11:38:50 +0300 Subject: [PATCH 07/13] Refactor: move topalette to separate file --- setup.py | 1 + src/libImaging/Convert.c | 201 +------------------------ src/libImaging/ConvertToPalette.c | 234 ++++++++++++++++++++++++++++++ src/libImaging/ConvertToPalette.h | 4 + 4 files changed, 240 insertions(+), 200 deletions(-) create mode 100644 src/libImaging/ConvertToPalette.c create mode 100644 src/libImaging/ConvertToPalette.h diff --git a/setup.py b/setup.py index 168d4f237c9..f98a9a463d6 100644 --- a/setup.py +++ b/setup.py @@ -87,6 +87,7 @@ def get_version() -> str: "Chops", "ColorLUT", "Convert", + "ConvertToPalette", "ConvertYCbCr", "Copy", "Crop", diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 1fd14a944ef..31fee0df3c7 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -33,6 +33,7 @@ */ #include "Imaging.h" +#include "ConvertToPalette.h" #define MAX(a, b) (a) > (b) ? (a) : (b) #define MIN(a, b) (a) < (b) ? (a) : (b) @@ -1144,206 +1145,6 @@ frompalette(Imaging imOut, Imaging imIn, const ModeID mode) { #if defined(_MSC_VER) #pragma optimize("", off) #endif -static Imaging -topalette( - Imaging imOut, Imaging imIn, const ModeID mode, ImagingPalette inpalette, int dither -) { - ImagingSectionCookie cookie; - int alpha; - int x, y; - ImagingPalette palette = inpalette; - - /* Map L or RGB/RGBX/RGBA/RGBa to palette image */ - if (imIn->mode != IMAGING_MODE_L && imIn->mode != IMAGING_MODE_RGB && - imIn->mode != IMAGING_MODE_RGBX && imIn->mode != IMAGING_MODE_RGBA && - imIn->mode != IMAGING_MODE_RGBa) { - return (Imaging)ImagingError_ValueError("conversion not supported"); - } - - alpha = mode == IMAGING_MODE_PA; - - if (palette == NULL) { - /* FIXME: make user configurable */ - if (imIn->bands == 1) { - palette = ImagingPaletteNew(IMAGING_MODE_RGB); - - palette->size = 256; - int i; - for (i = 0; i < 256; i++) { - palette->palette[i * 4] = palette->palette[i * 4 + 1] = - palette->palette[i * 4 + 2] = (UINT8)i; - } - } else { - palette = ImagingPaletteNewBrowser(); /* Standard colour cube */ - } - } - - if (!palette) { - return (Imaging)ImagingError_ValueError("no palette"); - } - - imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut) { - if (palette != inpalette) { - ImagingPaletteDelete(palette); - } - return NULL; - } - - ImagingPaletteDelete(imOut->palette); - imOut->palette = ImagingPaletteDuplicate(palette); - - if (imIn->bands == 1) { - /* grayscale image */ - - /* Grayscale palette: copy data as is */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - if (alpha) { - l2rgb((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); - } else { - memcpy(imOut->image[y], imIn->image[y], imIn->linesize); - } - } - ImagingSectionLeave(&cookie); - - } else { - /* colour image */ - - /* Create mapping cache */ - if (ImagingPaletteCachePrepare(palette) < 0) { - ImagingDelete(imOut); - if (palette != inpalette) { - ImagingPaletteDelete(palette); - } - return NULL; - } - - if (dither) { - /* floyd-steinberg dither */ - - int *errors; - errors = calloc(imIn->xsize + 1, sizeof(int) * 3); - if (!errors) { - ImagingDelete(imOut); - return ImagingError_MemoryError(); - } - - /* Map each pixel to the nearest palette entry */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - int r, r0, r1, r2; - int g, g0, g1, g2; - int b, b0, b1, b2; - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; - int *e = errors; - - r = r0 = r1 = 0; - g = g0 = g1 = 0; - b = b0 = b1 = b2 = 0; - - for (x = 0; x < imIn->xsize; x++, in += 4) { - int d2; - INT16 *cache; - - r = CLIP8(in[0] + (r + e[3 + 0]) / 16); - g = CLIP8(in[1] + (g + e[3 + 1]) / 16); - b = CLIP8(in[2] + (b + e[3 + 2]) / 16); - - /* get closest colour */ - cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); - } - if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; - out[x * 4 + 3] = 255; - } else { - out[x] = (UINT8)cache[0]; - } - - r -= (int)palette->palette[cache[0] * 4]; - g -= (int)palette->palette[cache[0] * 4 + 1]; - b -= (int)palette->palette[cache[0] * 4 + 2]; - - /* propagate errors (don't ask ;-) */ - r2 = r; - d2 = r + r; - r += d2; - e[0] = r + r0; - r += d2; - r0 = r + r1; - r1 = r2; - r += d2; - g2 = g; - d2 = g + g; - g += d2; - e[1] = g + g0; - g += d2; - g0 = g + g1; - g1 = g2; - g += d2; - b2 = b; - d2 = b + b; - b += d2; - e[2] = b + b0; - b += d2; - b0 = b + b1; - b1 = b2; - b += d2; - - e += 3; - } - - e[0] = b0; - e[1] = b1; - e[2] = b2; - } - ImagingSectionLeave(&cookie); - free(errors); - - } else { - /* closest colour */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - int r, g, b; - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; - - for (x = 0; x < imIn->xsize; x++, in += 4) { - INT16 *cache; - - r = in[0]; - g = in[1]; - b = in[2]; - - /* get closest colour */ - cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); - } - if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; - out[x * 4 + 3] = 255; - } else { - out[x] = (UINT8)cache[0]; - } - } - } - ImagingSectionLeave(&cookie); - } - if (inpalette != palette) { - ImagingPaletteCacheDelete(palette); - } - } - - if (inpalette != palette) { - ImagingPaletteDelete(palette); - } - - return imOut; -} static Imaging tobilevel(Imaging imOut, Imaging imIn) { diff --git a/src/libImaging/ConvertToPalette.c b/src/libImaging/ConvertToPalette.c new file mode 100644 index 00000000000..477c124b56d --- /dev/null +++ b/src/libImaging/ConvertToPalette.c @@ -0,0 +1,234 @@ +/* + * The Python Imaging Library + * + * See Convert.c for legacy history of this file. + * + * Copyright (c) 1997-2005 by Secret Labs AB. + * Copyright (c) 1995-1997 by Fredrik Lundh. + * + * See the README file for details on usage and redistribution. + */ + +#include "Imaging.h" + +// TODO: copied from Convert.c, will be removed soon +static void +l2rgb(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++) { + UINT8 v = *in++; + *out++ = v; + *out++ = v; + *out++ = v; + *out++ = 255; + } +} + +#if defined(_MSC_VER) +#pragma optimize("", off) +#endif + +Imaging +topalette( + Imaging imOut, Imaging imIn, const ModeID mode, ImagingPalette inpalette, int dither +) { + ImagingSectionCookie cookie; + int alpha; + int x, y; + ImagingPalette palette = inpalette; + + /* Map L or RGB/RGBX/RGBA/RGBa to palette image */ + if (imIn->mode != IMAGING_MODE_L && imIn->mode != IMAGING_MODE_RGB && + imIn->mode != IMAGING_MODE_RGBX && imIn->mode != IMAGING_MODE_RGBA && + imIn->mode != IMAGING_MODE_RGBa) { + return (Imaging)ImagingError_ValueError("conversion not supported"); + } + + alpha = mode == IMAGING_MODE_PA; + + if (palette == NULL) { + /* FIXME: make user configurable */ + if (imIn->bands == 1) { + palette = ImagingPaletteNew(IMAGING_MODE_RGB); + + palette->size = 256; + int i; + for (i = 0; i < 256; i++) { + palette->palette[i * 4] = palette->palette[i * 4 + 1] = + palette->palette[i * 4 + 2] = (UINT8)i; + } + } else { + palette = ImagingPaletteNewBrowser(); /* Standard colour cube */ + } + } + + if (!palette) { + return (Imaging)ImagingError_ValueError("no palette"); + } + + imOut = ImagingNew2Dirty(mode, imOut, imIn); + if (!imOut) { + if (palette != inpalette) { + ImagingPaletteDelete(palette); + } + return NULL; + } + + ImagingPaletteDelete(imOut->palette); + imOut->palette = ImagingPaletteDuplicate(palette); + + if (imIn->bands == 1) { + /* grayscale image */ + + /* Grayscale palette: copy data as is */ + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + if (alpha) { + l2rgb((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); + } else { + memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + } + } + ImagingSectionLeave(&cookie); + + } else { + /* colour image */ + + /* Create mapping cache */ + if (ImagingPaletteCachePrepare(palette) < 0) { + ImagingDelete(imOut); + if (palette != inpalette) { + ImagingPaletteDelete(palette); + } + return NULL; + } + + if (dither) { + /* floyd-steinberg dither */ + + int *errors; + errors = calloc(imIn->xsize + 1, sizeof(int) * 3); + if (!errors) { + ImagingDelete(imOut); + return ImagingError_MemoryError(); + } + + /* Map each pixel to the nearest palette entry */ + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + int r, r0, r1, r2; + int g, g0, g1, g2; + int b, b0, b1, b2; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; + int *e = errors; + + r = r0 = r1 = 0; + g = g0 = g1 = 0; + b = b0 = b1 = b2 = 0; + + for (x = 0; x < imIn->xsize; x++, in += 4) { + int d2; + INT16 *cache; + + r = CLIP8(in[0] + (r + e[3 + 0]) / 16); + g = CLIP8(in[1] + (g + e[3 + 1]) / 16); + b = CLIP8(in[2] + (b + e[3 + 2]) / 16); + + /* get closest colour */ + cache = &ImagingPaletteCache(palette, r, g, b); + if (cache[0] == 0x100) { + ImagingPaletteCacheUpdate(palette, r, g, b); + } + if (alpha) { + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4 + 3] = 255; + } else { + out[x] = (UINT8)cache[0]; + } + + r -= (int)palette->palette[cache[0] * 4]; + g -= (int)palette->palette[cache[0] * 4 + 1]; + b -= (int)palette->palette[cache[0] * 4 + 2]; + + /* propagate errors (don't ask ;-) */ + r2 = r; + d2 = r + r; + r += d2; + e[0] = r + r0; + r += d2; + r0 = r + r1; + r1 = r2; + r += d2; + g2 = g; + d2 = g + g; + g += d2; + e[1] = g + g0; + g += d2; + g0 = g + g1; + g1 = g2; + g += d2; + b2 = b; + d2 = b + b; + b += d2; + e[2] = b + b0; + b += d2; + b0 = b + b1; + b1 = b2; + b += d2; + + e += 3; + } + + e[0] = b0; + e[1] = b1; + e[2] = b2; + } + ImagingSectionLeave(&cookie); + free(errors); + + } else { + /* closest colour */ + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + int r, g, b; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; + + for (x = 0; x < imIn->xsize; x++, in += 4) { + INT16 *cache; + + r = in[0]; + g = in[1]; + b = in[2]; + + /* get closest colour */ + cache = &ImagingPaletteCache(palette, r, g, b); + if (cache[0] == 0x100) { + ImagingPaletteCacheUpdate(palette, r, g, b); + } + if (alpha) { + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4 + 3] = 255; + } else { + out[x] = (UINT8)cache[0]; + } + } + } + ImagingSectionLeave(&cookie); + } + if (inpalette != palette) { + ImagingPaletteCacheDelete(palette); + } + } + + if (inpalette != palette) { + ImagingPaletteDelete(palette); + } + + return imOut; +} + +#if defined(_MSC_VER) +#pragma optimize("", on) +#endif diff --git a/src/libImaging/ConvertToPalette.h b/src/libImaging/ConvertToPalette.h new file mode 100644 index 00000000000..b4d4e1f0a6f --- /dev/null +++ b/src/libImaging/ConvertToPalette.h @@ -0,0 +1,4 @@ +Imaging +topalette( + Imaging imOut, Imaging imIn, const ModeID mode, ImagingPalette inpalette, int dither +); From 080b05c481a7462add9d97e329db831b84530da6 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 28 Jul 2026 12:11:01 +0300 Subject: [PATCH 08/13] Split topalette converter functions for less nesting --- src/libImaging/ConvertToPalette.c | 311 ++++++++++++++++-------------- 1 file changed, 170 insertions(+), 141 deletions(-) diff --git a/src/libImaging/ConvertToPalette.c b/src/libImaging/ConvertToPalette.c index 477c124b56d..46bc596e9ea 100644 --- a/src/libImaging/ConvertToPalette.c +++ b/src/libImaging/ConvertToPalette.c @@ -11,30 +11,177 @@ #include "Imaging.h" -// TODO: copied from Convert.c, will be removed soon +#if defined(_MSC_VER) +#pragma optimize("", off) +#endif + +/** + * Internal function to convert a grayscale image to a palette image. + * The palette is assumed to be the grayscale ramp, so we can just copy the data as is. + * imOut and imIn MUST be different images. + */ static void -l2rgb(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++) { - UINT8 v = *in++; - *out++ = v; - *out++ = v; - *out++ = v; - *out++ = 255; +topalette_grayscale(Imaging imOut, Imaging imIn) { + int alpha = imOut->mode == IMAGING_MODE_PA; + /* Grayscale palette: copy data as is */ + ImagingSectionCookie cookie; + ImagingSectionEnter(&cookie); + int xsize = imIn->xsize, ysize = imIn->ysize; + if (alpha) { + for (int y = 0; y < ysize; y++) { + // Restrict safe: we know imOut and imIn to be different images + UINT8 *restrict in = (UINT8 *)imIn->image[y]; + UINT8 *restrict out = (UINT8 *)imOut->image[y]; + for (int x = 0; x < xsize; x++) { + UINT8 v = *in++; + *out++ = v; + *out++ = v; + *out++ = v; + *out++ = 255; + } + } + } else { + for (int y = 0; y < imIn->ysize; y++) { + memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + } } + ImagingSectionLeave(&cookie); } -#if defined(_MSC_VER) -#pragma optimize("", off) -#endif +/** + * Internal function to convert a colour image to a palette image using + * the Floyd-Steinberg dithering algorithm. + * imOut and imIn MUST be different images. + * @return 0 on success, 1 on memory allocation failure (PyErr is set). + */ +static int +topalette_colour_floyd_steinberg(Imaging imOut, Imaging imIn, ImagingPalette palette) { + int alpha = imOut->mode == IMAGING_MODE_PA; + int *errors = calloc(imIn->xsize + 1, sizeof(int) * 3); + if (!errors) { + ImagingError_MemoryError(); // Sets the exception + return 1; + } + + /* Map each pixel to the nearest palette entry */ + + ImagingSectionCookie cookie; + ImagingSectionEnter(&cookie); + for (int y = 0; y < imIn->ysize; y++) { + int r, r0, r1, r2; + int g, g0, g1, g2; + int b, b0, b1, b2; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; + int *e = errors; + + r = r0 = r1 = 0; + g = g0 = g1 = 0; + b = b0 = b1 = b2 = 0; + + for (int x = 0; x < imIn->xsize; x++, in += 4) { + int d2; + INT16 *cache; + + r = CLIP8(in[0] + (r + e[3 + 0]) / 16); + g = CLIP8(in[1] + (g + e[3 + 1]) / 16); + b = CLIP8(in[2] + (b + e[3 + 2]) / 16); + + /* get closest colour */ + cache = &ImagingPaletteCache(palette, r, g, b); + if (cache[0] == 0x100) { + ImagingPaletteCacheUpdate(palette, r, g, b); + } + if (alpha) { + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4 + 3] = 255; + } else { + out[x] = (UINT8)cache[0]; + } + + r -= (int)palette->palette[cache[0] * 4]; + g -= (int)palette->palette[cache[0] * 4 + 1]; + b -= (int)palette->palette[cache[0] * 4 + 2]; + + /* propagate errors (don't ask ;-) */ + r2 = r; + d2 = r + r; + r += d2; + e[0] = r + r0; + r += d2; + r0 = r + r1; + r1 = r2; + r += d2; + g2 = g; + d2 = g + g; + g += d2; + e[1] = g + g0; + g += d2; + g0 = g + g1; + g1 = g2; + g += d2; + b2 = b; + d2 = b + b; + b += d2; + e[2] = b + b0; + b += d2; + b0 = b + b1; + b1 = b2; + b += d2; + + e += 3; + } + + e[0] = b0; + e[1] = b1; + e[2] = b2; + } + ImagingSectionLeave(&cookie); + free(errors); + return 0; +} + +/** + * Internal function to convert a colour image to a palette image using the closest + * colour. imOut and imIn MUST be different images. + */ +static void +topalette_colour_closest(Imaging imOut, Imaging imIn, ImagingPalette palette) { + int alpha = imOut->mode == IMAGING_MODE_PA; + ImagingSectionCookie cookie; + ImagingSectionEnter(&cookie); + for (int y = 0; y < imIn->ysize; y++) { + int r, g, b; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; + + for (int x = 0; x < imIn->xsize; x++, in += 4) { + INT16 *cache; + + r = in[0]; + g = in[1]; + b = in[2]; + + /* get closest colour */ + cache = &ImagingPaletteCache(palette, r, g, b); + if (cache[0] == 0x100) { + ImagingPaletteCacheUpdate(palette, r, g, b); + } + if (alpha) { + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4 + 3] = 255; + } else { + out[x] = (UINT8)cache[0]; + } + } + } + ImagingSectionLeave(&cookie); +} Imaging topalette( Imaging imOut, Imaging imIn, const ModeID mode, ImagingPalette inpalette, int dither ) { - ImagingSectionCookie cookie; - int alpha; - int x, y; ImagingPalette palette = inpalette; /* Map L or RGB/RGBX/RGBA/RGBa to palette image */ @@ -44,16 +191,13 @@ topalette( return (Imaging)ImagingError_ValueError("conversion not supported"); } - alpha = mode == IMAGING_MODE_PA; - if (palette == NULL) { /* FIXME: make user configurable */ if (imIn->bands == 1) { palette = ImagingPaletteNew(IMAGING_MODE_RGB); palette->size = 256; - int i; - for (i = 0; i < 256; i++) { + for (int i = 0; i < 256; i++) { palette->palette[i * 4] = palette->palette[i * 4 + 1] = palette->palette[i * 4 + 2] = (UINT8)i; } @@ -78,19 +222,7 @@ topalette( imOut->palette = ImagingPaletteDuplicate(palette); if (imIn->bands == 1) { - /* grayscale image */ - - /* Grayscale palette: copy data as is */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - if (alpha) { - l2rgb((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); - } else { - memcpy(imOut->image[y], imIn->image[y], imIn->linesize); - } - } - ImagingSectionLeave(&cookie); - + topalette_grayscale(imOut, imIn); } else { /* colour image */ @@ -104,125 +236,22 @@ topalette( } if (dither) { - /* floyd-steinberg dither */ - - int *errors; - errors = calloc(imIn->xsize + 1, sizeof(int) * 3); - if (!errors) { + if (topalette_colour_floyd_steinberg(imOut, imIn, palette) != 0) { + // Exception will have been set by the work function ImagingDelete(imOut); - return ImagingError_MemoryError(); - } - - /* Map each pixel to the nearest palette entry */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - int r, r0, r1, r2; - int g, g0, g1, g2; - int b, b0, b1, b2; - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; - int *e = errors; - - r = r0 = r1 = 0; - g = g0 = g1 = 0; - b = b0 = b1 = b2 = 0; - - for (x = 0; x < imIn->xsize; x++, in += 4) { - int d2; - INT16 *cache; - - r = CLIP8(in[0] + (r + e[3 + 0]) / 16); - g = CLIP8(in[1] + (g + e[3 + 1]) / 16); - b = CLIP8(in[2] + (b + e[3 + 2]) / 16); - - /* get closest colour */ - cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); - } - if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; - out[x * 4 + 3] = 255; - } else { - out[x] = (UINT8)cache[0]; - } - - r -= (int)palette->palette[cache[0] * 4]; - g -= (int)palette->palette[cache[0] * 4 + 1]; - b -= (int)palette->palette[cache[0] * 4 + 2]; - - /* propagate errors (don't ask ;-) */ - r2 = r; - d2 = r + r; - r += d2; - e[0] = r + r0; - r += d2; - r0 = r + r1; - r1 = r2; - r += d2; - g2 = g; - d2 = g + g; - g += d2; - e[1] = g + g0; - g += d2; - g0 = g + g1; - g1 = g2; - g += d2; - b2 = b; - d2 = b + b; - b += d2; - e[2] = b + b0; - b += d2; - b0 = b + b1; - b1 = b2; - b += d2; - - e += 3; - } - - e[0] = b0; - e[1] = b1; - e[2] = b2; + return NULL; } - ImagingSectionLeave(&cookie); - free(errors); - } else { - /* closest colour */ - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - int r, g, b; - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; - - for (x = 0; x < imIn->xsize; x++, in += 4) { - INT16 *cache; - - r = in[0]; - g = in[1]; - b = in[2]; - - /* get closest colour */ - cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); - } - if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; - out[x * 4 + 3] = 255; - } else { - out[x] = (UINT8)cache[0]; - } - } - } - ImagingSectionLeave(&cookie); + topalette_colour_closest(imOut, imIn, palette); } if (inpalette != palette) { + // If we created a temporary palette, delete the cache to free memory ImagingPaletteCacheDelete(palette); } } if (inpalette != palette) { + // If we created a temporary palette, delete it to free memory ImagingPaletteDelete(palette); } From b924b84d1b2e756322eac57b3cbeba8507a5ef47 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 28 Jul 2026 15:11:42 +0300 Subject: [PATCH 09/13] Fix possible null pointer derefs in topalette() --- src/libImaging/ConvertToPalette.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libImaging/ConvertToPalette.c b/src/libImaging/ConvertToPalette.c index 46bc596e9ea..52f1d9f35e3 100644 --- a/src/libImaging/ConvertToPalette.c +++ b/src/libImaging/ConvertToPalette.c @@ -195,6 +195,9 @@ topalette( /* FIXME: make user configurable */ if (imIn->bands == 1) { palette = ImagingPaletteNew(IMAGING_MODE_RGB); + if (!palette) { // Exception has been set by ImagingPaletteNew + return NULL; + } palette->size = 256; for (int i = 0; i < 256; i++) { @@ -220,6 +223,9 @@ topalette( ImagingPaletteDelete(imOut->palette); imOut->palette = ImagingPaletteDuplicate(palette); + if (!imOut->palette) { // Exception has been set by ImagingPaletteDuplicate + goto done; + } if (imIn->bands == 1) { topalette_grayscale(imOut, imIn); From 8cd3a7020622a90d451a94b48b8497ed2a57adaa Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 28 Jul 2026 12:20:17 +0300 Subject: [PATCH 10/13] Make to-palette conversion use exact colors if available This affects `.quantize(palette=...)`. Refs #1852 --- Tests/benchmarks.py | 3 +- Tests/test_image_quantize.py | 37 ++++++++ src/libImaging/ConvertToPalette.c | 148 +++++++++++++++++++++--------- 3 files changed, 145 insertions(+), 43 deletions(-) diff --git a/Tests/benchmarks.py b/Tests/benchmarks.py index 92fd59d2974..c87b8bbc7be 100644 --- a/Tests/benchmarks.py +++ b/Tests/benchmarks.py @@ -634,5 +634,6 @@ def test_quantize_to_palette( else: result = bench(lambda: im._new(im.im.convert(output_mode, dither, palette.im))) assert result.mode == output_mode - + if palette_type == "exact": + assert result.convert("RGB").tobytes() == im.tobytes() benchmark_save(result) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 8876285605a..9902de0fe69 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -94,6 +94,43 @@ def test_quantize_no_dither2() -> None: assert px[x, 0] == (0 if x < 5 else 1) +@pytest.mark.parametrize("dither", (Image.Dither.NONE, Image.Dither.FLOYDSTEINBERG)) +def test_quantize_exact_palette_matches(dither: Image.Dither) -> None: + # These colors share a slot in the nearest-color cache. + colors = ((252, 252, 252), (255, 255, 255)) + im = Image.new("RGB", (2, 1)) + im.putdata(colors) + + palette = Image.new("P", (1, 1)) + palette.putpalette(tuple(channel for color in colors for channel in color)) + quantized = im.quantize(palette=palette, dither=dither) + + assert quantized.tobytes() == b"\x00\x01" + assert quantized.convert("RGB").tobytes() == im.tobytes() + + +def test_quantize_exact_palette_matches_maximum_colors() -> None: + colors = tuple((r, 0, 0) for r in range(256)) + im = Image.new("RGB", (256, 1)) + im.putdata(colors) + + palette = Image.new("P", (1, 1)) + palette.putpalette(tuple(channel for color in colors for channel in color)) + quantized = im.quantize(palette=palette, dither=Image.Dither.NONE) + + assert quantized.tobytes() == bytes(range(256)) + assert quantized.convert("RGB").tobytes() == im.tobytes() + + +def test_quantize_duplicate_exact_palette_color_uses_first_index() -> None: + color = (17, 34, 51) + palette = Image.new("P", (1, 1)) + palette.putpalette(color * 2) + + im = Image.new("RGB", (1, 1), color) + assert im.quantize(palette=palette, dither=Image.Dither.NONE).getpixel((0, 0)) == 0 + + def test_quantize_dither_diff() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: diff --git a/src/libImaging/ConvertToPalette.c b/src/libImaging/ConvertToPalette.c index 52f1d9f35e3..b4600393762 100644 --- a/src/libImaging/ConvertToPalette.c +++ b/src/libImaging/ConvertToPalette.c @@ -11,6 +11,64 @@ #include "Imaging.h" +// The hash will always have up to 50% fill factor. +#define EXACT_COLOR_HASH_SIZE (IMAGING_PALETTE_MAX_ENTRIES * 2) + +struct ExactColorHashInstance { + UINT32 keys[EXACT_COLOR_HASH_SIZE]; + UINT8 values[EXACT_COLOR_HASH_SIZE]; +}; +typedef struct ExactColorHashInstance *ExactColorHash; + +static UINT32 +exact_color_key(int r, int g, int b) { + UINT32 key = ((UINT32)r << 16) | ((UINT32)g << 8) | (UINT32)b; + // Zero will mean "empty slot", so increment all keys by 1. + return key + 1; +} + +static UINT32 +exact_color_hash(UINT32 key) { + // Fibonacci hashing distributes the 25-bit keys across the 512-entry table. + return (key * 2654435761U) >> 23; +} + +static void +prepare_exact_color_hash(ImagingPalette palette, ExactColorHash ech) { + memset(ech->keys, 0, EXACT_COLOR_HASH_SIZE * sizeof(UINT32)); + for (int i = 0; i < palette->size; i++) { + UINT32 key = exact_color_key( + palette->palette[i * 4], + palette->palette[i * 4 + 1], + palette->palette[i * 4 + 2] + ); + UINT32 hash = exact_color_hash(key); + // Linear probe for an empty slot. + // Given the guarantee of at most 50% fill factor (see EXACT_COLOR_HASH_SIZE), + // this will always terminate. + while (ech->keys[hash] != 0 && ech->keys[hash] != key) { + hash = (hash + 1) % EXACT_COLOR_HASH_SIZE; + } + if (ech->keys[hash] == 0) { + ech->keys[hash] = key; + ech->values[hash] = (UINT8)i; + } + } +} + +static int +find_exact_color(const ExactColorHash ech, int r, int g, int b) { + UINT32 key = exact_color_key(r, g, b); + UINT32 hash = exact_color_hash(key); + while (ech->keys[hash] != 0) { + if (ech->keys[hash] == key) { + return ech->values[hash]; + } + hash = (hash + 1) % EXACT_COLOR_HASH_SIZE; + } + return -1; +} + #if defined(_MSC_VER) #pragma optimize("", off) #endif @@ -55,7 +113,9 @@ topalette_grayscale(Imaging imOut, Imaging imIn) { * @return 0 on success, 1 on memory allocation failure (PyErr is set). */ static int -topalette_colour_floyd_steinberg(Imaging imOut, Imaging imIn, ImagingPalette palette) { +topalette_colour_floyd_steinberg( + Imaging imOut, Imaging imIn, ImagingPalette palette, ExactColorHash ech +) { int alpha = imOut->mode == IMAGING_MODE_PA; int *errors = calloc(imIn->xsize + 1, sizeof(int) * 3); if (!errors) { @@ -81,27 +141,30 @@ topalette_colour_floyd_steinberg(Imaging imOut, Imaging imIn, ImagingPalette pal for (int x = 0; x < imIn->xsize; x++, in += 4) { int d2; - INT16 *cache; r = CLIP8(in[0] + (r + e[3 + 0]) / 16); g = CLIP8(in[1] + (g + e[3 + 1]) / 16); b = CLIP8(in[2] + (b + e[3 + 2]) / 16); - /* get closest colour */ - cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); + int palette_index = find_exact_color(ech, r, g, b); + if (palette_index < 0) { + /* get closest colour */ + INT16 *cache = &ImagingPaletteCache(palette, r, g, b); + if (cache[0] == 0x100) { + ImagingPaletteCacheUpdate(palette, r, g, b); + } + palette_index = cache[0]; } if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)palette_index; out[x * 4 + 3] = 255; } else { - out[x] = (UINT8)cache[0]; + out[x] = (UINT8)palette_index; } - r -= (int)palette->palette[cache[0] * 4]; - g -= (int)palette->palette[cache[0] * 4 + 1]; - b -= (int)palette->palette[cache[0] * 4 + 2]; + r -= (int)palette->palette[palette_index * 4]; + g -= (int)palette->palette[palette_index * 4 + 1]; + b -= (int)palette->palette[palette_index * 4 + 2]; /* propagate errors (don't ask ;-) */ r2 = r; @@ -146,32 +209,32 @@ topalette_colour_floyd_steinberg(Imaging imOut, Imaging imIn, ImagingPalette pal * colour. imOut and imIn MUST be different images. */ static void -topalette_colour_closest(Imaging imOut, Imaging imIn, ImagingPalette palette) { +topalette_colour_closest( + Imaging imOut, Imaging imIn, ImagingPalette palette, ExactColorHash ech +) { int alpha = imOut->mode == IMAGING_MODE_PA; ImagingSectionCookie cookie; ImagingSectionEnter(&cookie); for (int y = 0; y < imIn->ysize; y++) { - int r, g, b; UINT8 *in = (UINT8 *)imIn->image[y]; UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; for (int x = 0; x < imIn->xsize; x++, in += 4) { - INT16 *cache; - - r = in[0]; - g = in[1]; - b = in[2]; - - /* get closest colour */ - cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); + int r = in[0], g = in[1], b = in[2]; + int palette_index = find_exact_color(ech, r, g, b); + if (palette_index < 0) { + /* get closest colour */ + INT16 *cache = &ImagingPaletteCache(palette, r, g, b); + if (cache[0] == 0x100) { + ImagingPaletteCacheUpdate(palette, r, g, b); + } + palette_index = cache[0]; } if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)palette_index; out[x * 4 + 3] = 255; } else { - out[x] = (UINT8)cache[0]; + out[x] = (UINT8)palette_index; } } } @@ -182,6 +245,7 @@ Imaging topalette( Imaging imOut, Imaging imIn, const ModeID mode, ImagingPalette inpalette, int dither ) { + int palette_is_temporary = inpalette == NULL; ImagingPalette palette = inpalette; /* Map L or RGB/RGBX/RGBA/RGBa to palette image */ @@ -215,10 +279,7 @@ topalette( imOut = ImagingNew2Dirty(mode, imOut, imIn); if (!imOut) { - if (palette != inpalette) { - ImagingPaletteDelete(palette); - } - return NULL; + goto done; } ImagingPaletteDelete(imOut->palette); @@ -232,31 +293,34 @@ topalette( } else { /* colour image */ - /* Create mapping cache */ + // An ECH instance is only about 3 KiB; should be safe to allocate on the stack. + struct ExactColorHashInstance exact_color_hash; + prepare_exact_color_hash(palette, &exact_color_hash); + if (ImagingPaletteCachePrepare(palette) < 0) { + // Failed allocation for cache ImagingDelete(imOut); - if (palette != inpalette) { - ImagingPaletteDelete(palette); - } - return NULL; + imOut = NULL; + goto done; } if (dither) { - if (topalette_colour_floyd_steinberg(imOut, imIn, palette) != 0) { + if (topalette_colour_floyd_steinberg( + imOut, imIn, palette, &exact_color_hash + ) != 0) { // Exception will have been set by the work function ImagingDelete(imOut); - return NULL; + imOut = NULL; + goto done; } } else { - topalette_colour_closest(imOut, imIn, palette); - } - if (inpalette != palette) { - // If we created a temporary palette, delete the cache to free memory - ImagingPaletteCacheDelete(palette); + topalette_colour_closest(imOut, imIn, palette, &exact_color_hash); } } - if (inpalette != palette) { +done: + + if (palette_is_temporary) { // If we created a temporary palette, delete it to free memory ImagingPaletteDelete(palette); } From b75358cb593cf8c03383d46acc57a37c24a7d358 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 28 Jul 2026 15:56:43 +0300 Subject: [PATCH 11/13] Apply hoist and restrict optimizations to palettization --- src/libImaging/ConvertToPalette.c | 32 ++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/libImaging/ConvertToPalette.c b/src/libImaging/ConvertToPalette.c index b4600393762..e2b432e5d21 100644 --- a/src/libImaging/ConvertToPalette.c +++ b/src/libImaging/ConvertToPalette.c @@ -117,7 +117,8 @@ topalette_colour_floyd_steinberg( Imaging imOut, Imaging imIn, ImagingPalette palette, ExactColorHash ech ) { int alpha = imOut->mode == IMAGING_MODE_PA; - int *errors = calloc(imIn->xsize + 1, sizeof(int) * 3); + int xsize = imIn->xsize, ysize = imIn->ysize; + int *errors = calloc(xsize + 1, sizeof(int) * 3); if (!errors) { ImagingError_MemoryError(); // Sets the exception return 1; @@ -127,19 +128,20 @@ topalette_colour_floyd_steinberg( ImagingSectionCookie cookie; ImagingSectionEnter(&cookie); - for (int y = 0; y < imIn->ysize; y++) { + for (int y = 0; y < ysize; y++) { int r, r0, r1, r2; int g, g0, g1, g2; int b, b0, b1, b2; - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; + // Restrict safe: we know imOut and imIn to be different images + UINT8 *restrict in = (UINT8 *)imIn->image[y]; + UINT8 *restrict out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; int *e = errors; r = r0 = r1 = 0; g = g0 = g1 = 0; b = b0 = b1 = b2 = 0; - for (int x = 0; x < imIn->xsize; x++, in += 4) { + for (int x = 0; x < xsize; x++, in += 4) { int d2; r = CLIP8(in[0] + (r + e[3 + 0]) / 16); @@ -156,8 +158,9 @@ topalette_colour_floyd_steinberg( palette_index = cache[0]; } if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)palette_index; - out[x * 4 + 3] = 255; + UINT32 v = + MAKE_UINT32(palette_index, palette_index, palette_index, 255); + memcpy(out + x * 4, &v, sizeof(v)); } else { out[x] = (UINT8)palette_index; } @@ -215,11 +218,13 @@ topalette_colour_closest( int alpha = imOut->mode == IMAGING_MODE_PA; ImagingSectionCookie cookie; ImagingSectionEnter(&cookie); - for (int y = 0; y < imIn->ysize; y++) { - UINT8 *in = (UINT8 *)imIn->image[y]; - UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; + int xsize = imIn->xsize, ysize = imIn->ysize; + for (int y = 0; y < ysize; y++) { + // Restrict safe: we know imOut and imIn to be different images + UINT8 *restrict in = (UINT8 *)imIn->image[y]; + UINT8 *restrict out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; - for (int x = 0; x < imIn->xsize; x++, in += 4) { + for (int x = 0; x < xsize; x++, in += 4) { int r = in[0], g = in[1], b = in[2]; int palette_index = find_exact_color(ech, r, g, b); if (palette_index < 0) { @@ -231,8 +236,9 @@ topalette_colour_closest( palette_index = cache[0]; } if (alpha) { - out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)palette_index; - out[x * 4 + 3] = 255; + UINT32 v = + MAKE_UINT32(palette_index, palette_index, palette_index, 255); + memcpy(out + x * 4, &v, sizeof(v)); } else { out[x] = (UINT8)palette_index; } From be0d717ea2ee2a35136e5b33e3253b3b47313797 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 29 Jul 2026 08:40:24 +0300 Subject: [PATCH 12/13] Move palette cache functions to ConvertToPalette.c They were used nowhere else. Keeping them together, as a private API, allows for future improvements to the palette cache's shape. --- src/libImaging/ConvertToPalette.c | 180 +++++++++++++++++++++++++++- src/libImaging/Imaging.h | 10 -- src/libImaging/Palette.c | 193 ------------------------------ 3 files changed, 174 insertions(+), 209 deletions(-) diff --git a/src/libImaging/ConvertToPalette.c b/src/libImaging/ConvertToPalette.c index e2b432e5d21..17e164d7260 100644 --- a/src/libImaging/ConvertToPalette.c +++ b/src/libImaging/ConvertToPalette.c @@ -1,7 +1,7 @@ /* * The Python Imaging Library * - * See Convert.c for legacy history of this file. + * See Convert.c and Palette.c for legacy history of this file. * * Copyright (c) 1997-2005 by Secret Labs AB. * Copyright (c) 1995-1997 by Fredrik Lundh. @@ -11,6 +11,174 @@ #include "Imaging.h" +/* + * The coarse colour mapping is loosely based on the corresponding code in + * the IJG JPEG library by Thomas G. Lane. Original algorithms by + * Paul Heckbert and Spencer W. Thomas. + */ + +#define DIST(a, b, s) (a - b) * (a - b) * s + +#define RSCALE 1 +#define GSCALE 1 +#define BSCALE 1 + +#define RDIST(a, b) DIST(a, b, RSCALE *RSCALE) +#define GDIST(a, b) DIST(a, b, GSCALE *GSCALE) +#define BDIST(a, b) DIST(a, b, BSCALE *BSCALE) + +#define RSTEP (4 * RSCALE) +#define GSTEP (4 * GSCALE) +#define BSTEP (4 * BSCALE) + +#define BOX 8 +#define BOXVOLUME (BOX * BOX * BOX) + +static INT16 * +palette_cache(ImagingPalette palette, int r, int g, int b) { + return &palette->cache[(r >> 2) + (g >> 2) * 64 + (b >> 2) * 64 * 64]; +} + +static void +palette_cache_update(ImagingPalette palette, int r, int g, int b) { + int i, j; + unsigned int dmin[IMAGING_PALETTE_MAX_ENTRIES], dmax; + int r0, g0, b0; + int r1, g1, b1; + int rc, gc, bc; + unsigned int d[BOXVOLUME]; + UINT8 c[BOXVOLUME]; + + /* Get box boundaries for the given (r,g,b)-triplet. Each box + covers eight cache slots (32 colour values, that is). */ + + r0 = r & 0xe0; + r1 = r0 + 0x1f; + rc = (r0 + r1) / 2; + g0 = g & 0xe0; + g1 = g0 + 0x1f; + gc = (g0 + g1) / 2; + b0 = b & 0xe0; + b1 = b0 + 0x1f; + bc = (b0 + b1) / 2; + + /* Step 1 -- Select relevant palette entries (after Heckbert) */ + + /* For each palette entry, calculate the min and max distances to + * any position in the box given by the colour we're looking for. */ + + dmax = (unsigned int)~0; + + for (i = 0; i < palette->size; i++) { + int r, g, b; + unsigned int tmin, tmax; + + /* Find min and max distances to any point in the box */ + r = palette->palette[i * 4 + 0]; + tmin = (r < r0) ? RDIST(r, r0) : (r > r1) ? RDIST(r, r1) : 0; + tmax = (r <= rc) ? RDIST(r, r1) : RDIST(r, r0); + + g = palette->palette[i * 4 + 1]; + tmin += (g < g0) ? GDIST(g, g0) : (g > g1) ? GDIST(g, g1) : 0; + tmax += (g <= gc) ? GDIST(g, g1) : GDIST(g, g0); + + b = palette->palette[i * 4 + 2]; + tmin += (b < b0) ? BDIST(b, b0) : (b > b1) ? BDIST(b, b1) : 0; + tmax += (b <= bc) ? BDIST(b, b1) : BDIST(b, b0); + + dmin[i] = tmin; + if (tmax < dmax) { + dmax = tmax; /* keep the smallest max distance only */ + } + } + + /* Step 2 -- Incrementally update cache slot (after Thomas) */ + + /* Find the box containing the nearest palette entry, and update + * all slots in that box. We only check boxes for which the min + * distance is less than or equal the smallest max distance */ + + for (i = 0; i < BOXVOLUME; i++) { + d[i] = (unsigned int)~0; + } + + for (i = 0; i < palette->size; i++) { + if (dmin[i] <= dmax) { + int rd, gd, bd; + int ri, gi, bi; + int rx, gx, bx; + + ri = (r0 - palette->palette[i * 4 + 0]) * RSCALE; + gi = (g0 - palette->palette[i * 4 + 1]) * GSCALE; + bi = (b0 - palette->palette[i * 4 + 2]) * BSCALE; + + rd = ri * ri + gi * gi + bi * bi; + + ri = ri * (2 * RSTEP) + RSTEP * RSTEP; + gi = gi * (2 * GSTEP) + GSTEP * GSTEP; + bi = bi * (2 * BSTEP) + BSTEP * BSTEP; + + rx = ri; + for (r = j = 0; r < BOX; r++) { + gd = rd; + gx = gi; + for (g = 0; g < BOX; g++) { + bd = gd; + bx = bi; + for (b = 0; b < BOX; b++) { + if ((unsigned int)bd < d[j]) { + d[j] = bd; + c[j] = (UINT8)i; + } + bd += bx; + bx += 2 * BSTEP * BSTEP; + j++; + } + gd += gx; + gx += 2 * GSTEP * GSTEP; + } + rd += rx; + rx += 2 * RSTEP * RSTEP; + } + } + } + + /* Step 3 -- Update cache */ + + /* The c array now contains the closest match for each + * cache slot in the box. Update the cache. */ + + j = 0; + for (r = r0; r < r1; r += 4) { + for (g = g0; g < g1; g += 4) { + for (b = b0; b < b1; b += 4) { + *palette_cache(palette, r, g, b) = c[j++]; + } + } + } +} + +static int +palette_cache_prepare(ImagingPalette palette) { + int entries = 64 * 64 * 64; + + if (palette->cache == NULL) { + /* malloc check ok, small constant allocation */ + palette->cache = (INT16 *)malloc(entries * sizeof(INT16)); + if (!palette->cache) { + (void)ImagingError_MemoryError(); + return -1; + } + + /* Mark all entries as empty */ + for (int i = 0; i < entries; i++) { + palette->cache[i] = 0x100; + } + } + + return 0; +} + // The hash will always have up to 50% fill factor. #define EXACT_COLOR_HASH_SIZE (IMAGING_PALETTE_MAX_ENTRIES * 2) @@ -151,9 +319,9 @@ topalette_colour_floyd_steinberg( int palette_index = find_exact_color(ech, r, g, b); if (palette_index < 0) { /* get closest colour */ - INT16 *cache = &ImagingPaletteCache(palette, r, g, b); + INT16 *cache = palette_cache(palette, r, g, b); if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); + palette_cache_update(palette, r, g, b); } palette_index = cache[0]; } @@ -229,9 +397,9 @@ topalette_colour_closest( int palette_index = find_exact_color(ech, r, g, b); if (palette_index < 0) { /* get closest colour */ - INT16 *cache = &ImagingPaletteCache(palette, r, g, b); + INT16 *cache = palette_cache(palette, r, g, b); if (cache[0] == 0x100) { - ImagingPaletteCacheUpdate(palette, r, g, b); + palette_cache_update(palette, r, g, b); } palette_index = cache[0]; } @@ -303,7 +471,7 @@ topalette( struct ExactColorHashInstance exact_color_hash; prepare_exact_color_hash(palette, &exact_color_hash); - if (ImagingPaletteCachePrepare(palette) < 0) { + if (palette_cache_prepare(palette) < 0) { // Failed allocation for cache ImagingDelete(imOut); imOut = NULL; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 34e32a821f3..b354b5f1664 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -240,16 +240,6 @@ ImagingPaletteDuplicate(ImagingPalette palette); extern void ImagingPaletteDelete(ImagingPalette palette); -extern int -ImagingPaletteCachePrepare(ImagingPalette palette); -extern void -ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b); -extern void -ImagingPaletteCacheDelete(ImagingPalette palette); - -#define ImagingPaletteCache(p, r, g, b) \ - p->cache[(r >> 2) + (g >> 2) * 64 + (b >> 2) * 64 * 64] - extern Imaging ImagingQuantize(Imaging im, int colours, int mode, int kmeans); diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 95702af78ac..6e2d177e20b 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -18,8 +18,6 @@ #include "Imaging.h" -#include - ImagingPalette ImagingPaletteNew(const ModeID mode) { /* Create a palette object */ @@ -114,194 +112,3 @@ ImagingPaletteDelete(ImagingPalette palette) { free(palette); } } - -/* -------------------------------------------------------------------- */ -/* Colour mapping */ -/* -------------------------------------------------------------------- */ - -/* This code is used to map RGB triplets to palette indices, using - a palette index cache. */ - -/* - * This implementation is loosely based on the corresponding code in - * the IJG JPEG library by Thomas G. Lane. Original algorithms by - * Paul Heckbert and Spencer W. Thomas. - * - * The IJG JPEG library is copyright (C) 1991-1995, Thomas G. Lane. */ - -#define DIST(a, b, s) (a - b) * (a - b) * s - -/* Colour weights (no scaling, for now) */ -#define RSCALE 1 -#define GSCALE 1 -#define BSCALE 1 - -/* Calculated scaled distances */ -#define RDIST(a, b) DIST(a, b, RSCALE *RSCALE) -#define GDIST(a, b) DIST(a, b, GSCALE *GSCALE) -#define BDIST(a, b) DIST(a, b, BSCALE *BSCALE) - -/* Incremental steps */ -#define RSTEP (4 * RSCALE) -#define GSTEP (4 * GSCALE) -#define BSTEP (4 * BSCALE) - -#define BOX 8 - -#define BOXVOLUME BOX * BOX * BOX - -void -ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { - int i, j; - unsigned int dmin[IMAGING_PALETTE_MAX_ENTRIES], dmax; - int r0, g0, b0; - int r1, g1, b1; - int rc, gc, bc; - unsigned int d[BOXVOLUME]; - UINT8 c[BOXVOLUME]; - - /* Get box boundaries for the given (r,g,b)-triplet. Each box - covers eight cache slots (32 colour values, that is). */ - - r0 = r & 0xe0; - r1 = r0 + 0x1f; - rc = (r0 + r1) / 2; - g0 = g & 0xe0; - g1 = g0 + 0x1f; - gc = (g0 + g1) / 2; - b0 = b & 0xe0; - b1 = b0 + 0x1f; - bc = (b0 + b1) / 2; - - /* Step 1 -- Select relevant palette entries (after Heckbert) */ - - /* For each palette entry, calculate the min and max distances to - * any position in the box given by the colour we're looking for. */ - - dmax = (unsigned int)~0; - - for (i = 0; i < palette->size; i++) { - int r, g, b; - unsigned int tmin, tmax; - - /* Find min and max distances to any point in the box */ - r = palette->palette[i * 4 + 0]; - tmin = (r < r0) ? RDIST(r, r0) : (r > r1) ? RDIST(r, r1) : 0; - tmax = (r <= rc) ? RDIST(r, r1) : RDIST(r, r0); - - g = palette->palette[i * 4 + 1]; - tmin += (g < g0) ? GDIST(g, g0) : (g > g1) ? GDIST(g, g1) : 0; - tmax += (g <= gc) ? GDIST(g, g1) : GDIST(g, g0); - - b = palette->palette[i * 4 + 2]; - tmin += (b < b0) ? BDIST(b, b0) : (b > b1) ? BDIST(b, b1) : 0; - tmax += (b <= bc) ? BDIST(b, b1) : BDIST(b, b0); - - dmin[i] = tmin; - if (tmax < dmax) { - dmax = tmax; /* keep the smallest max distance only */ - } - } - - /* Step 2 -- Incrementally update cache slot (after Thomas) */ - - /* Find the box containing the nearest palette entry, and update - * all slots in that box. We only check boxes for which the min - * distance is less than or equal the smallest max distance */ - - for (i = 0; i < BOXVOLUME; i++) { - d[i] = (unsigned int)~0; - } - - for (i = 0; i < palette->size; i++) { - if (dmin[i] <= dmax) { - int rd, gd, bd; - int ri, gi, bi; - int rx, gx, bx; - - ri = (r0 - palette->palette[i * 4 + 0]) * RSCALE; - gi = (g0 - palette->palette[i * 4 + 1]) * GSCALE; - bi = (b0 - palette->palette[i * 4 + 2]) * BSCALE; - - rd = ri * ri + gi * gi + bi * bi; - - ri = ri * (2 * RSTEP) + RSTEP * RSTEP; - gi = gi * (2 * GSTEP) + GSTEP * GSTEP; - bi = bi * (2 * BSTEP) + BSTEP * BSTEP; - - rx = ri; - for (r = j = 0; r < BOX; r++) { - gd = rd; - gx = gi; - for (g = 0; g < BOX; g++) { - bd = gd; - bx = bi; - for (b = 0; b < BOX; b++) { - if ((unsigned int)bd < d[j]) { - d[j] = bd; - c[j] = (UINT8)i; - } - bd += bx; - bx += 2 * BSTEP * BSTEP; - j++; - } - gd += gx; - gx += 2 * GSTEP * GSTEP; - } - rd += rx; - rx += 2 * RSTEP * RSTEP; - } - } - } - - /* Step 3 -- Update cache */ - - /* The c array now contains the closest match for each - * cache slot in the box. Update the cache. */ - - j = 0; - for (r = r0; r < r1; r += 4) { - for (g = g0; g < g1; g += 4) { - for (b = b0; b < b1; b += 4) { - ImagingPaletteCache(palette, r, g, b) = c[j++]; - } - } - } -} - -int -ImagingPaletteCachePrepare(ImagingPalette palette) { - /* Add a colour cache to a palette */ - - int i; - int entries = 64 * 64 * 64; - - if (palette->cache == NULL) { - /* The cache is 512k. It might be a good idea to break it - up into a pointer array (e.g. an 8-bit image?) */ - - /* malloc check ok, small constant allocation */ - palette->cache = (INT16 *)malloc(entries * sizeof(INT16)); - if (!palette->cache) { - (void)ImagingError_MemoryError(); - return -1; - } - - /* Mark all entries as empty */ - for (i = 0; i < entries; i++) { - palette->cache[i] = 0x100; - } - } - - return 0; -} - -void -ImagingPaletteCacheDelete(ImagingPalette palette) { - /* Release the colour cache, if any */ - - if (palette && palette->cache) { - free(palette->cache); - palette->cache = NULL; - } -} From 3eb55097db8a7dd683e106f0df5ae15737da1b88 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 29 Jul 2026 10:01:10 +0300 Subject: [PATCH 13/13] Use palette cache to figure out if we might have an exact match --- src/libImaging/ConvertToPalette.c | 84 +++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/src/libImaging/ConvertToPalette.c b/src/libImaging/ConvertToPalette.c index 17e164d7260..e1ceb609a62 100644 --- a/src/libImaging/ConvertToPalette.c +++ b/src/libImaging/ConvertToPalette.c @@ -11,6 +11,14 @@ #include "Imaging.h" +#if defined(_MSC_VER) +#define NOINLINE __declspec(noinline) +#elif defined(__GNUC__) +#define NOINLINE __attribute__((noinline)) +#else +#define NOINLINE +#endif + /* * The coarse colour mapping is loosely based on the corresponding code in * the IJG JPEG library by Thomas G. Lane. Original algorithms by @@ -34,12 +42,19 @@ #define BOX 8 #define BOXVOLUME (BOX * BOX * BOX) +// PIL palettes may contain up to 256 entries. +#define PALETTE_CACHE_INDEX_MASK 0xff +// Cache entry is empty. +#define PALETTE_CACHE_EMPTY 0x100 +// The box described by this cache entry may contain an exact palette color. +#define PALETTE_CACHE_HAS_EXACT 0x200 + static INT16 * palette_cache(ImagingPalette palette, int r, int g, int b) { return &palette->cache[(r >> 2) + (g >> 2) * 64 + (b >> 2) * 64 * 64]; } -static void +static NOINLINE void palette_cache_update(ImagingPalette palette, int r, int g, int b) { int i, j; unsigned int dmin[IMAGING_PALETTE_MAX_ENTRIES], dmax; @@ -152,7 +167,8 @@ palette_cache_update(ImagingPalette palette, int r, int g, int b) { for (r = r0; r < r1; r += 4) { for (g = g0; g < g1; g += 4) { for (b = b0; b < b1; b += 4) { - *palette_cache(palette, r, g, b) = c[j++]; + INT16 *cache = palette_cache(palette, r, g, b); + *cache = c[j++] | (*cache & PALETTE_CACHE_HAS_EXACT); } } } @@ -172,7 +188,17 @@ palette_cache_prepare(ImagingPalette palette) { /* Mark all entries as empty */ for (int i = 0; i < entries; i++) { - palette->cache[i] = 0x100; + palette->cache[i] = PALETTE_CACHE_EMPTY; + } + + /* Mark cells that might contain an exact palette colour. */ + for (int i = 0; i < palette->size; i++) { + *palette_cache( + palette, + palette->palette[i * 4], + palette->palette[i * 4 + 1], + palette->palette[i * 4 + 2] + ) |= PALETTE_CACHE_HAS_EXACT; } } @@ -237,6 +263,38 @@ find_exact_color(const ExactColorHash ech, int r, int g, int b) { return -1; } +static int +find_palette_color( + ImagingPalette palette, const ExactColorHash ech, int r, int g, int b +) { + INT16 *cache = palette_cache(palette, r, g, b); + INT16 cached = *cache; + + if (cached <= PALETTE_CACHE_INDEX_MASK) { + // Filled, and there is no possibility + // for an exact color in this box, + // so return the cached index. + return cached; + } + + if (cached & PALETTE_CACHE_HAS_EXACT) { + int palette_index = find_exact_color(ech, r, g, b); + if (palette_index >= 0) { + // Found it! We do _not_ update the palette cache, + // because it's a relatively slow operation, + // and we have the answer here already. + return palette_index; + } + } + + if (cached & PALETTE_CACHE_EMPTY) { + palette_cache_update(palette, r, g, b); + cached = *cache; + } + + return cached & PALETTE_CACHE_INDEX_MASK; +} + #if defined(_MSC_VER) #pragma optimize("", off) #endif @@ -316,15 +374,7 @@ topalette_colour_floyd_steinberg( g = CLIP8(in[1] + (g + e[3 + 1]) / 16); b = CLIP8(in[2] + (b + e[3 + 2]) / 16); - int palette_index = find_exact_color(ech, r, g, b); - if (palette_index < 0) { - /* get closest colour */ - INT16 *cache = palette_cache(palette, r, g, b); - if (cache[0] == 0x100) { - palette_cache_update(palette, r, g, b); - } - palette_index = cache[0]; - } + int palette_index = find_palette_color(palette, ech, r, g, b); if (alpha) { UINT32 v = MAKE_UINT32(palette_index, palette_index, palette_index, 255); @@ -394,15 +444,7 @@ topalette_colour_closest( for (int x = 0; x < xsize; x++, in += 4) { int r = in[0], g = in[1], b = in[2]; - int palette_index = find_exact_color(ech, r, g, b); - if (palette_index < 0) { - /* get closest colour */ - INT16 *cache = palette_cache(palette, r, g, b); - if (cache[0] == 0x100) { - palette_cache_update(palette, r, g, b); - } - palette_index = cache[0]; - } + int palette_index = find_palette_color(palette, ech, r, g, b); if (alpha) { UINT32 v = MAKE_UINT32(palette_index, palette_index, palette_index, 255);