Merge branch 'main' into fix-imagegrab-with-wl-paste

This commit is contained in:
Andrew Murray 2023-05-23 08:47:42 +10:00
commit c656583b84
47 changed files with 569 additions and 299 deletions

View File

@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Cygwin - name: Install Cygwin
uses: cygwin/cygwin-install-action@v3 uses: cygwin/cygwin-install-action@v4
with: with:
platform: x86_64 platform: x86_64
packages: > packages: >

View File

@ -39,10 +39,9 @@ jobs:
centos-stream-8-amd64, centos-stream-8-amd64,
centos-stream-9-amd64, centos-stream-9-amd64,
debian-11-bullseye-x86, debian-11-bullseye-x86,
fedora-36-amd64,
fedora-37-amd64, fedora-37-amd64,
fedora-38-amd64,
gentoo, gentoo,
ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,
] ]

View File

@ -80,7 +80,7 @@ jobs:
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd
- name: Build Pillow - name: Build Pillow
run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" .
- name: Test Pillow - name: Test Pillow
run: | run: |

View File

@ -57,7 +57,7 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/tox-ini-fmt - repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.0.0 rev: 1.3.0
hooks: hooks:
- id: tox-ini-fmt - id: tox-ini-fmt

View File

@ -1,5 +1,7 @@
version: 2 version: 2
formats: [pdf]
build: build:
os: ubuntu-22.04 os: ubuntu-22.04
tools: tools:

View File

@ -5,6 +5,42 @@ Changelog (Pillow)
10.0.0 (unreleased) 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]
- Prefer screenshots using XCB over gnome-screenshot #7143
[nulano, radarhere]
- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151
[radarhere]
- Support reading signed 8-bit TIFF images #7111
[radarhere]
- 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]
- Use later value for duplicate xref entries in PdfParser #7102
[radarhere]
- Load before getting size in __getstate__ #7105
[bigcat88, radarhere]
- Fixed type handling for include and lib directories #7069 - Fixed type handling for include and lib directories #7069
[adisbladis, radarhere] [adisbladis, radarhere]

View File

@ -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") 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): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables) im.save(test_output, "JPEG", qtables=qtables)

BIN
Tests/images/8bit.s.tif Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

View File

@ -922,6 +922,19 @@ class TestFileJpeg:
im.load() im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_repr_jpeg(self):
im = hopper()
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_jpeg_error(self):
im = hopper("F")
with pytest.raises(ValueError):
im._repr_jpeg_()
@pytest.mark.skipif(not is_win32(), reason="Windows only") @pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg") @skip_unless_feature("jpg")

View File

@ -198,6 +198,12 @@ class TestFileTiff:
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(outfile) 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): def test_little_endian(self):
with Image.open("Tests/images/16bit.cropped.tif") as im: with Image.open("Tests/images/16bit.cropped.tif") as im:
assert im.getpixel((0, 0)) == 480 assert im.getpixel((0, 0)) == 480

View File

@ -4,7 +4,7 @@ import pytest
from PIL import Image from PIL import Image
from .helper import hopper from .helper import hopper, skip_unless_feature
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
@ -42,3 +42,10 @@ def test_copy_zero():
out = im.copy() out = im.copy()
assert out.mode == im.mode assert out.mode == im.mode
assert out.size == im.size assert out.size == im.size
@skip_unless_feature("libtiff")
def test_deepcopy():
with Image.open("Tests/images/g4_orientation_5.tif") as im:
out = copy.deepcopy(im)
assert out.size == (590, 88)

View File

