From ff504f8e1394007c6f33ecc6265ced5763719347 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 10 Sep 2026 16:09:10 +0300 Subject: [PATCH 01/11] Move I;16 drawing tests to their own file --- Tests/test_imagedraw.py | 39 ---------------------------- Tests/test_imagedraw_i16.py | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 39 deletions(-) create mode 100644 Tests/test_imagedraw_i16.py diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 680a68c302b..f5304fe41af 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -625,18 +625,6 @@ def test_point(points: Coords) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -def test_point_I16() -> None: - # Arrange - im = Image.new("I;16", (1, 1)) - draw = ImageDraw.Draw(im) - - # Act - draw.point((0, 0), fill=0x1234) - - # Assert - assert im.getpixel((0, 0)) == 0x1234 - - @pytest.mark.parametrize("points", POINTS) def test_polygon(points: Coords) -> None: # Arrange @@ -650,19 +638,6 @@ def test_polygon(points: Coords) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") -@pytest.mark.parametrize("points", POINTS) -def test_polygon_width_I16(points: Coords) -> None: - # Arrange - im = Image.new("I;16", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.polygon(points, outline=0xFFFF, width=2) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon_width_I.tiff") - - @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("kite_points", KITE_POINTS) def test_polygon_kite( @@ -791,20 +766,6 @@ def test_rectangle_zero_width(bbox: Coords) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") -@pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox: Coords) -> None: - # Arrange - im = Image.new("I;16", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.rectangle(bbox, outline=0xCDEF) - - # Assert - assert im.getpixel((X0, Y0)) == 0xCDEF - assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") - - @pytest.mark.parametrize("bbox", BBOX) def test_rectangle_translucent_outline(bbox: Coords) -> None: # Arrange diff --git a/Tests/test_imagedraw_i16.py b/Tests/test_imagedraw_i16.py new file mode 100644 index 00000000000..46020040025 --- /dev/null +++ b/Tests/test_imagedraw_i16.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import pytest + +from PIL import Image, ImageDraw +from Tests.helper import assert_image_equal_tofile +from Tests.test_imagedraw import BBOX, POINTS, X0, Y0, H, W + +TYPE_CHECKING = False + +if TYPE_CHECKING: + from PIL._typing import Coords + + +def test_point_I16() -> None: + # Arrange + im = Image.new("I;16", (1, 1)) + draw = ImageDraw.Draw(im) + + # Act + draw.point((0, 0), fill=0x1234) + + # Assert + assert im.getpixel((0, 0)) == 0x1234 + + +@pytest.mark.parametrize("points", POINTS) +def test_polygon_width_I16(points: Coords) -> None: + # Arrange + im = Image.new("I;16", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.polygon(points, outline=0xFFFF, width=2) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon_width_I.tiff") + + +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_I16(bbox: Coords) -> None: + # Arrange + im = Image.new("I;16", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, outline=0xCDEF) + + # Assert + assert im.getpixel((X0, Y0)) == 0xCDEF + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") From 5d3d3d7f550f884269bb99a11a6a79efa1b3a8f3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 10 Sep 2026 16:11:59 +0300 Subject: [PATCH 02/11] Add more comprehensive tests for I;16 drawing Co-authored-by: Andrew Murray --- Tests/test_imagedraw_i16.py | 62 +++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/Tests/test_imagedraw_i16.py b/Tests/test_imagedraw_i16.py index 46020040025..d5b2a65f512 100644 --- a/Tests/test_imagedraw_i16.py +++ b/Tests/test_imagedraw_i16.py @@ -11,17 +11,13 @@ if TYPE_CHECKING: from PIL._typing import Coords +I16_MODES = ("I;16", "I;16L", "I;16B", "I;16N") +I16_INK = 0x1234 -def test_point_I16() -> None: - # Arrange - im = Image.new("I;16", (1, 1)) - draw = ImageDraw.Draw(im) - # Act - draw.point((0, 0), fill=0x1234) - - # Assert - assert im.getpixel((0, 0)) == 0x1234 +def create_I16_image_draw(mode: str) -> tuple[Image.Image, ImageDraw.ImageDraw]: + img = Image.new(mode, (8, 8)) + return img, ImageDraw.Draw(img) @pytest.mark.parametrize("points", POINTS) @@ -49,3 +45,51 @@ def test_rectangle_I16(bbox: Coords) -> None: # Assert assert im.getpixel((X0, Y0)) == 0xCDEF assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") + + +@pytest.mark.parametrize("mode", I16_MODES) +def test_point_I16(mode: str) -> None: + img, draw = create_I16_image_draw(mode) + draw.point((4, 4), fill=I16_INK) + assert img.getpixel((4, 4)) == I16_INK + + +@pytest.mark.parametrize("mode", I16_MODES) +@pytest.mark.parametrize( + "xy", + # horizontal, vertical, diagonal + ((0, 4, 7, 4), (4, 0, 4, 7), (0, 0, 7, 7)), +) +def test_line_I16(mode: str, xy: tuple[int, int, int, int]) -> None: + img, draw = create_I16_image_draw(mode) + draw.line(xy, fill=I16_INK) + assert img.getpixel((4, 4)) == I16_INK + + +@pytest.mark.parametrize("mode", I16_MODES) +def test_rectangle_fill_I16(mode: str) -> None: + img, draw = create_I16_image_draw(mode) + draw.rectangle((0, 0, 7, 7), fill=I16_INK) + assert img.getpixel((4, 4)) == I16_INK + + +@pytest.mark.parametrize("mode", I16_MODES) +def test_polygon_I16(mode: str) -> None: + img, draw = create_I16_image_draw(mode) + draw.polygon([(0, 0), (7, 0), (7, 7), (0, 7)], fill=I16_INK) + assert img.getpixel((4, 4)) == I16_INK + + +@pytest.mark.parametrize("mode", I16_MODES) +def test_masked_polygon_I16(mode: str) -> None: + # A polygon outline wider than one pixel is drawn through a mask. + img, draw = create_I16_image_draw(mode) + draw.polygon([(0, 0), (7, 0), (7, 7), (0, 7)], outline=I16_INK, width=8) + assert img.getpixel((4, 4)) == I16_INK + + +@pytest.mark.parametrize("mode", I16_MODES) +def test_ellipse_I16(mode: str) -> None: + img, draw = create_I16_image_draw(mode) + draw.ellipse((0, 0, 7, 7), fill=I16_INK) + assert img.getpixel((4, 4)) == I16_INK From 7ed02f34eb54bd03dca3c28140ca000931cd906c Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 6 Jul 2026 13:29:55 +0300 Subject: [PATCH 03/11] De-triplicate Bresenham walkers; use hline when possible --- src/libImaging/Draw.c | 273 +++++++++++------------------------------- 1 file changed, 69 insertions(+), 204 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index b3233405bf3..3679e587231 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -205,221 +205,86 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { } } -static inline void -line8(Imaging im, int x0, int y0, int x1, int y1, int ink) { - int i, n, e; - int dx, dy; - int xs, ys; - - /* normalize coordinates */ - dx = x1 - x0; - if (dx < 0) { - dx = -dx, xs = -1; - } else { - xs = 1; - } - dy = y1 - y0; - if (dy < 0) { - dy = -dy, ys = -1; - } else { - ys = 1; +#define GEN_LINE(point, hline) \ + { \ + int i, n, e, dx, dy, xs, ys; \ + /* normalize coordinates */ \ + dy = y1 - y0; \ + if (dy < 0) { \ + dy = -dy, ys = -1; \ + } else { \ + ys = 1; \ + } \ + if (dy == 0) { /* horizontal, exclude endpoint */ \ + if (x1 > x0) { \ + hline(im, x0, y0, x1 - 1, ink, NULL); \ + } else if (x0 > x1) { \ + hline(im, x1 + 1, y0, x0, ink, NULL); \ + } \ + return; \ + } \ + dx = x1 - x0; \ + if (dx < 0) { \ + dx = -dx, xs = -1; \ + } else { \ + xs = 1; \ + } \ + \ + n = (dx > dy) ? dx : dy; \ + \ + if (dx == 0) { /* vertical */ \ + for (i = 0; i < dy; i++) { \ + point(im, x0, y0, ink); \ + y0 += ys; \ + } \ + } else if (dx > dy) { /* bresenham, horizontal slope */ \ + n = dx; \ + dy += dy; \ + e = dy - dx; \ + dx += dx; \ + \ + for (i = 0; i < n; i++) { \ + point(im, x0, y0, ink); \ + if (e >= 0) { \ + y0 += ys; \ + e -= dx; \ + } \ + e += dy; \ + x0 += xs; \ + } \ + } else { /* bresenham, vertical slope */ \ + n = dy; \ + dx += dx; \ + e = dx - dy; \ + dy += dy; \ + \ + for (i = 0; i < n; i++) { \ + point(im, x0, y0, ink); \ + if (e >= 0) { \ + x0 += xs; \ + e -= dy; \ + } \ + e += dx; \ + y0 += ys; \ + } \ + } \ } - n = (dx > dy) ? dx : dy; - - if (dx == 0) { - /* vertical */ - for (i = 0; i < dy; i++) { - point8(im, x0, y0, ink); - y0 += ys; - } - - } else if (dy == 0) { - /* horizontal */ - for (i = 0; i < dx; i++) { - point8(im, x0, y0, ink); - x0 += xs; - } - - } else if (dx > dy) { - /* bresenham, horizontal slope */ - n = dx; - dy += dy; - e = dy - dx; - dx += dx; - - for (i = 0; i < n; i++) { - point8(im, x0, y0, ink); - if (e >= 0) { - y0 += ys; - e -= dx; - } - e += dy; - x0 += xs; - } - - } else { - /* bresenham, vertical slope */ - n = dy; - dx += dx; - e = dx - dy; - dy += dy; - - for (i = 0; i < n; i++) { - point8(im, x0, y0, ink); - if (e >= 0) { - x0 += xs; - e -= dy; - } - e += dx; - y0 += ys; - } - } +static inline void +line8(Imaging im, int x0, int y0, int x1, int y1, int ink) { + GEN_LINE(point8, hline8); } static inline void line32(Imaging im, int x0, int y0, int x1, int y1, int ink) { - int i, n, e; - int dx, dy; - int xs, ys; - - /* normalize coordinates */ - dx = x1 - x0; - if (dx < 0) { - dx = -dx, xs = -1; - } else { - xs = 1; - } - dy = y1 - y0; - if (dy < 0) { - dy = -dy, ys = -1; - } else { - ys = 1; - } - - n = (dx > dy) ? dx : dy; - - if (dx == 0) { - /* vertical */ - for (i = 0; i < dy; i++) { - point32(im, x0, y0, ink); - y0 += ys; - } - - } else if (dy == 0) { - /* horizontal */ - for (i = 0; i < dx; i++) { - point32(im, x0, y0, ink); - x0 += xs; - } - - } else if (dx > dy) { - /* bresenham, horizontal slope */ - n = dx; - dy += dy; - e = dy - dx; - dx += dx; - - for (i = 0; i < n; i++) { - point32(im, x0, y0, ink); - if (e >= 0) { - y0 += ys; - e -= dx; - } - e += dy; - x0 += xs; - } - - } else { - /* bresenham, vertical slope */ - n = dy; - dx += dx; - e = dx - dy; - dy += dy; - - for (i = 0; i < n; i++) { - point32(im, x0, y0, ink); - if (e >= 0) { - x0 += xs; - e -= dy; - } - e += dx; - y0 += ys; - } - } + GEN_LINE(point32, hline32); } static inline void line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) { - int i, n, e; - int dx, dy; - int xs, ys; - - /* normalize coordinates */ - dx = x1 - x0; - if (dx < 0) { - dx = -dx, xs = -1; - } else { - xs = 1; - } - dy = y1 - y0; - if (dy < 0) { - dy = -dy, ys = -1; - } else { - ys = 1; - } - - n = (dx > dy) ? dx : dy; - - if (dx == 0) { - /* vertical */ - for (i = 0; i < dy; i++) { - point32rgba(im, x0, y0, ink); - y0 += ys; - } - - } else if (dy == 0) { - /* horizontal */ - for (i = 0; i < dx; i++) { - point32rgba(im, x0, y0, ink); - x0 += xs; - } - - } else if (dx > dy) { - /* bresenham, horizontal slope */ - n = dx; - dy += dy; - e = dy - dx; - dx += dx; - - for (i = 0; i < n; i++) { - point32rgba(im, x0, y0, ink); - if (e >= 0) { - y0 += ys; - e -= dx; - } - e += dy; - x0 += xs; - } - - } else { - /* bresenham, vertical slope */ - n = dy; - dx += dx; - e = dx - dy; - dy += dy; - - for (i = 0; i < n; i++) { - point32rgba(im, x0, y0, ink); - if (e >= 0) { - x0 += xs; - e -= dy; - } - e += dx; - y0 += ys; - } - } + GEN_LINE(point32rgba, hline32rgba); } +#undef GEN_LINE static int x_cmp(const void *x0, const void *x1) { From 83303d701faf2e76c82e60f83d2a04ccffd390bd Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 10 Sep 2026 16:12:05 +0300 Subject: [PATCH 04/11] Implement correct draw functions for 16-bpp modes --- docs/releasenotes/13.0.0.rst | 6 ++ src/libImaging/Draw.c | 122 ++++++++++++++++++++++++----------- 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index b9906b7f19d..7da0b68dce5 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -134,6 +134,12 @@ escape sequences. Other changes ============= +Drawing on I;16B images +^^^^^^^^^^^^^^^^^^^^^^^ + +When drawing on an ``I;16B`` image, some drawing methods wrote in the wrong byte order. +All drawing operations now use the byte order of the image mode. + Python 3.15 ^^^^^^^^^^^ diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 3679e587231..760525d1f0e 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -41,7 +41,36 @@ #define FLOOR(v) ((v) >= 0.0 ? (int)(v) : (int)floor(v)) #define INK8(ink) (*(UINT8 *)ink) -#define INK16(ink) (*(UINT16 *)ink) + +// True when the given I;16 mode stores its pixels most significant byte first. +static inline int +isModeI16BigEndian(const ModeID mode) { + return mode == IMAGING_MODE_I_16B +#ifdef WORDS_BIGENDIAN + || mode == IMAGING_MODE_I_16N +#endif + ; +} + +// Convert getink()'s ink value into the image's storage order +// and return in the native order so the drawing functions don't +// need to care about it. +static inline INT32 +ink16(Imaging im, const void *ink_) { + const UINT8 *in = ink_; + UINT8 out[2]; + UINT16 ink; + + if (isModeI16BigEndian(im->mode)) { + out[0] = in[1]; + out[1] = in[0]; + } else { + out[0] = in[0]; + out[1] = in[1]; + } + memcpy(&ink, out, sizeof(ink)); + return ink; +} /* * Rounds around zero (up=away from zero, down=towards zero) @@ -68,17 +97,14 @@ typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging); static inline void point8(Imaging im, int x, int y, int ink) { if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { - if (isModeI16(im->mode)) { -#ifdef WORDS_BIGENDIAN - im->image8[y][x * 2] = (UINT8)(ink >> 8); - im->image8[y][x * 2 + 1] = (UINT8)ink; -#else - im->image8[y][x * 2] = (UINT8)ink; - im->image8[y][x * 2 + 1] = (UINT8)(ink >> 8); -#endif - } else { - im->image8[y][x] = (UINT8)ink; - } + im->image8[y][x] = (UINT8)ink; + } +} + +static inline void +point16(Imaging im, int x, int y, int ink) { + if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { + ((UINT16 *)im->image8[y])[x] = (UINT16)ink; } } @@ -116,33 +142,46 @@ hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { x1 = im->xsize - 1; } if (x0 <= x1) { - int bigendian = -1; - if (isModeI16(im->mode)) { - bigendian = - ( -#ifdef WORDS_BIGENDIAN - im->mode == IMAGING_MODE_I_16 || im->mode == IMAGING_MODE_I_16L -#else - im->mode == IMAGING_MODE_I_16B -#endif - ) - ? 1 - : 0; + UINT8 *p = im->image8[y0]; + if (mask == NULL) { + memset(p + x0, (UINT8)ink, (x1 - x0 + 1)); + } else { + UINT8 *mask_row = mask->image8[y0]; + for (; x0 <= x1; x0++) { + if (mask_row[x0]) { + p[x0] = (UINT8)ink; + } + } } - if (mask == NULL && bigendian == -1) { - memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1)); + } + } +} + +static inline void +hline16(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { + if (y0 >= 0 && y0 < im->ysize) { + if (x0 < 0) { + x0 = 0; + } else if (x0 >= im->xsize) { + return; + } + if (x1 < 0) { + return; + } else if (x1 >= im->xsize) { + x1 = im->xsize - 1; + } + if (x0 <= x1) { + UINT16 *p = (UINT16 *)im->image8[y0]; + if (mask == NULL) { + for (; x0 <= x1; x0++) { + p[x0] = (UINT16)ink; + } } else { - UINT8 *p = im->image8[y0]; - while (x0 <= x1) { - if (mask == NULL || mask->image8[y0][x0]) { - if (bigendian == -1) { - p[x0] = ink; - } else { - p[x0 * 2 + (bigendian ? 1 : 0)] = ink; - p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8; - } + UINT8 *mask_row = mask->image8[y0]; + for (; x0 <= x1; x0++) { + if (mask_row[x0]) { + p[x0] = (UINT16)ink; } - x0++; } } } @@ -275,6 +314,11 @@ line8(Imaging im, int x0, int y0, int x1, int y1, int ink) { GEN_LINE(point8, hline8); } +static inline void +line16(Imaging im, int x0, int y0, int x1, int y1, int ink) { + GEN_LINE(point16, hline16); +} + static inline void line32(Imaging im, int x0, int y0, int x1, int y1, int ink) { GEN_LINE(point32, hline32); @@ -531,6 +575,7 @@ typedef struct { } DRAW; DRAW draw8 = {point8, hline8, line8}; +DRAW draw16 = {point16, hline16, line16}; DRAW draw32 = {point32, hline32, line32}; DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; @@ -540,10 +585,11 @@ DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; #define DRAWINIT() \ if (im->image8) { \ - draw = &draw8; \ if (isModeI16(im->mode)) { \ - ink = INK16(ink_); \ + draw = &draw16; \ + ink = ink16(im, ink_); \ } else { \ + draw = &draw8; \ ink = INK8(ink_); \ } \ } else { \ From 343cda4fc0374c0e3703ec546ed8fef84cc7cc75 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Oct 2026 16:03:08 +1100 Subject: [PATCH 05/11] Inline function --- src/libImaging/Draw.c | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 760525d1f0e..52d5f3cdc80 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -42,16 +42,6 @@ #define INK8(ink) (*(UINT8 *)ink) -// True when the given I;16 mode stores its pixels most significant byte first. -static inline int -isModeI16BigEndian(const ModeID mode) { - return mode == IMAGING_MODE_I_16B -#ifdef WORDS_BIGENDIAN - || mode == IMAGING_MODE_I_16N -#endif - ; -} - // Convert getink()'s ink value into the image's storage order // and return in the native order so the drawing functions don't // need to care about it. @@ -61,7 +51,12 @@ ink16(Imaging im, const void *ink_) { UINT8 out[2]; UINT16 ink; - if (isModeI16BigEndian(im->mode)) { + if ( + mode == IMAGING_MODE_I_16B +#ifdef WORDS_BIGENDIAN + || mode == IMAGING_MODE_I_16N +#endif + ) { out[0] = in[1]; out[1] = in[0]; } else { From 0bccc1a131e63b7ad972475f0116a3ae8e27cc23 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 12 Oct 2026 15:09:15 +0900 Subject: [PATCH 06/11] Correct inlined function --- src/libImaging/Draw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 52d5f3cdc80..b2f4ff5b7c7 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -52,9 +52,9 @@ ink16(Imaging im, const void *ink_) { UINT16 ink; if ( - mode == IMAGING_MODE_I_16B + im->mode == IMAGING_MODE_I_16B #ifdef WORDS_BIGENDIAN - || mode == IMAGING_MODE_I_16N + || im->mode == IMAGING_MODE_I_16N #endif ) { out[0] = in[1]; From dd3e4bb099e59c614a7e90747a29c71f6b24ee85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Oct 2026 18:34:41 +1100 Subject: [PATCH 07/11] Move horizontal line check inside earlier block --- src/libImaging/Draw.c | 125 +++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index b2f4ff5b7c7..8377ba6f4c2 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -239,69 +239,68 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { } } -#define GEN_LINE(point, hline) \ - { \ - int i, n, e, dx, dy, xs, ys; \ - /* normalize coordinates */ \ - dy = y1 - y0; \ - if (dy < 0) { \ - dy = -dy, ys = -1; \ - } else { \ - ys = 1; \ - } \ - if (dy == 0) { /* horizontal, exclude endpoint */ \ - if (x1 > x0) { \ - hline(im, x0, y0, x1 - 1, ink, NULL); \ - } else if (x0 > x1) { \ - hline(im, x1 + 1, y0, x0, ink, NULL); \ - } \ - return; \ - } \ - dx = x1 - x0; \ - if (dx < 0) { \ - dx = -dx, xs = -1; \ - } else { \ - xs = 1; \ - } \ - \ - n = (dx > dy) ? dx : dy; \ - \ - if (dx == 0) { /* vertical */ \ - for (i = 0; i < dy; i++) { \ - point(im, x0, y0, ink); \ - y0 += ys; \ - } \ - } else if (dx > dy) { /* bresenham, horizontal slope */ \ - n = dx; \ - dy += dy; \ - e = dy - dx; \ - dx += dx; \ - \ - for (i = 0; i < n; i++) { \ - point(im, x0, y0, ink); \ - if (e >= 0) { \ - y0 += ys; \ - e -= dx; \ - } \ - e += dy; \ - x0 += xs; \ - } \ - } else { /* bresenham, vertical slope */ \ - n = dy; \ - dx += dx; \ - e = dx - dy; \ - dy += dy; \ - \ - for (i = 0; i < n; i++) { \ - point(im, x0, y0, ink); \ - if (e >= 0) { \ - x0 += xs; \ - e -= dy; \ - } \ - e += dx; \ - y0 += ys; \ - } \ - } \ +#define GEN_LINE(point, hline) \ + { \ + int i, n, e, dx, dy, xs, ys; \ + /* normalize coordinates */ \ + dy = y1 - y0; \ + if (dy < 0) { \ + dy = -dy, ys = -1; \ + } else if (dy == 0) { /* horizontal, exclude endpoint */ \ + if (x1 > x0) { \ + hline(im, x0, y0, x1 - 1, ink, NULL); \ + } else if (x0 > x1) { \ + hline(im, x1 + 1, y0, x0, ink, NULL); \ + } \ + return; \ + } else { \ + ys = 1; \ + } \ + dx = x1 - x0; \ + if (dx < 0) { \ + dx = -dx, xs = -1; \ + } else { \ + xs = 1; \ + } \ + \ + n = (dx > dy) ? dx : dy; \ + \ + if (dx == 0) { /* vertical */ \ + for (i = 0; i < dy; i++) { \ + point(im, x0, y0, ink); \ + y0 += ys; \ + } \ + } else if (dx > dy) { /* bresenham, horizontal slope */ \ + n = dx; \ + dy += dy; \ + e = dy - dx; \ + dx += dx; \ + \ + for (i = 0; i < n; i++) { \ + point(im, x0, y0, ink); \ + if (e >= 0) { \ + y0 += ys; \ + e -= dx; \ + } \ + e += dy; \ + x0 += xs; \ + } \ + } else { /* bresenham, vertical slope */ \ + n = dy; \ + dx += dx; \ + e = dx - dy; \ + dy += dy; \ + \ + for (i = 0; i < n; i++) { \ + point(im, x0, y0, ink); \ + if (e >= 0) { \ + x0 += xs; \ + e -= dy; \ + } \ + e += dx; \ + y0 += ys; \ + } \ + } \ } static inline void From 30657fbd791e6dee0a34553bea7899f1ffaab9f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Oct 2026 18:39:03 +1100 Subject: [PATCH 08/11] Replace GEN_LINE with function --- src/libImaging/Draw.c | 208 ++++++++++++++++++++++++------------------ 1 file changed, 119 insertions(+), 89 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 8377ba6f4c2..a7d3a870a73 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -86,6 +86,8 @@ typedef struct { float dx; } Edge; +typedef void (*point_handler)(Imaging, int, int, int); + /* Type used in "polygon*" functions */ typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging); @@ -239,90 +241,85 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { } } -#define GEN_LINE(point, hline) \ - { \ - int i, n, e, dx, dy, xs, ys; \ - /* normalize coordinates */ \ - dy = y1 - y0; \ - if (dy < 0) { \ - dy = -dy, ys = -1; \ - } else if (dy == 0) { /* horizontal, exclude endpoint */ \ - if (x1 > x0) { \ - hline(im, x0, y0, x1 - 1, ink, NULL); \ - } else if (x0 > x1) { \ - hline(im, x1 + 1, y0, x0, ink, NULL); \ - } \ - return; \ - } else { \ - ys = 1; \ - } \ - dx = x1 - x0; \ - if (dx < 0) { \ - dx = -dx, xs = -1; \ - } else { \ - xs = 1; \ - } \ - \ - n = (dx > dy) ? dx : dy; \ - \ - if (dx == 0) { /* vertical */ \ - for (i = 0; i < dy; i++) { \ - point(im, x0, y0, ink); \ - y0 += ys; \ - } \ - } else if (dx > dy) { /* bresenham, horizontal slope */ \ - n = dx; \ - dy += dy; \ - e = dy - dx; \ - dx += dx; \ - \ - for (i = 0; i < n; i++) { \ - point(im, x0, y0, ink); \ - if (e >= 0) { \ - y0 += ys; \ - e -= dx; \ - } \ - e += dy; \ - x0 += xs; \ - } \ - } else { /* bresenham, vertical slope */ \ - n = dy; \ - dx += dx; \ - e = dx - dy; \ - dy += dy; \ - \ - for (i = 0; i < n; i++) { \ - point(im, x0, y0, ink); \ - if (e >= 0) { \ - x0 += xs; \ - e -= dy; \ - } \ - e += dx; \ - y0 += ys; \ - } \ - } \ +static inline void +line( + Imaging im, + int x0, + int y0, + int x1, + int y1, + int ink, + point_handler point, + hline_handler hline +) { + int i, n, e, dx, dy, xs, ys; + + /* normalize coordinates */ + dy = y1 - y0; + if (dy < 0) { + dy = -dy, ys = -1; + } else if (dy == 0) { + /* horizontal */ + if (x1 > x0) { + hline(im, x0, y0, x1 - 1, ink, NULL); + } else if (x0 > x1) { + hline(im, x1 + 1, y0, x0, ink, NULL); + } + return; + } else { + ys = 1; + } + dx = x1 - x0; + if (dx < 0) { + dx = -dx, xs = -1; + } else { + xs = 1; } -static inline void -line8(Imaging im, int x0, int y0, int x1, int y1, int ink) { - GEN_LINE(point8, hline8); -} + n = (dx > dy) ? dx : dy; -static inline void -line16(Imaging im, int x0, int y0, int x1, int y1, int ink) { - GEN_LINE(point16, hline16); -} + if (dx == 0) { + /* vertical */ + for (i = 0; i < dy; i++) { + point(im, x0, y0, ink); + y0 += ys; + } -static inline void -line32(Imaging im, int x0, int y0, int x1, int y1, int ink) { - GEN_LINE(point32, hline32); -} + } else if (dx > dy) { + /* bresenham, horizontal slope */ + n = dx; + dy += dy; + e = dy - dx; + dx += dx; + + for (i = 0; i < n; i++) { + point(im, x0, y0, ink); + if (e >= 0) { + y0 += ys; + e -= dx; + } + e += dy; + x0 += xs; + } -static inline void -line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) { - GEN_LINE(point32rgba, hline32rgba); + } else { + /* bresenham, vertical slope */ + n = dy; + dx += dx; + e = dx - dy; + dy += dy; + + for (i = 0; i < n; i++) { + point(im, x0, y0, ink); + if (e >= 0) { + x0 += xs; + e -= dy; + } + e += dx; + y0 += ys; + } + } } -#undef GEN_LINE static int x_cmp(const void *x0, const void *x1) { @@ -565,13 +562,12 @@ add_edge(Edge *e, int x0, int y0, int x1, int y1) { typedef struct { void (*point)(Imaging im, int x, int y, int ink); void (*hline)(Imaging im, int x0, int y0, int x1, int ink, Imaging mask); - void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink); } DRAW; -DRAW draw8 = {point8, hline8, line8}; -DRAW draw16 = {point16, hline16, line16}; -DRAW draw32 = {point32, hline32, line32}; -DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; +DRAW draw8 = {point8, hline8}; +DRAW draw16 = {point16, hline16}; +DRAW draw32 = {point32, hline32}; +DRAW draw32rgba = {point32rgba, hline32rgba}; /* -------------------------------------------------------------------- */ /* Interface */ @@ -610,7 +606,7 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, in DRAWINIT(); - draw->line(im, x0, y0, x1, y1, ink); + line(im, x0, y0, x1, y1, ink, draw->point, draw->hline); return 0; } @@ -719,8 +715,26 @@ ImagingDrawRectangle( for (i = 0; i < width; i++) { draw->hline(im, x0, y0 + i, x1, ink, NULL); draw->hline(im, x0, y1 - i, x1, ink, NULL); - draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink); - draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink); + line( + im, + x1 - i, + y0 + width, + x1 - i, + y1 - width + 1, + ink, + draw->point, + draw->hline + ); + line( + im, + x0 + i, + y0 + width, + x0 + i, + y1 - width + 1, + ink, + draw->point, + draw->hline + ); } } @@ -787,11 +801,27 @@ ImagingDrawPolygon( /* Outline */ if (width == 1) { for (i = 0; i < count - 1; i++) { - draw->line( - im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink + line( + im, + xy[i * 2], + xy[i * 2 + 1], + xy[i * 2 + 2], + xy[i * 2 + 3], + ink, + draw->point, + draw->hline ); } - draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink); + line( + im, + xy[i * 2], + xy[i * 2 + 1], + xy[0], + xy[1], + ink, + draw->point, + draw->hline + ); } else { for (i = 0; i < count - 1; i++) { ImagingDrawWideLine( From 178309f04d488dd22694762e2db7780a9fee9ab6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Oct 2026 18:40:56 +1100 Subject: [PATCH 09/11] Simplify little-endian path --- src/libImaging/Draw.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index a7d3a870a73..aa273e30f7b 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -47,24 +47,22 @@ // need to care about it. static inline INT32 ink16(Imaging im, const void *ink_) { - const UINT8 *in = ink_; - UINT8 out[2]; - UINT16 ink; - if ( im->mode == IMAGING_MODE_I_16B #ifdef WORDS_BIGENDIAN || im->mode == IMAGING_MODE_I_16N #endif ) { + const UINT8 *in = ink_; + UINT8 out[2]; + UINT16 ink; out[0] = in[1]; out[1] = in[0]; + memcpy(&ink, out, sizeof(ink)); + return ink; } else { - out[0] = in[0]; - out[1] = in[1]; + return (*(UINT16 *)ink_); } - memcpy(&ink, out, sizeof(ink)); - return ink; } /* From 5e9bfa64414b7d9e5d1c64feec84f12aa297d079 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Oct 2026 21:22:51 +1100 Subject: [PATCH 10/11] Moved release note --- docs/releasenotes/13.0.0.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index b103b6b6428..9ccfdcdb45f 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -134,12 +134,6 @@ escape sequences. Other changes ============= -Drawing on I;16B images -^^^^^^^^^^^^^^^^^^^^^^^ - -When drawing on an ``I;16B`` image, some drawing methods wrote in the wrong byte order. -All drawing operations now use the byte order of the image mode. - Python 3.15 ^^^^^^^^^^^ @@ -208,6 +202,12 @@ Fixed ImageChops.offset() for 16-bit images :py:meth:`~PIL.ImageChops.offset` now works correctly for 16-bit-per-channel images. +Fixed drawing on I;16B images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When drawing on an ``I;16B`` image, some drawing methods wrote in the wrong byte order. +All drawing operations now use the byte order of the image mode. + Fixed histograms for masked LA, La and PA images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 0f1f3d097b78b1f3ae897d95b8099fc4cb35295d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Oct 2026 21:35:20 +1100 Subject: [PATCH 11/11] Use relative imports --- Tests/test_imagedraw_i16.py | 59 +++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/Tests/test_imagedraw_i16.py b/Tests/test_imagedraw_i16.py index d5b2a65f512..f2918d83dcb 100644 --- a/Tests/test_imagedraw_i16.py +++ b/Tests/test_imagedraw_i16.py @@ -3,8 +3,9 @@ import pytest from PIL import Image, ImageDraw -from Tests.helper import assert_image_equal_tofile -from Tests.test_imagedraw import BBOX, POINTS, X0, Y0, H, W + +from .helper import assert_image_equal_tofile +from .test_imagedraw import BBOX, POINTS, X0, Y0, H, W TYPE_CHECKING = False @@ -20,33 +21,6 @@ def create_I16_image_draw(mode: str) -> tuple[Image.Image, ImageDraw.ImageDraw]: return img, ImageDraw.Draw(img) -@pytest.mark.parametrize("points", POINTS) -def test_polygon_width_I16(points: Coords) -> None: - # Arrange - im = Image.new("I;16", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.polygon(points, outline=0xFFFF, width=2) - - # Assert - assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon_width_I.tiff") - - -@pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox: Coords) -> None: - # Arrange - im = Image.new("I;16", (W, H)) - draw = ImageDraw.Draw(im) - - # Act - draw.rectangle(bbox, outline=0xCDEF) - - # Assert - assert im.getpixel((X0, Y0)) == 0xCDEF - assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") - - @pytest.mark.parametrize("mode", I16_MODES) def test_point_I16(mode: str) -> None: img, draw = create_I16_image_draw(mode) @@ -73,6 +47,20 @@ def test_rectangle_fill_I16(mode: str) -> None: assert img.getpixel((4, 4)) == I16_INK +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_I16(bbox: Coords) -> None: + # Arrange + im = Image.new("I;16", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, outline=0xCDEF) + + # Assert + assert im.getpixel((X0, Y0)) == 0xCDEF + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") + + @pytest.mark.parametrize("mode", I16_MODES) def test_polygon_I16(mode: str) -> None: img, draw = create_I16_image_draw(mode) @@ -80,6 +68,19 @@ def test_polygon_I16(mode: str) -> None: assert img.getpixel((4, 4)) == I16_INK +@pytest.mark.parametrize("points", POINTS) +def test_polygon_width_I16(points: Coords) -> None: + # Arrange + im = Image.new("I;16", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.polygon(points, outline=0xFFFF, width=2) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon_width_I.tiff") + + @pytest.mark.parametrize("mode", I16_MODES) def test_masked_polygon_I16(mode: str) -> None: # A polygon outline wider than one pixel is drawn through a mask.