From e3dd4de1934c35144430de1a411d1df5bad84732 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 20:08:37 -0600 Subject: [PATCH 01/83] parametrize check_jpeg_leaks::test_qtables_leak() Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/check_jpeg_leaks.py | 71 +++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index ab8d77719..940c0b00d 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -75,43 +75,42 @@ post-patch: """ -def test_qtables_leak(): +standard_l_qtable = ( + # fmt: off + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, + 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, + 72, 92, 95, 98, 112, 100, 103, 99, + # fmt: on +) + +standard_chrominance_qtable = ( + # fmt: off + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + # fmt: on +) + + +@pytest.mark.parametrize( + "qtables", + ( + (standard_l_qtable, standard_chrominance_qtable), + [standard_l_qtable, standard_chrominance_qtable], + ), +) +def test_qtables_leak(qtables): im = hopper("RGB") - - standard_l_qtable = [ - int(s) - for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split( - None - ) - ] - - standard_chrominance_qtable = [ - int(s) - for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split( - None - ) - ] - - qtables = [standard_l_qtable, standard_chrominance_qtable] - for _ in range(iterations): test_output = BytesIO() im.save(test_output, "JPEG", qtables=qtables) From e8307d74064d555524558a1dcb8828006ab3d65a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 18 Jan 2023 23:03:13 -0600 Subject: [PATCH 02/83] more imagepath tests --- Tests/test_imagepath.py | 83 +++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 861fb64f0..2b378d333 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -8,7 +8,6 @@ from PIL import Image, ImagePath def test_path(): - p = ImagePath.Path(list(range(10))) # sequence interface @@ -39,48 +38,76 @@ def test_path(): p.transform((1, 0, 1, 0, 1, 1)) assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] - # alternative constructors - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0.0, 1.0]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([(0, 1)]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(0)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(1)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(array.array("f", [0, 1])) - assert list(p) == [(0.0, 1.0)] - arr = array.array("f", [0, 1]) - p = ImagePath.Path(arr.tobytes()) +@pytest.mark.parametrize( + "coords", + ( + (0, 1), + [0, 1], + (0.0, 1.0), + [0.0, 1.0], + ((0, 1),), + [(0, 1)], + ((0.0, 1.0),), + [(0.0, 1.0)], + array.array("f", [0, 1]), + array.array("f", [0.0, 1.0]), + ImagePath.Path((0, 1)), + ), +) +def test_path_constructors(coords): + # Arrange / Act + p = ImagePath.Path(coords) + + # Assert assert list(p) == [(0.0, 1.0)] -def test_invalid_coords(): +def test_path_constructor_text(): # Arrange - coords = ["a", "b"] + arr = array.array("f", (0, 1)) - # Act / Assert + # Act + p = ImagePath.Path(arr.tobytes()) + + # Assert + assert list(p) == [(0.0, 1.0)] + + +@pytest.mark.parametrize( + "coords", + ( + ("a", "b"), + ([0, 1],), + [[0, 1]], + ([0.0, 1.0],), + [[0.0, 1.0]], + ), +) +def test_invalid_path_constructors(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "incorrect coordinate type" -def test_path_odd_number_of_coordinates(): - # Arrange - coords = [0] - - # Act / Assert +@pytest.mark.parametrize( + "coords", + ( + (0,), + [0], + (0, 1, 2), + [0, 1, 2], + ), +) +def test_path_odd_number_of_coordinates(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "wrong number of coordinates" From 4e8de9ac9a1e14593e17a2cd1ff82c45ed0900cd Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 25 Jan 2023 08:13:40 -0600 Subject: [PATCH 03/83] add path-from-bytes test Also `array.array("f", [0, 1]) == array.array("f", [0.0, 1.0])` so we didn't need both of them. --- Tests/test_imagepath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 2b378d333..7a517b6f6 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -51,7 +51,7 @@ def test_path(): ((0.0, 1.0),), [(0.0, 1.0)], array.array("f", [0, 1]), - array.array("f", [0.0, 1.0]), + array.array("f", [0, 1]).tobytes(), ImagePath.Path((0, 1)), ), ) From b05bc346045f4d243cdaecf85ff61567e5ca377c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 15:42:38 +1100 Subject: [PATCH 04/83] Test lists and tuples --- Tests/check_jpeg_leaks.py | 12 +-- Tests/test_imagedraw.py | 177 ++++++++++++++++++++++---------------- Tests/test_imagedraw2.py | 30 ++++--- 3 files changed, 128 insertions(+), 91 deletions(-) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index ab8d77719..5d95ca29c 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -110,11 +110,13 @@ def test_qtables_leak(): ) ] - qtables = [standard_l_qtable, standard_chrominance_qtable] - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) + for qtables in ( + (standard_l_qtable, standard_chrominance_qtable), + [standard_l_qtable, standard_chrominance_qtable], + ): + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) def test_exif_leak(): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index d4723c924..f6f27d32d 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -27,15 +27,21 @@ X1 = int(X0 * 3) Y0 = int(H / 4) Y1 = int(X0 * 3) -# Two kinds of bounding box -BBOX1 = [(X0, Y0), (X1, Y1)] -BBOX2 = [X0, Y0, X1, Y1] +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) -# Two kinds of coordinate sequences -POINTS1 = [(10, 10), (20, 40), (30, 30)] -POINTS2 = [10, 10, 20, 40, 30, 30] +# Coordinate sequences +POINTS = ( + ((10, 10), (20, 40), (30, 30)), + [(10, 10), (20, 40), (30, 30)], + (10, 10, 20, 40, 30, 30), + [10, 10, 20, 40, 30, 30], +) -KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +KITE_POINTS = ( + ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), + [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], +) def test_sanity(): @@ -63,7 +69,7 @@ def test_mode_mismatch(): ImageDraw.ImageDraw(im, mode="L") -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) def test_arc(bbox, start, end): # Arrange @@ -77,7 +83,8 @@ def test_arc(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) -def test_arc_end_le_start(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_end_le_start(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -85,13 +92,14 @@ def test_arc_end_le_start(): end = 0 # Act - draw.arc(BBOX1, start=start, end=end) + draw.arc(bbox, start=start, end=end) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png") -def test_arc_no_loops(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_no_loops(bbox): # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -100,57 +108,61 @@ def test_arc_no_loops(): end = 370 # Act - draw.arc(BBOX1, start=start, end=end) + draw.arc(bbox, start=start, end=end) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) -def test_arc_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, width=5) + draw.arc(bbox, 10, 260, width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) -def test_arc_width_pieslice_large(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_pieslice_large(bbox): # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=100) + draw.arc(bbox, 10, 260, fill="yellow", width=100) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) -def test_arc_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=5) + draw.arc(bbox, 10, 260, fill="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) -def test_arc_width_non_whole_angle(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_non_whole_angle(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" # Act - draw.arc(BBOX1, 10, 259.5, width=5) + draw.arc(bbox, 10, 259.5, width=5) # Assert assert_image_similar_tofile(im, expected, 1) @@ -184,7 +196,7 @@ def test_bitmap(): @pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_chord(mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -198,37 +210,40 @@ def test_chord(mode, bbox): assert_image_similar_tofile(im, expected, 1) -def test_chord_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, outline="yellow", width=5) + draw.chord(bbox, 10, 260, outline="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) -def test_chord_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) -def test_chord_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0) + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") @@ -247,7 +262,7 @@ def test_chord_too_fat(): @pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_ellipse(mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -261,13 +276,14 @@ def test_ellipse(mode, bbox): assert_image_similar_tofile(im, expected, 1) -def test_ellipse_translucent(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_translucent(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") # Act - draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) + draw.ellipse(bbox, fill=(0, 255, 0, 127)) # Assert expected = "Tests/images/imagedraw_ellipse_translucent.png" @@ -297,13 +313,14 @@ def test_ellipse_symmetric(): assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)) -def test_ellipse_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, outline="blue", width=5) + draw.ellipse(bbox, outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) @@ -321,25 +338,27 @@ def test_ellipse_width_large(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) -def test_ellipse_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, fill="green", outline="blue", width=5) + draw.ellipse(bbox, fill="green", outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) -def test_ellipse_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, fill="green", outline="blue", width=0) + draw.ellipse(bbox, fill="green", outline="blue", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") @@ -380,7 +399,7 @@ def test_ellipse_various_sizes_filled(): ) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -452,7 +471,7 @@ def test_transform(): assert_image_equal(im, expected) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) def test_pieslice(bbox, start, end): # Arrange @@ -466,38 +485,41 @@ def test_pieslice(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) -def test_pieslice_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) + draw.pieslice(bbox, 10, 260, outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) -def test_pieslice_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_pieslice_width_fill.png" # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=5) # Assert assert_image_similar_tofile(im, expected, 1) -def test_pieslice_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=0) + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") @@ -545,7 +567,7 @@ def test_pieslice_no_spikes(): assert_image_equal(im, im_pre_erase) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_point(points): # Arrange im = Image.new("RGB", (W, H)) @@ -558,7 +580,7 @@ def test_point(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) @@ -572,7 +594,8 @@ def test_polygon(points): @pytest.mark.parametrize("mode", ("RGB", "L")) -def test_polygon_kite(mode): +@pytest.mark.parametrize("kite_points", KITE_POINTS) +def test_polygon_kite(mode, kite_points): # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -581,7 +604,7 @@ def test_polygon_kite(mode): expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + draw.polygon(kite_points, fill="blue", outline="yellow") # Assert assert_image_equal_tofile(im, expected) @@ -628,7 +651,7 @@ def test_polygon_translucent(): assert_image_equal_tofile(im, expected) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -655,63 +678,68 @@ def test_big_rectangle(): assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) -def test_rectangle_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width.png" # Act - draw.rectangle(BBOX1, outline="green", width=5) + draw.rectangle(bbox, outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width_fill.png" # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=5) + draw.rectangle(bbox, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=0) + draw.rectangle(bbox, fill="blue", outline="green", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") -def test_rectangle_I16(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_I16(bbox): # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="black", outline="green") + draw.rectangle(bbox, fill="black", outline="green") # Assert assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") -def test_rectangle_translucent_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_translucent_outline(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") # Act - draw.rectangle(BBOX1, fill="black", outline=(0, 255, 0, 127), width=5) + draw.rectangle(bbox, fill="black", outline=(0, 255, 0, 127), width=5) # Assert assert_image_equal_tofile( @@ -758,13 +786,14 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type): ) -def test_rounded_rectangle_zero_radius(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rounded_rectangle_zero_radius(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5) + draw.rounded_rectangle(bbox, 0, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") @@ -794,14 +823,15 @@ def test_rounded_rectangle_translucent(xy, suffix): ) -def test_floodfill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill(bbox): red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -826,13 +856,14 @@ def test_floodfill(): assert_image_equal(im, Image.new("RGB", (1, 1), red)) -def test_floodfill_border(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_border(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -847,13 +878,14 @@ def test_floodfill_border(): assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") -def test_floodfill_thresh(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_thresh(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="darkgreen", fill="green") + draw.rectangle(bbox, outline="darkgreen", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -1291,7 +1323,8 @@ def test_setting_default_font(): assert isinstance(draw.getfont(), ImageFont.ImageFont) -def test_same_color_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_same_color_outline(bbox): # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1307,12 +1340,12 @@ def test_same_color_outline(): for mode in ["RGB", "L"]: for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: for operation, args in { - "chord": [BBOX1, 0, 180], - "ellipse": [BBOX1], + "chord": [bbox, 0, 180], + "ellipse": [bbox], "shape": [s], - "pieslice": [BBOX1, -90, 45], + "pieslice": [bbox, -90, 45], "polygon": [[(18, 30), (85, 30), (60, 72)]], - "rectangle": [BBOX1], + "rectangle": [bbox], }.items(): # Arrange im = Image.new(mode, (W, H)) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 6fc829f1a..143341b0a 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -27,15 +27,16 @@ X1 = int(X0 * 3) Y0 = int(H / 4) Y1 = int(X0 * 3) -# Two kinds of bounding box -BBOX1 = [(X0, Y0), (X1, Y1)] -BBOX2 = [X0, Y0, X1, Y1] +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) -# Two kinds of coordinate sequences -POINTS1 = [(10, 10), (20, 40), (30, 30)] -POINTS2 = [10, 10, 20, 40, 30, 30] - -KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +# Coordinate sequences +POINTS = ( + ((10, 10), (20, 40), (30, 30)), + [(10, 10), (20, 40), (30, 30)], + (10, 10, 20, 40, 30, 30), + [10, 10, 20, 40, 30, 30], +) FONT_PATH = "Tests/fonts/FreeMono.ttf" @@ -52,7 +53,7 @@ def test_sanity(): draw.line(list(range(10)), pen) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_ellipse(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -80,7 +81,7 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -94,7 +95,8 @@ def test_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_line_pen_as_brush(): +@pytest.mark.parametrize("points", POINTS) +def test_line_pen_as_brush(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -103,13 +105,13 @@ def test_line_pen_as_brush(): # Act # Pass in the pen as the brush parameter - draw.line(POINTS1, pen, brush) + draw.line(points, pen, brush) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) @@ -124,7 +126,7 @@ def test_polygon(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) From 2203afeafa76519fcc01682d8c35e5c5d569d7c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 19:36:06 +1100 Subject: [PATCH 05/83] Do not set size unnecessarily if image failed to open --- src/PIL/EpsImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c88d22c7..2f7fee901 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -354,7 +354,6 @@ class EpsImageFile(ImageFile.ImageFile): check_required_header_comments() if not self._size: - self._size = 1, 1 # errors if this isn't set. why (1,1)? msg = "cannot determine EPS bounding box" raise OSError(msg) From ca2bf046d35ec41251716cac152fc9209964a359 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Apr 2023 09:57:16 +1000 Subject: [PATCH 06/83] Use "/sbin/ldconfig" if ldconfig is not found --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 07d6c66d6..f9670d4c0 100755 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ import os import re +import shutil import struct import subprocess import sys @@ -150,6 +151,7 @@ def _dbg(s, tp=None): def _find_library_dirs_ldconfig(): # Based on ctypes.util from Python 2 + ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" @@ -166,14 +168,14 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache - args = ["ldconfig", "-p"] + args = [ldconfig, "-p"] expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" elif sys.platform.startswith("freebsd"): - args = ["ldconfig", "-r"] + args = [ldconfig, "-r"] expr = r".* => (.*)" env = {} From dd15f15d08bf3fd32c41ef9f2502286778c9f993 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 09:06:20 +1000 Subject: [PATCH 07/83] Added further field sizes --- Tests/test_file_tiff.py | 9 ++++++++- src/PIL/TiffImagePlugin.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 97a02ac96..43181d1b3 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -96,10 +96,17 @@ class TestFileTiff: assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self): + def test_bigtiff(self, tmp_path): with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + # multistrip support not yet implemented + del im.tag_v2[273] + + outfile = str(tmp_path / "temp.tif") + im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) + def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910a..5b7d3f302 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1892,6 +1892,10 @@ class AppendingTiffWriter: 8, # srational 4, # float 8, # double + 4, # ifd + 2, # unicode + 4, # complex + 8, # long8 ] # StripOffsets = 273 From 099d696dc7d3c349265ae3cdbb5f949bca1e2866 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Sat, 15 Apr 2023 18:24:19 +0800 Subject: [PATCH 08/83] Fix ImageGrab with wl-paste --- Tests/test_imagegrab.py | 19 +++++++++++++++++++ src/PIL/ImageGrab.py | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index fa88065f4..e7c2c6c9f 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -98,3 +98,22 @@ $ms = new-object System.IO.MemoryStream(, $bytes) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, "Tests/images/hopper.png") + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ["wl-paste", "wl-copy"]) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize( + "image_path", ["Tests/images/hopper.gif", "Tests/images/hopper.png"] + ) + def test_grabclipboard_wl_clipboard(self, image_path): + with open(image_path, mode="rb") as raw_image: + try: + subprocess.call(["wl-copy"], stdin=raw_image) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) + except OSError as e: + pytest.skip(str(e)) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 982f77f20..175eb4671 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -135,6 +135,12 @@ def grabclipboard(): else: if shutil.which("wl-paste"): args = ["wl-paste"] + output = subprocess.check_output(["wl-paste", "-l"]).decode() + mime_types = output.splitlines() + for image_type in ["image/gif", "image/png"]: + if image_type in mime_types: + args.extend(["-t", image_type]) + break elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: From 6d12581385688c3af964e6707a1b4ed2651d31a5 Mon Sep 17 00:00:00 2001 From: Carl Weaver Date: Sun, 16 Apr 2023 15:37:38 +0800 Subject: [PATCH 09/83] Update src/PIL/ImageGrab.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 175eb4671..6550a7706 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -136,10 +136,11 @@ def grabclipboard(): if shutil.which("wl-paste"): args = ["wl-paste"] output = subprocess.check_output(["wl-paste", "-l"]).decode() - mime_types = output.splitlines() - for image_type in ["image/gif", "image/png"]: - if image_type in mime_types: - args.extend(["-t", image_type]) + clipboard_mimetypes = output.splitlines() + Image.preinit() + for mimetype in Image.MIME.values(): + if mimetype in clipboard_mimetypes: + args.extend(["-t", mimetype]) break elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] From 3d54b8e2b2419255a6b5a74dd0f2841ea4de7416 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Sun, 16 Apr 2023 15:41:14 +0800 Subject: [PATCH 10/83] Remove useless try catch block --- Tests/test_imagegrab.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index e7c2c6c9f..703472c4a 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -111,9 +111,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes) ) def test_grabclipboard_wl_clipboard(self, image_path): with open(image_path, mode="rb") as raw_image: - try: - subprocess.call(["wl-copy"], stdin=raw_image) - im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, image_path) - except OSError as e: - pytest.skip(str(e)) + subprocess.call(["wl-copy"], stdin=raw_image) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) From aa2e662995eaf67e2f9f53d6817a173c12b45a19 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Mon, 17 Apr 2023 16:44:43 +0800 Subject: [PATCH 11/83] Add sway and wl-clipboard dependencies to GitHub CI workflow --- .ci/install.sh | 3 ++- .github/workflows/test.yml | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 17c349ab1..d5cbd8248 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -22,7 +22,8 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ - cmake meson imagemagick libharfbuzz-dev libfribidi-dev + cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ + sway wl-clipboard fi python3 -m pip install --upgrade pip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fced6113b..53b7ee688 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,7 +84,15 @@ jobs: python3 -m pip install pytest-reverse fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh + export XDG_RUNTIME_DIR="/tmp/headless-sway" + export SWAYSOCK="$XDG_RUNTIME_DIR/sway.sock" + export WLR_BACKENDS=headless + export WLR_LIBINPUT_NO_DEVICES=1 + mkdir "$XDG_RUNTIME_DIR" + xvfb-run -s '-screen 0 1024x768x24'\ + sway -V -d -c /dev/null& + export WAYLAND_DISPLAY=wayland-1 + .ci/test.sh else .ci/test.sh fi From b7585b0597855f15ccf998d84684d90488a1133a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Apr 2023 10:27:36 +1000 Subject: [PATCH 12/83] Removed unnecessary settings --- .github/workflows/test.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53b7ee688..afb8fb56c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,13 +84,7 @@ jobs: python3 -m pip install pytest-reverse fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - export XDG_RUNTIME_DIR="/tmp/headless-sway" - export SWAYSOCK="$XDG_RUNTIME_DIR/sway.sock" - export WLR_BACKENDS=headless - export WLR_LIBINPUT_NO_DEVICES=1 - mkdir "$XDG_RUNTIME_DIR" - xvfb-run -s '-screen 0 1024x768x24'\ - sway -V -d -c /dev/null& + xvfb-run -s '-screen 0 1024x768x24' sway& export WAYLAND_DISPLAY=wayland-1 .ci/test.sh else From f15d7265f779c04d4e01b425f1e8b7211422a7dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Apr 2023 10:33:31 +1000 Subject: [PATCH 13/83] Call init() if mimetype is not found with preinit() --- Tests/test_imagegrab.py | 11 +++++------ src/PIL/ImageGrab.py | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 703472c4a..065c9c1b5 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -106,11 +106,10 @@ $ms = new-object System.IO.MemoryStream(, $bytes) ), reason="Linux with wl-clipboard only", ) - @pytest.mark.parametrize( - "image_path", ["Tests/images/hopper.gif", "Tests/images/hopper.png"] - ) - def test_grabclipboard_wl_clipboard(self, image_path): - with open(image_path, mode="rb") as raw_image: - subprocess.call(["wl-copy"], stdin=raw_image) + @pytest.mark.parametrize("ext", ("gif", "png", "ico")) + def test_grabclipboard_wl_clipboard(self, ext): + image_path = "Tests/images/hopper." + ext + with open(image_path, "rb") as fp: + subprocess.call(["wl-copy"], stdin=fp) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, image_path) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 6550a7706..55b50fb48 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -137,11 +137,19 @@ def grabclipboard(): args = ["wl-paste"] output = subprocess.check_output(["wl-paste", "-l"]).decode() clipboard_mimetypes = output.splitlines() + + def find_mimetype(): + for mime in Image.MIME.values(): + if mime in clipboard_mimetypes: + return mime + Image.preinit() - for mimetype in Image.MIME.values(): - if mimetype in clipboard_mimetypes: - args.extend(["-t", mimetype]) - break + mimetype = find_mimetype() + if not mimetype: + Image.init() + mimetype = find_mimetype() + if mimetype: + args.extend(["-t", mimetype]) elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: From d0b41da094c48077c3511988e98c168bd45f84dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 21:22:01 +1000 Subject: [PATCH 14/83] Support I mode for BuiltinFilter --- Tests/images/hopper_emboss_I.png | Bin 0 -> 13273 bytes Tests/images/hopper_emboss_more_I.png | Bin 0 -> 14624 bytes Tests/test_image_filter.py | 43 +++++---- src/libImaging/Filter.c | 133 ++++++++++++++++++-------- 4 files changed, 120 insertions(+), 56 deletions(-) create mode 100644 Tests/images/hopper_emboss_I.png create mode 100644 Tests/images/hopper_emboss_more_I.png diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png new file mode 100644 index 0000000000000000000000000000000000000000..f4dab388fe5c2dce3771133525df2c09b0fada45 GIT binary patch literal 13273 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 WC}ig2^aR-e0000= pow(2, 31) - 1) { + return pow(2, 31) - 1; + } + return (INT32)in; +} + Imaging ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { Imaging imOut; @@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[2]) + (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ + _i2f(in0[x + d]) * (kernel)[2]) int x = 0, y = 0; @@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 1; y < im->ysize - 1; y++) { - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 1; y < im->ysize - 1; y++) { + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - for (x = 1; x < im->xsize - 1; x++) { - float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip32(ss); + } + out[x] = in0[x]; + } + } else { + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip8(ss); + } + out[x] = in0[x]; } - out[x] = in0[x]; } } else { // Add one time for rounding @@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { void ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x5(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \ - _i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[3] + \ - _i2f((UINT8)in0[x + d + d]) * (kernel)[4]) + (_i2f(in0[x - d - d]) * (kernel)[0] + \ + _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \ + _i2f(in0[x + d]) * (kernel)[3] + \ + _i2f(in0[x + d + d]) * (kernel)[4]) int x = 0, y = 0; @@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 2; y < im->ysize - 2; y++) { - UINT8 *in_2 = (UINT8 *)im->image[y - 2]; - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *in2 = (UINT8 *)im->image[y + 2]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 2; y < im->ysize - 2; y++) { + INT32 *in_2 = (INT32 *)im->image[y - 2]; + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *in2 = (INT32 *)im->image[y + 2]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - out[1] = in0[1]; - for (x = 2; x < im->xsize - 2; x++) { - float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip32(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; + } + } else { + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; } } else { // Add one time for rounding @@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; ImagingSectionCookie cookie; - if (!im || im->type != IMAGING_TYPE_UINT8) { + if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { return (Imaging)ImagingError_ModeError(); } From ab3d0c071e60da904062d22f6b9a73e8fc6cdcb9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Apr 2023 21:03:27 +1000 Subject: [PATCH 15/83] Raise error from stderr of Linux grabclipboard command --- src/PIL/ImageGrab.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 982f77f20..2592ba2df 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -141,8 +141,11 @@ def grabclipboard(): msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) fh, filepath = tempfile.mkstemp() - subprocess.call(args, stdout=fh) + err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr os.close(fh) + if err: + msg = f"{args[0]} error: {err.strip().decode()}" + raise ChildProcessError(msg) im = Image.open(filepath) im.load() os.unlink(filepath) From ff003bfbcc9cfd7d281030f836616c0cdd59cfa6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Apr 2023 14:49:40 +1000 Subject: [PATCH 16/83] Added unpacker from I;16B to I;16 --- Tests/test_lib_pack.py | 1 + src/libImaging/Unpack.c | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index de3e7d156..f7812f62b 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -757,6 +757,7 @@ class TestLibUnpack: def test_I16(self): self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 7eeadf944..a0fa22c7d 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1149,6 +1149,16 @@ unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { } } static void +unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + out[0] = in[1]; + out[1] = in[0]; + in += 2; + out += 2; + } +} +static void unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) { int i; for (i = 0; i < pixels; i++) { @@ -1764,6 +1774,7 @@ static struct { {"I;16L", "I;16L", 16, copy2}, {"I;16N", "I;16N", 16, copy2}, + {"I;16", "I;16B", 16, unpackI16B_I16}, {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16B", "I;16N", 16, unpackI16N_I16B}, From db7326674e5261e19fa35f3a32efaf6dea48a5cc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 May 2023 13:03:31 +1000 Subject: [PATCH 17/83] Updated libimagequant to 4.2.0 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 362ad95a2..fd6000ee1 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.1.1 +archive=libimagequant-4.2.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index a254ec8c2..ad27b67ee 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -181,7 +181,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.1.1** + * Pillow has been tested with libimagequant **2.6-4.2** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 3fc446c2770b091b07d3b6cff63e87385c19e7fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 May 2023 22:54:18 +1000 Subject: [PATCH 18/83] Added width argument to regular_polygon --- Tests/images/imagedraw_triangle_width.png | Bin 0 -> 499 bytes Tests/test_imagedraw.py | 20 ++++++++++---------- docs/reference/ImageDraw.rst | 3 ++- src/PIL/ImageDraw.py | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 Tests/images/imagedraw_triangle_width.png diff --git a/Tests/images/imagedraw_triangle_width.png b/Tests/images/imagedraw_triangle_width.png new file mode 100644 index 0000000000000000000000000000000000000000..3d35326e73b92ffb24b9d64cc771662eee87000c GIT binary patch literal 499 zcmeAS@N?(olHy`uVBq!ia0vp^DImD;uumf=k2YHeTNK0SOcs7 z|DV6msp$lZm}+J5+nH&zb(QAFEMItk*2G_{>lmti1tOXb2(e64*uc`L%Aw$qU_*5&+_>uupeX5C~oixu{%*F`cV5M`^+^t6UwtW;ym>{J}i7cmHX4mfDJwq z^7*X=RE1MQ*4H&8pJJBW`?dD-_sjpAPcv~^--$oPB)K)#^@mrH!?$&-I!`hF^gpP5 zlEE@dmQkZ-LOj3W7puq3J-@zfU|z3hc0x)1)AJh&?jqZS^iDLr&gkVSN^#UqY|`1) zcdbJqeeX_1?T3v$R>iUgB4Q_QhKC;5dYoye(vPc086$r<|C3>U^2%Ifj}Y656|C1U zN--{ZwW?{|61kQzLoZ#AukA7wy5BZkXDqONb1i$vo=pi?8!lYG7o7YVvOvzStWc37?}*7u6{1-oD!M<@Fc=% literal 0 HcmV?d00001 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 7ffd7969d..406f44c06 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1347,20 +1347,20 @@ def test_same_color_outline(): @pytest.mark.parametrize( - "n_sides, rotation, polygon_name", - [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], + "n_sides, polygon_name, args", + [ + (4, "square", {}), + (8, "regular_octagon", {}), + (4, "square_rotate_45", {"rotation": 45}), + (3, "triangle_width", {"width": 5, "outline": "yellow"}), + ], ) -def test_draw_regular_polygon(n_sides, rotation, polygon_name): +def test_draw_regular_polygon(n_sides, polygon_name, args): im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) - filename_base = f"Tests/images/imagedraw_{polygon_name}" - filename = ( - f"{filename_base}.png" - if rotation == 0 - else f"{filename_base}_rotate_{rotation}.png" - ) + filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) bounding_circle = ((W // 2, H // 2), 25) - draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") + draw.regular_polygon(bounding_circle, n_sides, fill="red", **args) assert_image_equal_tofile(im, filename) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index aec7a3ef8..29115120c 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -296,7 +296,7 @@ Methods :param width: The line width, in pixels. -.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1) Draws a regular polygon inscribed in ``bounding_circle``, with ``n_sides``, and rotation of ``rotation`` degrees. @@ -311,6 +311,7 @@ Methods (e.g. ``rotation=90``, applies a 90 degree rotation). :param fill: Color to use for the fill. :param outline: Color to use for the outline. + :param width: The line width, in pixels. .. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e9ccf8041..1e4eeab25 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -279,11 +279,11 @@ class ImageDraw: self.im.paste(im.im, (0, 0) + im.size, mask.im) def regular_polygon( - self, bounding_circle, n_sides, rotation=0, fill=None, outline=None + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 ): """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - self.polygon(xy, fill, outline) + self.polygon(xy, fill, outline, width) def rectangle(self, xy, fill=None, outline=None, width=1): """Draw a rectangle.""" From a4986ba9866797648b602ce62fee393f43b522df Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 07:54:30 +1000 Subject: [PATCH 19/83] Support reading signed 8-bit TIFF images --- Tests/images/8bit.s.tif | Bin 0 -> 16518 bytes Tests/test_file_tiff.py | 6 ++++++ src/PIL/TiffImagePlugin.py | 2 ++ 3 files changed, 8 insertions(+) create mode 100644 Tests/images/8bit.s.tif diff --git a/Tests/images/8bit.s.tif b/Tests/images/8bit.s.tif new file mode 100644 index 0000000000000000000000000000000000000000..043cba6af8bbfe6d7386ac7ca247c5235c0dc387 GIT binary patch literal 16518 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 z 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 TR=N?=Kr4|!DMe+Lt#1AQlp!)* literal 0 HcmV?d00001 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 97a02ac96..30c6303a2 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -198,6 +198,12 @@ class TestFileTiff: with pytest.raises(OSError): im.save(outfile) + def test_8bit_s(self): + with Image.open("Tests/images/8bit.s.tif") as im: + im.load() + assert im.mode == "L" + assert im.getpixel((50, 50)) == 184 + def test_little_endian(self): with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910a..1ca1b6ea9 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -170,6 +170,8 @@ OPEN_INFO = { (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (II, 1, (1,), 1, (8,), ()): ("L", "L"), (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (2,), 1, (8,), ()): ("L", "L"), + (MM, 1, (2,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), From 9154a6b22d2915d4ca689054a239ceeed078ef2d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 08:01:48 +1000 Subject: [PATCH 20/83] Added release notes --- docs/releasenotes/10.0.0.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 3ee1a9973..1004ba57d 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -159,7 +159,8 @@ TODO Other Changes ============= -TODO -^^^^ +Support reading signed 8-bit TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +TIFF images with signed integer data, 8 bits per sample and a photometric +interpretaton of BlackIsZero can now be read. From 2467db492e7e50efaf39d445c92190c713e40f6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 08:15:48 +1000 Subject: [PATCH 21/83] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7b13900a8..93517e1cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added width argument to ImageDraw regular_polygon #7132 + [radarhere] + +- Support I mode for ImageFilter.BuiltinFilter #7108 + [radarhere] + +- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112 + [radarhere] + +- Added unpacker from I;16B to I;16 #7125 + [radarhere] + - Support float font sizes #7107 [radarhere] From 9f0c4164694ce99e14e7ec7fbfa8938acfb74823 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 15:30:17 +1000 Subject: [PATCH 22/83] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 93517e1cd..f8844daca 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Support reading signed 8-bit TIFF images #7111 + [radarhere] + - Added width argument to ImageDraw regular_polygon #7132 [radarhere] From 3ae321832a52cf185ae2c10a5b17f7ea1c6f2dbe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 15:37:35 +1000 Subject: [PATCH 23/83] Added release notes for #7132 --- docs/releasenotes/10.0.0.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 1004ba57d..e2005b710 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -135,10 +135,11 @@ TODO API Changes =========== -TODO -^^^^ +Added line width parameter to ImageDraw regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +An optional line ``width`` parameter has been added to +``ImageDraw.Draw.regular_polygon``. API Additions ============= From 5377b0735f44ad78184a1b0092a327e22203a02a Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Thu, 4 May 2023 21:43:57 +0530 Subject: [PATCH 24/83] add _repr_jpg_ for ipython display Signed-off-by: Ishant Mrinal Haloi --- Tests/test_file_jpeg.py | 13 +++++++++++++ src/PIL/Image.py | 23 ++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 73a00386f..3676c8f07 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -922,6 +922,19 @@ class TestFileJpeg: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False + def test_repr_jpg(self): + im = hopper() + + with Image.open(BytesIO(im._repr_jpg_())) as repr_jpg: + assert repr_jpg.format == "JPEG" + assert_image_equal(im, repr_jpg) + + def test_repr_jpg_error(self): + im = hopper("F") + + with pytest.raises(ValueError): + im._repr_jpg_() + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bee9e23d0..557810f6c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -633,19 +633,36 @@ class Image: ) ) - def _repr_png_(self): + def _repr_image(self, format): """iPython display hook support + :param format: Image format. :returns: png version of the image as bytes """ b = io.BytesIO() try: - self.save(b, "PNG") + self.save(b, format) except Exception as e: - msg = "Could not save to PNG for display" + msg = f"Could not save to {format} for display" raise ValueError(msg) from e return b.getvalue() + def _repr_png_(self): + """iPython display hook support for PNG format. + + :returns: png version of the image as bytes + """ + return self._repr_image("PNG") + + def _repr_jpg_(self): + """iPython display hook support for JPEG format. + + :returns: jpg version of the image as bytes + """ + return self._repr_image("JPEG") + + _repr_jpeg_ = _repr_jpg_ + @property def __array_interface__(self): # numpy array interface support From c5f90af56c7ca2b0e1ee3d3a95b5e7cd5291df5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 07:19:13 +1000 Subject: [PATCH 25/83] Updated xz to 5.4.3 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9b5fc5d18..05df77a68 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.2.tar.gz/download", - "filename": "xz-5.4.2.tar.gz", - "dir": "xz-5.4.2", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download", + "filename": "xz-5.4.3.tar.gz", + "dir": "xz-5.4.3", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), From 93e507294bb5f54ea8ac43a596fd0d2677e2cdad Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 08:19:43 +1000 Subject: [PATCH 26/83] Only assert image is similar --- Tests/test_file_jpeg.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3676c8f07..0247527f5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -922,18 +922,18 @@ class TestFileJpeg: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_repr_jpg(self): + def test_repr_jpeg(self): im = hopper() - with Image.open(BytesIO(im._repr_jpg_())) as repr_jpg: - assert repr_jpg.format == "JPEG" - assert_image_equal(im, repr_jpg) + with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: + assert repr_jpeg.format == "JPEG" + assert_image_similar(im, repr_jpeg, 17) - def test_repr_jpg_error(self): + def test_repr_jpeg_error(self): im = hopper("F") with pytest.raises(ValueError): - im._repr_jpg_() + im._repr_jpeg_() @pytest.mark.skipif(not is_win32(), reason="Windows only") From 04191d15f6fee33c50536991e734454195c2da8a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 17:54:42 +1000 Subject: [PATCH 27/83] Removed separate test for array tobytes() --- Tests/test_imagepath.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 7a517b6f6..5082f9a79 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -63,17 +63,6 @@ def test_path_constructors(coords): assert list(p) == [(0.0, 1.0)] -def test_path_constructor_text(): - # Arrange - arr = array.array("f", (0, 1)) - - # Act - p = ImagePath.Path(arr.tobytes()) - - # Assert - assert list(p) == [(0.0, 1.0)] - - @pytest.mark.parametrize( "coords", ( From 17fbafb10b6fbb7d364ff4e6474149c12bc03a42 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 18:12:10 +1000 Subject: [PATCH 28/83] Updated ImagePath tolist() default --- docs/reference/ImagePath.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 7c1a3ad70..500096ef7 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -48,7 +48,7 @@ vector data. Path objects can be passed to the methods on the Maps the path through a function. -.. py:method:: PIL.ImagePath.Path.tolist(flat=0) +.. py:method:: PIL.ImagePath.Path.tolist(flat=False) Converts the path to a Python list [(x, y), …]. From 38c40d81d2d0a97e208c7f3bcf468a81176f6288 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 18:25:05 +1000 Subject: [PATCH 29/83] Use boolean instead of integer --- Tests/test_imagepath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8f8a9f449..5c40d4756 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -28,7 +28,7 @@ def test_path(): (6.0, 7.0), (8.0, 9.0), ] - assert p.tolist(1) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + assert p.tolist(True) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] assert p.getbbox() == (0.0, 1.0, 8.0, 9.0) From 2d841e16c2d7b16f6fe0b156c79a13affd9ac630 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sat, 6 May 2023 10:31:58 +0530 Subject: [PATCH 30/83] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 557810f6c..3c0094817 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -637,7 +637,7 @@ class Image: """iPython display hook support :param format: Image format. - :returns: png version of the image as bytes + :returns: image as bytes, saved into the given format. """ b = io.BytesIO() try: From ccdce1791dab1e754df17d21a771c8ac073b7c58 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sat, 6 May 2023 10:35:28 +0530 Subject: [PATCH 31/83] rename format to image_format --- src/PIL/Image.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3c0094817..33984e594 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -633,17 +633,17 @@ class Image: ) ) - def _repr_image(self, format): + def _repr_image(self, image_format): """iPython display hook support - :param format: Image format. + :param image_format: Image format. :returns: image as bytes, saved into the given format. """ b = io.BytesIO() try: - self.save(b, format) + self.save(b, image_format) except Exception as e: - msg = f"Could not save to {format} for display" + msg = f"Could not save to {image_format} for display" raise ValueError(msg) from e return b.getvalue() From f67fcf131a53f7436a2f4a540ed251c927af2c05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 May 2023 11:58:05 +1000 Subject: [PATCH 32/83] If the clipboard fails to open on Windows, wait and try again --- src/display.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/display.c b/src/display.c index e8e7b62c2..754a6ae78 100644 --- a/src/display.c +++ b/src/display.c @@ -437,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; if (!OpenClipboard(NULL)) { - PyErr_SetString(PyExc_OSError, "failed to open clipboard"); - return NULL; + // Maybe the clipboard is temporarily in use by another process. + // Wait and try again + Sleep(500); + + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } } // find best format as set by clipboard owner From 2f896ee4ac1f86cd05659c6a3053d38ed0b15aff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 May 2023 21:16:34 +1000 Subject: [PATCH 33/83] Clarify that line() and polygon() include xy pixels --- docs/reference/ImageDraw.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 29115120c..524f821fb 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -243,6 +243,7 @@ Methods .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the ``xy`` list. + The coordinate pixels are included in the drawn line. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. @@ -287,7 +288,7 @@ Methods The polygon outline consists of straight lines between the given coordinates, plus a straight line between the last and the first - coordinate. + coordinate. The coordinate pixels are included in the drawn polygon. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. From bb18abc603a285b9c1c3705f03d7ee3955f935d7 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 8 May 2023 22:30:11 +0100 Subject: [PATCH 34/83] prefer screenshots using XCB over gnome-screenshot --- docs/reference/ImageGrab.rst | 5 +++-- src/PIL/ImageGrab.py | 23 +++++++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 3086ba8c3..6437307c0 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -15,8 +15,9 @@ or the clipboard to a PIL image memory. returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, the entire screen is copied. - On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it - is installed. To capture the default X11 display instead, pass ``xdisplay=""``. + On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return + a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is + installed. To disable this behaviour, pass ``xdisplay=""`` instead. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 2592ba2df..db993836d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -61,7 +61,17 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N left, top, right, bottom = bbox im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im - elif shutil.which("gnome-screenshot"): + try: + if not Image.core.HAVE_XCB: + msg = "Pillow was built without XCB support" + raise OSError(msg) + size, data = Image.core.grabscreen_x11(xdisplay) + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im + except OSError: + if xdisplay is None and shutil.which("gnome-screenshot"): fh, filepath = tempfile.mkstemp(".png") os.close(fh) subprocess.call(["gnome-screenshot", "-f", filepath]) @@ -73,15 +83,8 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N im.close() return im_cropped return im - # use xdisplay=None for default display on non-win32/macOS systems - if not Image.core.HAVE_XCB: - msg = "Pillow was built without XCB support" - raise OSError(msg) - size, data = Image.core.grabscreen_x11(xdisplay) - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im + else: + raise def grabclipboard(): From a0b691a219d274a561784781476cc952e79c1b8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 May 2023 12:12:16 +1000 Subject: [PATCH 35/83] Fixed combining single duration across duplicate PNG frames --- Tests/test_file_apng.py | 6 ++++++ src/PIL/PngImagePlugin.py | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f78c086eb..c62231cd4 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,6 +440,12 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 + # test removal of duplicated frames with a single duration + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 82a74b267..aaf242b1d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1146,11 +1146,14 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") ): - if isinstance(duration, (list, tuple)): - previous["encoderinfo"]["duration"] += encoderinfo["duration"] + previous["encoderinfo"]["duration"] += encoderinfo.get( + "duration", duration + ) continue else: bbox = None + if "duration" not in encoderinfo: + encoderinfo["duration"] = duration im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) # animation control @@ -1175,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data["encoderinfo"] - frame_duration = int(round(encoderinfo.get("duration", duration))) + frame_duration = int(round(encoderinfo["duration"])) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control From b9b685fc5699cbeb18f18096e53a1130d1cd6789 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 May 2023 12:35:59 +1000 Subject: [PATCH 36/83] Updated harfbuzz to 7.3.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9b5fc5d18..1e5a54f64 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -337,9 +337,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.2.0.zip", - "filename": "harfbuzz-7.2.0.zip", - "dir": "harfbuzz-7.2.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.3.0.zip", + "filename": "harfbuzz-7.3.0.zip", + "dir": "harfbuzz-7.3.0", "license": "COPYING", "build": [ *cmds_cmake( From c68c508e27935f31ffa357e7e3bc7763cbe461e5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 May 2023 13:25:35 +1000 Subject: [PATCH 37/83] Fixed joined corners for odd dimensions --- .../images/imagedraw_rounded_rectangle_x_odd.png | Bin 0 -> 565 bytes .../images/imagedraw_rounded_rectangle_y_odd.png | Bin 0 -> 527 bytes Tests/test_imagedraw.py | 2 ++ src/PIL/ImageDraw.py | 4 ++-- 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_x_odd.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_y_odd.png diff --git a/Tests/images/imagedraw_rounded_rectangle_x_odd.png b/Tests/images/imagedraw_rounded_rectangle_x_odd.png new file mode 100644 index 0000000000000000000000000000000000000000..f23f1945e1fec32362830cc6e872c08956fe7758 GIT binary patch literal 565 zcmeAS@N?(olHy`uVBq!ia0vp^DImiE{w+sGn_n7?5SDmN3P}2njzRbCo#{c2^^YHh}*S}w5dCT|M zE9R}USk2jA&%3xWWowR(cyO##q+A$Fa4b`#&^6JO{qORB7e5qTwD-`)Up}=9!fJO| zY>HUZcg1$uF22YK;R<2iS#wsda}|3i+FGjhGgMXbv*XuKnWT5Ms_^&yc>Az@ zA4^o_6StUTA+6Q{U6F?LqSp{H{qj(<0ZTvZx)z3Ao!v58r+w;qX>O8ZUel0MKZ z`qzKc(vOPHX4$`@a&I0tNU%*>yN%8LYyLH*?5$5_)&3M;tIrb2-hRV-?bT~}OD0-R zJS)WH&7reUam`oxi0F04#b&FrOcRK3O5kiHi@}@tc3)ZP)6>g>Z}FD>zMYf)GjX6^Gd-AAG-p+a`)s@1J==Xy$i$t|mNo$vAZ{+h@*&h+msciMi&SuU3; zteYmFzL~S|*?fWJo?L}~D+Saaa20ObB(S`UyHL(X5C|?#{>56pvSWg^N}?GsIWTy- L`njxgN@xNA*hcbx literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_y_odd.png b/Tests/images/imagedraw_rounded_rectangle_y_odd.png new file mode 100644 index 0000000000000000000000000000000000000000..96441bc7289eb3e70f23df0f9569a72ca407046d GIT binary patch literal 527 zcmeAS@N?(olHy`uVBq!ia0vp^DIm66s@eeHX4ax2z2 zHumxGnrmG<+MA`k>ge%ZwwI%FHl2v*Kea_{s*cXIrB|NMkUq8kp?mumu8z`qnE?xw zpFgyk{PmcXW@+KFt&>x1_wGuPox02L@~uFla{c->cZ2v(ZrL{b%=OoYw>Rl5oz@)s zv#zi@NmVP<;DC1H)>?8~Y_h7tw1x<#B+d>5Bjd=enzOk-pU;aiv$pylHUFfL zfVYE=qxH2T+jU>IZ8eA#@?T@wc6uXs@S4--s_&fG#Vopy=IX&tG`Z(a= x1 - x0 + full_x = d >= x1 - x0 - 1 if full_x: # The two left and two right corners are joined d = x1 - x0 - full_y = d >= y1 - y0 + full_y = d >= y1 - y0 - 1 if full_y: # The two top and two bottom corners are joined d = y1 - y0 From 3ec03c6720e4a4a0d4bd5a893ccac6b1df14418a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 10 May 2023 13:53:55 +1000 Subject: [PATCH 38/83] Only check for gnome-screenshot on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> --- src/PIL/ImageGrab.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index db993836d..361077110 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -71,7 +71,11 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N im = im.crop(bbox) return im except OSError: - if xdisplay is None and shutil.which("gnome-screenshot"): + if ( + xdisplay is None + and sys.platform not in ("darwin", "win32") + and shutil.which("gnome-screenshot") + ): fh, filepath = tempfile.mkstemp(".png") os.close(fh) subprocess.call(["gnome-screenshot", "-f", filepath]) From 8bbccba8250d4ec05a88fce3381f85c7dbb882e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 May 2023 08:13:33 +1000 Subject: [PATCH 39/83] Updated redirected URL --- docs/deprecations.rst | 2 +- docs/releasenotes/10.0.0.rst | 2 +- docs/releasenotes/9.2.0.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 45b2f4200..62687d869 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -210,7 +210,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. Image.coerce_e ~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index e2005b710..d71ca0fa6 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -117,7 +117,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. Image.coerce_e ^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 8d8bfc9f8..b875edf8e 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -15,7 +15,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed in Pillow 10 (2023-07-01). Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. FreeTypeFont.getmask2 fill parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 848fd7c2dbaf07bc8e0a24d8efe1400cc8140f99 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 May 2023 16:49:08 +1000 Subject: [PATCH 40/83] Added linkcheck_allowed_redirects --- docs/conf.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 2ebcd6b2e..a2c825292 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -317,6 +317,17 @@ def setup(app): app.add_css_file("css/dark.css") +linkcheck_allowed_redirects = { + r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501 + r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501 + r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501 + r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501 + r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", + r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501 + r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501 + r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501 +} + # sphinx.ext.extlinks # This config is a dictionary of external sites, # mapping unique short aliases to a base URL and a prefix. From 7e29efd518b0b5b2d3c5ed25446f12cf90f63338 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 May 2023 20:08:10 +1000 Subject: [PATCH 41/83] Do not catch OSError raised when loading image --- src/PIL/ImageGrab.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 361077110..a51294cb5 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -66,10 +66,6 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N msg = "Pillow was built without XCB support" raise OSError(msg) size, data = Image.core.grabscreen_x11(xdisplay) - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im except OSError: if ( xdisplay is None @@ -89,6 +85,11 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N return im else: raise + else: + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im def grabclipboard(): From 46708099b10a99b3d35d7eae624e83daa6d67bf9 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Fri, 12 May 2023 21:56:40 +0530 Subject: [PATCH 42/83] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 33984e594..21305d52a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -654,15 +654,13 @@ class Image: """ return self._repr_image("PNG") - def _repr_jpg_(self): + def _repr_jpeg_(self): """iPython display hook support for JPEG format. :returns: jpg version of the image as bytes """ return self._repr_image("JPEG") - _repr_jpeg_ = _repr_jpg_ - @property def __array_interface__(self): # numpy array interface support From e063ed772c0be2b878e97d55b69d59c573cd40fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 May 2023 11:02:53 +1000 Subject: [PATCH 43/83] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f8844daca..7dd99af99 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 + [radarhere] + - Support reading signed 8-bit TIFF images #7111 [radarhere] From 2db9c68571f3be1d29888b3497f4e2af518a2d36 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sat, 13 May 2023 07:32:02 +0530 Subject: [PATCH 44/83] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 21305d52a..3522ff6d0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -634,8 +634,7 @@ class Image: ) def _repr_image(self, image_format): - """iPython display hook support - + """Helper function for iPython display hook :param image_format: Image format. :returns: image as bytes, saved into the given format. """ @@ -657,7 +656,7 @@ class Image: def _repr_jpeg_(self): """iPython display hook support for JPEG format. - :returns: jpg version of the image as bytes + :returns: jpeg version of the image as bytes """ return self._repr_image("JPEG") From 59b7a48570cd9c7a2fae1e0876c5072bdd780338 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 May 2023 12:24:50 +1000 Subject: [PATCH 45/83] Updated docstrings --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3522ff6d0..105c83a8b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -634,7 +634,8 @@ class Image: ) def _repr_image(self, image_format): - """Helper function for iPython display hook + """Helper function for iPython display hook. + :param image_format: Image format. :returns: image as bytes, saved into the given format. """ @@ -1122,7 +1123,6 @@ class Image: Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` (default). :returns: A new image - """ self.load() From 6df8716025686fab55b7565df489e01f3a23e5b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 May 2023 21:38:01 +1000 Subject: [PATCH 46/83] Update grabclipboard() documentation after #6783 --- docs/reference/ImageGrab.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 3086ba8c3..10c580a74 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -39,9 +39,11 @@ or the clipboard to a PIL image memory. .. py:function:: grabclipboard() - Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported. + Take a snapshot of the clipboard image, if any. - .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS) + On Linux, ``wl-paste`` or ``xclip`` is required. + + .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) :return: On Windows, an image, a list of filenames, or None if the clipboard does not contain image data or filenames. @@ -49,3 +51,5 @@ or the clipboard to a PIL image memory. On Mac, an image, or None if the clipboard does not contain image data. + + On Linux, an image. From f3283837630e25f88f1d8f73961c898d88ab1aee Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sun, 14 May 2023 11:11:56 +0530 Subject: [PATCH 47/83] Apply suggestions from code review Co-authored-by: Hugo van Kemenade --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 105c83a8b..e0fb6a885 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -650,14 +650,14 @@ class Image: def _repr_png_(self): """iPython display hook support for PNG format. - :returns: png version of the image as bytes + :returns: PNG version of the image as bytes """ return self._repr_image("PNG") def _repr_jpeg_(self): """iPython display hook support for JPEG format. - :returns: jpeg version of the image as bytes + :returns: JPEG version of the image as bytes """ return self._repr_image("JPEG") From 9754c8d18dc8f013193d18810b9de4f2b68694ff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 May 2023 22:42:39 +1000 Subject: [PATCH 48/83] Added release notes --- docs/releasenotes/10.0.0.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index e2005b710..ececfa20d 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -160,6 +160,18 @@ TODO Other Changes ============= +Support display_jpeg() in IPython +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to ``display()`` and ``display_png``, ``display_jpeg()`` can now +also be used to display images in IPython:: + + from PIL import Image + from IPython.display import display_jpeg + + im = Image.new("RGB", (100, 100), (255, 0, 0)) + display_jpeg(im) + Support reading signed 8-bit TIFF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From ce22ad96b71dd3a64ed65d00f36640b81beafd31 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 May 2023 09:47:09 +1000 Subject: [PATCH 49/83] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7dd99af99..b3a6c45a4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Prefer screenshots using XCB over gnome-screenshot #7143 + [nulano, radarhere] + - Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 [radarhere] From 82e57b8a90bf0c062f6fb87b4bc3ed465e2b2c88 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 28 Apr 2023 12:53:17 +0300 Subject: [PATCH 50/83] Build only PDF in addition to default html --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index ec3300dd1..bda03d944 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,6 +1,6 @@ version: 2 -formats: all +formats: [pdf] build: os: ubuntu-22.04 From ac2d283065e38672a844730e0b11dd9835d588b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 May 2023 07:08:02 +1000 Subject: [PATCH 51/83] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b3a6c45a4..c67b9b432 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Use "/sbin/ldconfig" if ldconfig is not found #7068 + [radarhere] + - Prefer screenshots using XCB over gnome-screenshot #7143 [nulano, radarhere] From 53e73fd0941a8148e293245dfe9edf8412dacbaa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 May 2023 08:21:59 +1000 Subject: [PATCH 52/83] Updated fribidi to 1.0.13 --- winbuild/build_prepare.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1e5a54f64..19552f3c7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -352,12 +352,12 @@ deps = { "libs": [r"*.lib"], }, "fribidi": { - "url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip", - "filename": "fribidi-1.0.12.zip", - "dir": "fribidi-1.0.12", + "url": "https://github.com/fribidi/fribidi/archive/v1.0.13.zip", + "filename": "fribidi-1.0.13.zip", + "dir": "fribidi-1.0.13", "license": "COPYING", "build": [ - cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), + cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), *cmds_cmake("fribidi"), ], From 0e21e47768315f6af1afcbd88ee24a4ab68b5d36 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 May 2023 08:25:25 +1000 Subject: [PATCH 53/83] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c67b9b432..c79274d64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added `_repr_jpeg_` for IPython display_jpeg #7135 + [n3011, radarhere, nulano] + - Use "/sbin/ldconfig" if ldconfig is not found #7068 [radarhere] From 599979caae111c22bd15f0103320bf74eb53d963 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 May 2023 16:53:42 +1000 Subject: [PATCH 54/83] Update CHANGES.rst [ci skip] --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c79274d64..626b8b231 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- -- Added `_repr_jpeg_` for IPython display_jpeg #7135 +- Added _repr_jpeg_() for IPython display_jpeg #7135 [n3011, radarhere, nulano] - Use "/sbin/ldconfig" if ldconfig is not found #7068 From b39c807dde8909b1ce4afd85f37563165224e073 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 May 2023 22:14:40 +1000 Subject: [PATCH 55/83] Removed rectangle example from co-ordinate system documentation --- docs/handbook/concepts.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index e40ed4687..e0975a121 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -95,9 +95,8 @@ in the upper left corner. Note that the coordinates refer to the implied pixel corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5). Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles -are represented as 4-tuples, with the upper left corner given first. For -example, a rectangle covering all of an 800x600 pixel image is written as (0, -0, 800, 600). +are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given +first. Palette ------- From 546f6cbc27e178bfc742d8216a1af508b3e26e02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 17:11:43 +1000 Subject: [PATCH 56/83] Replaced absolute PIL import with relative import --- src/PIL/IcnsImagePlugin.py | 4 ++-- src/PIL/ImageCms.py | 6 +++--- src/PIL/ImageShow.py | 2 +- src/PIL/SpiderImagePlugin.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index c2f050edd..27cb89f73 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -22,11 +22,11 @@ import os import struct import sys -from PIL import Image, ImageFile, PngImagePlugin, features +from . import Image, ImageFile, PngImagePlugin, features enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: - from PIL import Jpeg2KImagePlugin + from . import Jpeg2KImagePlugin MAGIC = b"icns" HEADERSIZE = 8 diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 38cbab19c..3a337f9f2 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -18,10 +18,10 @@ import sys from enum import IntEnum -from PIL import Image +from . import Image try: - from PIL import _imagingcms + from . import _imagingcms except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. @@ -271,7 +271,7 @@ def get_display_profile(handle=None): if sys.platform != "win32": return None - from PIL import ImageWin + from . import ImageWin if isinstance(handle, ImageWin.HDC): profile = core.get_display_profile_win32(handle, 1) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 3f68a2696..8b1c3f8bb 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -17,7 +17,7 @@ import subprocess import sys from shlex import quote -from PIL import Image +from . import Image _viewers = [] diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index eac27e679..5614957c1 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -36,7 +36,7 @@ import os import struct import sys -from PIL import Image, ImageFile +from . import Image, ImageFile def isInt(f): @@ -191,7 +191,7 @@ class SpiderImageFile(ImageFile.ImageFile): # returns a ImageTk.PhotoImage object, after rescaling to 0..255 def tkPhotoImage(self): - from PIL import ImageTk + from . import ImageTk return ImageTk.PhotoImage(self.convert2byte(), palette=256) From dc6d0641b3c5e93b26b214a251754e549b84260a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 May 2023 19:39:25 +1000 Subject: [PATCH 57/83] Updated redirected URLs --- codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index f3afccc1c..b794632fa 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,9 @@ -# Documentation: https://docs.codecov.io/docs/codecov-yaml +# Documentation: https://docs.codecov.com/docs/codecov-yaml codecov: # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" # https://github.com/codecov/support/issues/363 - # https://docs.codecov.io/docs/comparing-commits + # https://docs.codecov.com/docs/comparing-commits allow_coverage_offsets: true comment: false From fffcb558f64f2350789b67ec5eb55681408a93d5 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Tue, 23 May 2023 18:44:25 +0800 Subject: [PATCH 58/83] Use image/png mime type for ImageGrab (wl-paste) if possible, otherwise the first mime type taken --- src/PIL/ImageGrab.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 3771e6a79..b7f416321 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -147,15 +147,12 @@ def grabclipboard(): clipboard_mimetypes = output.splitlines() def find_mimetype(): - for mime in Image.MIME.values(): - if mime in clipboard_mimetypes: - return mime + if "image/png" in clipboard_mimetypes: + return "image/png" + if clipboard_mimetypes: + return clipboard_mimetypes[0] - Image.preinit() mimetype = find_mimetype() - if not mimetype: - Image.init() - mimetype = find_mimetype() if mimetype: args.extend(["-t", mimetype]) elif shutil.which("xclip"): From bce0f0d5a64c008b9d9ffbea33e98a79ffdae8c3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 21:25:11 +1000 Subject: [PATCH 59/83] Moved function code inline --- src/PIL/ImageGrab.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index b7f416321..7f6d50af4 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -142,17 +142,16 @@ def grabclipboard(): return None else: if shutil.which("wl-paste"): - args = ["wl-paste"] output = subprocess.check_output(["wl-paste", "-l"]).decode() - clipboard_mimetypes = output.splitlines() + mimetypes = output.splitlines() + if "image/png" in mimetypes: + mimetype = "image/png" + elif mimetypes: + mimetype = mimetypes[0] + else: + mimetype = None - def find_mimetype(): - if "image/png" in clipboard_mimetypes: - return "image/png" - if clipboard_mimetypes: - return clipboard_mimetypes[0] - - mimetype = find_mimetype() + args = ["wl-paste"] if mimetype: args.extend(["-t", mimetype]) elif shutil.which("xclip"): From 26d5f4fcb1fa23c920f42c56e187de092da544a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 21:27:55 +1000 Subject: [PATCH 60/83] Use tuple instead of list --- Tests/test_imagegrab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 065c9c1b5..f8059eca4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -102,7 +102,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes) @pytest.mark.skipif( ( sys.platform != "linux" - or not all(shutil.which(cmd) for cmd in ["wl-paste", "wl-copy"]) + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) ), reason="Linux with wl-clipboard only", ) @@ -111,5 +111,5 @@ $ms = new-object System.IO.MemoryStream(, $bytes) image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) - im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, image_path) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) From b8719033ca91ef57f58128d32df675457431bbce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 22:53:16 +1000 Subject: [PATCH 61/83] Removed unused INT64 definition --- src/libImaging/ImPlatform.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index af9996ca9..94781f9ec 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -58,12 +58,6 @@ #error Cannot find required 32-bit integer type #endif -#if SIZEOF_LONG == 8 -#define INT64 long -#elif SIZEOF_LONG_LONG == 8 -#define INT64 long -#endif - #define INT8 signed char #define UINT8 unsigned char From 922e239cca2a45d239dd02f0a4b85b72a7918917 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 May 2023 08:55:14 +1000 Subject: [PATCH 62/83] Fixed saving multiple 1 mode images to GIF --- Tests/test_file_gif.py | 13 +++++++++++++ src/PIL/GifImagePlugin.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 8522f486a..0e50ee1ab 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -252,6 +252,19 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 +def test_roundtrip_save_all_1(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == 255 + + @pytest.mark.parametrize( "path, mode", ( diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index eadee1560..2f92e9467 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -879,7 +879,7 @@ def _get_palette_bytes(im): :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return im.palette.palette + return im.palette.palette if im.palette else b"" def _get_background(im, info_background): From 117618b01f959f016833158b7b128e896c6d38b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 May 2023 22:47:43 +1000 Subject: [PATCH 63/83] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 626b8b231..190751ad2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Improved wl-paste mimetype handling in ImageGrab #7094 + [rrcgat, radarhere] + - Added _repr_jpeg_() for IPython display_jpeg #7135 [n3011, radarhere, nulano] From e6d7f1f3477b915d4f2fb7d71d609af74e47a444 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 May 2023 19:52:13 +1000 Subject: [PATCH 64/83] Install setuptools on Windows --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index a00880111..076b80839 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -65,8 +65,8 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + - name: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install From 7a5ddc1712240b21d89581602acbb851c3897e4a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 May 2023 10:28:38 +1000 Subject: [PATCH 65/83] Do not test PyQt6 on Python 3.12 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index d5cbd8248..6e87d386d 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -42,7 +42,7 @@ if [[ $(uname) != CYGWIN* ]]; then if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 - if [[ $GHA_PYTHON_VERSION == 3.* ]]; then + if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi From 07eccd9798387a79db84557102d34de2f2f4c28d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 May 2023 19:14:56 +1000 Subject: [PATCH 66/83] Fixed calling putpalette() on L and LA images before load() --- Tests/test_image_putpalette.py | 8 ++++++++ src/libImaging/Unpack.c | 2 ++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 3b29769a7..665e08a7e 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -32,6 +32,14 @@ def test_putpalette(): with pytest.raises(ValueError): palette("YCbCr") + with Image.open("Tests/images/hopper_gray.jpg") as im: + assert im.mode == "L" + im.putpalette(list(range(256)) * 3) + + with Image.open("Tests/images/la.tga") as im: + assert im.mode == "LA" + im.putpalette(list(range(256)) * 3) + def test_imagepalette(): im = hopper("P") diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index a0fa22c7d..206403ba6 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1552,10 +1552,12 @@ static struct { {"P", "P;4L", 4, unpackP4L}, {"P", "P", 8, copy1}, {"P", "P;R", 8, unpackLR}, + {"P", "L", 8, copy1}, /* palette w. alpha */ {"PA", "PA", 16, unpackLA}, {"PA", "PA;L", 16, unpackLAL}, + {"PA", "LA", 16, unpackLA}, /* true colour */ {"RGB", "RGB", 24, ImagingUnpackRGB}, From c45019fe0ccbf54c925aba914329371a7f188a48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 May 2023 12:28:03 +1000 Subject: [PATCH 67/83] Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 --- src/_imagingft.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index 78e3f7f10..80f862bb7 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -132,6 +132,27 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { return NULL; } +#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11 + PyConfig config; + PyConfig_InitPythonConfig(&config); + if (!PyArg_ParseTupleAndKeywords( + args, + kw, + "etf|nsy#n", + kwlist, + config.filesystem_encoding, + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size, + &layout_engine)) { + PyConfig_Clear(&config); + return NULL; + } + PyConfig_Clear(&config); +#else if (!PyArg_ParseTupleAndKeywords( args, kw, @@ -147,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &layout_engine)) { return NULL; } +#endif self = PyObject_New(FontObject, &Font_Type); if (!self) { From e01a0195dd9f54b8174f322d47d4f618f0cf6c50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Jun 2023 22:53:07 +1000 Subject: [PATCH 68/83] Removed duplicate config --- .editorconfig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index 449530717..d74549fe2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,10 +13,6 @@ indent_style = space trim_trailing_whitespace = true -[*.rst] -# Four-space indentation -indent_size = 4 - [*.yml] # Two-space indentation indent_size = 2 From ea3e4242d8fd8bb5cfc9e528f863ce16e20b529f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jun 2023 08:07:05 +1000 Subject: [PATCH 69/83] Removed files and types override --- .pre-commit-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4882a317f..0ddc6beb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,9 +4,6 @@ repos: hooks: - id: black args: [--target-version=py38] - # Only .py files, until https://github.com/psf/black/issues/402 resolved - files: \.py$ - types: [] - repo: https://github.com/PyCQA/isort rev: 5.12.0 From 3693b84ba0b44f71119cec73c8517ec32d1774b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jun 2023 09:21:47 +1000 Subject: [PATCH 70/83] Lint fixes --- docs/Guardfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Guardfile b/docs/Guardfile index b689b079a..6cbf07b06 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -2,7 +2,7 @@ from livereload.compiler import shell from livereload.task import Task -Task.add('*.rst', shell('make html')) -Task.add('*/*.rst', shell('make html')) -Task.add('Makefile', shell('make html')) -Task.add('conf.py', shell('make html')) +Task.add("*.rst", shell("make html")) +Task.add("*/*.rst", shell("make html")) +Task.add("Makefile", shell("make html")) +Task.add("conf.py", shell("make html")) From 97bd53392ce136617ead36c11d50def9d32ab3e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Jun 2023 18:36:41 +1000 Subject: [PATCH 71/83] Do not use temporary file when grabbing clipboard on Linux --- src/PIL/ImageGrab.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 7f6d50af4..39ecdf420 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,6 +15,7 @@ # See the README file for information on usage and redistribution. # +import io import os import shutil import subprocess @@ -128,8 +129,6 @@ def grabclipboard(): files = data[o:].decode("mbcs").split("\0") return files[: files.index("")] if isinstance(data, bytes): - import io - data = io.BytesIO(data) if fmt == "png": from . import PngImagePlugin @@ -159,13 +158,12 @@ def grabclipboard(): else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) - fh, filepath = tempfile.mkstemp() - err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr - os.close(fh) + p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + err = p.stderr if err: msg = f"{args[0]} error: {err.strip().decode()}" raise ChildProcessError(msg) - im = Image.open(filepath) + data = io.BytesIO(p.stdout) + im = Image.open(data) im.load() - os.unlink(filepath) return im From 3b65261c966648e5d4f87cd49bb12cba5345547d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 17:54:55 +1000 Subject: [PATCH 72/83] Remove temporary file when error is raised --- src/PIL/EpsImagePlugin.py | 7 +++++++ src/PIL/JpegImagePlugin.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c88d22c7..bdac874c4 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -134,6 +134,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): if gs_windows_binary is not None: if not gs_windows_binary: + try: + os.unlink(outfile) + if infile_temp: + os.unlink(infile_temp) + except OSError: + pass + msg = "Unable to locate Ghostscript on paths" raise OSError(msg) command[0] = gs_windows_binary diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 5dd1a61af..dfc7e6e9f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -457,6 +457,11 @@ class JpegImageFile(ImageFile.ImageFile): if os.path.exists(self.filename): subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: + try: + os.unlink(path) + except OSError: + pass + msg = "Invalid Filename" raise ValueError(msg) From 97df237dc81c930d983b4025b7b3a97d043dfd7c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 18:04:39 +1000 Subject: [PATCH 73/83] Moved test into separate function --- Tests/test_file_apng.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index c62231cd4..a22ac581d 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,12 +440,6 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 - # test removal of duplicated frames with a single duration - frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) - with Image.open(test_file) as im: - assert im.n_frames == 1 - assert im.info.get("duration") == 1500 - # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) @@ -453,6 +447,17 @@ def test_apng_save_duration_loop(tmp_path): assert im.info.get("duration") == 750 +def test_apng_save_duplicate_duration(tmp_path): + test_file = str(tmp_path / "temp.png") + frame = Image.new("RGB", (1, 1)) + + # Test a single duration is correctly combined across duplicate frames + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + + def test_apng_save_disposal(tmp_path): test_file = str(tmp_path / "temp.png") size = (128, 64) From 7c533276f28518ec2825e1cae3f0df427b5c565b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 19:53:50 +1000 Subject: [PATCH 74/83] Update CHANGES.rst [ci skip] --- CHANGES.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 190751ad2..c51f8fb94 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,27 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed combining single duration across duplicate APNG frames #7146 + [radarhere] + +- Remove temporary file when error is raised #7148 + [radarhere] + +- Do not use temporary file when grabbing clipboard on Linux #7200 + [radarhere] + +- If the clipboard fails to open on Windows, wait and try again #7141 + [radarhere] + +- Fixed saving multiple 1 mode frames to GIF #7181 + [radarhere] + +- Replaced absolute PIL import with relative import #7173 + [radarhere] + +- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192 + [radarhere] + - Improved wl-paste mimetype handling in ImageGrab #7094 [rrcgat, radarhere] From 15edb6d625f94e0f7e9047ab76ed08762ab2f53a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Jun 2023 22:33:55 +1000 Subject: [PATCH 75/83] Fixed signedness comparison warning --- src/libImaging/Jpeg2KEncode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 0d7e896b7..de8586706 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -464,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { } if (!context->num_resolutions) { - while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) { + while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) { params.numresolution -= 1; } } From da6b2ec28506a132fad9674e1badb1624aed7a8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Jun 2023 10:47:20 +1000 Subject: [PATCH 76/83] Document order of kernel weights --- src/PIL/ImageFilter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 63d6dcf5c..33bc7cc2e 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -35,7 +35,7 @@ class BuiltinFilter(MultibandFilter): class Kernel(BuiltinFilter): """ - Create a convolution kernel. The current version only + Create a convolution kernel. The current version only supports 3x3 and 5x5 integer and floating point kernels. In the current version, kernels can only be applied to @@ -43,9 +43,10 @@ class Kernel(BuiltinFilter): :param size: Kernel size, given as (width, height). In the current version, this must be (3,3) or (5,5). - :param kernel: A sequence containing kernel weights. + :param kernel: A sequence containing kernel weights. The kernel will + be flipped vertically before being applied to the image. :param scale: Scale factor. If given, the result for each pixel is - divided by this value. The default is the sum of the + divided by this value. The default is the sum of the kernel weights. :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. From 748a4d0fcd517e0e6e86ae15f4be9b0bcf65747d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 14:26:28 +1000 Subject: [PATCH 77/83] Removed unused variable --- src/libImaging/Storage.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7cf00ef35..128595f65 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -37,8 +37,6 @@ #include "Imaging.h" #include -int ImagingNewCount = 0; - /* -------------------------------------------------------------------- * Standard image object. */ From aeb6e9909e94d1ad6c86ebf04a6db6cd77e016a3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 15:57:05 +1000 Subject: [PATCH 78/83] Removed unused argument --- src/PIL/Image.py | 2 +- src/_imaging.c | 5 ++--- src/libImaging/Filter.c | 2 +- src/libImaging/Imaging.h | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e0fb6a885..fa70f674b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1254,7 +1254,7 @@ class Image: if ymargin is None: ymargin = xmargin self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) + return self._new(self.im.expand(xmargin, ymargin)) def filter(self, filter): """ diff --git a/src/_imaging.c b/src/_imaging.c index 281f3a4d2..5c6380fee 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1027,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) { static PyObject * _expand_image(ImagingObject *self, PyObject *args) { int x, y; - int mode = 0; - if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) { + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return NULL; } - return PyImagingNew(ImagingExpand(self->image, x, y, mode)); + return PyImagingNew(ImagingExpand(self->image, x, y)); } static PyObject * diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 4b8d2bf05..4dcd368ca 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -49,7 +49,7 @@ clip32(float in) { } Imaging -ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { +ImagingExpand(Imaging imIn, int xmargin, int ymargin) { Imaging imOut; int x, y; ImagingSectionCookie cookie; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index d9ded1852..beec8a8f2 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -290,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); extern Imaging -ImagingExpand(Imaging im, int x, int y, int mode); +ImagingExpand(Imaging im, int x, int y); extern Imaging ImagingFill(Imaging im, const void *ink); extern int From 389ad11693deb5ea39b8edf0eb47263582a3f5f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 17:10:42 +1000 Subject: [PATCH 79/83] Only call text_layout once in getmask2 --- src/PIL/ImageFont.py | 34 +++---- src/_imagingft.c | 215 ++++++++++++++++++++++++++----------------- 2 files changed, 144 insertions(+), 105 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea4549cf5..7b4ca5814 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,7 +26,6 @@ # import base64 -import math import os import sys import warnings @@ -551,28 +550,23 @@ class FreeTypeFont: :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - size, offset = self.font.getsize( - text, mode, direction, features, language, anchor - ) if start is None: start = (0, 0) - size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) - offset = offset[0] - stroke_width, offset[1] - stroke_width + im, size, offset = self.font.render( + text, + Image.core.fill, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start[0], + start[1], + Image.MAX_IMAGE_PIXELS, + ) Image._decompression_bomb_check(size) - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0) - if min(size): - self.font.render( - text, - im.id, - mode, - direction, - features, - language, - stroke_width, - ink, - start[0], - start[1], - ) return im, offset def font_variant( diff --git a/src/_imagingft.c b/src/_imagingft.c index 80f862bb7..95f12eb5a 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -551,73 +551,25 @@ font_getlength(FontObject *self, PyObject *args) { return PyLong_FromLong(length); } -static PyObject * -font_getsize(FontObject *self, PyObject *args) { +static int +bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) { int position; /* pen position along primary axis, in 26.6 precision */ int advanced; /* pen position along primary axis, in pixels */ int px, py; /* position of current glyph, in pixels */ int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ - int load_flags; /* FreeType load_flags parameter */ int error; - FT_Face face; FT_Glyph glyph; - FT_BBox bbox; /* glyph bounding box */ - GlyphInfo *glyph_info = NULL; /* computed text layout */ - size_t i, count; /* glyph_info index and length */ - int horizontal_dir; /* is primary axis horizontal? */ - int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ - int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; - const char *dir = NULL; - const char *lang = NULL; - const char *anchor = NULL; - PyObject *features = Py_None; - PyObject *string; - - /* calculate size and bearing for a given string */ - - if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { - return NULL; - } - - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; - - if (anchor == NULL) { - anchor = horizontal_dir ? "la" : "lt"; - } - if (strlen(anchor) != 2) { - goto bad_anchor; - } - - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); - if (PyErr_Occurred()) { - return NULL; - } - - load_flags = FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - + FT_BBox bbox; /* glyph bounding box */ + size_t i; /* glyph_info index */ /* * text bounds are given by: * - bounding boxes of individual glyphs * - pen line, i.e. 0 to `advanced` along primary axis * this means point (0, 0) is part of the text bounding box */ - face = NULL; position = x_min = x_max = y_min = y_max = 0; for (i = 0; i < count; i++) { - face = self->face; - if (horizontal_dir) { px = PIXEL(position + glyph_info[i].x_offset); py = PIXEL(glyph_info[i].y_offset); @@ -640,12 +592,14 @@ font_getsize(FontObject *self, PyObject *args) { error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + return 1; } error = FT_Get_Glyph(face->glyph, &glyph); if (error) { - return geterror(error); + geterror(error); + return 1; } FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); @@ -669,13 +623,15 @@ font_getsize(FontObject *self, PyObject *args) { FT_Done_Glyph(glyph); } - if (glyph_info) { - PyMem_Free(glyph_info); - glyph_info = NULL; + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; } x_anchor = y_anchor = 0; - if (face) { + if (count) { if (horizontal_dir) { switch (anchor[0]) { case 'l': // left @@ -693,15 +649,15 @@ font_getsize(FontObject *self, PyObject *args) { } switch (anchor[1]) { case 'a': // ascender - y_anchor = PIXEL(self->face->size->metrics.ascender); + y_anchor = PIXEL(face->size->metrics.ascender); break; case 't': // top y_anchor = y_max; break; case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( - (self->face->size->metrics.ascender + - self->face->size->metrics.descender) / + (face->size->metrics.ascender + + face->size->metrics.descender) / 2); break; case 's': // horizontal baseline @@ -711,7 +667,7 @@ font_getsize(FontObject *self, PyObject *args) { y_anchor = y_min; break; case 'd': // descender - y_anchor = PIXEL(self->face->size->metrics.descender); + y_anchor = PIXEL(face->size->metrics.descender); break; default: goto bad_anchor; @@ -751,17 +707,74 @@ font_getsize(FontObject *self, PyObject *args) { } } } - - return Py_BuildValue( - "(ii)(ii)", - (x_max - x_min), - (y_max - y_min), - (-x_anchor + x_min), - -(-y_anchor + y_max)); + *width = x_max - x_min; + *height = y_max - y_min; + *x_offset = -x_anchor + x_min; + *y_offset = -(-y_anchor + y_max); + return 0; bad_anchor: PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); - return NULL; + return 1; +} + +static PyObject * +font_getsize(FontObject *self, PyObject *args) { + int width, height, x_offset, y_offset; + int load_flags; /* FreeType load_flags parameter */ + int error; + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t count; /* glyph_info length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple( + args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + return NULL; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + if (error) { + return NULL; + } + + return Py_BuildValue( + "(ii)(ii)", + width, + height, + x_offset, + y_offset); } static PyObject * @@ -785,6 +798,7 @@ font_render(FontObject *self, PyObject *args) { unsigned int bitmap_y; /* glyph bitmap y index */ unsigned char *source; /* glyph bitmap source buffer */ unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ + PyObject *image; Imaging im; Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ @@ -795,27 +809,34 @@ font_render(FontObject *self, PyObject *args) { const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; + const char *anchor = NULL; PyObject *features = Py_None; PyObject *string; + PyObject *fill; float x_start = 0; float y_start = 0; + int width, height, x_offset, y_offset; + int horizontal_dir; /* is primary axis horizontal? */ + PyObject *max_image_pixels = Py_None; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ if (!PyArg_ParseTuple( args, - "On|zzOziLff:render", + "OO|zzOzizLffO:render", &string, - &id, + &fill, &mode, &dir, &features, &lang, &stroke_width, + &anchor, &foreground_ink_long, &x_start, - &y_start)) { + &y_start, + &max_image_pixels)) { return NULL; } @@ -841,8 +862,41 @@ font_render(FontObject *self, PyObject *args) { if (PyErr_Occurred()) { return NULL; } - if (count == 0) { - Py_RETURN_NONE; + + load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (error) { + PyMem_Del(glyph_info); + return NULL; + } + + width += stroke_width * 2 + ceil(x_start); + height += stroke_width * 2 + ceil(y_start); + if (max_image_pixels != Py_None) { + if (width * height > PyLong_AsLong(max_image_pixels) * 2) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); + } + } + + image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height); + id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); + im = (Imaging)id; + + x_offset -= stroke_width; + y_offset -= stroke_width; + if (count == 0 || width == 0 || height == 0) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); } if (stroke_width) { @@ -859,15 +913,6 @@ font_render(FontObject *self, PyObject *args) { 0); } - im = (Imaging)id; - load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - /* * calculate x_min and y_max * must match font_getsize or there may be clipping! @@ -1064,7 +1109,7 @@ font_render(FontObject *self, PyObject *args) { } FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - Py_RETURN_NONE; + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); glyph_error: if (stroker != NULL) { From 4dcca33d3099e29110b24cc507e8f0799e1d1ab7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 19:06:25 +1000 Subject: [PATCH 80/83] Removed unused arguments --- src/_imagingft.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 80f862bb7..8fc1fa7d0 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -254,9 +254,7 @@ text_layout_raqm( const char *dir, PyObject *features, const char *lang, - GlyphInfo **glyph_info, - int mask, - int color) { + GlyphInfo **glyph_info) { size_t i = 0, count = 0, start = 0; raqm_t *rq; raqm_glyph_t *glyphs = NULL; @@ -493,7 +491,7 @@ text_layout( #ifdef HAVE_RAQM if (have_raqm && self->layout_engine == LAYOUT_RAQM) { count = text_layout_raqm( - string, self, dir, features, lang, glyph_info, mask, color); + string, self, dir, features, lang, glyph_info); } else #endif { From 16d82c2dfd473836e7903165917584bf938b0345 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 19:37:54 +1000 Subject: [PATCH 81/83] Improved coverage --- Tests/test_imagefont.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 7ea485a55..4a40d1d1d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -463,6 +463,11 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") +@pytest.mark.parametrize("mode", (None, "1", "RGBA")) +def test_getbbox(font, mode): + assert (0, 4, 12, 16) == font.getbbox("A", mode) + + def test_getbbox_empty(font): # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") From 1756df461561234184611dfe8b42f1b7f33de1f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 20:24:34 +1000 Subject: [PATCH 82/83] Removed unused private method --- src/PIL/ImageFont.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea4549cf5..abcb88520 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -226,10 +226,6 @@ class FreeTypeFont: path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def _multiline_split(self, text): - split_character = "\n" if isinstance(text, str) else b"\n" - return text.split(split_character) - def getname(self): """ :return: A tuple of the font family (e.g. Helvetica) and the font style From 5a0fb8ec127b7b23d13d22d7a8ade5505835435f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 11 Jun 2023 00:05:47 +0300 Subject: [PATCH 83/83] Add Debian 12 Bookworm --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 4f01abe44..3bcb8cfbc 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,6 +39,7 @@ jobs: centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-x86, + debian-12-bookworm-x86, fedora-37-amd64, fedora-38-amd64, gentoo, diff --git a/docs/installation.rst b/docs/installation.rst index ad27b67ee..ac54b037d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -448,6 +448,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ +| Debian 12 Bookworm | 3.11 | x86 | ++----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 38 | 3.11 | x86-64 |