@ -30,15 +30,16 @@ from .helper import assert_image_equal, hopper
ImageFilter.UnsharpMask(10), ImageFilter.UnsharpMask(10),
), ),
) )
@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity(filter_to_apply, mode): def test_sanity(filter_to_apply, mode):
im = hopper(mode) im = hopper(mode)
out = im.filter(filter_to_apply) if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
assert out.mode == im.mode out = im.filter(filter_to_apply)
assert out.size == im.size assert out.mode == im.mode
assert out.size == im.size
@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity_error(mode): def test_sanity_error(mode):
with pytest.raises(TypeError): with pytest.raises(TypeError):
im = hopper(mode) im = hopper(mode)
@ -130,10 +131,12 @@ def test_kernel_not_enough_coefficients():
ImageFilter.Kernel((3, 3), (0, 0)) ImageFilter.Kernel((3, 3), (0, 0))
@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_3x3(mode): def test_consistency_3x3(mode):
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss.bmp") as reference: reference_name = "hopper_emboss"
reference_name += "_I.png" if mode == "I" else ".bmp"
with Image.open("Tests/images/" + reference_name) as reference:
kernel = ImageFilter.Kernel( kernel = ImageFilter.Kernel(
(3, 3), (3, 3),
# fmt: off # fmt: off
@ -146,16 +149,20 @@ def test_consistency_3x3(mode):
source = source.split() * 2 source = source.split() * 2
reference = reference.split() * 2 reference = reference.split() * 2
assert_image_equal( if mode == "I":
Image.merge(mode, source[: len(mode)]).filter(kernel), source = source[0].convert(mode)
Image.merge(mode, reference[: len(mode)]), else:
) source = Image.merge(mode, source[: len(mode)])
reference = Image.merge(mode, reference[: len(mode)])
assert_image_equal(source.filter(kernel), reference)
@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_5x5(mode): def test_consistency_5x5(mode):
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: reference_name = "hopper_emboss_more"
reference_name += "_I.png" if mode == "I" else ".bmp"
with Image.open("Tests/images/" + reference_name) as reference:
kernel = ImageFilter.Kernel( kernel = ImageFilter.Kernel(
(5, 5), (5, 5),
# fmt: off # fmt: off
@ -170,10 +177,12 @@ def test_consistency_5x5(mode):
source = source.split() * 2 source = source.split() * 2
reference = reference.split() * 2 reference = reference.split() * 2
assert_image_equal( if mode == "I":
Image.merge(mode, source[: len(mode)]).filter(kernel), source = source[0].convert(mode)
Image.merge(mode, reference[: len(mode)]), else:
) source = Image.merge(mode, source[: len(mode)])
reference = Image.merge(mode, reference[: len(mode)])
assert_image_equal(source.filter(kernel), reference)
def test_invalid_box_blur_filter(): def test_invalid_box_blur_filter():

View File

@ -27,15 +27,21 @@ X1 = int(X0 * 3)
Y0 = int(H / 4) Y0 = int(H / 4)
Y1 = int(X0 * 3) Y1 = int(X0 * 3)
# Two kinds of bounding box # Bounding boxes
BBOX1 = [(X0, Y0), (X1, Y1)] BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1])
BBOX2 = [X0, Y0, X1, Y1]
# Two kinds of coordinate sequences # Coordinate sequences
POINTS1 = [(10, 10), (20, 40), (30, 30)] POINTS = (
POINTS2 = [10, 10, 20, 40, 30, 30] ((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(): def test_sanity():
@ -63,7 +69,7 @@ def test_mode_mismatch():
ImageDraw.ImageDraw(im, mode="L") 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))) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
def test_arc(bbox, start, end): def test_arc(bbox, start, end):
# Arrange # Arrange
@ -77,7 +83,8 @@ def test_arc(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -85,13 +92,14 @@ def test_arc_end_le_start():
end = 0 end = 0
# Act # Act
draw.arc(BBOX1, start=start, end=end) draw.arc(bbox, start=start, end=end)
# Assert # Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png") 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 # No need to go in loops
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -100,57 +108,61 @@ def test_arc_no_loops():
end = 370 end = 370
# Act # Act
draw.arc(BBOX1, start=start, end=end) draw.arc(bbox, start=start, end=end)
# Assert # Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.arc(BBOX1, 10, 260, width=5) draw.arc(bbox, 10, 260, width=5)
# Assert # Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) 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 # Tests an arc with a large enough width that it is a pieslice
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.arc(BBOX1, 10, 260, fill="yellow", width=100) draw.arc(bbox, 10, 260, fill="yellow", width=100)
# Assert # Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.arc(BBOX1, 10, 260, fill="yellow", width=5) draw.arc(bbox, 10, 260, fill="yellow", width=5)
# Assert # Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png"
# Act # Act
draw.arc(BBOX1, 10, 259.5, width=5) draw.arc(bbox, 10, 259.5, width=5)
# Assert # Assert
assert_image_similar_tofile(im, expected, 1) assert_image_similar_tofile(im, expected, 1)
@ -184,7 +196,7 @@ def test_bitmap():
@pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) @pytest.mark.parametrize("bbox", BBOX)
def test_chord(mode, bbox): def test_chord(mode, bbox):
# Arrange # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
@ -198,37 +210,40 @@ def test_chord(mode, bbox):
assert_image_similar_tofile(im, expected, 1) assert_image_similar_tofile(im, expected, 1)
def test_chord_width(): @pytest.mark.parametrize("bbox", BBOX)
def test_chord_width(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.chord(BBOX1, 10, 260, outline="yellow", width=5) draw.chord(bbox, 10, 260, outline="yellow", width=5)
# Assert # Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # 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
assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # 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
assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") 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("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(mode, bbox): def test_ellipse(mode, bbox):
# Arrange # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
@ -261,13 +276,14 @@ def test_ellipse(mode, bbox):
assert_image_similar_tofile(im, expected, 1) assert_image_similar_tofile(im, expected, 1)
def test_ellipse_translucent(): @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_translucent(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA") draw = ImageDraw.Draw(im, "RGBA")
# Act # Act
draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) draw.ellipse(bbox, fill=(0, 255, 0, 127))
# Assert # Assert
expected = "Tests/images/imagedraw_ellipse_translucent.png" 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)) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.ellipse(BBOX1, outline="blue", width=5) draw.ellipse(bbox, outline="blue", width=5)
# Assert # Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) 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) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.ellipse(BBOX1, fill="green", outline="blue", width=5) draw.ellipse(bbox, fill="green", outline="blue", width=5)
# Assert # Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.ellipse(BBOX1, fill="green", outline="blue", width=0) draw.ellipse(bbox, fill="green", outline="blue", width=0)
# Assert # Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png")
@ -386,7 +405,7 @@ def test_ellipse_various_sizes_filled():
) )
@pytest.mark.parametrize("points", (POINTS1, POINTS2)) @pytest.mark.parametrize("points", POINTS)
def test_line(points): def test_line(points):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -458,7 +477,7 @@ def test_transform():
assert_image_equal(im, expected) 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))) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
def test_pieslice(bbox, start, end): def test_pieslice(bbox, start, end):
# Arrange # Arrange
@ -472,38 +491,41 @@ def test_pieslice(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) draw.pieslice(bbox, 10, 260, outline="blue", width=5)
# Assert # Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_pieslice_width_fill.png" expected = "Tests/images/imagedraw_pieslice_width_fill.png"
# Act # 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
assert_image_similar_tofile(im, expected, 1) assert_image_similar_tofile(im, expected, 1)
def test_pieslice_zero_width(): @pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_zero_width(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # 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
assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png")
@ -551,7 +573,7 @@ def test_pieslice_no_spikes():
assert_image_equal(im, im_pre_erase) assert_image_equal(im, im_pre_erase)
@pytest.mark.parametrize("points", (POINTS1, POINTS2)) @pytest.mark.parametrize("points", POINTS)
def test_point(points): def test_point(points):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -564,7 +586,7 @@ def test_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") 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): def test_polygon(points):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -578,7 +600,8 @@ def test_polygon(points):
@pytest.mark.parametrize("mode", ("RGB", "L")) @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 # Test drawing lines of different gradients (dx>dy, dy>dx) and
# vertical (dx==0) and horizontal (dy==0) lines # vertical (dx==0) and horizontal (dy==0) lines
# Arrange # Arrange
@ -587,7 +610,7 @@ def test_polygon_kite(mode):
expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png"
# Act # Act
draw.polygon(KITE_POINTS, fill="blue", outline="yellow") draw.polygon(kite_points, fill="blue", outline="yellow")
# Assert # Assert
assert_image_equal_tofile(im, expected) assert_image_equal_tofile(im, expected)
@ -634,7 +657,7 @@ def test_polygon_translucent():
assert_image_equal_tofile(im, expected) assert_image_equal_tofile(im, expected)
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) @pytest.mark.parametrize("bbox", BBOX)
def test_rectangle(bbox): def test_rectangle(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -661,63 +684,68 @@ def test_big_rectangle():
assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_rectangle_width.png" expected = "Tests/images/imagedraw_rectangle_width.png"
# Act # Act
draw.rectangle(BBOX1, outline="green", width=5) draw.rectangle(bbox, outline="green", width=5)
# Assert # Assert
assert_image_equal_tofile(im, expected) assert_image_equal_tofile(im, expected)
def test_rectangle_width_fill(): @pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_width_fill(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_rectangle_width_fill.png" expected = "Tests/images/imagedraw_rectangle_width_fill.png"
# Act # Act
draw.rectangle(BBOX1, fill="blue", outline="green", width=5) draw.rectangle(bbox, fill="blue", outline="green", width=5)
# Assert # Assert
assert_image_equal_tofile(im, expected) assert_image_equal_tofile(im, expected)
def test_rectangle_zero_width(): @pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_zero_width(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.rectangle(BBOX1, fill="blue", outline="green", width=0) draw.rectangle(bbox, fill="blue", outline="green", width=0)
# Assert # Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") 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 # Arrange
im = Image.new("I;16", (W, H)) im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.rectangle(BBOX1, fill="black", outline="green") draw.rectangle(bbox, fill="black", outline="green")
# Assert # Assert
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA") draw = ImageDraw.Draw(im, "RGBA")
# Act # 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
assert_image_equal_tofile( assert_image_equal_tofile(
@ -794,13 +822,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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # 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
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png")
@ -810,7 +839,9 @@ def test_rounded_rectangle_zero_radius():
"xy, suffix", "xy, suffix",
[ [
((20, 10, 80, 90), "x"), ((20, 10, 80, 90), "x"),
((20, 10, 81, 90), "x_odd"),
((10, 20, 90, 80), "y"), ((10, 20, 90, 80), "y"),
((10, 20, 90, 81), "y_odd"),
((20, 20, 80, 80), "both"), ((20, 20, 80, 80), "both"),
], ],
) )
@ -830,14 +861,15 @@ def test_rounded_rectangle_translucent(xy, suffix):
) )
def test_floodfill(): @pytest.mark.parametrize("bbox", BBOX)
def test_floodfill(bbox):
red = ImageColor.getrgb("red") red = ImageColor.getrgb("red")
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
# Arrange # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im) 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)) centre_point = (int(W / 2), int(H / 2))
# Act # Act
@ -862,13 +894,14 @@ def test_floodfill():
assert_image_equal(im, Image.new("RGB", (1, 1), red)) 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 # floodfill() is experimental
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) 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)) centre_point = (int(W / 2), int(H / 2))
# Act # Act
@ -883,13 +916,14 @@ def test_floodfill_border():
assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") 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 # floodfill() is experimental
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) 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)) centre_point = (int(W / 2), int(H / 2))
# Act # Act
@ -1309,7 +1343,8 @@ def test_setting_default_font():
assert isinstance(draw.getfont(), ImageFont.ImageFont) assert isinstance(draw.getfont(), ImageFont.ImageFont)
def test_same_color_outline(): @pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox):
# Prepare shape # Prepare shape
x0, y0 = 5, 5 x0, y0 = 5, 5
x1, y1 = 5, 50 x1, y1 = 5, 50
@ -1325,12 +1360,12 @@ def test_same_color_outline():
for mode in ["RGB", "L"]: for mode in ["RGB", "L"]:
for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]:
for operation, args in { for operation, args in {
"chord": [BBOX1, 0, 180], "chord": [bbox, 0, 180],
"ellipse": [BBOX1], "ellipse": [bbox],
"shape": [s], "shape": [s],
"pieslice": [BBOX1, -90, 45], "pieslice": [bbox, -90, 45],
"polygon": [[(18, 30), (85, 30), (60, 72)]], "polygon": [[(18, 30), (85, 30), (60, 72)]],
"rectangle": [BBOX1], "rectangle": [bbox],
}.items(): }.items():
# Arrange # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
@ -1347,20 +1382,20 @@ def test_same_color_outline():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"n_sides, rotation, polygon_name", "n_sides, polygon_name, args",
[(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], [
(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)) im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0))
filename_base = f"Tests/images/imagedraw_{polygon_name}" filename = f"Tests/images/imagedraw_{polygon_name}.png"
filename = (
f"{filename_base}.png"
if rotation == 0
else f"{filename_base}_rotate_{rotation}.png"
)
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
bounding_circle = ((W // 2, H // 2), 25) 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) assert_image_equal_tofile(im, filename)

View File

@ -27,15 +27,16 @@ X1 = int(X0 * 3)
Y0 = int(H / 4) Y0 = int(H / 4)
Y1 = int(X0 * 3) Y1 = int(X0 * 3)
# Two kinds of bounding box # Bounding boxes
BBOX1 = [(X0, Y0), (X1, Y1)] BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1])
BBOX2 = [X0, Y0, X1, Y1]
# Two kinds of coordinate sequences # Coordinate sequences
POINTS1 = [(10, 10), (20, 40), (30, 30)] POINTS = (
POINTS2 = [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)] (10, 10, 20, 40, 30, 30),
[10, 10, 20, 40, 30, 30],
)
FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_PATH = "Tests/fonts/FreeMono.ttf"
@ -52,7 +53,7 @@ def test_sanity():
draw.line(list(range(10)), pen) draw.line(list(range(10)), pen)
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(bbox): def test_ellipse(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) 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) 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): def test_line(points):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -94,7 +95,8 @@ def test_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im) draw = ImageDraw2.Draw(im)
@ -103,13 +105,13 @@ def test_line_pen_as_brush():
# Act # Act
# Pass in the pen as the brush parameter # Pass in the pen as the brush parameter
draw.line(POINTS1, pen, brush) draw.line(points, pen, brush)
# Assert # Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") 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): def test_polygon(points):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -124,7 +126,7 @@ def test_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") 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): def test_rectangle(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))

