Skip to content
Merged
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
20 changes: 20 additions & 0 deletions Tests/test_imagechops.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,26 @@ def test_offset() -> None:
assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset)


@pytest.mark.parametrize("mode", Image.MODES)
def test_offset_modes(mode: str) -> None:
# Arrange
im = hopper(mode)
assert_image_equal(ImageChops.offset(im, 0, 0), im) # check no-op

xoffset = 45
yoffset = 20

# Act
new = ImageChops.offset(im, xoffset, yoffset)

# Assert
w, h = im.size
for x in range(w):
for y in range(h):
wrapped_coord = ((x + xoffset) % w, (y + yoffset) % h)
assert new.getpixel(wrapped_coord) == im.getpixel((x, y))


@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_offset_zero_size(size: tuple[int, int]) -> None:
im = Image.new("RGB", size)
Expand Down
5 changes: 5 additions & 0 deletions docs/releasenotes/13.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,8 @@ to help projects prepare for 3.15, and to ensure Pillow could be used
immediately at the release of 3.15.0 final (2026-10-01, :pep:`790`).

Pillow 13.0.0 now officially supports Python 3.15.

Fixed ImageChops.offset() for 16-bit images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

:py:meth:`~PIL.ImageChops.offset` now works correctly for 16-bit-per-channel images.
49 changes: 21 additions & 28 deletions src/libImaging/Offset.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,53 +24,46 @@
*/
Imaging
ImagingOffset(Imaging im, int xoffset, int yoffset) {
int x, y;
Imaging imOut;

if (!im) {
return (Imaging)ImagingError_ModeError();
}

imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize);
int xsize = im->xsize, ysize = im->ysize;

Imaging imOut = ImagingNewDirty(im->mode, xsize, ysize);
if (!imOut) {
return NULL;
}

ImagingCopyPalette(imOut, im);

/* make offsets positive to avoid negative coordinates */
Comment thread
akx marked this conversation as resolved.
if (im->xsize == 0 || im->ysize == 0) {
if (xsize == 0 || ysize == 0) {
return imOut;
}
xoffset %= im->xsize;
xoffset = im->xsize - xoffset;

xoffset %= xsize;
if (xoffset < 0) {
xoffset += im->xsize;
xoffset += xsize;
}

yoffset %= im->ysize;
yoffset = im->ysize - yoffset;
yoffset %= ysize;
if (yoffset < 0) {
yoffset += im->ysize;
yoffset += ysize;
}

// yi depends only on y, so compute it (and both row pointers) once per
// row instead of redoing the modulo and pointer chase for every x.
#define OFFSET(type, image) \
for (y = 0; y < im->ysize; y++) { \
int yi = (y + yoffset) % im->ysize; \
type *restrict in = im->image[yi]; \
type *restrict out = imOut->image[y]; \
for (x = 0; x < im->xsize; x++) { \
int xi = (x + xoffset) % im->xsize; \
out[x] = in[xi]; \
} \
}
int head = xoffset * im->pixelsize;
int tail = im->linesize - head;

if (im->image8) {
OFFSET(UINT8, image8)
} else {
OFFSET(INT32, image32)
// Restrict safe: im is read-only, imOut is write-only and a new allocation.
for (int y = 0; y < ysize; y++) {
int yi = y - yoffset;
if (yi < 0) {
yi += ysize;
}
const char *restrict in = im->image[yi];
char *restrict out = imOut->image[y];
memcpy(out, in + tail, head);
memcpy(out + head, in, tail);
}

return imOut;
Expand Down
Loading