Skip to content
Open
54 changes: 54 additions & 0 deletions Tests/test_image_resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,3 +627,57 @@ def test_skip_vertical(self, flt: Image.Resampling) -> None:
0.4,
f">>> {size} {box} {flt}",
)


class TestCoreResample16bpc:
def test_resampling_clamp_overflow(self) -> None:
# Lanczos weighting during downsampling can push accumulated float sums
# above 65535. These must be clamped to 65535, not corrupted byte-by-byte.
ims = {}
width, height = 100, 10
for mode in ("I;16", "F"):
# Left half = 0, right half = 65535
im = Image.new(mode, (width, height))
for y in range(height):
for x in range(width // 2, width):
im.putpixel((x, y), 65535)

# 5x downsampling with Lanczos creates ~8.7% overshoot at the step edge
ims[mode] = im.resize((20, height), Image.Resampling.LANCZOS)

for y in range(height):
for x in range(20):
v = ims["F"].getpixel((x, y))
assert isinstance(v, float)
expected = max(0, min(65535, round(v)))

value = ims["I;16"].getpixel((x, y))
assert (
value == expected
), f"Pixel ({x}, {y}): expected {expected}, got {value}"

def test_resampling_clamp_underflow(self) -> None:
# Lanczos weighting during downsampling can push accumulated float sums
# below 0. These must be clamped to 0, not corrupted byte-by-byte.
ims = {}
width, height = 100, 10
for mode in ("I;16", "F"):
# Left half = 65535, right half = 0
im = Image.new(mode, (width, height))
for y in range(height):
for x in range(width // 2):
im.putpixel((x, y), 65535)

# 5x downsampling with Lanczos creates ~8.7% undershoot at the step edge
ims[mode] = im.resize((20, height), Image.Resampling.LANCZOS)

for y in range(height):
for x in range(20):
v = ims["F"].getpixel((x, y))
assert isinstance(v, float)
expected = max(0, min(65535, round(v)))

value = ims["I;16"].getpixel((x, y))
assert (
value == expected
), f"Pixel ({x}, {y}): expected {expected}, got {value}"
18 changes: 14 additions & 4 deletions src/libImaging/Resample.c
Original file line number Diff line number Diff line change
Expand Up @@ -493,8 +493,13 @@ ImagingResampleHorizontal_16bpc(
k[x];
}
ss_int = ROUND_UP(ss);
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
if (ss_int < 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to have this in ImagingUtils.h:

#define CLIP16(v) ((v) <= 0 ? 0 : (v) < 65536 ? (v) : 65535)

and then

ss_int = CLIP16(ROUND_UP(ss));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I updated.

ss_int = 0;
} else if (ss_int > 65535) {
ss_int = 65535;
}
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF;
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8;
}
}
ImagingSectionLeave(&cookie);
Expand Down Expand Up @@ -532,8 +537,13 @@ ImagingResampleVertical_16bpc(
k[y];
}
ss_int = ROUND_UP(ss);
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
if (ss_int < 0) {
ss_int = 0;
} else if (ss_int > 65535) {
ss_int = 65535;
}
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF;
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8;
}
}
ImagingSectionLeave(&cookie);
Expand Down
Loading