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..b99d0589a30 --- /dev/null +++ b/Tests/test_imagedraw_i16.py @@ -0,0 +1,104 @@ +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 + +I16_MODES = ("I;16", "I;16L", "I;16B", "I;16N") +I16_INK = 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) +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) + draw.point((4, 4), fill=I16_INK) + assert img.getpixel((4, 4)) == I16_INK + + +@pytest.mark.parametrize("mode", I16_MODES) +def test_horizontal_line_I16(mode: str) -> None: + img, draw = create_I16_image_draw(mode) + draw.line((0, 4, 7, 4), fill=I16_INK) + assert img.getpixel((4, 4)) == I16_INK + + +@pytest.mark.parametrize("mode", I16_MODES) +def test_vertical_line_I16(mode: str) -> None: + img, draw = create_I16_image_draw(mode) + draw.line((4, 0, 4, 7), fill=I16_INK) + assert img.getpixel((4, 4)) == I16_INK + + +@pytest.mark.parametrize("mode", I16_MODES) +def test_diagonal_line_I16(mode: str) -> None: + img, draw = create_I16_image_draw(mode) + draw.line((0, 0, 7, 7), 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 diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index 46dbed3b58b..9c5968c3a31 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -126,6 +126,12 @@ Two new filters are available for :py:meth:`~PIL.Image.Image.resize` and 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 3217953a3e8..c67ac3bb4cd 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++; } } } @@ -205,221 +244,91 @@ 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; +#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; \ + } \ + } \ } - 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++) { - 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; - } +static inline void +line8(Imaging im, int x0, int y0, int x1, int y1, int ink) { + GEN_LINE(point8, hline8); +} - } 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 +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) { - 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) { @@ -668,6 +577,7 @@ typedef struct { } DRAW; DRAW draw8 = {point8, hline8, line8}; +DRAW draw16 = {point16, hline16, line16}; DRAW draw32 = {point32, hline32, line32}; DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; @@ -677,10 +587,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 { \