diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 0e314b8bf..520b6e320 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.23.3 +cibuildwheel==3.0.0 diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 2e3610478..a9c18ae2b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,9 +1,10 @@ -mypy==1.15.0 +mypy==1.16.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython numpy packaging +pyarrow-stubs pytest sphinx types-atheris diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml new file mode 100644 index 000000000..e6a5f6e77 --- /dev/null +++ b/.github/workflows/test-valgrind-memory.yml @@ -0,0 +1,60 @@ +name: Test Valgrind Memory Leaks + +# like the Docker tests, but running valgrind only on *.c/*.h changes. + +# this is very expensive. Only run on the pull request. +on: + # push: + # branches: + # - "**" + # paths: + # - ".github/workflows/test-valgrind.yml" + # - "**.c" + # - "**.h" + pull_request: + paths: + - ".github/workflows/test-valgrind.yml" + - "**.c" + - "**.h" + - "depends/docker-test-valgrind-memory.sh" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + docker: [ + ubuntu-22.04-jammy-amd64-valgrind, + ] + dockerTag: [main] + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Docker pull + run: | + docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Build and Run Valgrind + run: | + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE + docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh + sudo chown -R runner $GITHUB_WORKSPACE diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bfa4c7cd3..6d8acc44f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -31,16 +31,15 @@ env: jobs: build: - runs-on: ${{ matrix.os }} + runs-on: windows-latest strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] architecture: ["x64"] - os: ["windows-latest"] include: # Test the oldest Python on 32-bit - - { python-version: "3.9", architecture: "x86", os: "windows-2019" } + - { python-version: "3.9", architecture: "x86" } timeout-minutes: 45 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 006d574f3..b4b516228 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,6 +43,7 @@ jobs: python-version: [ "pypy3.11", "pypy3.10", + "3.14t", "3.14", "3.13t", "3.13", @@ -55,6 +56,7 @@ jobs: - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.10", PYTHONOPTIMIZE: 2 } # Free-threaded + - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } # M1 only available for 3.10+ - { os: "macos-13", python-version: "3.9" } diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 60e96b9c2..0139a5215 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 LIBPNG_VERSION=1.6.48 -JPEGTURBO_VERSION=3.1.0 +JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index a1edc14ef..54e7fbbfc 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -9,17 +9,21 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null } $env:path += ";$pillow\winbuild\build\bin\" -& "$venv\Scripts\activate.ps1" +if (Test-Path $venv\Scripts\pypy.exe) { + $python = "pypy.exe" +} else { + $python = "python.exe" +} & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f if ("$venv" -like "*\cibw-run-*-win_amd64\*") { - & python -m pip install numpy + & $venv\Scripts\$python -m pip install numpy } cd $pillow -& python -VV +& $venv\Scripts\$python -VV if (!$?) { exit $LASTEXITCODE } -& python selftest.py +& $venv\Scripts\$python selftest.py if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests\check_wheel.py +& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests +& $venv\Scripts\$python -m pytest -vx Tests if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9b05e3a22..c67ab4997 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -110,7 +110,6 @@ jobs: CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} - CIBW_SKIP: pp39-* MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - uses: actions/upload-artifact@v4 @@ -188,7 +187,6 @@ jobs: CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy - CIBW_SKIP: pp39-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e15e6f639..a1a054e00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 + rev: v0.11.12 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.3 + rev: v20.1.5 hooks: - id: clang-format types: [c] @@ -58,7 +58,7 @@ repos: - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.6.0 + rev: v1.9.0 hooks: - id: zizmor @@ -68,7 +68,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.5.1 + rev: v2.6.0 hooks: - id: pyproject-fmt diff --git a/Makefile b/Makefile index 5a8152454..ce780acbc 100644 --- a/Makefile +++ b/Makefile @@ -97,13 +97,27 @@ test: python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest python3 -m pytest -qq +.PHONY: test-p +test-p: + python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist + python3 -m pytest -qq -n auto + + .PHONY: valgrind valgrind: python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output +.PHONY: valgrind-leak +valgrind-leak: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind + PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ + --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output + .PHONY: readme readme: python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 582fc92c2..1e331cfe7 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -12,13 +12,14 @@ from .helper import is_pypy def test_wheel_modules() -> None: expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} - # tkinter is not available in cibuildwheel installed CPython on Windows - try: - import tkinter + if sys.platform == "win32": + # tkinter is not available in cibuildwheel installed CPython on Windows + try: + import tkinter - assert tkinter - except ImportError: - expected_modules.remove("tkinter") + assert tkinter + except ImportError: + expected_modules.remove("tkinter") # libavif is not available on Windows for x86 and ARM64 architectures if sys.platform == "win32": diff --git a/Tests/helper.py b/Tests/helper.py index 909fff879..ec61cd263 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -161,6 +161,12 @@ def assert_tuple_approx_equal( pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) +def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator: + if "PILLOW_VALGRIND_TEST" in os.environ: + return pytest.mark.pil_noop_mark() + return pytest.mark.timeout(timeout) + + def skip_unless_feature(feature: str) -> pytest.MarkDecorator: reason = f"{feature} not available" return pytest.mark.skipif(not features.check(feature), reason=reason) diff --git a/Tests/images/imagedraw_rectangle_I.tiff b/Tests/images/imagedraw_rectangle_I.tiff index 9b9eda883..f0cb534b6 100644 Binary files a/Tests/images/imagedraw_rectangle_I.tiff and b/Tests/images/imagedraw_rectangle_I.tiff differ diff --git a/Tests/images/op_index.qoi b/Tests/images/op_index.qoi new file mode 100644 index 000000000..e626aafe6 Binary files /dev/null and b/Tests/images/op_index.qoi differ diff --git a/Tests/images/p_4_planes.pcx b/Tests/images/p_4_planes.pcx new file mode 100644 index 000000000..8c5743a98 Binary files /dev/null and b/Tests/images/p_4_planes.pcx differ diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 36385d672..4803497ad 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -14,3 +14,23 @@ fun:_TIFFReadEncodedTileAndAllocBuffer ... } + +{ + + Memcheck:Leak + match-leak-kinds: all + fun:malloc + fun:_PyMem_RawMalloc + fun:PyObject_Malloc + ... +} + +{ + + Memcheck:Leak + match-leak-kinds: all + fun:malloc + fun:_PyMem_RawRealloc + fun:PyMem_Realloc + ... +} diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 82ff14181..88479ff0d 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -47,7 +47,6 @@ def test_unknown_version() -> None: ], ) def test_old_version(deprecated: str, plural: bool, expected: str) -> None: - expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index b484a8cfa..d94de7287 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -15,6 +15,7 @@ from .helper import ( is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() @@ -398,7 +399,7 @@ def test_emptyline() -> None: assert image.format == "EPS" -@pytest.mark.timeout(timeout=5) +@timeout_unless_slower_valgrind(5) @pytest.mark.parametrize( "test_file", ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 81df1ab0b..0fadd01d0 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -7,7 +7,12 @@ import pytest from PIL import FliImagePlugin, Image, ImageFile -from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + is_pypy, + timeout_unless_slower_valgrind, +) # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. @@ -189,7 +194,7 @@ def test_seek() -> None: "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", ], ) -@pytest.mark.timeout(timeout=3) +@timeout_unless_slower_valgrind(3) def test_timeouts(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 79f0ec1a8..2827937cf 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -32,6 +32,7 @@ from .helper import ( is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -144,14 +145,16 @@ class TestFileJpeg: assert k > 0.9 # roundtrip, and check again im = self.roundtrip(im) - c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + cmyk = im.getpixel((0, 0)) + assert isinstance(cmyk, tuple) + c, m, y, k = (x / 255.0 for x in cmyk) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) + cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) + assert isinstance(cmyk, tuple) + k = cmyk[3] / 255.0 assert k > 0.9 def test_rgb(self) -> None: @@ -1033,7 +1036,7 @@ class TestFileJpeg: with pytest.raises(ValueError): im.save(f, xmp=b"1" * 65505) - @pytest.mark.timeout(timeout=1) + @timeout_unless_slower_valgrind(1) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 5d7fd1c1b..2e999eff6 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -37,6 +37,11 @@ def test_sanity(tmp_path: Path) -> None: im.save(f) +def test_p_4_planes() -> None: + with Image.open("Tests/images/p_4_planes.pcx") as im: + assert im.getpixel((0, 0)) == 3 + + def test_bad_image_size() -> None: with open("Tests/images/pil184.pcx", "rb") as fp: data = fp.read() diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index bde1e3ab8..a2218673b 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -13,7 +13,12 @@ import pytest from PIL import Image, PdfParser, features -from .helper import hopper, mark_if_feature_version, skip_unless_feature +from .helper import ( + hopper, + mark_if_feature_version, + skip_unless_feature, + timeout_unless_slower_valgrind, +) def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: @@ -339,8 +344,7 @@ def test_pdf_append_to_bytesio() -> None: assert len(f.getvalue()) > initial_size -@pytest.mark.timeout(1) -@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") +@timeout_unless_slower_valgrind(1) @pytest.mark.parametrize("newline", (b"\r", b"\n")) def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 41e2b5416..c7d1f4df4 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -288,12 +288,13 @@ def test_non_integer_token(tmp_path: Path) -> None: pass -def test_header_token_too_long(tmp_path: Path) -> None: +@pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890")) +def test_header_token_too_long(tmp_path: Path, data: bytes) -> None: path = tmp_path / "temp.ppm" with open(path, "wb") as f: - f.write(b"P6\n 01234567890") + f.write(data) - with pytest.raises(ValueError, match="Token too long in file header: 01234567890"): + with pytest.raises(ValueError, match="Token too long in file header: "): with Image.open(path): pass diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index fd4b981ce..7ce1da209 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -28,3 +28,9 @@ def test_invalid_file() -> None: with pytest.raises(SyntaxError): QoiImagePlugin.QoiImageFile(invalid_file) + + +def test_op_index() -> None: + # QOI_OP_INDEX as the first chunk + with Image.open("Tests/images/op_index.qoi") as im: + assert im.getpixel((0, 0)) == (0, 0, 0, 0) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 502d9df9a..73046eb5f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -14,6 +14,7 @@ from PIL import ( ImageFile, JpegImagePlugin, TiffImagePlugin, + TiffTags, UnidentifiedImageError, ) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -26,6 +27,7 @@ from .helper import ( hopper, is_pypy, is_win32, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -899,6 +901,29 @@ class TestFileTiff: assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_getxmp_undefined(self, tmp_path: Path) -> None: + tmpfile = tmp_path / "temp.tif" + im = Image.new("L", (1, 1)) + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd.tagtype[700] = TiffTags.UNDEFINED + with Image.open("Tests/images/lab.tif") as im_xmp: + ifd[700] = im_xmp.info["xmp"] + im.save(tmpfile, tiffinfo=ifd) + + with Image.open(tmpfile) as im_reloaded: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im_reloaded.getxmp() == {} + else: + assert "xmp" in im_reloaded.info + xmp = im_reloaded.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) @@ -988,7 +1013,7 @@ class TestFileTiff: with pytest.raises(OSError): im.load() - @pytest.mark.timeout(6) + @timeout_unless_slower_valgrind(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: @@ -1001,7 +1026,7 @@ class TestFileTiff: "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", ], ) - @pytest.mark.timeout(2) + @timeout_unless_slower_valgrind(2) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): diff --git a/Tests/test_image.py b/Tests/test_image.py index 7e6118d52..b018b4309 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -34,6 +34,7 @@ from .helper import ( is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -572,10 +573,7 @@ class TestImage: i = Image.new("RGB", [1, 1]) assert isinstance(i.size, tuple) - @pytest.mark.timeout(0.75) - @pytest.mark.skipif( - "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" - ) + @timeout_unless_slower_valgrind(0.75) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) def test_empty_image(self, size: tuple[int, int]) -> None: Image.new("RGB", size) @@ -673,6 +671,7 @@ class TestImage: im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) assert im.palette is not None + assert im_remapped.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode @@ -975,6 +974,11 @@ class TestImage: assert tag not in exif.get_ifd(0x8769) assert exif.get_ifd(0xA005) + def test_exif_from_xmp_bytes(self) -> None: + im = Image.new("RGB", (1, 1)) + im.info["xmp"] = b'\xff tiff:Orientation="2"' + assert im.getexif()[274] == 2 + def test_empty_xmp(self) -> None: with Image.open("Tests/images/hopper.gif") as im: if ElementTree is None: @@ -991,7 +995,7 @@ class TestImage: im = Image.new("RGB", (1, 1)) im.info["xmp"] = ( b'\n' - b'\n\x00\x00' + b'\n\x00\x00 ' ) if ElementTree is None: with pytest.warns( diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ffe9c0979..37669a2e5 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -783,9 +783,10 @@ def test_rectangle_I16(bbox: Coords) -> None: draw = ImageDraw.Draw(im) # Act - draw.rectangle(bbox, outline=0xFFFF) + draw.rectangle(bbox, outline=0xCDEF) # Assert + assert im.getpixel((X0, Y0)) == 0xCDEF assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 695aecbde..3eb98d379 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -7,7 +7,7 @@ import pytest from PIL import Image, ImageDraw, ImageFont, _util, features -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal_tofile, timeout_unless_slower_valgrind fonts = [ImageFont.load_default_imagefont()] if not features.check_module("freetype2"): @@ -72,7 +72,7 @@ def test_decompression_bomb() -> None: font.getmask("A" * 1_000_000) -@pytest.mark.timeout(4) +@timeout_unless_slower_valgrind(4) def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index ece9f8f26..8dad94fe0 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any # undone +from typing import Any, NamedTuple import pytest @@ -10,30 +10,73 @@ from .helper import ( assert_deep_equal, assert_image_equal, hopper, + is_big_endian, ) -pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") +TYPE_CHECKING = False +if TYPE_CHECKING: + import pyarrow +else: + pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") TEST_IMAGE_SIZE = (10, 10) def _test_img_equals_pyarray( - img: Image.Image, arr: Any, mask: list[int] | None + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 ) -> None: - assert img.height * img.width == len(arr) + assert img.height * img.width * elts_per_pixel == len(arr) px = img.load() assert px is not None + if elts_per_pixel > 1 and mask is None: + # have to do element-wise comparison when we're comparing + # flattened r,g,b,a to a pixel. + mask = list(range(elts_per_pixel)) for x in range(0, img.size[0], int(img.size[0] / 10)): for y in range(0, img.size[1], int(img.size[1] / 10)): if mask: + pixel = px[x, y] + assert isinstance(pixel, tuple) for ix, elt in enumerate(mask): - pixel = px[x, y] - assert isinstance(pixel, tuple) - assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + if elts_per_pixel == 1: + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert ( + pixel[ix] + == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + ) else: assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) +def _test_img_equals_int32_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if mask is None: + # have to do element-wise comparison when we're comparing + # flattened rgba in an uint32 to a pixel. + mask = list(range(elts_per_pixel)) + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + pixel = px[x, y] + assert isinstance(pixel, tuple) + arr_pixel_int = arr[y * img.width + x].as_py() + arr_pixel_tuple = ( + arr_pixel_int % 256, + (arr_pixel_int // 256) % 256, + (arr_pixel_int // 256**2) % 256, + (arr_pixel_int // 256**3), + ) + if is_big_endian(): + arr_pixel_tuple = arr_pixel_tuple[::-1] + + for ix, elt in enumerate(mask): + assert pixel[ix] == arr_pixel_tuple[elt] + + # really hard to get a non-nullable list type fl_uint8_4_type = pyarrow.field( "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) @@ -55,14 +98,14 @@ fl_uint8_4_type = pyarrow.field( ("HSV", fl_uint8_4_type, [0, 1, 2]), ), ) -def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None: +def test_to_array(mode: str, dtype: pyarrow.DataType, mask: list[int] | None) -> None: img = hopper(mode) # Resize to non-square img = img.crop((3, 0, 124, 127)) assert img.size == (121, 127) - arr = pyarrow.array(img) + arr = pyarrow.array(img) # type: ignore[call-overload] _test_img_equals_pyarray(img, arr, mask) assert arr.type == dtype @@ -79,8 +122,8 @@ def test_lifetime() -> None: img = hopper("L") - arr_1 = pyarrow.array(img) - arr_2 = pyarrow.array(img) + arr_1 = pyarrow.array(img) # type: ignore[call-overload] + arr_2 = pyarrow.array(img) # type: ignore[call-overload] del img @@ -97,8 +140,8 @@ def test_lifetime2() -> None: img = hopper("L") - arr_1 = pyarrow.array(img) - arr_2 = pyarrow.array(img) + arr_1 = pyarrow.array(img) # type: ignore[call-overload] + arr_2 = pyarrow.array(img) # type: ignore[call-overload] assert arr_1.sum().as_py() > 0 del arr_1 @@ -110,3 +153,94 @@ def test_lifetime2() -> None: px = img2.load() assert px # make mypy happy assert isinstance(px[0, 0], int) + + +class DataShape(NamedTuple): + dtype: pyarrow.DataType + # Strictly speaking, elt should be a pixel or pixel component, so + # list[uint8][4], float, int, uint32, uint8, etc. But more + # correctly, it should be exactly the dtype from the line above. + elt: Any + elts_per_pixel: int + + +UINT_ARR = DataShape( + dtype=fl_uint8_4_type, + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel + elts_per_pixel=1, # only one array per pixel +) + +UINT = DataShape( + dtype=pyarrow.uint8(), + elt=3, # one uint8, + elts_per_pixel=4, # but repeated 4x per pixel +) + +UINT32 = DataShape( + dtype=pyarrow.uint32(), + elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000 + elts_per_pixel=1, # one per pixel +) + +INT32 = DataShape( + dtype=pyarrow.uint32(), + elt=0x12CDEF45, # one packed int + elts_per_pixel=1, # one per pixel +) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("L", DataShape(pyarrow.uint8(), 3, 1), None), + ("I", DataShape(pyarrow.int32(), 1 << 24, 1), None), + ("F", DataShape(pyarrow.float32(), 3.14159, 1), None), + ("LA", UINT_ARR, [0, 3]), + ("LA", UINT, [0, 3]), + ("RGB", UINT_ARR, [0, 1, 2]), + ("RGBA", UINT_ARR, None), + ("CMYK", UINT_ARR, None), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), + ("RGB", UINT, [0, 1, 2]), + ("RGBA", UINT, None), + ("CMYK", UINT, None), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), + ), +) +def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("LA", UINT32, [0, 3]), + ("RGB", UINT32, [0, 1, 2]), + ("RGBA", UINT32, None), + ("CMYK", UINT32, None), + ("YCbCr", UINT32, [0, 1, 2]), + ("HSV", UINT32, [0, 1, 2]), + ("LA", INT32, [0, 3]), + ("RGB", INT32, [0, 1, 2]), + ("RGBA", INT32, None), + ("CMYK", INT32, None), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), + ), +) +def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 073e5415c..976f62384 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -52,3 +52,17 @@ def test_tiff_crashes(test_file: str) -> None: pytest.skip("test image not found") except OSError: pass + + +def test_tiff_mmap() -> None: + try: + with Image.open("Tests/images/crash_mmap.tif") as im: + im.seek(1) + im.load() + + im.seek(0) + im.load() + except FileNotFoundError: + if on_ci(): + raise + pytest.skip("test image not found") diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh new file mode 100755 index 000000000..f0d1d851d --- /dev/null +++ b/depends/docker-test-valgrind-memory.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +## Run this as the test script in the Docker valgrind image. +## Note -- can be included directly into the Docker image, +## but requires the current python.supp. + +source /vpy3/bin/activate +cd /Pillow +make clean +make install +make valgrind-leak diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index c72568b20..8988a92ce 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -194,9 +194,9 @@ Many of Pillow's features require external libraries: pacman -S \ mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools + mingw-w64-x86_64-python \ + mingw-w64-x86_64-python-pip \ + mingw-w64-x86_64-python-setuptools Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 93486d034..a56f94316 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -40,18 +40,20 @@ These platforms are built and tested for every change. | macOS 13 Ventura | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | -| | PyPy3 | | +| | 3.14, PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, | -| | | ppc64le, s390x | +| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, 3.13, 3.14, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.12 | arm64v8, ppc64le, | +| | | s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2019 | 3.9 | x86 | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 | -| | PyPy3 | | +| Windows Server 2022 | 3.9 | x86 | +| +----------------------------+---------------------+ +| | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| | 3.14, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 (MinGW) | x86-64 | | +----------------------------+---------------------+ diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 8b2f92323..aac55fe6b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -18,6 +18,9 @@ OpenType fonts (as well as other font formats supported by the FreeType library). For earlier versions, TrueType support is only available as part of the imToolkit package. +When measuring text sizes, this module will not break at newline characters. For +multiline text, see the :py:mod:`~PIL.ImageDraw` module. + .. warning:: To protect against potential DOS attacks when using arbitrary strings as text input, Pillow will raise a :py:exc:`ValueError` if the number of characters diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst new file mode 100644 index 000000000..b0595def9 --- /dev/null +++ b/docs/releasenotes/11.3.0.rst @@ -0,0 +1,56 @@ +11.3.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +TODO +^^^^ + +TODO + +Other changes +============= + +Python 3.14 beta +^^^^^^^^^^^^^^^^ + +To help other projects prepare for Python 3.14, wheels are now built for the +3.14 beta as a preview. This is not official support for Python 3.14, but rather +an opportunity for you to test how Pillow works with the beta and report any +problems. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 5d7b21d59..a85f1e075 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 11.3.0 11.2.1 11.1.0 11.0.0 diff --git a/pyproject.toml b/pyproject.toml index a3ff9723b..683ab24ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ optional-dependencies.tests = [ "pytest", "pytest-cov", "pytest-timeout", + "pytest-xdist", "trove-classifiers>=2024.10.12", ] diff --git a/setup.py b/setup.py index ab36c6b17..3716a7b9f 100644 --- a/setup.py +++ b/setup.py @@ -163,7 +163,7 @@ def _find_library_dirs_ldconfig() -> list[str]: args: list[str] env: dict[str, str] expr: str - if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): + if sys.platform.startswith(("linux", "gnu")): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" else: @@ -623,11 +623,7 @@ class pil_build_ext(build_ext): for extension in self.extensions: extension.extra_compile_args = ["-Wno-nullability-completeness"] - elif ( - sys.platform.startswith("linux") - or sys.platform.startswith("gnu") - or sys.platform.startswith("freebsd") - ): + elif sys.platform.startswith(("linux", "gnu", "freebsd")): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 26307817c..f9ade18f9 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -1,5 +1,5 @@ """ -A Pillow loader for .dds files (S3TC-compressed aka DXTC) +A Pillow plugin for .dds files (S3TC-compressed aka DXTC) Jerome Leclanche Documentation: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4392c4cb9..c98e02f69 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, Any, Literal, NamedTuple, Union +from typing import IO, Any, Literal, NamedTuple, Union, cast from . import ( Image, @@ -350,12 +350,15 @@ class GifImageFile(ImageFile.ImageFile): if self._frame_palette: if color * 3 + 3 > len(self._frame_palette.palette): color = 0 - return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + return cast( + tuple[int, int, int], + tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), + ) else: return (color, color, color) self.dispose = None - self.dispose_extent = frame_dispose_extent + self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent if self.dispose_extent and self.disposal_method >= 2: try: if self.disposal_method == 2: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ded40bc5d..7e9540e48 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -767,18 +767,20 @@ class Image: .. warning:: - This method returns the raw image data from the internal - storage. For compressed image data (e.g. PNG, JPEG) use - :meth:`~.save`, with a BytesIO parameter for in-memory - data. + This method returns raw image data derived from Pillow's internal + storage. For compressed image data (e.g. PNG, JPEG) use + :meth:`~.save`, with a BytesIO parameter for in-memory data. - :param encoder_name: What encoder to use. The default is to - use the standard "raw" encoder. + :param encoder_name: What encoder to use. - A list of C encoders can be seen under - codecs section of the function array in - :file:`_imaging.c`. Python encoders are - registered within the relevant plugins. + The default is to use the standard "raw" encoder. + To see how this packs pixel data into the returned + bytes, see :file:`libImaging/Pack.c`. + + A list of C encoders can be seen under codecs + section of the function array in + :file:`_imaging.c`. Python encoders are registered + within the relevant plugins. :param args: Extra arguments to the encoder. :returns: A :py:class:`bytes` object. """ @@ -800,7 +802,9 @@ class Image: e = _getencoder(self.mode, encoder_name, encoder_args) e.setimage(self.im) - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c + from . import ImageFile + + bufsize = max(ImageFile.MAXBLOCK, self.size[0] * 4) # see RawEncode.c output = [] while True: @@ -1507,7 +1511,7 @@ class Image: return {} if "xmp" not in self.info: return {} - root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00")) + root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 ")) return {get_name(root.tag): get_value(root)} def getexif(self) -> Exif: @@ -1538,10 +1542,11 @@ class Image: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") + pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' if not xmp_tags and (xmp_tags := self.info.get("xmp")): - xmp_tags = xmp_tags.decode("utf-8") + pattern = rb'tiff:Orientation(="|>)([0-9])' if xmp_tags: - match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) + match = re.search(pattern, xmp_tags) if match: self._exif[ExifTags.Base.Orientation] = int(match[2]) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e6c7b0298..98ae67539 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -365,22 +365,10 @@ class ImageDraw: # use the fill as a mask mask = Image.new("1", self.im.size) mask_ink = self._getink(1)[0] - - fill_im = mask.copy() - draw = Draw(fill_im) + draw = Draw(mask) draw.draw.draw_polygon(xy, mask_ink, 1) - ink_im = mask.copy() - draw = Draw(ink_im) - width = width * 2 - 1 - draw.draw.draw_polygon(xy, mask_ink, 0, width) - - mask.paste(ink_im, mask=fill_im) - - im = Image.new(self.mode, self.im.size) - draw = Draw(im) - draw.draw.draw_polygon(xy, ink, 0, width) - self.im.paste(im.im, (0, 0) + im.size, mask.im) + self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im) def regular_polygon( self, diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index c29350b7a..1eb450734 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -134,10 +134,10 @@ def grabclipboard() -> Image.Image | list[str] | None: import struct o = struct.unpack_from("I", data)[0] - if data[16] != 0: - files = data[o:].decode("utf-16le").split("\0") - else: + if data[16] == 0: files = data[o:].decode("mbcs").split("\0") + else: + files = data[o:].decode("utf-16le").split("\0") return files[: files.index("")] if isinstance(data, bytes): data = io.BytesIO(data) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 969528841..defe9f773 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -762,8 +762,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - xmp = info.get("xmp") - if xmp: + if xmp := info.get("xmp"): overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len if len(xmp) > max_data_bytes_in_marker: @@ -772,8 +771,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: size = o16(2 + overhead_len + len(xmp)) extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp - icc_profile = info.get("icc_profile") - if icc_profile: + if icc_profile := info.get("icc_profile"): overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len markers = [] @@ -831,7 +829,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is # channels*size, this is a value that's been used in a django patch. # https://github.com/matthewwithanm/django-imagekit/issues/50 - bufsize = 0 if optimize or progressive: # CMYK can be bigger if im.mode == "CMYK": @@ -848,7 +845,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: else: # The EXIF info needs to be written as one block, + APP1, + one spare byte. # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) + bufsize = max(len(exif) + 5, len(extra) + 1) ImageFile._save( im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 5aa00d05b..47ebe9d62 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -33,11 +33,7 @@ class BitStream: def peek(self, bits: int) -> int: while self.bits < bits: - c = self.next() - if c < 0: - self.bits = 0 - continue - self.bitbuffer = (self.bitbuffer << 8) + c + self.bitbuffer = (self.bitbuffer << 8) + self.next() self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 299405ae0..458d586c4 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -54,7 +54,7 @@ class PcxImageFile(ImageFile.ImageFile): # header assert self.fp is not None - s = self.fp.read(128) + s = self.fp.read(68) if not _accept(s): msg = "not a PCX file" raise SyntaxError(msg) @@ -66,6 +66,8 @@ class PcxImageFile(ImageFile.ImageFile): raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) + offset = self.fp.tell() + 60 + # format version = s[1] bits = s[3] @@ -102,7 +104,6 @@ class PcxImageFile(ImageFile.ImageFile): break if mode == "P": self.palette = ImagePalette.raw("RGB", s[1:]) - self.fp.seek(128) elif version == 5 and bits == 8 and planes == 3: mode = "RGB" @@ -128,9 +129,7 @@ class PcxImageFile(ImageFile.ImageFile): bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) - self.tile = [ - ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride)) - ] + self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))] # -------------------------------------------------------------------- diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 03afa2d2e..db34d107a 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -94,8 +94,8 @@ class PpmImageFile(ImageFile.ImageFile): msg = "Reached EOF while reading header" raise ValueError(msg) elif len(token) > 10: - msg = f"Token too long in file header: {token.decode()}" - raise ValueError(msg) + msg_too_long = b"Token too long in file header: %s" % token + raise ValueError(msg_too_long) return token def _open(self) -> None: diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index df552243e..75070abd7 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -51,7 +51,7 @@ class QoiDecoder(ImageFile.PyDecoder): assert self.fd is not None self._previously_seen_pixels = {} - self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) + self._previous_pixel = bytearray((0, 0, 0, 255)) data = bytearray() bands = Image.getmodebands(self.mode) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e..946fbd531 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1217,9 +1217,10 @@ class TiffImageFile(ImageFile.ImageFile): return self._seek(frame) if self._im is not None and ( - self.im.size != self._tile_size or self.im.mode != self.mode + self.im.size != self._tile_size + or self.im.mode != self.mode + or self.readonly ): - # The core image will no longer be used self._im = None def _seek(self, frame: int) -> None: @@ -1259,7 +1260,10 @@ class TiffImageFile(ImageFile.ImageFile): self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) if XMP in self.tag_v2: - self.info["xmp"] = self.tag_v2[XMP] + xmp = self.tag_v2[XMP] + if isinstance(xmp, tuple) and len(xmp) == 1: + xmp = xmp[0] + self.info["xmp"] = xmp elif "xmp" in self.info: del self.info["xmp"] self._reload_exif() diff --git a/src/_imaging.c b/src/_imaging.c index 72f122143..2a7bc8d3f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -308,9 +308,9 @@ _new_arrow(PyObject *self, PyObject *args) { } // ImagingBorrowArrow is responsible for retaining the array_capsule - ret = - PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) - ); + ret = PyImagingNew( + ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) + ); if (!ret) { return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); } @@ -1665,7 +1665,8 @@ _putdata(ImagingObject *self, PyObject *args) { int bigendian = 0; if (image->type == IMAGING_TYPE_SPECIAL) { // I;16* - if (strcmp(image->mode, "I;16B") == 0 + if ( + strcmp(image->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(image->mode, "I;16N") == 0 #endif @@ -2226,6 +2227,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) { } if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { + ImagingDelete(imOut); return NULL; } @@ -3218,7 +3220,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { (int)p[3], &ink, width, - self->blend + self->blend, + NULL ) < 0) { free(xy); return NULL; @@ -3356,7 +3359,10 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { int ink; int fill = 0; int width = 0; - if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple( + args, "Oi|iiO!", &data, &ink, &fill, &width, &Imaging_Type, &maskp + )) { return NULL; } @@ -3386,8 +3392,16 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { free(xy); - if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < - 0) { + if (ImagingDrawPolygon( + self->image->image, + n, + ixy, + &ink, + fill, + width, + self->blend, + maskp ? maskp->image : NULL + ) < 0) { free(ixy); return NULL; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 62dab73e5..0d70544a5 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -275,6 +275,7 @@ text_layout_raqm( if (!text || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ + PyMem_Free(text); goto failed; } set_text = raqm_set_text(rq, text, size); @@ -425,6 +426,7 @@ text_layout_fallback( "setting text direction, language or font features is not supported " "without libraqm" ); + return 0; } if (PyUnicode_Check(string)) { diff --git a/src/_webp.c b/src/_webp.c index 3aa4c408b..a7809c40e 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -641,6 +641,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ImagingSectionLeave(&cookie); WebPPictureFree(&pic); + + output = writer.mem; + ret_size = writer.size; + if (!ok) { int error_code = (&pic)->error_code; char message[50] = ""; @@ -652,10 +656,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); + free(output); return NULL; } - output = writer.mem; - ret_size = writer.size; { /* I want to truncate the *_size items that get passed into WebP diff --git a/src/display.c b/src/display.c index 11742a895..3215f6691 100644 --- a/src/display.c +++ b/src/display.c @@ -327,11 +327,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { // added in Windows 10 (1607) // loaded dynamically to avoid link errors user32 = LoadLibraryA("User32.dll"); - SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext - )GetProcAddress(user32, "SetThreadDpiAwarenessContext"); + SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext) + GetProcAddress(user32, "SetThreadDpiAwarenessContext"); if (SetThreadDpiAwarenessContext_function != NULL) { - GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext - )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); + GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext) + GetProcAddress(user32, "GetWindowDpiAwarenessContext"); if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) { dpiAwareness = GetWindowDpiAwarenessContext_function(wnd); } diff --git a/src/encode.c b/src/encode.c index 7c365a74f..e56494036 100644 --- a/src/encode.c +++ b/src/encode.c @@ -703,6 +703,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { return NULL; } + encoder->cleanup = ImagingLibTiffEncodeCleanup; + num_core_tags = sizeof(core_tags) / sizeof(int); for (pos = 0; pos < tags_size; pos++) { item = PyList_GetItemRef(tags, pos); diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 33ff2ce77..ccafe33b9 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -36,7 +36,10 @@ ReleaseExportedSchema(struct ArrowSchema *array) { child->release(child); child->release = NULL; } - // UNDONE -- should I be releasing the children? + free(array->children[i]); + } + if (array->children) { + free(array->children); } // Release dictionary @@ -98,7 +101,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { } /* for now, single block images */ - if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + if (im->blocks_count > 1) { return IMAGING_ARROW_MEMORY_LAYOUT; } @@ -117,6 +120,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); if (retval != 0) { free(schema->children[0]); + free(schema->children); schema->release(schema); return retval; } @@ -127,9 +131,7 @@ static void release_const_array(struct ArrowArray *array) { Imaging im = (Imaging)array->private_data; - if (array->n_children == 0) { - ImagingDelete(im); - } + ImagingDelete(im); // Free the buffers and the buffers array if (array->buffers) { @@ -157,7 +159,7 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) { int length = im->xsize * im->ysize; /* for now, single block images */ - if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + if (im->blocks_count > 1) { return IMAGING_ARROW_MEMORY_LAYOUT; } @@ -200,7 +202,7 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { int length = im->xsize * im->ysize; /* for now, single block images */ - if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + if (im->blocks_count > 1) { return IMAGING_ARROW_MEMORY_LAYOUT; } diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index d5aff8709..27cac687e 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -63,7 +63,7 @@ typedef struct { } Edge; /* Type used in "polygon*" functions */ -typedef void (*hline_handler)(Imaging, int, int, int, int); +typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging); static inline void point8(Imaging im, int x, int y, int ink) { @@ -103,9 +103,7 @@ point32rgba(Imaging im, int x, int y, int ink) { } static inline void -hline8(Imaging im, int x0, int y0, int x1, int ink) { - int pixelwidth; - +hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { if (y0 >= 0 && y0 < im->ysize) { if (x0 < 0) { x0 = 0; @@ -118,16 +116,41 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { x1 = im->xsize - 1; } if (x0 <= x1) { - pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; - memset( - im->image8[y0] + x0 * pixelwidth, (UINT8)ink, (x1 - x0 + 1) * pixelwidth - ); + int bigendian = -1; + if (strncmp(im->mode, "I;16", 4) == 0) { + bigendian = + ( +#ifdef WORDS_BIGENDIAN + strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0 +#else + strcmp(im->mode, "I;16B") == 0 +#endif + ) + ? 1 + : 0; + } + if (mask == NULL && bigendian == -1) { + memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1)); + } else { + UINT8 *p = im->image8[y0]; + while (x0 <= x1) { + if (mask == NULL || mask->image8[y0][x0]) { + if (bigendian == -1) { + p[x0] = ink; + } else { + p[x0 * 2 + (bigendian ? 1 : 0)] = ink; + p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8; + } + } + x0++; + } + } } } } static inline void -hline32(Imaging im, int x0, int y0, int x1, int ink) { +hline32(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { INT32 *p; if (y0 >= 0 && y0 < im->ysize) { @@ -143,13 +166,16 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { } p = im->image32[y0]; while (x0 <= x1) { - p[x0++] = ink; + if (mask == NULL || mask->image8[y0][x0]) { + p[x0] = ink; + } + x0++; } } } static inline void -hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { +hline32rgba(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { @@ -167,9 +193,11 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { - out[0] = BLEND(in[3], out[0], in[0], tmp); - out[1] = BLEND(in[3], out[1], in[1], tmp); - out[2] = BLEND(in[3], out[2], in[2], tmp); + if (mask == NULL || mask->image8[y0][x0]) { + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); + } x0++; out += 4; } @@ -407,7 +435,14 @@ x_cmp(const void *x0, const void *x1) { static void draw_horizontal_lines( - Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline + Imaging im, + int n, + Edge *e, + int ink, + int *x_pos, + int y, + hline_handler hline, + Imaging mask ) { int i; for (i = 0; i < n; i++) { @@ -429,7 +464,7 @@ draw_horizontal_lines( } } - (*hline)(im, xmin, e[i].ymin, xmax, ink); + (*hline)(im, xmin, e[i].ymin, xmax, ink, mask); *x_pos = xmax + 1; } } @@ -440,7 +475,7 @@ draw_horizontal_lines( */ static inline int polygon_generic( - Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha + Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, Imaging mask ) { Edge **edge_table; float *xx; @@ -461,6 +496,7 @@ polygon_generic( return -1; } + int hasAlpha = hline == hline32rgba; for (i = 0; i < n; i++) { if (ymin > e[i].ymin) { ymin = e[i].ymin; @@ -470,7 +506,7 @@ polygon_generic( } if (e[i].ymin == e[i].ymax) { if (hasAlpha != 1) { - (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); + (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink, mask); } continue; } @@ -558,7 +594,7 @@ polygon_generic( // Line would be before the current position continue; } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask); if (x_end < x_pos) { // Line would be before the current position continue; @@ -574,13 +610,13 @@ polygon_generic( continue; } } - (*hline)(im, x_start, ymin, x_end, ink); + (*hline)(im, x_start, ymin, x_end, ink, mask); x_pos = x_end + 1; } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask); } else { for (i = 1; i < j; i += 2) { - (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); + (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink, mask); } } } @@ -590,21 +626,6 @@ polygon_generic( return 0; } -static inline int -polygon8(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline8, 0); -} - -static inline int -polygon32(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32, 0); -} - -static inline int -polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32rgba, 1); -} - static inline void add_edge(Edge *e, int x0, int y0, int x1, int y1) { /* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */ @@ -639,14 +660,13 @@ add_edge(Edge *e, int x0, int y0, int x1, int y1) { typedef struct { void (*point)(Imaging im, int x, int y, int ink); - void (*hline)(Imaging im, int x0, int y0, int x1, int ink); + void (*hline)(Imaging im, int x0, int y0, int x1, int ink, Imaging mask); void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink); - int (*polygon)(Imaging im, int n, Edge *e, int ink, int eofill); } DRAW; -DRAW draw8 = {point8, hline8, line8, polygon8}; -DRAW draw32 = {point32, hline32, line32, polygon32}; -DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba, polygon32rgba}; +DRAW draw8 = {point8, hline8, line8}; +DRAW draw32 = {point32, hline32, line32}; +DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; /* -------------------------------------------------------------------- */ /* Interface */ @@ -691,7 +711,15 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, in int ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink_, + int width, + int op, + Imaging mask ) { DRAW *draw; INT32 ink; @@ -731,7 +759,7 @@ ImagingDrawWideLine( add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); - draw->polygon(im, 4, e, ink, 0); + polygon_generic(im, 4, e, ink, 0, draw->hline, mask); } return 0; } @@ -774,7 +802,7 @@ ImagingDrawRectangle( } for (y = y0; y <= y1; y++) { - draw->hline(im, x0, y, x1, ink); + draw->hline(im, x0, y, x1, ink, NULL); } } else { @@ -783,8 +811,8 @@ ImagingDrawRectangle( width = 1; } for (i = 0; i < width; i++) { - draw->hline(im, x0, y0 + i, x1, ink); - draw->hline(im, x0, y1 - i, x1, ink); + draw->hline(im, x0, y0 + i, x1, ink, NULL); + draw->hline(im, x0, y1 - i, x1, ink, NULL); draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink); draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink); } @@ -795,7 +823,14 @@ ImagingDrawRectangle( int ImagingDrawPolygon( - Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op + Imaging im, + int count, + int *xy, + const void *ink_, + int fill, + int width, + int op, + Imaging mask ) { int i, n, x0, y0, x1, y1; DRAW *draw; @@ -839,7 +874,7 @@ ImagingDrawPolygon( if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); } - draw->polygon(im, n, e, ink, 0); + polygon_generic(im, n, e, ink, 0, draw->hline, mask); free(e); } else { @@ -861,11 +896,12 @@ ImagingDrawPolygon( xy[i * 2 + 3], ink_, width, - op + op, + mask ); } ImagingDrawWideLine( - im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op + im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op, mask ); } } @@ -1536,7 +1572,9 @@ ellipseNew( ellipse_init(&st, a, b, width); int32_t X0, Y, X1; while (ellipse_next(&st, &X0, &Y, &X1) != -1) { - draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + draw->hline( + im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL + ); } return 0; } @@ -1571,7 +1609,9 @@ clipEllipseNew( int32_t X0, Y, X1; int next_code; while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) { - draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + draw->hline( + im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL + ); } clip_ellipse_free(&st); return next_code == -1 ? 0 : -1; @@ -1989,7 +2029,7 @@ ImagingDrawOutline( DRAWINIT(); - draw->polygon(im, outline->count, outline->edges, ink, 0); + polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline, NULL); return 0; } diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 8fb481e7e..28f427370 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -118,8 +118,9 @@ ImagingFillRadialGradient(const char *mode) { for (y = 0; y < 256; y++) { for (x = 0; x < 256; x++) { - d = (int - )sqrt((double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0); + d = (int)sqrt( + (double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0 + ); if (d >= 255) { d = 255; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 7b7b2e429..c46dd3cd1 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -155,7 +155,8 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif @@ -308,7 +309,8 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 234f9943c..39ecdbff6 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -510,7 +510,15 @@ extern int ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op); extern int ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int width, + int op, + Imaging mask ); extern int ImagingDrawPieslice( @@ -530,7 +538,14 @@ extern int ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); extern int ImagingDrawPolygon( - Imaging im, int points, int *xy, const void *ink, int fill, int width, int op + Imaging im, + int points, + int *xy, + const void *ink, + int fill, + int width, + int op, + Imaging mask ); extern int ImagingDrawRectangle( diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 34d1a2294..61e095ad6 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -207,8 +207,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { if (params->cp_cinema == OPJ_CINEMA4K_24) { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_24_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_24_CS_LENGTH * 8)); params->POC[0].tile = 1; params->POC[0].resno0 = 0; @@ -243,8 +243,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { params->max_comp_size = COMP_24_CS_MAX_LENGTH; } else { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_48_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_48_CS_LENGTH * 8)); for (n = 0; n < params->tcp_numlayers; ++n) { rate = 0; diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 3c11eac22..79a38e12f 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -131,6 +131,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { break; default: state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } @@ -161,6 +162,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Would subsample the green and blue channels, which doesn't make sense */ state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } jpeg_set_colorspace(&context->cinfo, JCS_RGB); diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index 942c8dc22..a65952fb1 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -60,15 +60,25 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt } if (state->x >= state->bytes) { - if (state->bytes % state->xsize && state->bytes > state->xsize) { - int bands = state->bytes / state->xsize; - int stride = state->bytes / bands; + int bands; + int xsize = 0; + int stride = 0; + if (state->bits == 2 || state->bits == 4) { + xsize = (state->xsize + 7) / 8; + bands = state->bits; + stride = state->bytes / state->bits; + } else { + xsize = state->xsize; + bands = state->bytes / state->xsize; + if (bands != 0) { + stride = state->bytes / bands; + } + } + if (stride > xsize) { int i; for (i = 1; i < bands; i++) { // note -- skipping first band memmove( - &state->buffer[i * state->xsize], - &state->buffer[i * stride], - state->xsize + &state->buffer[i * xsize], &state->buffer[i * stride], xsize ); } } diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 6a4060b4b..b11ea62ed 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -197,8 +197,9 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { return imOut; mode_mismatch: - return (Imaging - )ImagingError_ValueError("point operation not supported for this mode"); + return (Imaging)ImagingError_ValueError( + "point operation not supported for this mode" + ); } Imaging diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index f5e386dc2..b114e0023 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -470,7 +470,8 @@ ImagingResampleHorizontal_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif @@ -509,7 +510,8 @@ ImagingResampleVertical_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 4fa4ecd1c..11d6c06cc 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -602,8 +602,9 @@ ImagingBorrowArrow( } if (!borrowed_buffer) { - return (Imaging - )ImagingError_ValueError("Arrow Array, exactly 2 buffers required"); + return (Imaging)ImagingError_ValueError( + "Arrow Array, exactly 2 buffers required" + ); } for (y = i = 0; y < im->ysize; y++) { @@ -723,6 +724,8 @@ ImagingNewArrow( int64_t pixels = (int64_t)xsize * (int64_t)ysize; // fmt:off // don't reformat this + // stored as a single array, one element per pixel, either single band + // or multiband, where each pixel is an I32. if (((strcmp(schema->format, "I") == 0 // int32 && im->pixelsize == 4 // 4xchar* storage && im->bands >= 2) // INT32 into any INT32 Storage mode @@ -735,6 +738,7 @@ ImagingNewArrow( return im; } } + // Stored as [[r,g,b,a],...] if (strcmp(schema->format, "+w:4") == 0 // 4 up array && im->pixelsize == 4 // storage as 32 bpc && schema->n_children > 0 // make sure schema is well formed. @@ -750,6 +754,17 @@ ImagingNewArrow( return im; } } + // Stored as [r,g,b,a,r,g,b,a,...] + if (strcmp(schema->format, "C") == 0 // uint8 + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children == 0 // make sure schema is well formed. + && strcmp(im->arrow_band_format, "C") == 0 // expected format + && 4 * pixels == external_array->length) { // expected length + // single flat array, interleaved storage. + if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { + return im; + } + } // fmt: on ImagingDelete(im); return NULL; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 9a2db95b4..2e83fb847 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -557,7 +557,8 @@ _decodeStrip( (tdata_t)state->buffer, strip_size ) == -1) { - TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) + TRACE( + ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) ); state->errcode = IMAGING_CODEC_BROKEN; return -1; @@ -929,6 +930,27 @@ ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) { return status; } +int +ImagingLibTiffEncodeCleanup(ImagingCodecState state) { + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + TIFF *tiff = clientstate->tiff; + + if (!tiff) { + return 0; + } + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup + if (clientstate->fp) { + // Python will manage the closing of the file rather than libtiff + // So only call TIFFCleanup + TIFFCleanup(tiff); + } else { + // When tif_closeproc refers to our custom _tiffCloseProc though, + // that is fine, as it does not close the file + TIFFClose(tiff); + } + return 0; +} + int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) { /* One shot encoder. Encode everything to the tiff in the clientstate. @@ -1010,16 +1032,6 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup - if (clientstate->fp) { - // Python will manage the closing of the file rather than libtiff - // So only call TIFFCleanup - TIFFCleanup(tiff); - } else { - // When tif_closeproc refers to our custom _tiffCloseProc though, - // that is fine, as it does not close the file - TIFFClose(tiff); - } if (!clientstate->fp) { free(clientstate->data); } @@ -1036,22 +1048,11 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Error flushing the tiff")); // likely reason is memory. state->errcode = IMAGING_CODEC_MEMORY; - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } if (!clientstate->fp) { free(clientstate->data); } return -1; } - TRACE(("Closing \n")); - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } // reset the clientstate metadata to use it to read out the buffer. clientstate->loc = 0; clientstate->size = clientstate->eof; // redundant? diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 22361210d..77808b543 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -40,6 +40,8 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int +ImagingLibTiffEncodeCleanup(ImagingCodecState state); +extern int ImagingLibTiffMergeFieldInfo( ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length ); diff --git a/src/map.c b/src/map.c index c66702981..9a3144ab9 100644 --- a/src/map.c +++ b/src/map.c @@ -137,6 +137,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { } } + im->read_only = view.readonly; im->destroy = mapping_destroy_buffer; Py_INCREF(target); diff --git a/tox.ini b/tox.ini index 4065245ee..967d4b537 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 313, 312, 311, 310, 39} + py{py3, 314, 313, 312, 311, 310, 39} [testenv] deps = diff --git a/winbuild/README.md b/winbuild/README.md index c474f12ce..0d3ec8d8a 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,8 +11,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires CMake 3.15 or newer (available as Visual Studio component). -* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server - 2019 with Visual Studio 2019 Enterprise (GitHub Actions). +* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). Here's an example script to build on Windows: diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 26cc52ec0..a0ebe3101 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ V = { "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "11.2.1", - "JPEGTURBO": "3.1.0", + "JPEGTURBO": "3.1.1", "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4",