Skip to content

Commit 7681016

Browse files
committed
Fix integer overflow in 16-bit resampling
This commit fixes a bug in Resample.c where downsampling 16-bit images (I;16) using filters with negative lobes (such as Image.Resampling.LANCZOS) could result in byte corruption. Because Lanczos weighting can create overshoots (ringing artifacts) near sharp edges, the accumulated floating-point sum can sometimes exceed the 16-bit maximum (65535) or fall below zero. Previously, these out-of-bounds values were not correctly clamped before being cast or packed into the 16-bit output buffer, leading to integer overflow/underflow and corrupted pixels. This update correctly clamps the accumulated float values to the [0, 65535] range for I;16 images during resampling.
1 parent a4b0e3e commit 7681016

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

Tests/test_image_resample.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,3 +627,37 @@ def test_skip_vertical(self, flt: Image.Resampling) -> None:
627627
0.4,
628628
f">>> {size} {box} {flt}",
629629
)
630+
631+
632+
class TestCoreResample16bpc:
633+
def test_resampling_clamp(self) -> None:
634+
# Lanczos weighting during downsampling can push accumulated float sums
635+
# above 65535. These must be clamped to 65535, not corrupted byte-by-byte.
636+
width, height = 100, 10
637+
# I;16 image: left half = 0, right half = 65535
638+
im_16 = Image.new("I;16", (width, height))
639+
for y in range(height):
640+
for x in range(width // 2, width):
641+
im_16.putpixel((x, y), 65535)
642+
# F image: same values as float reference
643+
im_f = Image.new("F", (width, height))
644+
for y in range(height):
645+
for x in range(width // 2, width):
646+
im_f.putpixel((x, y), 65535.0)
647+
648+
# 5x downsampling with Lanczos creates ~8.7% overshoot at the step edge
649+
result_16 = im_16.resize((20, height), Image.Resampling.LANCZOS)
650+
result_f = im_f.resize((20, height), Image.Resampling.LANCZOS)
651+
652+
px_16 = result_16.load()
653+
px_f = result_f.load()
654+
assert px_16 is not None
655+
assert px_f is not None
656+
for y in range(height):
657+
for x in range(20):
658+
v = px_f[x, y]
659+
assert isinstance(v, float)
660+
expected = max(0, min(65535, round(v)))
661+
assert (
662+
px_16[x, y] == expected
663+
), f"Pixel ({x}, {y}): expected {expected}, got {px_16[x, y]}"

src/libImaging/Resample.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,13 @@ ImagingResampleHorizontal_16bpc(
493493
k[x];
494494
}
495495
ss_int = ROUND_UP(ss);
496-
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
497-
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
496+
if (ss_int < 0) {
497+
ss_int = 0;
498+
} else if (ss_int > 65535) {
499+
ss_int = 65535;
500+
}
501+
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF;
502+
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8;
498503
}
499504
}
500505
ImagingSectionLeave(&cookie);
@@ -532,8 +537,13 @@ ImagingResampleVertical_16bpc(
532537
k[y];
533538
}
534539
ss_int = ROUND_UP(ss);
535-
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
536-
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
540+
if (ss_int < 0) {
541+
ss_int = 0;
542+
} else if (ss_int > 65535) {
543+
ss_int = 65535;
544+
}
545+
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF;
546+
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8;
537547
}
538548
}
539549
ImagingSectionLeave(&cookie);

0 commit comments

Comments
 (0)