diff --git a/Tests/benchmarks.py b/Tests/benchmarks.py index 0c67d298dcb..c87b8bbc7be 100644 --- a/Tests/benchmarks.py +++ b/Tests/benchmarks.py @@ -4,7 +4,11 @@ from __future__ import annotations +import hashlib +import os import pathlib +import re +import warnings from importlib.util import find_spec from io import BytesIO @@ -17,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, ) @@ -24,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"] @@ -75,15 +84,65 @@ 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], - 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 +155,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 +204,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 +466,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 @@ -506,3 +565,75 @@ 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 + 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/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/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` 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(); 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..e1ceb609a62 --- /dev/null +++ b/src/libImaging/ConvertToPalette.c @@ -0,0 +1,549 @@ +/* + * The Python Imaging Library + * + * 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. + * + * See the README file for details on usage and redistribution. + */ + +#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 + * 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) + +// 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 NOINLINE 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) { + INT16 *cache = palette_cache(palette, r, g, b); + *cache = c[j++] | (*cache & PALETTE_CACHE_HAS_EXACT); + } + } + } +} + +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] = 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; + } + } + + return 0; +} + +// 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; +} + +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 + +/** + * 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 +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); +} + +/** + * 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, ExactColorHash ech +) { + int alpha = imOut->mode == IMAGING_MODE_PA; + int xsize = imIn->xsize, ysize = imIn->ysize; + int *errors = calloc(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 < ysize; y++) { + int r, r0, r1, r2; + int g, g0, g1, g2; + int b, b0, b1, b2; + // 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 < xsize; x++, in += 4) { + int d2; + + 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); + + int palette_index = find_palette_color(palette, ech, r, g, b); + if (alpha) { + UINT32 v = + MAKE_UINT32(palette_index, palette_index, palette_index, 255); + memcpy(out + x * 4, &v, sizeof(v)); + } else { + out[x] = (UINT8)palette_index; + } + + 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; + 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, ExactColorHash ech +) { + int alpha = imOut->mode == IMAGING_MODE_PA; + ImagingSectionCookie cookie; + ImagingSectionEnter(&cookie); + 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 < xsize; x++, in += 4) { + int r = in[0], g = in[1], b = in[2]; + int palette_index = find_palette_color(palette, ech, r, g, b); + if (alpha) { + UINT32 v = + MAKE_UINT32(palette_index, palette_index, palette_index, 255); + memcpy(out + x * 4, &v, sizeof(v)); + } else { + out[x] = (UINT8)palette_index; + } + } + } + ImagingSectionLeave(&cookie); +} + +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 */ + 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"); + } + + if (palette == NULL) { + /* 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++) { + 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) { + goto done; + } + + 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); + } else { + /* colour image */ + + // 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 (palette_cache_prepare(palette) < 0) { + // Failed allocation for cache + ImagingDelete(imOut); + imOut = NULL; + goto done; + } + + if (dither) { + if (topalette_colour_floyd_steinberg( + imOut, imIn, palette, &exact_color_hash + ) != 0) { + // Exception will have been set by the work function + ImagingDelete(imOut); + imOut = NULL; + goto done; + } + } else { + topalette_colour_closest(imOut, imIn, palette, &exact_color_hash); + } + } + +done: + + if (palette_is_temporary) { + // If we created a temporary palette, delete it to free memory + 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 +); diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 472bda5d0fd..b354b5f1664 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 { @@ -239,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 b2dacf656b5..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 */ @@ -40,7 +38,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 */ } @@ -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[256], 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; - } -}