View File

@ -191,6 +191,16 @@ def test_getlength(
assert length == length_raqm assert length == length_raqm
def test_float_size():
lengths = []
for size in (48, 48.5, 49):
f = ImageFont.truetype(
"Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine
)
lengths.append(f.getlength("text"))
assert lengths[0] != lengths[1] != lengths[2]
def test_render_multiline(font): def test_render_multiline(font):
im = Image.new(mode="RGB", size=(300, 100)) im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)

View File

@ -28,7 +28,7 @@ def test_path():
(6.0, 7.0), (6.0, 7.0),
(8.0, 9.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) assert p.getbbox() == (0.0, 1.0, 8.0, 9.0)
@ -38,48 +38,65 @@ def test_path():
p.transform((1, 0, 1, 0, 1, 1)) p.transform((1, 0, 1, 0, 1, 1))
assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] 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]) @pytest.mark.parametrize(
p = ImagePath.Path(arr.tobytes()) "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, 1]).tobytes(),
ImagePath.Path((0, 1)),
),
)
def test_path_constructors(coords):
# Arrange / Act
p = ImagePath.Path(coords)
# Assert
assert list(p) == [(0.0, 1.0)] assert list(p) == [(0.0, 1.0)]
def test_invalid_coords(): @pytest.mark.parametrize(
# Arrange "coords",
coords = ["a", "b"] (
("a", "b"),
# Act / Assert ([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: with pytest.raises(ValueError) as e:
ImagePath.Path(coords) ImagePath.Path(coords)
# Assert
assert str(e.value) == "incorrect coordinate type" assert str(e.value) == "incorrect coordinate type"
def test_path_odd_number_of_coordinates(): @pytest.mark.parametrize(
# Arrange "coords",
coords = [0] (
(0,),
# Act / Assert [0],
(0, 1, 2),
[0, 1, 2],
),
)
def test_path_odd_number_of_coordinates(coords):
# Act
with pytest.raises(ValueError) as e: with pytest.raises(ValueError) as e:
ImagePath.Path(coords) ImagePath.Path(coords)
# Assert
assert str(e.value) == "wrong number of coordinates" assert str(e.value) == "wrong number of coordinates"

View File

@ -757,6 +757,7 @@ class TestLibUnpack:
def test_I16(self): def test_I16(self):
self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) 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;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506)
self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605)
self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040)

