Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ImagingAccess for I;16N on big-endian #7921

Merged
merged 11 commits into from
Apr 25, 2024
27 changes: 27 additions & 0 deletions Tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,33 @@
uploader = "github_actions"


modes = (
"1",
"L",
"LA",
"La",
"P",
"PA",
"F",
"I",
"I;16",
"I;16L",
"I;16B",
"I;16N",
"RGB",
"RGBA",
"RGBa",
"RGBX",
"BGR;15",
"BGR;16",
"BGR;24",
"CMYK",
"YCbCr",
"HSV",
"LAB",
)


def upload(a: Image.Image, b: Image.Image) -> str | None:
if uploader == "show":
# local img.show for errors.
Expand Down
48 changes: 10 additions & 38 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,43 +28,16 @@
assert_image_similar_tofile,
assert_not_all_same,
hopper,
is_big_endian,
is_win32,
mark_if_feature_version,
modes,
skip_unless_feature,
)

# name, pixel size
image_modes = (
("1", 1),
("L", 1),
("LA", 4),
("La", 4),
("P", 1),
("PA", 4),
("F", 4),
("I", 4),
("I;16", 2),
("I;16L", 2),
("I;16B", 2),
("I;16N", 2),
("RGB", 4),
("RGBA", 4),
("RGBa", 4),
("RGBX", 4),
("BGR;15", 2),
("BGR;16", 2),
("BGR;24", 3),
("CMYK", 4),
("YCbCr", 4),
("HSV", 4),
("LAB", 4),
)

image_mode_names = [name for name, _ in image_modes]


class TestImage:
@pytest.mark.parametrize("mode", image_mode_names)
@pytest.mark.parametrize("mode", modes)
def test_image_modes_success(self, mode: str) -> None:
Image.new(mode, (1, 1))

Expand Down Expand Up @@ -1045,15 +1018,15 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None:


class TestImageBytes:
@pytest.mark.parametrize("mode", image_mode_names)
@pytest.mark.parametrize("mode", modes)
def test_roundtrip_bytes_constructor(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()

reloaded = Image.frombytes(mode, im.size, source_bytes)
assert reloaded.tobytes() == source_bytes

@pytest.mark.parametrize("mode", image_mode_names)
@pytest.mark.parametrize("mode", modes)
def test_roundtrip_bytes_method(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()
Expand All @@ -1062,12 +1035,11 @@ def test_roundtrip_bytes_method(self, mode: str) -> None:
reloaded.frombytes(source_bytes)
assert reloaded.tobytes() == source_bytes

@pytest.mark.parametrize(("mode", "pixelsize"), image_modes)
def test_getdata_putdata(self, mode: str, pixelsize: int) -> None:
im = Image.new(mode, (2, 2))
source_bytes = bytes(range(im.width * im.height * pixelsize))
im.frombytes(source_bytes)

@pytest.mark.parametrize("mode", modes)
def test_getdata_putdata(self, mode: str) -> None:
if is_big_endian and mode == "BGR;15":
pytest.xfail("Known failure of BGR;15 on big-endian")
im = hopper(mode)
reloaded = Image.new(mode, im.size)
reloaded.putdata(im.getdata())
assert_image_equal(im, reloaded)
Expand Down
70 changes: 18 additions & 52 deletions Tests/test_image_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from PIL import Image

from .helper import assert_image_equal, hopper, is_win32
from .helper import assert_image_equal, hopper, is_win32, modes

# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
Expand All @@ -33,7 +33,7 @@


class AccessTest:
# initial value
# Initial value
_init_cffi_access = Image.USE_CFFI_ACCESS
_need_cffi_access = False

Expand Down Expand Up @@ -138,8 +138,8 @@
if bands == 1:
return 1
if mode in ("BGR;15", "BGR;16"):
# These modes have less than 8 bits per band
# So (1, 2, 3) cannot be roundtripped
# These modes have less than 8 bits per band,
# so (1, 2, 3) cannot be roundtripped.
return (16, 32, 49)
return tuple(range(1, bands + 1))

Expand All @@ -151,7 +151,7 @@
self.color(mode) if expected_color_int is None else expected_color_int
)

# check putpixel
# Check putpixel
im = Image.new(mode, (1, 1), None)
im.putpixel((0, 0), expected_color)
actual_color = im.getpixel((0, 0))
Expand All @@ -160,74 +160,52 @@
f"expected {expected_color} got {actual_color}"
)

# check putpixel negative index
# Check putpixel negative index
im.putpixel((-1, -1), expected_color)
actual_color = im.getpixel((-1, -1))
assert actual_color == expected_color, (
f"put/getpixel roundtrip negative index failed for mode {mode}, "
f"expected {expected_color} got {actual_color}"
)

# Check 0
# Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None)
assert im.load() is not None

error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error):
im.putpixel((0, 0), expected_color)
with pytest.raises(error):
im.getpixel((0, 0))
# Check 0 negative index
# Check negative index
with pytest.raises(error):
im.putpixel((-1, -1), expected_color)
with pytest.raises(error):
im.getpixel((-1, -1))

