From 8dd5d7848de3970edd08d635c4dd2036002013bb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 21 Jul 2026 09:13:44 +0300 Subject: [PATCH 1/5] Convert.c: use PyErr_Format --- src/libImaging/Convert.c | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 1fd14a944ef..240a3a7e20c 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1622,19 +1622,12 @@ convert(Imaging imOut, Imaging imIn, ModeID mode, ImagingPalette palette, int di } if (!convert) { -#ifdef notdef - return (Imaging)ImagingError_ValueError("conversion not supported"); -#else - static char buf[100]; - snprintf( - buf, - 100, - "conversion from %.10s to %.10s not supported", + return (Imaging)PyErr_Format( + PyExc_ValueError, + "conversion from %s to %s not supported", getModeData(imIn->mode)->name, getModeData(mode)->name ); - return (Imaging)ImagingError_ValueError(buf); -#endif } imOut = ImagingNew2Dirty(mode, imOut, imIn); @@ -1707,15 +1700,12 @@ ImagingConvertTransparent(Imaging imIn, const ModeID mode, int r, int g, int b) } g = b = r; } else { - static char buf[100]; - snprintf( - buf, - 100, - "conversion from %.10s to %.10s not supported in convert_transparent", + return (Imaging)PyErr_Format( + PyExc_ValueError, + "conversion from %s to %s not supported in convert_transparent", getModeData(imIn->mode)->name, getModeData(mode)->name ); - return (Imaging)ImagingError_ValueError(buf); } imOut = ImagingNew2Dirty(mode, imOut, imIn); From 7d5d2c32a39ce4ed12ffab46132b208a5b402884 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 21 Jul 2026 09:23:52 +0300 Subject: [PATCH 2/5] Benchmark more conversions --- Tests/benchmarks.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Tests/benchmarks.py b/Tests/benchmarks.py index 0c67d298dcb..ddbbf4f26fa 100644 --- a/Tests/benchmarks.py +++ b/Tests/benchmarks.py @@ -163,10 +163,38 @@ def test_alpha_composite( @pytest.mark.parametrize( "mode_from, mode_to", [ + # bilevel <-> greyscale (in-place path: l2bit / bit2l) + ("L", "1"), + ("1", "L"), + # greyscale <-> colour (pack / expand) ("RGB", "L"), + ("L", "RGB"), + ("RGB", "1"), + ("LA", "RGBA"), + # channel add / drop / premultiply + ("RGB", "RGBA"), + ("RGBA", "RGB"), ("RGBA", "LA"), ("RGBa", "RGBA"), ("RGBA", "RGBa"), + # palette expansion (p2rgb / p2rgba / pa2rgb / pa2rgba) + ("P", "RGB"), + ("P", "RGBA"), + ("PA", "RGB"), + ("PA", "RGBA"), + # colourspace conversions + ("RGB", "YCbCr"), + ("YCbCr", "RGB"), + ("RGB", "HSV"), + ("HSV", "RGB"), + ("RGB", "CMYK"), + ("CMYK", "RGB"), + # integer / float modes + ("L", "I"), + ("I", "L"), + ("L", "F"), + ("F", "L"), + ("RGB", "I;16"), ], ) @pytest.mark.parametrize("size", SIZES, ids=_format_size) From ef05f68ace1718be673e6de9f9da5abd99a127b2 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 21 Jul 2026 10:54:44 +0300 Subject: [PATCH 3/5] Add some correctness tests for conversions --- Tests/test_image_convert.py | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 547a6c2c678..a5755ae3346 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,5 +1,6 @@ from __future__ import annotations +from itertools import chain from pathlib import Path import pytest @@ -90,6 +91,15 @@ def test_16bit_workaround() -> None: _test_float_conversion(im.convert("I")) +def test_i_to_l_saturation() -> None: + # Conversion from I (signed 32-bit integer pixels) to L (unsigned 8-bit) + # must saturate (clip) the values. + values = [-(2**19), -1, 0, 1, 127, 254, 255, 256, 2**19] + im = Image.new("I", (len(values), 1)) + im.putdata(values) + assert list(im.convert("L").getdata()) == [0, 0, 0, 1, 127, 254, 255, 255, 255] + + def test_opaque() -> None: alpha = hopper("P").convert("PA").getchannel("A") @@ -290,6 +300,35 @@ def test_p2pa_palette() -> None: assert im_pa.getpalette() == im.getpalette() +def test_p_to_rgb_exact() -> None: + # Arrange + rgba = [(i, (i * 3) % 256, (i * 7) % 256, (i * 11) % 256) for i in range(256)] + rgb = tuple(v[:3] for v in rgba) + rgb_bytes = bytes(b for i in range(256) for b in rgb[i]) # Flatten `rgb` + + # Act (1) + im = Image.frombytes("P", (16, 16), bytes(range(256))) + im.putpalette(tuple(chain.from_iterable(rgba)), rawmode="RGBA") + + # Assert (1) + # When converting P-with-Alpha-Palette to RGB, the palette is ignored. + assert im.convert("RGB").tobytes() == rgb_bytes + # When converting P-with-Alpha-Palette to RGBA, the alpha from the palette is used. + assert im.convert("RGBA").get_flattened_data(3) == tuple(v[3] for v in rgba) + + # Act (2) + # Replace the alpha with an inverted version + new_alpha = tuple(255 - i for i in range(256)) + im.putalpha(Image.frombytes("L", (16, 16), bytes(new_alpha))) + + # Assert (2) + assert im.mode == "PA" + # The RGB bytes should be the same still, nothing has changed + assert im.convert("RGB").tobytes() == rgb_bytes + # The alpha channel should have been replaced with the inverted version though + assert im.convert("RGBA").get_flattened_data(3) == new_alpha + + def test_matrix_illegal_conversion() -> None: # Arrange im = hopper("CMYK") From f10d7f7fc37d4a8590a2f89bc3be8282bf20c18b Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 21 Jul 2026 10:58:08 +0300 Subject: [PATCH 4/5] Speed up i2l with branchless operations so the loop autovectorizes --- src/libImaging/Convert.c | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 240a3a7e20c..ab784c28ac6 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -590,17 +590,11 @@ l2i(UINT8 *out_, const UINT8 *in, int xsize) { static void i2l(UINT8 *out, const UINT8 *in_, int xsize) { - int x; - for (x = 0; x < xsize; x++, out++, in_ += 4) { + for (int x = 0; x < xsize; x++, out++, in_ += 4) { INT32 v; memcpy(&v, in_, sizeof(v)); - if (v <= 0) { - *out = 0; - } else if (v >= 255) { - *out = 255; - } else { - *out = (UINT8)v; - } + // Branchless saturation + *out = (UINT8)(v <= 0 ? 0 : v >= 255 ? 255 : v); } } From 643f45bb79b0681963e39a126f2ea611e1e6f616 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 21 Jul 2026 11:07:13 +0300 Subject: [PATCH 5/5] Speed up palettized-to-RGB* conversions --- src/libImaging/Convert.c | 52 +++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index ab784c28ac6..3037ad9fcdf 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -977,27 +977,30 @@ pa2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { } } +// Set the alpha channel of the UINT32 `v` in-place to the given value. +#ifdef WORDS_BIGENDIAN +#define SET_ALPHA_32(v, alpha) v = ((v & 0xFFFFFF00u) | (alpha)) +#else +#define SET_ALPHA_32(v, alpha) v = ((v & 0x00FFFFFFu) | ((UINT32)(alpha) << 24)) +#endif + static void p2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++) { - const UINT8 *rgb = &palette->palette[*in++ * 4]; - *out++ = rgb[0]; - *out++ = rgb[1]; - *out++ = rgb[2]; - *out++ = 255; + for (int x = 0; x < xsize; x++, in++, out += 4) { + UINT32 v; + memcpy(&v, &palette->palette[in[0] * 4], sizeof(v)); + SET_ALPHA_32(v, 0xFF); + memcpy(out, &v, sizeof(v)); } } static void pa2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - const UINT8 *rgb = &palette->palette[in[0] * 4]; - *out++ = rgb[0]; - *out++ = rgb[1]; - *out++ = rgb[2]; - *out++ = 255; + for (int x = 0; x < xsize; x++, in += 4, out += 4) { + UINT32 v; + memcpy(&v, &palette->palette[in[0] * 4], sizeof(v)); + SET_ALPHA_32(v, 0xFF); + memcpy(out, &v, sizeof(v)); } } @@ -1023,25 +1026,18 @@ pa2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { static void p2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++) { - const UINT8 *rgba = &palette->palette[*in++ * 4]; - *out++ = rgba[0]; - *out++ = rgba[1]; - *out++ = rgba[2]; - *out++ = rgba[3]; + for (int x = 0; x < xsize; x++, in++, out += 4) { + memcpy(out, &palette->palette[in[0] * 4], 4); } } static void pa2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - const UINT8 *rgb = &palette->palette[in[0] * 4]; - *out++ = rgb[0]; - *out++ = rgb[1]; - *out++ = rgb[2]; - *out++ = in[3]; + for (int x = 0; x < xsize; x++, in += 4, out += 4) { + UINT32 v; + memcpy(&v, &palette->palette[in[0] * 4], sizeof(v)); + SET_ALPHA_32(v, in[3]); + memcpy(out, &v, sizeof(v)); } }