View File

@ -117,3 +117,9 @@ def test_pdf_repr():
assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)"
assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]"
assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>"
def test_duplicate_xref_entry():
pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf")
assert pdf.xref_table.existing_entries[6][0] == 1197
pdf.close()

View File

@ -1,9 +1,9 @@
# Documentation: https://docs.codecov.io/docs/codecov-yaml # Documentation: https://docs.codecov.com/docs/codecov-yaml
codecov: codecov:
# Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]"
# https://github.com/codecov/support/issues/363 # 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 allow_coverage_offsets: true
comment: false comment: false

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install libimagequant # 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 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -317,6 +317,17 @@ def setup(app):
app.add_css_file("css/dark.css") 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 # sphinx.ext.extlinks
# This config is a dictionary of external sites, # This config is a dictionary of external sites,
# mapping unique short aliases to a base URL and a prefix. # mapping unique short aliases to a base URL and a prefix.

View File

@ -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 Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or `PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead. `PySide6 <https://doc.qt.io/qtforpython-6/>`_ instead.
Image.coerce_e Image.coerce_e
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@ -181,7 +181,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **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 * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.
@ -448,17 +448,15 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 | | Debian 11 Bullseye | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 36 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 37 | 3.11 | x86-64 | | Fedora 37 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 | | Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | | macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, PyPy3 | | | | 3.12, PyPy3 | |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | | Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | | Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
@ -492,7 +490,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+ +==================================+===========================+==================+==============+
| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |arm | | macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+

