Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Tests/benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions Tests/test_image_convert.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from itertools import chain
from pathlib import Path

import pytest
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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")
Expand Down
86 changes: 33 additions & 53 deletions src/libImaging/Convert.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -983,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));
}
}

Expand All @@ -1029,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));
}
}

Expand Down Expand Up @@ -1622,19 +1612,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);
Expand Down Expand Up @@ -1707,15 +1690,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);
Expand Down
Loading