diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 9f9136557..d87d7956f 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.1.3 +cibuildwheel==3.1.4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 626824f38..761dc1125 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: name: Docs steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8e789a734..9827ef1cd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: name: Lint steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 47f2d3f0a..30e5c494d 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -68,7 +68,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 5a83c16c3..6c4206083 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index e6a5f6e77..0f36fe30d 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -41,7 +41,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 8818b3b23..30caa0d4e 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -39,7 +39,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c80bb6eb6..d55a8e5f5 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -47,19 +47,19 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout cached dependencies - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false repository: python-pillow/test-images diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0fad22a03..b17d08892 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d217d9292..24e78f965 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -99,14 +99,14 @@ jobs: cibw_arch: arm64_iphoneos - name: "iOS arm64 simulator" platform: ios - os: macos-latest + os: macos-14 cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios os: macos-13 cibw_arch: x86_64_iphonesimulator steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false submodules: true @@ -153,12 +153,12 @@ jobs: - cibw_arch: ARM64 os: windows-11-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout extra test images - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false repository: python-pillow/test-images @@ -234,7 +234,7 @@ jobs: if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/Tests/helper.py b/Tests/helper.py index e0dc8a9d4..dbdd30b42 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -175,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator: return pytest.mark.skipif(not features.check(feature), reason=reason) +def has_feature_version(feature: str, required: str) -> bool: + version = features.version(feature) + assert version is not None + version_required = parse_version(required) + version_available = parse_version(version) + return version_available >= version_required + + def skip_unless_feature_version( feature: str, required: str, reason: str | None = None ) -> pytest.MarkDecorator: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 503761374..600448fb9 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -4,13 +4,13 @@ from collections.abc import Generator from pathlib import Path import pytest -from packaging.version import parse as parse_version -from PIL import GifImagePlugin, Image, WebPImagePlugin, features +from PIL import GifImagePlugin, Image, WebPImagePlugin from .helper import ( assert_image_equal, assert_image_similar, + has_feature_version, is_big_endian, skip_unless_feature, ) @@ -53,11 +53,8 @@ def test_write_animation_L(tmp_path: Path) -> None: im.load() assert_image_similar(im, orig.convert("RGBA"), 32.9) - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") + if is_big_endian() and not has_feature_version("webp", "1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") orig.seek(orig.n_frames - 1) im.seek(im.n_frames - 1) orig.load() @@ -81,11 +78,8 @@ def test_write_animation_RGB(tmp_path: Path) -> None: assert_image_equal(im, frame1.convert("RGBA")) # Compare second frame to original - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") + if is_big_endian() and not has_feature_version("webp", "1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") im.seek(1) im.load() assert_image_equal(im, frame2.convert("RGBA")) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 6d313cb8c..d847c7440 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,11 +1,16 @@ from __future__ import annotations import pytest -from packaging.version import parse as parse_version -from PIL import Image, features +from PIL import Image -from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature +from .helper import ( + assert_image_similar, + has_feature_version, + hopper, + is_ppc64le, + skip_unless_feature, +) def test_sanity() -> None: @@ -23,11 +28,8 @@ def test_sanity() -> None: @skip_unless_feature("libimagequant") def test_libimagequant_quantize() -> None: image = hopper() - if is_ppc64le(): - version = features.version_feature("libimagequant") - assert version is not None - if parse_version(version) < parse_version("4"): - pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") + if is_ppc64le() and not has_feature_version("libimagequant", "4"): + pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 15) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 5954de874..95af3fda8 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -7,6 +7,7 @@ from PIL import Image, ImageDraw, ImageFont from .helper import ( assert_image_equal_tofile, assert_image_similar_tofile, + has_feature_version, skip_unless_feature, ) @@ -104,11 +105,9 @@ def test_text_direction_ttb() -> None: im = Image.new(mode="RGB", size=(100, 300)) draw = ImageDraw.Draw(im) - try: - draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") target = "Tests/images/test_direction_ttb.png" assert_image_similar_tofile(im, target, 2.8) @@ -119,19 +118,17 @@ def test_text_direction_ttb_stroke() -> None: im = Image.new(mode="RGB", size=(100, 300)) draw = ImageDraw.Draw(im) - try: - draw.text( - (27, 27), - "あい", - font=ttf, - fill=500, - direction="ttb", - stroke_width=2, - stroke_fill="#0f0", - ) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + draw.text( + (27, 27), + "あい", + font=ttf, + fill=500, + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", + ) target = "Tests/images/test_direction_ttb_stroke.png" assert_image_similar_tofile(im, target, 19.4) @@ -219,14 +216,9 @@ def test_getlength( im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) - try: - assert d.textlength(text, ttf, direction) == expected - except ValueError as ex: - if ( - direction == "ttb" - and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" - ): - pytest.skip("libraqm 0.7 or greater not available") + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + assert d.textlength(text, ttf, direction) == expected @pytest.mark.parametrize("mode", ("L", "1")) @@ -242,17 +234,12 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - try: - target = ttf.getlength("ii", mode, direction) - actual = ttf.getlength(text, mode, direction) + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + target = ttf.getlength("ii", mode, direction) + actual = ttf.getlength(text, mode, direction) - assert actual == target - except ValueError as ex: - if ( - direction == "ttb" - and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" - ): - pytest.skip("libraqm 0.7 or greater not available") + assert actual == target @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) @@ -265,11 +252,9 @@ def test_anchor_ttb(anchor: str) -> None: d = ImageDraw.Draw(im) d.line(((0, 200), (200, 200)), "gray") d.line(((100, 0), (100, 400)), "gray") - try: - d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) assert_image_similar_tofile(im, path, 1) # fails at 5 @@ -310,10 +295,12 @@ combine_tests = ( # this tests various combining characters for anchor alignment and clipping @pytest.mark.parametrize( - "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] + "name, text, anchor, direction, epsilon", + combine_tests, + ids=[r[0] for r in combine_tests], ) def test_combine( - name: str, text: str, dir: str | None, anchor: str | None, epsilon: float + name: str, text: str, direction: str | None, anchor: str | None, epsilon: float ) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -322,11 +309,9 @@ def test_combine( d = ImageDraw.Draw(im) d.line(((0, 200), (400, 200)), "gray") d.line(((200, 0), (200, 400)), "gray") - try: - d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + d.text((200, 200), text, fill="black", anchor=anchor, direction=direction, font=f) assert_image_similar_tofile(im, path, epsilon) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index ef54deeeb..f6acb3aff 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -28,15 +28,13 @@ def test_numpy_to_image() -> None: a = numpy.array(data, dtype=dtype) a.shape = TEST_IMAGE_SIZE i = Image.fromarray(a) - if list(i.getdata()) != data: - print("data mismatch for", dtype) + assert list(i.getdata()) == data else: data = list(range(100)) a = numpy.array([[x] * bands for x in data], dtype=dtype) a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands i = Image.fromarray(a) - if list(i.getchannel(0).getdata()) != list(range(100)): - print("data mismatch for", dtype) + assert list(i.getchannel(0).getdata()) == list(range(100)) return i # Check supported 1-bit integer formats diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 043559352..4c34ff812 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -74,5 +74,6 @@ Constants --------- .. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES +.. autodata:: PIL.ImageFile.MAXBLOCK .. autodata:: PIL.ImageFile.ERRORS :annotation: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7c185e0d..683c80762 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -103,7 +103,6 @@ try: raise ImportError(msg) except ImportError as v: - core = DeferredError.new(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 27b27127e..e33b846d4 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -46,6 +46,18 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) MAXBLOCK = 65536 +""" +By default, Pillow processes image data in blocks. This helps to prevent excessive use +of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``. + +When reading an image, this is the number of bytes to read at once. + +When writing an image, this is the number of bytes to write at once. +If the image width times 4 is greater, then that will be used instead. +Plugins may also set a greater number. + +User code may set this to another number. +""" SAFEBLOCK = 1024 * 1024 diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b801186c6..9bc215869 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -681,11 +681,7 @@ class FreeTypeFont: :returns: A list of the named styles in a variation font. :exception OSError: If the font is not a variation font. """ - try: - names = self.font.getvarnames() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + names = self.font.getvarnames() return [name.replace(b"\x00", b"") for name in names] def set_variation_by_name(self, name: str | bytes) -> None: @@ -712,11 +708,7 @@ class FreeTypeFont: :returns: A list of the axes in a variation font. :exception OSError: If the font is not a variation font. """ - try: - axes = self.font.getvaraxes() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + axes = self.font.getvaraxes() for axis in axes: if axis["name"]: axis["name"] = axis["name"].replace(b"\x00", b"") @@ -727,11 +719,7 @@ class FreeTypeFont: :param axes: A list of values for each axis. :exception OSError: If the font is not a variation font. """ - try: - self.font.setvaraxes(axes) - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + self.font.setvaraxes(axes) class TransposedFont: diff --git a/src/_imagingft.c b/src/_imagingft.c index 5e1192620..7978bb40f 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1259,8 +1259,6 @@ glyph_error: return NULL; } -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) static PyObject * font_getvarnames(FontObject *self) { int error; @@ -1470,7 +1468,6 @@ font_setvaraxes(FontObject *self, PyObject *args) { Py_RETURN_NONE; } -#endif static void font_dealloc(FontObject *self) { @@ -1490,13 +1487,10 @@ static PyMethodDef font_methods[] = { {"getsize", (PyCFunction)font_getsize, METH_VARARGS}, {"getlength", (PyCFunction)font_getlength, METH_VARARGS}, {"hascharacters", (PyCFunction)font_hascharacters, METH_VARARGS}, -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, -#endif {NULL, NULL} };