# check initial color
# Check initial color
im = Image.new(mode, (1, 1), expected_color)
actual_color = im.getpixel((0, 0))
assert actual_color == expected_color, (
f"initial color failed for mode {mode}, "
f"expected {expected_color} got {actual_color}"
)

# check initial color negative index
# Check initial color negative index
actual_color = im.getpixel((-1, -1))
assert actual_color == expected_color, (
f"initial color failed with negative index for mode {mode}, "
f"expected {expected_color} got {actual_color}"
)

# Check 0
# Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color)
with pytest.raises(error):
im.getpixel((0, 0))
# Check 0 negative index
# Check negative index
with pytest.raises(error):
im.getpixel((-1, -1))

@pytest.mark.parametrize(
"mode",
(
"1",
"L",
"LA",
"I",
"I;16",
"I;16B",
"F",
"P",
"PA",
"BGR;15",
"BGR;16",
"BGR;24",
"RGB",
"RGBA",
"RGBX",
"CMYK",
"YCbCr",
),
)
@pytest.mark.parametrize("mode", modes)
def test_basic(self, mode: str) -> None:
self.check(mode)

Expand All @@ -238,7 +216,7 @@
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
@pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
def test_signedness(self, mode: str, expected_color: int) -> None:
# see https://github.com/python-pillow/Pillow/issues/452
# See https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint*
self.check(mode, expected_color)

Expand Down Expand Up @@ -298,13 +276,6 @@
im = Image.new(mode, (10, 10), 40000)
self._test_get_access(im)

# These don't actually appear to be modes that I can actually make,
# as unpack sets them directly into the I mode.
# im = Image.new('I;32L', (10, 10), -2**10)
# self._test_get_access(im)
# im = Image.new('I;32B', (10, 10), 2**10)
# self._test_get_access(im)

def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image?

Expand Down Expand Up @@ -336,23 +307,18 @@
self._test_set_access(hopper("LA"), (128, 128))
self._test_set_access(hopper("1"), 255)
self._test_set_access(hopper("P"), 128)
# self._test_set_access(i, (128, 128)) #PA -- undone how to make
self._test_set_access(hopper("PA"), (128, 128))

Check warning on line 310 in Tests/test_image_access.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_image_access.py#L310

Added line #L310 was not covered by tests
self._test_set_access(hopper("F"), 1024.0)

for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
self._test_set_access(im, 45000)

# im = Image.new('I;32L', (10, 10), -(2**10))
# self._test_set_access(im, -(2**13)+1)
# im = Image.new('I;32B', (10, 10), 2**10)
# self._test_set_access(im, 2**13-1)

@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_not_implemented(self) -> None:
assert PyAccess.new(hopper("BGR;15")) is None

# ref https://github.com/python-pillow/Pillow/pull/2009
# Ref https://github.com/python-pillow/Pillow/pull/2009
def test_reference_counting(self) -> None:
size = 10

Expand All @@ -361,7 +327,7 @@
with pytest.warns(DeprecationWarning):
px = Image.new("L", (size, 1), 0).load()
for i in range(size):
# pixels can contain garbage if image is released
# Pixels can contain garbage if image is released
assert px[i, 0] == 0

@pytest.mark.parametrize("mode", ("P", "PA"))
Expand Down Expand Up @@ -478,7 +444,7 @@
env = os.environ.copy()
env["PATH"] = sys.prefix + ";" + env["PATH"]

# do not display the Windows Error Reporting dialog
# Do not display the Windows Error Reporting dialog
getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002)

process = subprocess.Popen(["embed_pil.exe"], env=env)
Expand Down
5 changes: 4 additions & 1 deletion Tests/test_lib_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@
)

def test_I16(self) -> None:
self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605)
if sys.byteorder == "little":
self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605)
else:
self.assert_pack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506)

Check warning on line 222 in Tests/test_lib_pack.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_lib_pack.py#L222

Added line #L222 was not covered by tests

def test_F_float(self) -> None:
self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34)
Expand Down
12 changes: 5 additions & 7 deletions src/libImaging/Access.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,6 @@ get_pixel_16B(Imaging im, int x, int y, void *color) {
#endif
}

static void
get_pixel_16(Imaging im, int x, int y, void *color) {
UINT8 *in = (UINT8 *)&im->image[y][x + x];
memcpy(color, in, sizeof(UINT16));
}

static void
get_pixel_BGR15(Imaging im, int x, int y, void *color) {
UINT8 *in = (UINT8 *)&im->image8[y][x * 2];
Expand Down Expand Up @@ -207,7 +201,11 @@ ImagingAccessInit() {
ADD("I;16", get_pixel_16L, put_pixel_16L);
ADD("I;16L", get_pixel_16L, put_pixel_16L);
ADD("I;16B", get_pixel_16B, put_pixel_16B);
ADD("I;16N", get_pixel_16, put_pixel_16L);
#ifdef WORDS_BIGENDIAN
ADD("I;16N", get_pixel_16B, put_pixel_16B);
#else
ADD("I;16N", get_pixel_16L, put_pixel_16L);
#endif
ADD("I;32L", get_pixel_32L, put_pixel_32L);
ADD("I;32B", get_pixel_32B, put_pixel_32B);
ADD("F", get_pixel_32, put_pixel_32);
Expand Down
Loading