diff --git a/.ci/install.sh b/.ci/install.sh
index 17c349ab1..6e87d386d 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
@@ -41,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
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
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 2b597a945..9a1e46705 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@v3
- name: Install Cygwin
- uses: cygwin/cygwin-install-action@v3
+ uses: cygwin/cygwin-install-action@v4
with:
platform: x86_64
packages: >
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 14592ea1d..3bcb8cfbc 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -39,10 +39,10 @@ jobs:
centos-stream-8-amd64,
centos-stream-9-amd64,
debian-11-bullseye-x86,
- fedora-36-amd64,
+ debian-12-bookworm-x86,
fedora-37-amd64,
+ fedora-38-amd64,
gentoo,
- ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
]
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index ddfafc9d7..a109ec0d8 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -80,7 +80,7 @@ jobs:
pushd depends && ./install_extra_test_images.sh && popd
- 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
run: |
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
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fced6113b..afb8fb56c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -84,7 +84,9 @@ jobs:
python3 -m pip install pytest-reverse
fi
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
- xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
+ xvfb-run -s '-screen 0 1024x768x24' sway&
+ export WAYLAND_DISPLAY=wayland-1
+ .ci/test.sh
else
.ci/test.sh
fi
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c3b6dc0a6..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
@@ -57,7 +54,7 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/tox-ini-fmt
- rev: 1.0.0
+ rev: 1.3.0
hooks:
- id: tox-ini-fmt
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 98d9e4425..bda03d944 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -1,5 +1,7 @@
version: 2
+formats: [pdf]
+
build:
os: ubuntu-22.04
tools:
diff --git a/CHANGES.rst b/CHANGES.rst
index cd0b95085..c51f8fb94 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,66 @@ 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]
+
+- 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
[adisbladis, radarhere]
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)
diff --git a/Tests/images/8bit.s.tif b/Tests/images/8bit.s.tif
new file mode 100644
index 000000000..043cba6af
Binary files /dev/null and b/Tests/images/8bit.s.tif differ
diff --git a/Tests/images/duplicate_xref_entry.pdf b/Tests/images/duplicate_xref_entry.pdf
new file mode 100644
index 000000000..f57a57d61
Binary files /dev/null and b/Tests/images/duplicate_xref_entry.pdf differ
diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png
new file mode 100644
index 000000000..f4dab388f
Binary files /dev/null and b/Tests/images/hopper_emboss_I.png differ
diff --git a/Tests/images/hopper_emboss_more_I.png b/Tests/images/hopper_emboss_more_I.png
new file mode 100644
index 000000000..c417c915f
Binary files /dev/null and b/Tests/images/hopper_emboss_more_I.png differ
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 000000000..f23f1945e
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_x_odd.png differ
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 000000000..96441bc72
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_y_odd.png differ
diff --git a/Tests/images/imagedraw_triangle_width.png b/Tests/images/imagedraw_triangle_width.png
new file mode 100644
index 000000000..3d35326e7
Binary files /dev/null and b/Tests/images/imagedraw_triangle_width.png differ
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index f78c086eb..a22ac581d 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -447,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)
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/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 73a00386f..0247527f5 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_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")
@skip_unless_feature("jpg")
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 97a02ac96..f13436ce8 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:
@@ -198,6 +205,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/Tests/test_image_copy.py b/Tests/test_image_copy.py
index 591832147..cd602fc76 100644
--- a/Tests/test_image_copy.py
+++ b/Tests/test_image_copy.py
@@ -4,7 +4,7 @@ import pytest
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"))
@@ -42,3 +42,10 @@ def test_copy_zero():
out = im.copy()
assert out.mode == im.mode
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)
diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py
index a2ef2280b..25b72298e 100644
--- a/Tests/test_image_filter.py
+++ b/Tests/test_image_filter.py
@@ -30,15 +30,16 @@ from .helper import assert_image_equal, hopper
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):
im = hopper(mode)
- out = im.filter(filter_to_apply)
- assert out.mode == im.mode
- assert out.size == im.size
+ if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
+ out = im.filter(filter_to_apply)
+ 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):
with pytest.raises(TypeError):
im = hopper(mode)
@@ -130,10 +131,12 @@ def test_kernel_not_enough_coefficients():
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):
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(
(3, 3),
# fmt: off
@@ -146,16 +149,20 @@ def test_consistency_3x3(mode):
source = source.split() * 2
reference = reference.split() * 2
- assert_image_equal(
- Image.merge(mode, source[: len(mode)]).filter(kernel),
- Image.merge(mode, reference[: len(mode)]),
- )
+ if mode == "I":
+ source = source[0].convert(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):
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(
(5, 5),
# fmt: off
@@ -170,10 +177,12 @@ def test_consistency_5x5(mode):
source = source.split() * 2
reference = reference.split() * 2
- assert_image_equal(
- Image.merge(mode, source[: len(mode)]).filter(kernel),
- Image.merge(mode, reference[: len(mode)]),
- )
+ if mode == "I":
+ source = source[0].convert(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():
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/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 7ffd7969d..7497fdc66 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")
@@ -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):
# Arrange
im = Image.new("RGB", (W, H))
@@ -458,7 +477,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
@@ -472,38 +491,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")
@@ -551,7 +573,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))
@@ -564,7 +586,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))
@@ -578,7 +600,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
@@ -587,7 +610,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)
@@ -634,7 +657,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))
@@ -661,63 +684,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(
@@ -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
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")
@@ -810,7 +839,9 @@ def test_rounded_rectangle_zero_radius():
"xy, suffix",
[
((20, 10, 80, 90), "x"),
+ ((20, 10, 81, 90), "x_odd"),
((10, 20, 90, 80), "y"),
+ ((10, 20, 90, 81), "y_odd"),
((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")
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
@@ -862,13 +894,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
@@ -883,13 +916,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
@@ -1309,7 +1343,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
@@ -1325,12 +1360,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))
@@ -1347,20 +1382,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/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py
index a8a2ee1fc..a2c2fa1f0 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))
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 623365d53..4a40d1d1d 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -191,6 +191,16 @@ def test_getlength(
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):
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -453,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("")
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index fa88065f4..f8059eca4 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -98,3 +98,18 @@ $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("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/Tests/test_imagepath.py b/Tests/test_imagepath.py
index 8f8a9f449..c112cfd87 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)
@@ -38,48 +38,65 @@ 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, 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)]
-def test_invalid_coords():
- # Arrange
- coords = ["a", "b"]
-
- # Act / Assert
+@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"
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/Tests/test_pdfparser.py b/Tests/test_pdfparser.py
index 43e244c7b..105a838d9 100644
--- a/Tests/test_pdfparser.py
+++ b/Tests/test_pdfparser.py
@@ -117,3 +117,9 @@ def test_pdf_repr():
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(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()
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
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/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"))
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.
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/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
-------
diff --git a/docs/installation.rst b/docs/installation.rst
index 7088657f9..ac54b037d 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.
@@ -448,17 +448,17 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+
-| Fedora 36 | 3.10 | x86-64 |
+| Debian 12 Bookworm | 3.11 | x86 |
+----------------------------------+----------------------------+---------------------+
| Fedora 37 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
+| Fedora 38 | 3.11 | x86-64 |
++----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | 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 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
@@ -492,7 +492,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | 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 |
+----------------------------------+---------------------------+------------------+--------------+
diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst
index 35a4c2110..41d3b8fce 100644
--- a/docs/reference/Image.rst
+++ b/docs/reference/Image.rst
@@ -439,7 +439,7 @@ Used to specify the dithering method to use for the
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
:members:
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index aec7a3ef8..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, ...]``.
@@ -296,7 +297,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 +312,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/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst
index 3086ba8c3..0b94032d5 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)
@@ -39,9 +40,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 +52,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.
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), …].
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index 3ee1a9973..3ffafafdc 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
^^^^^^^^^^^^^^
@@ -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
=============
@@ -159,7 +160,20 @@ TODO
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.
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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/setup.py b/setup.py
index 4aed148e8..0b6b02077 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 = {}
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 1c88d22c7..6b1b5947e 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
@@ -354,7 +361,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)
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):
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/Image.py b/src/PIL/Image.py
index 34b8bbcbd..b53aac19e 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -633,19 +633,34 @@ class Image:
)
)
- def _repr_png_(self):
- """iPython display hook support
+ def _repr_image(self, image_format):
+ """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()
try:
- self.save(b, "PNG")
+ self.save(b, image_format)
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
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
def __array_interface__(self):
# numpy array interface support
@@ -672,7 +687,8 @@ class Image:
return new
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):
Image.__init__(self)
@@ -1107,7 +1123,6 @@ class Image:
Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
(default).
:returns: A new image
-
"""
self.load()
@@ -1239,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/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 31b0e5a5e..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.
@@ -185,12 +185,8 @@ class ImageCmsProfile:
def _set(self, profile, filename=None):
self.profile = profile
self.filename = filename
- if profile:
- self.product_name = None # profile.product_name
- self.product_info = None # profile.product_info
- else:
- self.product_name = None
- self.product_info = None
+ self.product_name = None # profile.product_name
+ self.product_info = None # profile.product_info
def tobytes(self):
"""
@@ -275,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/ImageDraw.py b/src/PIL/ImageDraw.py
index e9ccf8041..7d1790faa 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."""
@@ -314,11 +314,11 @@ class ImageDraw:
full_x, full_y = False, False
if all(corners):
- full_x = d >= 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
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.
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index ea4549cf5..3ddc1aaad 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
@@ -226,10 +225,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
@@ -551,28 +546,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/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index 982f77f20..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
@@ -61,7 +62,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)
+ except OSError:
+ 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])
@@ -73,15 +84,13 @@ 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
+ else:
+ im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
+ if bbox:
+ im = im.crop(bbox)
+ return im
def grabclipboard():
@@ -120,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
@@ -134,16 +141,29 @@ def grabclipboard():
return None
else:
if shutil.which("wl-paste"):
+ output = subprocess.check_output(["wl-paste", "-l"]).decode()
+ mimetypes = output.splitlines()
+ if "image/png" in mimetypes:
+ mimetype = "image/png"
+ elif mimetypes:
+ mimetype = mimetypes[0]
+ else:
+ mimetype = None
+
args = ["wl-paste"]
+ if mimetype:
+ args.extend(["-t", mimetype])
elif shutil.which("xclip"):
args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
else:
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
raise NotImplementedError(msg)
- fh, filepath = tempfile.mkstemp()
- subprocess.call(args, stdout=fh)
- os.close(fh)
- im = Image.open(filepath)
+ 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)
+ data = io.BytesIO(p.stdout)
+ im = Image.open(data)
im.load()
- os.unlink(filepath)
return im
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/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)
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index 1b3cb52a2..dc1012f54 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -957,14 +957,11 @@ class PdfParser:
check_format_condition(m, "xref entry not found")
offset = m.end()
is_free = m.group(3) == b"f"
- generation = int(m.group(2))
if not is_free:
+ generation = int(m.group(2))
new_entry = (int(m.group(1)), generation)
- check_format_condition(
- i not in self.xref_table or self.xref_table[i] == new_entry,
- "xref entry duplicated (and not identical)",
- )
- self.xref_table[i] = new_entry
+ if i not in self.xref_table:
+ self.xref_table[i] = new_entry
return offset
def read_indirect(self, ref, max_nesting=-1):
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
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)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 7f8449ea6..86961e6ef 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"),
@@ -1892,6 +1894,10 @@ class AppendingTiffWriter:
8, # srational
4, # float
8, # double
+ 4, # ifd
+ 2, # unicode
+ 4, # complex
+ 8, # long8
]
# StripOffsets = 273
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/_imagingft.c b/src/_imagingft.c
index 19785a47f..25a4d3517 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -116,7 +116,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
int error = 0;
char *filename = NULL;
- Py_ssize_t size;
+ float size;
+ FT_Size_RequestRec req;
+ FT_Long width;
Py_ssize_t index = 0;
Py_ssize_t layout_engine = 0;
unsigned char *encoding;
@@ -130,10 +132,31 @@ 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,
- "etn|nsy#n",
+ "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,
+ "etf|nsy#n",
kwlist,
Py_FileSystemDefaultEncoding,
&filename,
@@ -145,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
&layout_engine)) {
return NULL;
}
+#endif
self = PyObject_New(FontObject, &Font_Type);
if (!self) {
@@ -179,7 +203,13 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
}
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) {
@@ -224,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;
@@ -463,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
{
@@ -521,73 +549,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);
@@ -610,12 +590,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);
@@ -639,13 +621,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
@@ -663,15 +647,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
@@ -681,7 +665,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;
@@ -721,17 +705,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 *
@@ -755,6 +796,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? */
@@ -765,27 +807,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;
}
@@ -811,8 +860,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) {
@@ -829,15 +911,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!
@@ -1034,7 +1107,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) {
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
diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c
index fab3b4948..4dcd368ca 100644
--- a/src/libImaging/Filter.c
+++ b/src/libImaging/Filter.c
@@ -37,8 +37,19 @@ clip8(float 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
-ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) {
+ImagingExpand(Imaging imIn, int xmargin, int ymargin) {
Imaging imOut;
int x, y;
ImagingSectionCookie cookie;
@@ -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();
}
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
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
diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c
index 8f6370061..de8586706 100644
--- a/src/libImaging/Jpeg2KEncode.c
+++ b/src/libImaging/Jpeg2KEncode.c
@@ -281,7 +281,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
int ret = -1;
unsigned prec = 8;
- unsigned bpp = 8;
unsigned _overflow_scale_factor;
stream = opj_stream_create(BUFFER_SIZE, OPJ_FALSE);
@@ -313,7 +312,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
color_space = OPJ_CLRSPC_GRAY;
pack = j2k_pack_i16;
prec = 16;
- bpp = 12;
} else if (strcmp(im->mode, "LA") == 0) {
components = 2;
color_space = OPJ_CLRSPC_GRAY;
@@ -342,7 +340,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
image_params[n].h = im->ysize;
image_params[n].x0 = image_params[n].y0 = 0;
image_params[n].prec = prec;
- image_params[n].bpp = bpp;
image_params[n].sgnd = context->sgnd == 0 ? 0 : 1;
}
@@ -467,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;
}
}
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.
*/
diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c
index 7eeadf944..206403ba6 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++) {
@@ -1542,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},
@@ -1764,6 +1776,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},
diff --git a/tox.ini b/tox.ini
index d7948ef6d..458a00107 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,7 @@
[tox]
-minversion = 1.9
-envlist =
+requires =
+ tox>=4.2
+env_list =
lint
py{py3, 311, 310, 39, 38}
@@ -23,7 +24,7 @@ skip_install = true
deps =
check-manifest
pre-commit
-passenv =
+pass_env =
PRE_COMMIT_COLOR
commands =
pre-commit run --all-files --show-diff-on-failure
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 3f639454b..89b2daad0 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"),
@@ -337,9 +337,9 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/7.1.0.zip",
- "filename": "harfbuzz-7.1.0.zip",
- "dir": "harfbuzz-7.1.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(
@@ -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"),
],