View File

@ -439,7 +439,7 @@ Used to specify the dithering method to use for the
Palettes Palettes
^^^^^^^^ ^^^^^^^^
Used to specify the pallete to use for the :meth:`~Image.convert` method. Used to specify the palette to use for the :meth:`~Image.convert` method.
.. autoclass:: Palette .. autoclass:: Palette
:members: :members:

View File

@ -243,6 +243,7 @@ Methods
.. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None)
Draws a line between the coordinates in the ``xy`` list. 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 :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or
numeric values like ``[x, y, x, y, ...]``. numeric values like ``[x, y, x, y, ...]``.
@ -287,7 +288,7 @@ Methods
The polygon outline consists of straight lines between the given The polygon outline consists of straight lines between the given
coordinates, plus a straight line between the last and the first 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 :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or
numeric values like ``[x, y, x, y, ...]``. numeric values like ``[x, y, x, y, ...]``.
@ -296,7 +297,7 @@ Methods
:param width: The line width, in pixels. :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``, Draws a regular polygon inscribed in ``bounding_circle``,
with ``n_sides``, and rotation of ``rotation`` degrees. with ``n_sides``, and rotation of ``rotation`` degrees.
@ -311,6 +312,7 @@ Methods
(e.g. ``rotation=90``, applies a 90 degree rotation). (e.g. ``rotation=90``, applies a 90 degree rotation).
:param fill: Color to use for the fill. :param fill: Color to use for the fill.
:param outline: Color to use for the outline. :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) .. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1)

View File

@ -15,8 +15,9 @@ or the clipboard to a PIL image memory.
returned as an "RGBA" on macOS, or an "RGB" image otherwise. returned as an "RGBA" on macOS, or an "RGB" image otherwise.
If the bounding box is omitted, the entire screen is copied. 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 On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return
is installed. To capture the default X11 display instead, pass ``xdisplay=""``. 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) .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux)
@ -39,9 +40,11 @@ or the clipboard to a PIL image memory.
.. py:function:: grabclipboard() .. 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, :return: On Windows, an image, a list of filenames,
or None if the clipboard does not contain image data or filenames. or None if the clipboard does not contain image data or filenames.
@ -49,3 +52,5 @@ or the clipboard to a PIL image memory.
On Mac, an image, On Mac, an image,
or None if the clipboard does not contain image data. or None if the clipboard does not contain image data.
On Linux, an image.

View File

@ -48,7 +48,7 @@ vector data. Path objects can be passed to the methods on the
Maps the path through a function. 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), …]. Converts the path to a Python list [(x, y), …].

View File

@ -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 Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or `PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead. `PySide6 <https://doc.qt.io/qtforpython-6/>`_ instead.
Image.coerce_e Image.coerce_e
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
@ -135,10 +135,11 @@ TODO
API Changes 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 API Additions
============= =============
@ -159,7 +160,20 @@ TODO
Other Changes Other Changes
============= =============
TODO Support display_jpeg() in IPython
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO 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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TIFF images with signed integer data, 8 bits per sample and a photometric
interpretaton of BlackIsZero can now be read.

View File

@ -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 Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
in Pillow 10 (2023-07-01). Upgrade to in Pillow 10 (2023-07-01). Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or `PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead. `PySide6 <https://doc.qt.io/qtforpython-6/>`_ instead.
FreeTypeFont.getmask2 fill parameter FreeTypeFont.getmask2 fill parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -10,6 +10,7 @@
import os import os
import re import re
import shutil
import struct import struct
import subprocess import subprocess
import sys import sys
@ -150,6 +151,7 @@ def _dbg(s, tp=None):
def _find_library_dirs_ldconfig(): def _find_library_dirs_ldconfig():
# Based on ctypes.util from Python 2 # 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 sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
if struct.calcsize("l") == 4: if struct.calcsize("l") == 4:
machine = os.uname()[4] + "-32" machine = os.uname()[4] + "-32"
@ -166,14 +168,14 @@ def _find_library_dirs_ldconfig():
# Assuming GLIBC's ldconfig (with option -p) # Assuming GLIBC's ldconfig (with option -p)
# Alpine Linux uses musl that can't print cache # Alpine Linux uses musl that can't print cache
args = ["ldconfig", "-p"] args = [ldconfig, "-p"]
expr = rf".*\({abi_type}.*\) => (.*)" expr = rf".*\({abi_type}.*\) => (.*)"
env = dict(os.environ) env = dict(os.environ)
env["LC_ALL"] = "C" env["LC_ALL"] = "C"
env["LANG"] = "C" env["LANG"] = "C"
elif sys.platform.startswith("freebsd"): elif sys.platform.startswith("freebsd"):
args = ["ldconfig", "-r"] args = [ldconfig, "-r"]
expr = r".* => (.*)" expr = r".* => (.*)"
env = {} env = {}

View File

@ -633,19 +633,34 @@ class Image:
) )
) )
def _repr_png_(self): def _repr_image(self, image_format):
"""iPython display hook support """Helper function for iPython display hook.
:returns: png version of the image as bytes :param image_format: Image format.
:returns: image as bytes, saved into the given format.
""" """
b = io.BytesIO() b = io.BytesIO()
try: try:
self.save(b, "PNG") self.save(b, image_format)
except Exception as e: except Exception as e:
msg = "Could not save to PNG for display" msg = f"Could not save to {image_format} for display"
raise ValueError(msg) from e raise ValueError(msg) from e
return b.getvalue() 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_jpeg_(self):
"""iPython display hook support for JPEG format.
:returns: JPEG version of the image as bytes
"""
return self._repr_image("JPEG")
@property @property
def __array_interface__(self): def __array_interface__(self):
# numpy array interface support # numpy array interface support
@ -672,7 +687,8 @@ class Image:
return new return new
def __getstate__(self): def __getstate__(self):
return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] im_data = self.tobytes() # load image first
return [self.info, self.mode, self.size, self.getpalette(), im_data]
def __setstate__(self, state): def __setstate__(self, state):
Image.__init__(self) Image.__init__(self)
@ -1107,7 +1123,6 @@ class Image:
Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
(default). (default).
:returns: A new image :returns: A new image
""" """
self.load() self.load()

View File

@ -185,12 +185,8 @@ class ImageCmsProfile:
def _set(self, profile, filename=None): def _set(self, profile, filename=None):
self.profile = profile self.profile = profile
self.filename = filename self.filename = filename
if profile: self.product_name = None # profile.product_name
self.product_name = None # profile.product_name self.product_info = None # profile.product_info
self.product_info = None # profile.product_info
else:
self.product_name = None
self.product_info = None
def tobytes(self): def tobytes(self):
""" """

View File

@ -279,11 +279,11 @@ class ImageDraw:
self.im.paste(im.im, (0, 0) + im.size, mask.im) self.im.paste(im.im, (0, 0) + im.size, mask.im)
def regular_polygon( 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.""" """Draw a regular polygon."""
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) 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): def rectangle(self, xy, fill=None, outline=None, width=1):
"""Draw a rectangle.""" """Draw a rectangle."""
@ -314,11 +314,11 @@ class ImageDraw:
full_x, full_y = False, False full_x, full_y = False, False
if all(corners): if all(corners):
full_x = d >= x1 - x0 full_x = d >= x1 - x0 - 1
if full_x: if full_x:
# The two left and two right corners are joined # The two left and two right corners are joined
d = x1 - x0 d = x1 - x0
full_y = d >= y1 - y0 full_y = d >= y1 - y0 - 1
if full_y: if full_y:
# The two top and two bottom corners are joined # The two top and two bottom corners are joined
d = y1 - y0 d = y1 - y0

View File

@ -61,7 +61,17 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
left, top, right, bottom = bbox left, top, right, bottom = bbox
im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im 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)
except OSError:
if (
xdisplay is None
and sys.platform not in ("darwin", "win32")
and shutil.which("gnome-screenshot")
):
fh, filepath = tempfile.mkstemp(".png") fh, filepath = tempfile.mkstemp(".png")
os.close(fh) os.close(fh)
subprocess.call(["gnome-screenshot", "-f", filepath]) subprocess.call(["gnome-screenshot", "-f", filepath])
@ -73,15 +83,13 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
im.close() im.close()
return im_cropped return im_cropped
return im return im
# use xdisplay=None for default display on non-win32/macOS systems else:
if not Image.core.HAVE_XCB: raise
msg = "Pillow was built without XCB support" else:
raise OSError(msg) im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
size, data = Image.core.grabscreen_x11(xdisplay) if bbox:
im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) im = im.crop(bbox)
if bbox: return im
im = im.crop(bbox)
return im
def grabclipboard(): def grabclipboard():
@ -156,8 +164,11 @@ def grabclipboard():
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
raise NotImplementedError(msg) raise NotImplementedError(msg)
fh, filepath = tempfile.mkstemp() fh, filepath = tempfile.mkstemp()
subprocess.call(args, stdout=fh) err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr
os.close(fh) os.close(fh)
if err:
msg = f"{args[0]} error: {err.strip().decode()}"
raise ChildProcessError(msg)
im = Image.open(filepath) im = Image.open(filepath)
im.load() im.load()
os.unlink(filepath) os.unlink(filepath)

View File

@ -957,14 +957,11 @@ class PdfParser:
check_format_condition(m, "xref entry not found") check_format_condition(m, "xref entry not found")
offset = m.end() offset = m.end()
is_free = m.group(3) == b"f" is_free = m.group(3) == b"f"
generation = int(m.group(2))
if not is_free: if not is_free:
generation = int(m.group(2))
new_entry = (int(m.group(1)), generation) new_entry = (int(m.group(1)), generation)
check_format_condition( if i not in self.xref_table:
i not in self.xref_table or self.xref_table[i] == new_entry, self.xref_table[i] = new_entry
"xref entry duplicated (and not identical)",
)
self.xref_table[i] = new_entry
return offset return offset
def read_indirect(self, ref, max_nesting=-1): def read_indirect(self, ref, max_nesting=-1):

View File

@ -170,6 +170,8 @@ OPEN_INFO = {
(MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
(II, 1, (1,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 1, (8,), ()): ("L", "L"),
(MM, 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"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
(MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
(II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),

View File

@ -116,7 +116,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
int error = 0; int error = 0;
char *filename = NULL; char *filename = NULL;
Py_ssize_t size; float size;
FT_Size_RequestRec req;
FT_Long width;
Py_ssize_t index = 0; Py_ssize_t index = 0;
Py_ssize_t layout_engine = 0; Py_ssize_t layout_engine = 0;
unsigned char *encoding; unsigned char *encoding;
@ -133,7 +135,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
if (!PyArg_ParseTupleAndKeywords( if (!PyArg_ParseTupleAndKeywords(
args, args,
kw, kw,
"etn|nsy#n", "etf|nsy#n",
kwlist, kwlist,
Py_FileSystemDefaultEncoding, Py_FileSystemDefaultEncoding,
&filename, &filename,
@ -179,7 +181,13 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
} }
if (!error) { if (!error) {
error = FT_Set_Pixel_Sizes(self->face, 0, size); width = size * 64;
req.type = FT_SIZE_REQUEST_TYPE_NOMINAL;
req.width = width;
req.height = width;
req.horiResolution = 0;
req.vertResolution = 0;
error = FT_Request_Size(self->face, &req);
} }
if (!error && encoding && strlen((char *)encoding) == 4) { if (!error && encoding && strlen((char *)encoding) == 4) {

View File

@ -37,6 +37,17 @@ clip8(float in) {
return (UINT8)in; return (UINT8)in;
} }
static inline INT32
clip32(float in) {
if (in <= 0.0) {
return 0;
}
if (in >= pow(2, 31) - 1) {
return pow(2, 31) - 1;
}
return (INT32)in;
}
Imaging Imaging
ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) {
Imaging imOut; Imaging imOut;
@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) {
void void
ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
#define KERNEL1x3(in0, x, kernel, d) \ #define KERNEL1x3(in0, x, kernel, d) \
(_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \ (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \
_i2f((UINT8)in0[x + d]) * (kernel)[2]) _i2f(in0[x + d]) * (kernel)[2])
int x = 0, y = 0; int x = 0, y = 0;
@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
if (im->bands == 1) { if (im->bands == 1) {
// Add one time for rounding // Add one time for rounding
offset += 0.5; offset += 0.5;
for (y = 1; y < im->ysize - 1; y++) { if (im->type == IMAGING_TYPE_INT32) {
UINT8 *in_1 = (UINT8 *)im->image[y - 1]; for (y = 1; y < im->ysize - 1; y++) {
UINT8 *in0 = (UINT8 *)im->image[y]; INT32 *in_1 = (INT32 *)im->image[y - 1];
UINT8 *in1 = (UINT8 *)im->image[y + 1]; INT32 *in0 = (INT32 *)im->image[y];
UINT8 *out = (UINT8 *)imOut->image[y]; INT32 *in1 = (INT32 *)im->image[y + 1];
INT32 *out = (INT32 *)imOut->image[y];
out[0] = in0[0]; out[0] = in0[0];
for (x = 1; x < im->xsize - 1; x++) { for (x = 1; x < im->xsize - 1; x++) {
float ss = offset; float ss = offset;
ss += KERNEL1x3(in1, x, &kernel[0], 1); ss += KERNEL1x3(in1, x, &kernel[0], 1);
ss += KERNEL1x3(in0, x, &kernel[3], 1); ss += KERNEL1x3(in0, x, &kernel[3], 1);
ss += KERNEL1x3(in_1, x, &kernel[6], 1); ss += KERNEL1x3(in_1, x, &kernel[6], 1);
out[x] = clip8(ss); 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 { } else {
// Add one time for rounding // Add one time for rounding
@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
void void
ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
#define KERNEL1x5(in0, x, kernel, d) \ #define KERNEL1x5(in0, x, kernel, d) \
(_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \ (_i2f(in0[x - d - d]) * (kernel)[0] + \
_i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \ _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \
_i2f((UINT8)in0[x + d]) * (kernel)[3] + \ _i2f(in0[x + d]) * (kernel)[3] + \
_i2f((UINT8)in0[x + d + d]) * (kernel)[4]) _i2f(in0[x + d + d]) * (kernel)[4])
int x = 0, y = 0; int x = 0, y = 0;
@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
if (im->bands == 1) { if (im->bands == 1) {
// Add one time for rounding // Add one time for rounding
offset += 0.5; offset += 0.5;
for (y = 2; y < im->ysize - 2; y++) { if (im->type == IMAGING_TYPE_INT32) {
UINT8 *in_2 = (UINT8 *)im->image[y - 2]; for (y = 2; y < im->ysize - 2; y++) {
UINT8 *in_1 = (UINT8 *)im->image[y - 1]; INT32 *in_2 = (INT32 *)im->image[y - 2];
UINT8 *in0 = (UINT8 *)im->image[y]; INT32 *in_1 = (INT32 *)im->image[y - 1];
UINT8 *in1 = (UINT8 *)im->image[y + 1]; INT32 *in0 = (INT32 *)im->image[y];
UINT8 *in2 = (UINT8 *)im->image[y + 2]; INT32 *in1 = (INT32 *)im->image[y + 1];
UINT8 *out = (UINT8 *)imOut->image[y]; INT32 *in2 = (INT32 *)im->image[y + 2];
INT32 *out = (INT32 *)imOut->image[y];
out[0] = in0[0]; out[0] = in0[0];
out[1] = in0[1]; out[1] = in0[1];
for (x = 2; x < im->xsize - 2; x++) { for (x = 2; x < im->xsize - 2; x++) {
float ss = offset; float ss = offset;
ss += KERNEL1x5(in2, x, &kernel[0], 1); ss += KERNEL1x5(in2, x, &kernel[0], 1);
ss += KERNEL1x5(in1, x, &kernel[5], 1); ss += KERNEL1x5(in1, x, &kernel[5], 1);
ss += KERNEL1x5(in0, x, &kernel[10], 1); ss += KERNEL1x5(in0, x, &kernel[10], 1);
ss += KERNEL1x5(in_1, x, &kernel[15], 1); ss += KERNEL1x5(in_1, x, &kernel[15], 1);
ss += KERNEL1x5(in_2, x, &kernel[20], 1); ss += KERNEL1x5(in_2, x, &kernel[20], 1);
out[x] = clip8(ss); 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 { } else {
// Add one time for rounding // Add one time for rounding
@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o
Imaging imOut; Imaging imOut;
ImagingSectionCookie cookie; ImagingSectionCookie cookie;
if (!im || im->type != IMAGING_TYPE_UINT8) { if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) {
return (Imaging)ImagingError_ModeError(); return (Imaging)ImagingError_ModeError();
} }

View File

@ -1149,6 +1149,16 @@ unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) {
} }
} }
static void 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) { unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) {
int i; int i;
for (i = 0; i < pixels; i++) { for (i = 0; i < pixels; i++) {
@ -1764,6 +1774,7 @@ static struct {
{"I;16L", "I;16L", 16, copy2}, {"I;16L", "I;16L", 16, copy2},
{"I;16N", "I;16N", 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;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian.
{"I;16L", "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}, {"I;16B", "I;16N", 16, unpackI16N_I16B},

View File

@ -1,6 +1,7 @@
[tox] [tox]
minversion = 1.9 requires =
envlist = tox>=4.2
env_list =
lint lint
py{py3, 311, 310, 39, 38} py{py3, 311, 310, 39, 38}
@ -23,7 +24,7 @@ skip_install = true
deps = deps =
check-manifest check-manifest
pre-commit pre-commit
passenv = pass_env =
PRE_COMMIT_COLOR PRE_COMMIT_COLOR
commands = commands =
pre-commit run --all-files --show-diff-on-failure pre-commit run --all-files --show-diff-on-failure

View File

@ -337,9 +337,9 @@ deps = {
"libs": [r"imagequant.lib"], "libs": [r"imagequant.lib"],
}, },
"harfbuzz": { "harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/7.1.0.zip", "url": "https://github.com/harfbuzz/harfbuzz/archive/7.3.0.zip",
"filename": "harfbuzz-7.1.0.zip", "filename": "harfbuzz-7.3.0.zip",
"dir": "harfbuzz-7.1.0", "dir": "harfbuzz-7.3.0",
"license": "COPYING", "license": "COPYING",
"build": [ "build": [
*cmds_cmake( *cmds_cmake(
@ -352,12 +352,12 @@ deps = {
"libs": [r"*.lib"], "libs": [r"*.lib"],
}, },
"fribidi": { "fribidi": {
"url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip", "url": "https://github.com/fribidi/fribidi/archive/v1.0.13.zip",
"filename": "fribidi-1.0.12.zip", "filename": "fribidi-1.0.13.zip",
"dir": "fribidi-1.0.12", "dir": "fribidi-1.0.13",
"license": "COPYING", "license": "COPYING",
"build": [ "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"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
*cmds_cmake("fribidi"), *cmds_cmake("fribidi"),
], ],