diff --git a/.ci/install.sh b/.ci/install.sh index aeb5e6514..9553eb8f4 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -53,7 +53,7 @@ pushd depends && ./install_imagequant.sh && popd pushd depends && sudo ./install_raqm.sh && popd # libavif -pushd depends && sudo ./install_libavif.sh && popd +pushd depends && ./install_libavif.sh && popd # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 7e771f1b7..3f78c98b6 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -24,6 +24,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: Fuzzing: runs-on: ubuntu-latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 44af3e3df..857881c01 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -48,6 +48,13 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libavif + uses: actions/cache@v5 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} + - name: Cache libimagequant uses: actions/cache@v5 id: cache-libimagequant @@ -55,12 +62,21 @@ jobs: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Cache libwebp + uses: actions/cache@v5 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + - name: Install Linux dependencies run: | .ci/install.sh env: GHA_PYTHON_VERSION: "3.x" + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - name: Build run: | diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a8ddef22c..12633284f 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -14,6 +14,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: update_release_draft: permissions: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1b0c3c654..9d1902838 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,6 +12,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: stale: if: github.repository_owner == 'python-pillow' diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 091edb222..08226738e 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: @@ -83,7 +86,7 @@ jobs: - name: Docker pull run: | - docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} - name: Docker build run: | diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index e247414c8..808373a65 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -28,6 +28,7 @@ concurrency: env: COVERAGE_CORE: sysmon + FORCE_COLOR: 1 jobs: build: diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index bd244aa5a..87eace643 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 81cfb8456..f14dab616 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -24,6 +24,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ed32be26d..45392a689 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -28,6 +28,7 @@ concurrency: env: COVERAGE_CORE: sysmon + FORCE_COLOR: 1 jobs: build: @@ -98,8 +99,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.6.0 --no-progress - echo "C:\Program Files\gs\gs10.06.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.7.0 --no-progress + echo "C:\Program Files\gs\gs10.07.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a206e269..80bbfb45f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } # Intel - - { os: "macos-15-intel", python-version: "3.10" } + - { os: "macos-26-intel", python-version: "3.10" } exclude: - { os: "macos-latest", python-version: "3.10" } @@ -91,6 +91,14 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libavif + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v5 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} + - name: Cache libimagequant if: startsWith(matrix.os, 'ubuntu') uses: actions/cache@v5 @@ -99,13 +107,23 @@ jobs: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Cache libwebp + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v5 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + - name: Install Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: | .ci/install.sh env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - name: Install macOS dependencies if: startsWith(matrix.os, 'macOS') diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index a9b779e81..107eeae9b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -90,13 +90,9 @@ fi ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds. -if [[ -n "$IOS_SDK" ]]; then - FREETYPE_VERSION=2.13.3 -else - FREETYPE_VERSION=2.14.1 -fi -HARFBUZZ_VERSION=12.3.2 -LIBPNG_VERSION=1.6.54 +FREETYPE_VERSION=2.14.3 +HARFBUZZ_VERSION=13.2.1 +LIBPNG_VERSION=1.6.56 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.2 @@ -108,7 +104,7 @@ LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.2.0 -LIBAVIF_VERSION=1.3.0 +LIBAVIF_VERSION=1.4.1 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi @@ -310,10 +306,6 @@ function build { if [[ -n "$IS_MACOS" ]]; then # Custom freetype build - if [[ -z "$IOS_SDK" ]]; then - build_simple sed 4.9 https://mirrors.middlendian.com/gnu/sed - fi - build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no else build_freetype diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 20379c753..af2f9b3e8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -53,19 +53,19 @@ jobs: include: - name: "macOS 10.10 x86_64" platform: macos - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64 build: "cp3{10,11}*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" platform: macos - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64 build: "cp3{12,13}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" platform: macos - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64 build: "{cp314,pp3}*" macosx_deployment_target: "10.15" @@ -250,7 +250,7 @@ jobs: runs-on: ubuntu-latest name: Count dists steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: dist-* path: dist @@ -269,7 +269,7 @@ jobs: runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: dist-!(sdist)* path: dist @@ -291,7 +291,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: dist-* path: dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7eb69d164..53fd0a3ca 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.14.14 + rev: v0.15.4 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.9.3 + rev: 1.9.4 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.8 + rev: v22.1.0 hooks: - id: clang-format types: [c] @@ -38,6 +38,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: + - id: check-case-conflict - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - id: check-merge-conflict @@ -51,7 +52,7 @@ repos: exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.1 + rev: 0.37.0 hooks: - id: check-github-workflows - id: check-readthedocs @@ -68,12 +69,12 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.12.1 + rev: v2.16.2 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.24.1 + rev: v0.25 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] diff --git a/Tests/fonts/fuzz_font-5203009437302784 b/Tests/fonts/fuzz_font-5203009437302784 index 0465e48c2..fb401fea2 100644 --- a/Tests/fonts/fuzz_font-5203009437302784 +++ b/Tests/fonts/fuzz_font-5203009437302784 @@ -1,10 +1,10 @@ STARTFONT FONT ÿ SIZE 10 -FONTBOUNDINGBOX -CHARS +FONTBOUNDINGBOX 1 1 0 0 +CHARS 1 STARTCHAR -ENCODING +ENCODING 65 BBX 2 5 ENDCHAR ENDFONT diff --git a/Tests/images/pal8rletrns.png b/Tests/images/pal8rletrns.png new file mode 100644 index 000000000..2362266ef Binary files /dev/null and b/Tests/images/pal8rletrns.png differ diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 8fbd73748..ea0853100 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -56,7 +56,7 @@ def test_questionable() -> None: im.load() if os.path.basename(f) not in supported: print(f"Please add {f} to the partially supported bmp specs.") - except Exception: # as msg: + except Exception: # noqa: PERF203 if os.path.basename(f) in supported: raise @@ -106,7 +106,7 @@ def test_good() -> None: assert_image_similar(im_converted, compare_converted, 5) - except Exception as msg: + except Exception as msg: # noqa: PERF203 # there are three here that are unsupported: unsupported = ( os.path.join(base, "g", "rgb32bf.bmp"), diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index ffc4ce021..a25f77177 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -145,14 +145,14 @@ class TestFileAvif: # avifdec hopper.avif avif/hopper_avif_write.png assert_image_similar_tofile( - reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02 + reloaded, "Tests/images/avif/hopper_avif_write.png", 6.88 ) # This test asserts that the images are similar. If the average pixel # difference between the two images is less than the epsilon value, # then we're going to accept that it's a reasonable lossy version of # the image. - assert_image_similar(reloaded, im, 8.62) + assert_image_similar(reloaded, im, 9.28) def test_AvifEncoder_with_invalid_args(self) -> None: """ @@ -461,12 +461,9 @@ class TestFileAvif: @pytest.mark.parametrize( "advanced", [ - { - "aq-mode": "1", - "enable-chroma-deltaq": "1", - }, - (("aq-mode", "1"), ("enable-chroma-deltaq", "1")), - [("aq-mode", "1"), ("enable-chroma-deltaq", "1")], + {"tune": "psnr"}, + (("tune", "psnr"),), + [("tune", "psnr")], ], ) def test_encoder_advanced_codec_options( diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 28e863459..2e0394b3b 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -221,6 +221,11 @@ def test_rle8_eof(file_name: str, length: int) -> None: im.load() +def test_rle_delta() -> None: + with Image.open("Tests/images/bmp/q/pal8rletrns.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/pal8rletrns.png") + + def test_unsupported_bmp_bitfields_layout() -> None: fp = io.BytesIO( o32(40) # header size diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 597ab5083..c73f2a40c 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -179,9 +179,7 @@ def test_iter(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 0, 120) # Act - data = [] - for line in container: - data.append(line) + data = list(container) # Assert if bytesmode: diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index e3fcec490..7b504233d 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -310,6 +310,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: assert reloaded.getpixel((0, 0)) == 255 +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = BytesIO() + im = Image.new("RGB", size) + with pytest.raises(ValueError, match="cannot write empty image"): + im.save(b, "GIF") + + @pytest.mark.parametrize( "path, mode", ( diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f4c8318a9..5103a7672 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -85,7 +85,7 @@ class TestFileJpeg: def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: f = tmp_path / "temp.jpg" im = Image.new("RGB", size) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="cannot write empty image"): im.save(f) def test_app(self) -> None: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 575d911de..0e60b59f5 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -148,6 +148,22 @@ def test_prog_res_rt(card: ImageFile.ImageFile) -> None: assert_image_equal(im, card) +def test_unknown_progression(tmp_path: Path) -> None: + outfile = tmp_path / "temp.jp2" + + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError, match="unknown progression"): + im.save(outfile, progression="invalid") + + +def test_unknown_cinema_mode(tmp_path: Path) -> None: + outfile = tmp_path / "temp.jp2" + + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError, match="unknown cinema mode"): + im.save(outfile, cinema_mode="invalid") + + @pytest.mark.parametrize("num_resolutions", range(2, 6)) def test_default_num_resolutions( card: ImageFile.ImageFile, num_resolutions: int @@ -440,11 +456,19 @@ def test_pclr() -> None: assert len(im.palette.colors) == 256 assert im.palette.colors[(255, 255, 255)] == 0 + for enumcs in (0, 15, 17): + with open(f"{EXTRA_DIR}/issue104_jpxstream.jp2", "rb") as fp: + data = bytearray(fp.read()) + data[114:115] = bytes([enumcs]) + with Image.open(BytesIO(data)) as im: + assert im.mode == "L" + with Image.open( f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" ) as im: assert im.mode == "P" assert im.palette is not None + assert im.palette.mode == "CMYK" assert len(im.palette.colors) == 139 assert im.palette.colors[(0, 0, 0, 0)] == 0 diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index a71c65cac..6f20900e4 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -224,10 +224,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/hopper_g4.tif") as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) for tag in im.tag_v2: - try: - del core_items[tag] - except KeyError: - pass + core_items.pop(tag, None) del core_items[320] # colormap is special, tested below # Type codes: @@ -1244,7 +1241,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) out = tmp_path / "temp.tif" - with pytest.raises(SystemError): + with pytest.raises(ValueError, match="cannot write empty image"): im.save(out, compression=compression) def test_save_many_compressed(self, tmp_path: Path) -> None: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 4db62bd6d..7f35693f5 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -6,7 +6,14 @@ from typing import Any import pytest -from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + MpoImagePlugin, + TiffImagePlugin, + _binary, +) from .helper import ( assert_image_equal, @@ -145,6 +152,32 @@ def test_parallax() -> None: assert exif.get_ifd(0x927C)[0xB211] == -3.125 +def test_truncated_makernote() -> None: + def check(ifd: TiffImagePlugin.ImageFileDirectory_v2) -> None: + fp = BytesIO() + ifd.save(fp) + + e = Image.Exif() + e.load(fp.getvalue()) + assert e.get_ifd(37500) == {} + + # Nintendo + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[271] = "Nintendo" + ifd[34665] = {37500: b" "} + check(ifd) + + # Fujifilm + for data in ( + b"FUJIFILM", + b"FUJIFILM" + _binary.o32le(50), + b"FUJIFILM" + _binary.o32le(0), + ): + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[34665] = {37500: data} + check(ifd) + + def test_reload_exif_after_seek() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 90740ab57..509d93469 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -37,6 +37,14 @@ def test_sanity(tmp_path: Path) -> None: im.save(f) +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = io.BytesIO() + im = Image.new("1", size) + with pytest.raises(ValueError): + im.save(b, "PCX") + + def test_p_4_planes() -> None: with Image.open("Tests/images/p_4_planes.pcx") as im: assert im.getpixel((0, 0)) == 3 diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 2e0af5041..3f08d1ad3 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -707,6 +707,16 @@ class TestFilePng: assert reloaded.png.im_palette is not None assert len(reloaded.png.im_palette[1]) == 3 + def test_plte_cmyk(self, tmp_path: Path) -> None: + im = Image.new("P", (1, 1)) + im.putpalette((0, 100, 150, 200), "CMYK") + + out = tmp_path / "temp.png" + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.convert("CMYK").getpixel((0, 0)) == (200, 222, 232, 0) + def test_getxmp(self) -> None: with Image.open("Tests/images/color_snakes.png") as im: if ElementTree is None: diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 03494523b..3b1953aac 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -68,6 +68,14 @@ def test_save(tmp_path: Path) -> None: assert im2.format == "SPIDER" +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = BytesIO() + im = Image.new("1", size) + with pytest.raises(ValueError, match="cannot write empty image"): + im.save(b, "SPIDER") + + def test_tempfile() -> None: # Arrange im = hopper() diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bb8d3eefc..7ec562342 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,11 +1,12 @@ from __future__ import annotations import os +from io import BytesIO from pathlib import Path import pytest -from PIL import Image, UnidentifiedImageError +from PIL import Image, UnidentifiedImageError, _binary from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -13,8 +14,6 @@ _TGA_DIR = os.path.join("Tests", "images", "tga") _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") -_ORIGINS = ("tl", "bl") - _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} @@ -29,7 +28,7 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} ("200x32", "RGBA"), ), ) -@pytest.mark.parametrize("origin", _ORIGINS) +@pytest.mark.parametrize("origin", _ORIGIN_TO_ORIENTATION) @pytest.mark.parametrize("rle", (True, False)) def test_sanity( size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path @@ -94,6 +93,25 @@ def test_rgba_16() -> None: assert im.getpixel((1, 0)) == (0, 255, 82, 0) +def test_v2_no_alpha() -> None: + test_file = "Tests/images/tga/common/200x32_rgba_tl_rle.tga" + with open(test_file, "rb") as fp: + data = fp.read() + data += ( + b"\x00" * 495 + + _binary.o32le(len(data)) + + _binary.o32le(0) + + b"TRUEVISION-XFILE.\x00" + ) + with Image.open(BytesIO(data)) as im: + with Image.open(test_file) as im2: + r, g, b = im2.split()[:3] + a = Image.new("L", im2.size, 255) + expected = Image.merge("RGBA", (r, g, b, a)) + + assert_image_equal(im, expected) + + def test_id_field() -> None: # tga file with id field test_file = "Tests/images/tga_id_field.tga" diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c6c8467d6..e442471d1 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -16,6 +16,7 @@ from PIL import ( TiffImagePlugin, TiffTags, UnidentifiedImageError, + _binary, ) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -941,6 +942,15 @@ class TestFileTiff: 4001, ] + def test_truncated_photoshop_blocks(self) -> None: + with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + im.tag_v2[34377] = b"8BIM" + assert im.get_photoshop_blocks() == {} + + im.tag_v2[34377] = b"8BIM" + _binary.o16be(0) + _binary.o8(2) + b" " * 5 + assert im.get_photoshop_blocks() == {} + def test_tiff_chunks(self, tmp_path: Path) -> None: tmpfile = tmp_path / "temp.tif" diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index 54bd2d183..72a0f3534 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -1,11 +1,7 @@ from __future__ import annotations -import pytest - from PIL import Image, ImageDraw, ImageFont -from .helper import skip_unless_feature - class TestFontCrash: def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: @@ -18,8 +14,6 @@ class TestFontCrash: draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) draw.text((10, 10), "Test Text", font=font, fill="#000") - @skip_unless_feature("freetype2") def test_segfault(self) -> None: - with pytest.raises(OSError): - font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") - self._fuzz_font(font) + font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") + self._fuzz_font(font) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 569c2e85b..321dd8560 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -75,6 +75,16 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") +def test_to_imagefont() -> None: + with open(fontname, "rb") as test_file: + pcffont = PcfFontFile.PcfFontFile(test_file) + imagefont = pcffont.to_imagefont() + im = Image.new("L", (130, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=imagefont) + assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") + + def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index 575dada86..1a9069fd8 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest @@ -7,6 +8,15 @@ import pytest from PIL import FontFile, Image +def test_puti16() -> None: + fp = BytesIO() + FontFile.puti16(fp, (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) + assert fp.getvalue() == ( + b"\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04" + b"\x00\x05\x00\x06\x00\x07\x00\x08\x00\t" + ) + + def test_compile() -> None: font = FontFile.FontFile() font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0))) @@ -24,5 +34,11 @@ def test_save(tmp_path: Path) -> None: tempname = str(tmp_path / "temp.pil") font = FontFile.FontFile() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="No bitmap created"): font.save(tempname) + + +def test_to_imagefont() -> None: + font = FontFile.FontFile() + with pytest.raises(ValueError, match="No bitmap created"): + font.to_imagefont() diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 661764b60..237de6330 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -91,6 +91,21 @@ def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: assert im.palette.colors == {(1, 2, 3, 4): 0} +@pytest.mark.parametrize( + "mode, palette", + ( + ("CMYK", (1, 2, 3, 4)), + ("CMYKX", (1, 2, 3, 4, 0)), + ), +) +def test_cmyk_palette(mode: str, palette: tuple[int, ...]) -> None: + im = Image.new("P", (1, 1)) + im.putpalette(palette, mode) + assert im.getpalette() == [250, 249, 248] + assert im.palette is not None + assert im.palette.colors == {(1, 2, 3, 4): 0} + + def test_empty_palette() -> None: im = Image.new("P", (1, 1)) assert im.getpalette() == [] diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 10b89a2c0..526beb656 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,6 +1,6 @@ from __future__ import annotations -from io import BytesIO +import io from pathlib import Path import pytest @@ -23,6 +23,13 @@ def test_reload() -> None: assert_image_equal(im.convert("RGB"), original.convert("RGB")) +def test_save_fp() -> None: + palette = ImagePalette.ImagePalette() + with io.StringIO() as fp: + palette.save(fp) + assert not fp.closed + + def test_getcolor() -> None: palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 @@ -204,7 +211,7 @@ def test_2bit_palette(tmp_path: Path) -> None: def test_getpalette() -> None: - b = BytesIO(b"0 1\n1 2 3 4") + b = io.BytesIO(b"0 1\n1 2 3 4") p = PaletteFile.PaletteFile(b) palette, rawmode = p.getpalette() @@ -216,6 +223,6 @@ def test_invalid_palette() -> None: with pytest.raises(OSError): ImagePalette.load("Tests/images/hopper.jpg") - b = BytesIO(b"1" * 101) + b = io.BytesIO(b"1" * 101) with pytest.raises(SyntaxError, match="bad palette file"): PaletteFile.PaletteFile(b) diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index 04bfbc755..520104753 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -5,7 +5,10 @@ archive=$1 url=$2 if [ ! -f $archive.tar.gz ]; then - wget --no-verbose -O $archive.tar.gz $url + wget -O $archive.tar.gz $url \ + --no-verbose \ + --retry-connrefused \ + --retry-on-http-error=429,503,504 fi rmdir $archive diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index a6686f3ef..2c5687391 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -1,68 +1,86 @@ #!/usr/bin/env bash set -eo pipefail -version=1.3.0 +version=1.4.1 -./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz +if [[ "$GHA_LIBAVIF_CACHE_HIT" == "true" ]]; then -pushd libavif-$version + LIBDIR=/usr/lib/x86_64-linux-gnu -# Apply patch for SVT-AV1 4.0 compatibility -# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971 -patch -p1 < ../libavif-svt4.patch + # Copy cached files into place + sudo cp ~/cache-libavif/lib/* $LIBDIR/ + sudo cp -r ~/cache-libavif/include/avif /usr/include/ -if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then - PREFIX=$(brew --prefix) else - PREFIX=/usr + + ./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz + + pushd libavif-$version + + if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then + PREFIX=$(brew --prefix) + else + PREFIX=/usr + fi + + PKGCONFIG=${PKGCONFIG:-pkg-config} + + LIBAVIF_CMAKE_FLAGS=() + HAS_DECODER=0 + HAS_ENCODER=0 + + if $PKGCONFIG --exists aom; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) + HAS_ENCODER=1 + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists libgav1; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists rav1e; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) + HAS_ENCODER=1 + fi + + if $PKGCONFIG --exists SvtAv1Enc; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) + HAS_ENCODER=1 + fi + + if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) + fi + + cmake \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MACOSX_RPATH=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + "${LIBAVIF_CMAKE_FLAGS[@]}" \ + . + + sudo make install + + if [ -n "$GITHUB_ACTIONS" ] && [ "$(uname)" != "Darwin" ]; then + # Copy to cache + LIBDIR=/usr/lib/x86_64-linux-gnu + rm -rf ~/cache-libavif + mkdir -p ~/cache-libavif/lib + mkdir -p ~/cache-libavif/include + cp $LIBDIR/libavif.so* ~/cache-libavif/lib/ + cp -r /usr/include/avif ~/cache-libavif/include/ + fi + + popd + fi - -PKGCONFIG=${PKGCONFIG:-pkg-config} - -LIBAVIF_CMAKE_FLAGS=() -HAS_DECODER=0 -HAS_ENCODER=0 - -if $PKGCONFIG --exists aom; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) - HAS_ENCODER=1 - HAS_DECODER=1 -fi - -if $PKGCONFIG --exists dav1d; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) - HAS_DECODER=1 -fi - -if $PKGCONFIG --exists libgav1; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) - HAS_DECODER=1 -fi - -if $PKGCONFIG --exists rav1e; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) - HAS_ENCODER=1 -fi - -if $PKGCONFIG --exists SvtAv1Enc; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) - HAS_ENCODER=1 -fi - -if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) -fi - -cmake \ - -DCMAKE_INSTALL_PREFIX=$PREFIX \ - -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_MACOSX_RPATH=OFF \ - -DAVIF_LIBSHARPYUV=LOCAL \ - -DAVIF_LIBYUV=LOCAL \ - "${LIBAVIF_CMAKE_FLAGS[@]}" \ - . - -make install - -popd diff --git a/depends/install_webp.sh b/depends/install_webp.sh index d7f3cd2f5..c328fe2c8 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -3,10 +3,30 @@ archive=libwebp-1.6.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz +if [[ "$GHA_LIBWEBP_CACHE_HIT" == "true" ]]; then -pushd $archive + # Copy cached files into place + sudo cp ~/cache-libwebp/lib/* /usr/lib/ + sudo cp -r ~/cache-libwebp/include/webp /usr/include/ -./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install +else -popd + ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + + pushd $archive + + ./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install + + if [ -n "$GITHUB_ACTIONS" ]; then + # Copy to cache + rm -rf ~/cache-libwebp + mkdir -p ~/cache-libwebp/lib + mkdir -p ~/cache-libwebp/include + cp /usr/lib/libwebp*.so* /usr/lib/libwebp*.a ~/cache-libwebp/lib/ + cp /usr/lib/libsharpyuv.so* /usr/lib/libsharpyuv.a ~/cache-libwebp/lib/ + cp -r /usr/include/webp ~/cache-libwebp/include/ + fi + + popd + +fi diff --git a/depends/libavif-svt4.patch b/depends/libavif-svt4.patch deleted file mode 100644 index 7abfc5299..000000000 --- a/depends/libavif-svt4.patch +++ /dev/null @@ -1,14 +0,0 @@ ---- a/src/codec_svt.c -+++ b/src/codec_svt.c -@@ -162,7 +162,11 @@ static avifResult svtCodecEncodeImage(avifEncoder * encoder, - #else - svt_config->logical_processors = encoder->maxThreads; - #endif -+#if SVT_AV1_CHECK_VERSION(4, 0, 0) -+ svt_config->aq_mode = 2; -+#else - svt_config->enable_adaptive_quantization = 2; -+#endif - // disable 2-pass - #if SVT_AV1_CHECK_VERSION(0, 9, 0) - svt_config->rc_stats_buffer = (SvtAv1FixedBuf) { NULL, 0 }; diff --git a/docs/handbook/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst index 1c7dfb5e9..200866499 100644 --- a/docs/handbook/third-party-plugins.rst +++ b/docs/handbook/third-party-plugins.rst @@ -7,6 +7,7 @@ itself. Here is a list of PyPI projects that offer additional plugins: +* :pypi:`amigainfo`: Adds support for Amiga Workbench .info icon files. * :pypi:`DjvuRleImagePlugin`: Plugin for the DjVu RLE image format as defined in the DjVuLibre docs. * :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library. * :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL. diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 7a8707b9a..74c63fb06 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -39,15 +39,15 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 15 Sequoia | 3.10 | x86-64 | -| +----------------------------+---------------------+ -| | 3.11, 3.12, 3.13, 3.14, | arm64 | -| | PyPy3 | | +| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 | +| | 3.15, PyPy3 | | ++----------------------------------+----------------------------+---------------------+ +| macOS 26 Tahoe | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 | -| | 3.14, PyPy3 | | +| | 3.14, 3.15, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 | arm64v8, ppc64le, | | | | s390x | @@ -55,7 +55,7 @@ These platforms are built and tested for every change. | Windows Server 2022 | 3.10 | x86 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | -| | PyPy3 | | +| | 3.15, PyPy3 | | | +----------------------------+---------------------+ | | 3.13 (MinGW) | x86-64 | +----------------------------------+----------------------------+---------------------+ diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index aac55fe6b..920a05e65 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -8,10 +8,14 @@ The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instance this class store bitmap fonts, and are used with the :py:meth:`PIL.ImageDraw.ImageDraw.text` method. -PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use -`pilfont.py `_ -from :pypi:`pillow-scripts` to convert BDF and -PCF font descriptors (X window font formats) to this format. +Pillow uses its own font file format to store bitmap fonts, limited to 256 characters. You +can use :py:meth:`~PIL.FontFile.FontFile.to_imagefont` to convert BDF and PCF font +descriptors (X Window font formats) to this format:: + + from PIL import PcfFontFile + with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: + font = PcfFontFile.PcfFontFile(fp) + imagefont = font.to_imagefont() Starting with version 1.1.4, PIL can be configured to support TrueType and OpenType fonts (as well as other font formats supported by the FreeType diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index 180dcb084..209fa782f 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -14,33 +14,34 @@ TODO TODO -Backwards incompatible changes -============================== - -TODO -^^^^ - -TODO - -Deprecations -============ - -TODO -^^^^ - -TODO - API changes =========== -TODO -^^^^ +Error when encoding an empty image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Attempting to encode an image with zero width or height would previously raise +a :py:exc:`SystemError`. That has now been changed to a :py:exc:`ValueError`. + +This does not add any new errors. SGI, ICNS and ICO formats are still able to +save (0, 0) images. API additions ============= +FontFile.to_imagefont() +^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.FontFile.FontFile` instances can now be directly converted to +:py:class:`~PIL.ImageFont.ImageFont` instances:: + + >>> from PIL import PcfFontFile + >>> with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: + ... pcffont = PcfFontFile.PcfFontFile(fp) + ... pcffont.to_imagefont() + ... + + ImageText.Text.wrap ^^^^^^^^^^^^^^^^^^^ @@ -69,7 +70,8 @@ or scaling, optionally with a font size limit:: Other changes ============= -TODO -^^^^ +Support reading JPEG2000 images with CMYK palettes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +JPEG2000 images with CMYK palettes can now be read. This is the first integration of +CMYK palettes into Pillow. diff --git a/pyproject.toml b/pyproject.toml index 91f4750e4..7eb9a3fbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,6 @@ optional-dependencies.test-arrow = [ "nanoarrow", "pyarrow", ] - optional-dependencies.tests = [ "check-manifest", "coverage>=7.4.2", @@ -77,16 +76,15 @@ optional-dependencies.tests = [ "pytest-xdist", "trove-classifiers>=2024.10.12", ] - optional-dependencies.xmp = [ "defusedxml", ] +urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Changelog = "https://github.com/python-pillow/Pillow/releases" urls.Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" urls.Homepage = "https://python-pillow.github.io" urls.Mastodon = "https://fosstodon.org/@pillow" -urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Source = "https://github.com/python-pillow/Pillow" [tool.setuptools] @@ -95,70 +93,50 @@ packages = [ ] include-package-data = true package-dir = { "" = "src" } - -[tool.setuptools.dynamic] -version = { attr = "PIL.__version__" } +dynamic.version = { attr = "PIL.__version__" } [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 - config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" - test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" test-requires = [ "numpy", ] -xbuild-tools = [ ] - -[tool.cibuildwheel.ios] +xbuild-tools = [] # Disable platform guessing on iOS, and disable raqm (since there won't be a # vendor version, and we can't distribute it due to licensing) -config-settings = "raqm=disable imagequant=disable platform-guessing=disable" - +ios.config-settings = "raqm=disable imagequant=disable platform-guessing=disable" # iOS needs to be given a specific pytest invocation and list of test sources. -test-sources = [ +ios.test-sources = [ "checks", "Tests", "selftest.py", ] -test-command = [ +ios.test-command = [ "python -m selftest", "python -m pytest -vv -x -W always checks/check_wheel.py Tests", ] - # There's no numpy wheel for iOS (yet...) -test-requires = [ ] - -[tool.cibuildwheel.macos] +ios.test-requires = [] # Disable platform guessing on macOS to avoid picking up Homebrew etc. -config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" - -[tool.cibuildwheel.macos.environment] +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" # Isolate macOS build environment from Homebrew etc. -PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" - -[[tool.cibuildwheel.overrides]] -# iOS environment is isolated by cibuildwheel, but needs the dependencies -select = "*_iphoneos" -environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" - -[[tool.cibuildwheel.overrides]] -# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies -select = "*_iphonesimulator" -environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" - -[[tool.cibuildwheel.overrides]] -select = "*-win32" -test-requires = [ ] +macos.environment.PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +overrides = [ + # iOS environment is isolated by cibuildwheel, but needs the dependencies + { select = "*_iphoneos", environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" }, + # iOS simulator environment is isolated by cibuildwheel, but needs the dependencies + { select = "*_iphonesimulator", environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" }, + { select = "*-win32", test-requires = [] }, +] [tool.black] exclude = "wheels/multibuild" [tool.ruff] exclude = [ "wheels/multibuild" ] - fix = true lint.select = [ "C4", # flake8-comprehensions @@ -168,6 +146,7 @@ lint.select = [ "I", # isort "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging + "PERF", # perflint "PGH", # pygrep-hooks "PIE", # flake8-pie "PT", # flake8-pytest-style @@ -207,8 +186,8 @@ lint.isort.required-imports = [ [tool.pyproject-fmt] max_supported_python = "3.14" -[tool.pytest.ini_options] -addopts = "-ra --color=auto" +[tool.pytest] +addopts = [ "-ra", "--color=auto" ] testpaths = [ "Tests", ] diff --git a/setup.py b/setup.py index 3d975950b..496c8cb1f 100644 --- a/setup.py +++ b/setup.py @@ -302,7 +302,7 @@ def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: subprocess.check_output(command_cflags).decode("utf8").strip(), )[::2][1:] return libs, cflags - except Exception: + except Exception: # noqa: PERF203 pass return None @@ -1078,10 +1078,10 @@ libraries: list[tuple[str, _BuildInfo]] = [ ] files: list[str | os.PathLike[str]] = ["src/_imaging.c"] -for src_file in _IMAGING: - files.append("src/" + src_file + ".c") -for src_file in _LIB_IMAGING: - files.append(os.path.join("src/libImaging", src_file + ".c")) +files.extend("src/" + src_file + ".c" for src_file in _IMAGING) +files.extend( + os.path.join("src/libImaging", src_file + ".c") for src_file in _LIB_IMAGING +) ext_modules = [ Extension("PIL._imaging", files), Extension("PIL._imagingft", ["src/_imagingft.c"]), diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index a12271370..5ee61b35b 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -369,7 +369,7 @@ class BmpRleDecoder(ImageFile.PyDecoder): bytes_read = self.fd.read(2) if len(bytes_read) < 2: break - right, up = self.fd.read(2) + right, up = bytes_read data += b"\x00" * (right + up * self.state.xsize) x = len(data) % self.state.xsize else: diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index c1c05cdba..a9522e761 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -289,6 +289,7 @@ class Base(IntEnum): OpcodeList2 = 0xC741 OpcodeList3 = 0xC74E NoiseProfile = 0xC761 + FrameRate = 0xC764 """Maps EXIF tags to tag names.""" diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 1e0c1c166..341431d3f 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -18,7 +18,7 @@ from __future__ import annotations import os from typing import BinaryIO -from . import Image, _binary +from . import Image, ImageFont, _binary WIDTH = 800 @@ -110,6 +110,22 @@ class FontFile: self.bitmap.paste(im.crop(src), s) self.metrics[i] = d, dst, s + def _encode_metrics(self) -> bytes: + values: list[int] = [] + for i in range(256): + m = self.metrics[i] + if m: + values.extend(m[0] + m[1] + m[2]) + else: + values.extend((0,) * 10) + + data = bytearray() + for v in values: + if v < 0: + v += 65536 + data += _binary.o16be(v) + return bytes(data) + def save(self, filename: str) -> None: """Save font""" @@ -126,9 +142,18 @@ class FontFile: fp.write(b"PILfont\n") fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! fp.write(b"DATA\n") - for id in range(256): - m = self.metrics[id] - if not m: - puti16(fp, (0,) * 10) - else: - puti16(fp, m[0] + m[1] + m[2]) + fp.write(self._encode_metrics()) + + def to_imagefont(self) -> ImageFont.ImageFont: + """Convert to ImageFont""" + + self.compile() + + # font data + if not self.bitmap: + msg = "No bitmap created" + raise ValueError(msg) + + imagefont = ImageFont.ImageFont() + imagefont._load(self.bitmap, self._encode_metrics()) + return imagefont diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 76a0d4ab9..b8db5d832 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -164,13 +164,13 @@ class GifImageFile(ImageFile.ImageFile): self._seek(0) last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: + try: + for f in range(self.__frame + 1, frame + 1): self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in GIF file" - raise EOFError(msg) from e + except EOFError as e: + self.seek(last_frame) + msg = "no more images in GIF file" + raise EOFError(msg) from e def _seek(self, frame: int, update_image: bool = True) -> None: if isinstance(self._fp, DeferredError): @@ -937,7 +937,13 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: :param info: encoderinfo :returns: list of indexes of palette entries in use, or None """ - if im.mode in ("P", "L") and info and info.get("optimize"): + if ( + im.mode in ("P", "L") + and info + and info.get("optimize") + and im.width != 0 + and im.height != 0 + ): # Potentially expensive operation. # The palette saves 3 bytes per color not used, but palette diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 023835fb7..cb7a74c2e 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -80,8 +80,7 @@ def read_32( if byte_int & 0x80: blocksize = byte_int - 125 byte = fobj.read(1) - for i in range(blocksize): - data.append(byte) + data.extend([byte] * blocksize) else: blocksize = byte_int + 1 data.append(fobj.read(blocksize)) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cc431a86a..6062857da 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -488,7 +488,7 @@ def init() -> bool: try: logger.debug("Importing %s", plugin) __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) - except ImportError as e: + except ImportError as e: # noqa: PERF203 logger.debug("Image: failed to import %s: %s", plugin, e) if OPEN or SAVE: @@ -885,7 +885,7 @@ class Image: # unpack data e = _getencoder(self.mode, encoder_name, encoder_args) - e.setimage(self.im) + e.setimage(self.im, (0, 0) + self.size) from . import ImageFile @@ -956,7 +956,7 @@ class Image: # unpack data d = _getdecoder(self.mode, decoder_name, decoder_args) - d.setimage(self.im) + d.setimage(self.im, (0, 0) + self.size) s = d.decode(data) if s[0] >= 0: @@ -2145,8 +2145,8 @@ class Image: Alternatively, an 8-bit string may be used instead of an integer sequence. :param data: A palette sequence (either a list or a string). - :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode - that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). + :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", "CMYK", or a + mode that can be transformed to one of those modes (e.g. "R", "RGBA;L"). """ from . import ImagePalette @@ -2165,7 +2165,12 @@ class Image: palette = ImagePalette.raw(rawmode, data) self._mode = "PA" if "A" in self.mode else "P" self.palette = palette - self.palette.mode = "RGBA" if "A" in rawmode else "RGB" + if rawmode.startswith("CMYK"): + self.palette.mode = "CMYK" + elif "A" in rawmode: + self.palette.mode = "RGBA" + else: + self.palette.mode = "RGB" self.load() # install new palette def putpixel( @@ -4229,80 +4234,83 @@ class Exif(_ExifBase): if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 - if tag_data.startswith(b"FUJIFILM"): - ifd_offset = i32le(tag_data, 8) - ifd_data = tag_data[ifd_offset:] + try: + if tag_data.startswith(b"FUJIFILM"): + ifd_offset = i32le(tag_data, 8) + ifd_data = tag_data[ifd_offset:] - makernote = {} - for i in range(struct.unpack(" 4: - (offset,) = struct.unpack(" 4: + (offset,) = struct.unpack("H", tag_data[:2])[0]): - ifd_tag, typ, count, data = struct.unpack( - ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] - ) - if ifd_tag == 0x1101: - # CameraInfo - (offset,) = struct.unpack(">L", data) - self.fp.seek(offset) + if not data: + continue - camerainfo: dict[str, int | bytes] = { - "ModelID": self.fp.read(4) - } + makernote[ifd_tag] = handler( + ImageFileDirectory_v2(), data, False + ) + self._ifds[tag] = dict(self._fixup_dict(makernote)) + elif self.get(0x010F) == "Nintendo": + makernote = {} + for i in range(struct.unpack(">H", tag_data[:2])[0]): + ifd_tag, typ, count, data = struct.unpack( + ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] + ) + if ifd_tag == 0x1101: + # CameraInfo + (offset,) = struct.unpack(">L", data) + self.fp.seek(offset) - self.fp.read(4) - # Seconds since 2000 - camerainfo["TimeStamp"] = i32le(self.fp.read(12)) + camerainfo: dict[str, int | bytes] = { + "ModelID": self.fp.read(4) + } - self.fp.read(4) - camerainfo["InternalSerialNumber"] = self.fp.read(4) + self.fp.read(4) + # Seconds since 2000 + camerainfo["TimeStamp"] = i32le(self.fp.read(12)) - self.fp.read(12) - parallax = self.fp.read(4) - handler = ImageFileDirectory_v2._load_dispatch[ - TiffTags.FLOAT - ][1] - camerainfo["Parallax"] = handler( - ImageFileDirectory_v2(), parallax, False - )[0] + self.fp.read(4) + camerainfo["InternalSerialNumber"] = self.fp.read(4) - self.fp.read(4) - camerainfo["Category"] = self.fp.read(2) + self.fp.read(12) + parallax = self.fp.read(4) + handler = ImageFileDirectory_v2._load_dispatch[ + TiffTags.FLOAT + ][1] + camerainfo["Parallax"] = handler( + ImageFileDirectory_v2(), parallax, False + )[0] - makernote = {0x1101: camerainfo} - self._ifds[tag] = makernote + self.fp.read(4) + camerainfo["Category"] = self.fp.read(2) + + makernote = {0x1101: camerainfo} + self._ifds[tag] = makernote + except struct.error: + pass else: # Interop ifd = self._get_ifd_dict(tag_data, tag) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 341435437..df2a82b73 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -215,8 +215,10 @@ class ImageFile(Image.Image): if subifd_offsets: if not isinstance(subifd_offsets, tuple): subifd_offsets = (subifd_offsets,) - for subifd_offset in subifd_offsets: - ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifds = [ + (exif._get_ifd_dict(subifd_offset), subifd_offset) + for subifd_offset in subifd_offsets + ] ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): assert exif._info is not None @@ -579,10 +581,7 @@ class Parser: pass # not enough data else: flag = hasattr(im, "load_seek") or hasattr(im, "load_read") - if flag or len(im.tile) != 1: - # custom load code, or multiple tiles - self.decode = None - else: + if not flag and len(im.tile) == 1: # initialize decoder im.load_prepare() d, e, o, a = im.tile[0] diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ae003d139..ec7c7cb08 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -149,6 +149,9 @@ class ImageFont: # read PILfont metrics data = file.read(256 * 20) + self._load(image, data) + + def _load(self, image: Image.Image, data: bytes) -> None: image.load() self.font = Image.core.font(image.im, data) @@ -927,7 +930,7 @@ def load_path(filename: str | bytes) -> ImageFont: for directory in sys.path: try: return load(os.path.join(directory, filename)) - except OSError: + except OSError: # noqa: PERF203 pass msg = f'cannot find font file "{filename}" in sys.path' if os.path.exists(filename): diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index eae7aea8f..2abbd46ea 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -191,19 +191,22 @@ class ImagePalette: if self.rawmode: msg = "palette contains raw palette data" raise ValueError(msg) + open_fp = False if isinstance(fp, str): fp = open(fp, "w") - fp.write("# Palette\n") - fp.write(f"# Mode: {self.mode}\n") - for i in range(256): - fp.write(f"{i}") - for j in range(i * len(self.mode), (i + 1) * len(self.mode)): - try: - fp.write(f" {self.palette[j]}") - except IndexError: - fp.write(" 0") - fp.write("\n") - fp.close() + open_fp = True + try: + fp.write("# Palette\n") + fp.write(f"# Mode: {self.mode}\n") + palette_len = len(self.palette) + for i in range(256): + fp.write(f"{i}") + for j in range(i * len(self.mode), (i + 1) * len(self.mode)): + fp.write(f" {self.palette[j] if j < palette_len else 0}") + fp.write("\n") + finally: + if open_fp: + fp.close() # -------------------------------------------------------------------- diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 6fc824e4c..9c8be8b4e 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -185,13 +185,9 @@ def getiptcinfo( data = None - info: dict[tuple[int, int], bytes | list[bytes]] = {} if isinstance(im, IptcImageFile): # return info dictionary right away - for k, v in im.info.items(): - if isinstance(k, tuple): - info[k] = v - return info + return {k: v for k, v in im.info.items() if isinstance(k, tuple)} elif isinstance(im, JpegImagePlugin.JpegImageFile): # extract the IPTC/NAA resource @@ -227,7 +223,4 @@ def getiptcinfo( except (IndexError, KeyError): pass # expected failure - for k, v in iptc_im.info.items(): - if isinstance(k, tuple): - info[k] = v - return info + return {k: v for k, v in iptc_im.info.items() if isinstance(k, tuple)} diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index d6ec38d43..cb3773530 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -176,6 +176,7 @@ def _parse_jp2_header( nc = None dpi = None # 2-tuple of DPI info, or None palette = None + colr = None while header.has_next_box(): tbox = header.next_box_type() @@ -196,11 +197,18 @@ def _parse_jp2_header( mode = "RGB" elif nc == 4: mode = "RGBA" - elif tbox == b"colr" and nc == 4: + elif tbox == b"colr": meth, _, _, enumcs = header.read_fields(">BBBI") - if meth == 1 and enumcs == 12: - mode = "CMYK" - elif tbox == b"pclr" and mode in ("L", "LA"): + if meth == 1: + if enumcs in (0, 15): + colr = "1" + elif enumcs == 12: + colr = "CMYK" + if nc == 4: + mode = "CMYK" + elif enumcs == 17: + colr = "L" + elif tbox == b"pclr" and mode in ("L", "LA") and colr not in ("1", "L"): ne, npc = header.read_fields(">HB") assert isinstance(ne, int) assert isinstance(npc, int) @@ -210,7 +218,11 @@ def _parse_jp2_header( if bitdepth > max_bitdepth: max_bitdepth = bitdepth if max_bitdepth <= 8: - palette = ImagePalette.ImagePalette("RGBA" if npc == 4 else "RGB") + if npc == 4: + palette_mode = "CMYK" if colr == "CMYK" else "RGBA" + else: + palette_mode = "RGB" + palette = ImagePalette.ImagePalette(palette_mode) for i in range(ne): color: list[int] = [] for value in header.read_fields(">" + ("B" * npc)): diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 894c1547d..46320eb3b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -127,8 +127,8 @@ def APP(self: JpegImageFile, marker: int) -> None: # parse the image resource block offset = 14 photoshop = self.info.setdefault("photoshop", {}) - while s[offset : offset + 4] == b"8BIM": - try: + try: + while s[offset : offset + 4] == b"8BIM": offset += 4 # resource code code = i16(s, offset) @@ -153,8 +153,8 @@ def APP(self: JpegImageFile, marker: int) -> None: photoshop[code] = data offset += size offset += offset & 1 # align - except struct.error: - break # insufficient data + except struct.error: + pass # insufficient data elif marker == 0xFFEE and s.startswith(b"Adobe"): self.info["adobe"] = i16(s, 5) @@ -661,10 +661,6 @@ def get_sampling(im: Image.Image) -> int: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.width == 0 or im.height == 0: - msg = "cannot write empty image as JPEG" - raise ValueError(msg) - try: rawmode = RAWMODE[im.mode] except KeyError as e: @@ -742,17 +738,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if not (0 < len(qtables) < 5): msg = "None or too many quantization tables" raise ValueError(msg) - for idx, table in enumerate(qtables): - try: + try: + for idx, table in enumerate(qtables): if len(table) != 64: msg = "Invalid quantization table" raise TypeError(msg) - table_array = array.array("H", table) - except TypeError as e: - msg = "Invalid quantization table" - raise ValueError(msg) from e - else: - qtables[idx] = list(table_array) + qtables[idx] = list(array.array("H", table)) + except TypeError as e: + msg = "Invalid quantization table" + raise ValueError(msg) from e return qtables if qtables == "keep": diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 9360061ba..bee0a56f9 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -59,11 +59,10 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + b"MPF\0" + b" " * ifd_length ) - exif = im_frame.encoderinfo.get("exif") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - im_frame.encoderinfo["exif"] = exif - if exif: + if exif := im_frame.encoderinfo.get("exif"): + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_frame.encoderinfo["exif"] = exif mpf_offset += 4 + len(exif) JpegImagePlugin._save(im_frame, fp, filename) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index a00e9b919..b923293b0 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -251,7 +251,7 @@ class PcfFontFile(FontFile.FontFile): ] if encoding_offset != 0xFFFF: encoding[i] = encoding_offset - except UnicodeDecodeError: + except UnicodeDecodeError: # noqa: PERF203 # character is not supported in selected encoding pass diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 6b16d5385..3e34e3c63 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -146,6 +146,10 @@ SAVE = { def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.width == 0 or im.height == 0: + msg = "Cannot write empty image as PCX" + raise ValueError(msg) + try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 9826a4cd1..76a15bd0d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -866,13 +866,13 @@ class PngImageFile(ImageFile.ImageFile): self._seek(0, True) last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: + try: + for f in range(self.__frame + 1, frame + 1): self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in APNG file" - raise EOFError(msg) from e + except EOFError as e: + self.seek(last_frame) + msg = "no more images in APNG file" + raise EOFError(msg) from e def _seek(self, frame: int, rewind: bool = False) -> None: assert self.png is not None @@ -1353,6 +1353,9 @@ def _save( mode = im.mode outmode = mode + palette = [] + if im.palette: + palette = im.getpalette() or [] if mode == "P": # # attempt to minimize storage requirements for palette images @@ -1362,7 +1365,7 @@ def _save( else: # check palette contents if im.palette: - colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1) + colors = max(min(len(palette) // 3, 256), 1) else: colors = 256 @@ -1403,8 +1406,7 @@ def _save( chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] - icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: + if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")): # ICC profile # according to PNG spec, the iCCP chunk contains: # Profile name 1-79 bytes (character string) @@ -1419,8 +1421,7 @@ def _save( # Disallow sRGB chunks when an iCCP-chunk has been emitted. chunks.remove(b"sRGB") - info = im.encoderinfo.get("pnginfo") - if info: + if info := im.encoderinfo.get("pnginfo"): chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] for info_chunk in info.chunks: cid, data = info_chunk[:2] @@ -1437,7 +1438,7 @@ def _save( if im.mode == "P": palette_byte_number = colors * 3 - palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] + palette_bytes = bytes(palette[:palette_byte_number]) while len(palette_bytes) < palette_byte_number: palette_bytes += b"\0" chunk(fp, b"PLTE", palette_bytes) @@ -1472,8 +1473,7 @@ def _save( alpha_bytes = colors chunk(fp, b"tRNS", alpha[:alpha_bytes]) - dpi = im.encoderinfo.get("dpi") - if dpi: + if dpi := im.encoderinfo.get("dpi"): chunk( fp, b"pHYs", @@ -1490,8 +1490,7 @@ def _save( chunks.remove(cid) chunk(fp, cid, data) - exif = im.encoderinfo.get("exif") - if exif: + if exif := im.encoderinfo.get("exif"): if isinstance(exif, Image.Exif): exif = exif.tobytes(8) if exif.startswith(b"Exif\x00\x00"): diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 848dccda5..11d90699d 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -244,7 +244,7 @@ def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | No def makeSpiderHeader(im: Image.Image) -> list[bytes]: nsam, nrow = im.size - lenbyt = nsam * 4 # There are labrec records in the header + lenbyt = max(1, nsam) * 4 # There are labrec records in the header labrec = int(1024 / lenbyt) if 1024 % lenbyt != 0: labrec += 1 diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 90d5b5cf4..b2989a4b7 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -17,11 +17,13 @@ # from __future__ import annotations +import os import warnings from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 +from ._binary import i32le as i32 from ._binary import o8 from ._binary import o16le as o16 @@ -157,6 +159,20 @@ class TgaImageFile(ImageFile.ImageFile): pass # cannot decode def load_end(self) -> None: + if self.mode == "RGBA": + assert self.fp is not None + self.fp.seek(-26, os.SEEK_END) + footer = self.fp.read(26) + if footer.endswith(b"TRUEVISION-XFILE.\x00"): + # version 2 + extension_offset = i32(footer) + if extension_offset: + self.fp.seek(extension_offset + 494) + attributes_type = self.fp.read(1) + if attributes_type == b"\x00": + # No alpha + self.im.fillband(3, 255) + if self._flip_horizontally: self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index de2ce066e..3eec94dca 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1287,10 +1287,13 @@ class TiffImageFile(ImageFile.ImageFile): blocks = {} val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: - while val.startswith(b"8BIM"): + while val.startswith(b"8BIM") and len(val) >= 12: id = i16(val[4:6]) n = math.ceil((val[6] + 1) / 2) * 2 - size = i32(val[6 + n : 10 + n]) + try: + size = i32(val[6 + n : 10 + n]) + except struct.error: + break data = val[10 + n : 10 + n + size] blocks[id] = {"data": data} diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 834634bd7..3e35f885f 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -124,8 +124,10 @@ PyImagingPhotoPut( if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { block.pixelSize = 1; block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; - } else if (im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || - im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa) { + } else if ( + im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa + ) { block.pixelSize = 4; block.offset[0] = 0; block.offset[1] = 1; diff --git a/src/_avif.c b/src/_avif.c index 3585297fe..1fe0cb986 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -425,7 +425,7 @@ end: return (PyObject *)self; } -PyObject * +void _encoder_dealloc(AvifEncoderObject *self) { if (self->encoder) { avifEncoderDestroy(self->encoder); @@ -433,7 +433,7 @@ _encoder_dealloc(AvifEncoderObject *self) { if (self->image) { avifImageDestroy(self->image); } - Py_RETURN_NONE; + Py_TYPE(self)->tp_free(self); } PyObject * @@ -485,7 +485,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { frame = image; } else { frame = avifImageCreateEmpty(); - if (image == NULL) { + if (frame == NULL) { PyErr_SetString(PyExc_ValueError, "Image creation failed"); return NULL; } @@ -687,13 +687,13 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { return (PyObject *)self; } -PyObject * +void _decoder_dealloc(AvifDecoderObject *self) { if (self->decoder) { avifDecoderDestroy(self->decoder); } PyBuffer_Release(&self->buffer); - Py_RETURN_NONE; + Py_TYPE(self)->tp_free(self); } PyObject * @@ -708,15 +708,27 @@ _decoder_get_info(AvifDecoderObject *self) { if (image->xmp.size) { xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size); + if (!xmp) { + return NULL; + } } if (image->exif.size) { exif = PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); + if (!exif) { + Py_XDECREF(xmp); + return NULL; + } } if (image->icc.size) { icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); + if (!icc) { + Py_XDECREF(xmp); + Py_XDECREF(exif); + return NULL; + } } ret = Py_BuildValue( @@ -799,6 +811,7 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) { PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size"); + avifRGBImageFreePixels(&rgb); return NULL; } @@ -806,6 +819,9 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); avifRGBImageFreePixels(&rgb); + if (!bytes) { + return NULL; + } ret = Py_BuildValue( "SKKK", diff --git a/src/_imaging.c b/src/_imaging.c index d2a195887..ac0317f78 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -254,6 +254,9 @@ void ReleaseArrowSchemaPyCapsule(PyObject *capsule) { struct ArrowSchema *schema = (struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema"); + if (!schema) { + return; + } if (schema->release != NULL) { schema->release(schema); } @@ -276,6 +279,9 @@ void ReleaseArrowArrayPyCapsule(PyObject *capsule) { struct ArrowArray *array = (struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array"); + if (!array) { + return; + } if (array->release != NULL) { array->release(array); } @@ -1216,7 +1222,9 @@ _getxy(PyObject *xy, int *x, int *y) { PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); if (int_value != NULL && PyLong_Check(int_value)) { *x = PyLong_AS_LONG(int_value); + Py_DECREF(int_value); } else { + Py_XDECREF(int_value); goto badval; } } @@ -1230,7 +1238,9 @@ _getxy(PyObject *xy, int *x, int *y) { PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); if (int_value != NULL && PyLong_Check(int_value)) { *y = PyLong_AS_LONG(int_value); + Py_DECREF(int_value); } else { + Py_XDECREF(int_value); goto badval; } } @@ -2469,6 +2479,9 @@ _split(ImagingObject *self) { } list = PyTuple_New(self->image->bands); + if (!list) { + return NULL; + } for (i = 0; i < self->image->bands; i++) { imaging_object = PyImagingNew(bands[i]); if (!imaging_object) { @@ -3765,6 +3778,9 @@ _ptr_destructor(PyObject *capsule) { static PyObject * _getattr_ptr(ImagingObject *self, void *closure) { PyObject *capsule = PyCapsule_New(self->image, IMAGING_MAGIC, _ptr_destructor); + if (!capsule) { + return NULL; + } Py_INCREF(self); PyCapsule_SetContext(capsule, self); return capsule; @@ -4320,8 +4336,9 @@ setup_module(PyObject *m) { #else have_libjpegturbo = Py_False; #endif - Py_INCREF(have_libjpegturbo); - PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); + if (PyModule_AddObjectRef(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo) < 0) { + return -1; + } PyObject *have_mozjpeg; #ifdef JPEG_C_PARAM_SUPPORTED @@ -4329,8 +4346,9 @@ setup_module(PyObject *m) { #else have_mozjpeg = Py_False; #endif - Py_INCREF(have_mozjpeg); - PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg); + if (PyModule_AddObjectRef(m, "HAVE_MOZJPEG", have_mozjpeg) < 0) { + return -1; + } PyObject *have_libimagequant; #ifdef HAVE_LIBIMAGEQUANT @@ -4344,8 +4362,9 @@ setup_module(PyObject *m) { #else have_libimagequant = Py_False; #endif - Py_INCREF(have_libimagequant); - PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", have_libimagequant); + if (PyModule_AddObjectRef(m, "HAVE_LIBIMAGEQUANT", have_libimagequant) < 0) { + return -1; + } #ifdef HAVE_LIBZ /* zip encoding strategies */ @@ -4373,8 +4392,9 @@ setup_module(PyObject *m) { #else have_zlibng = Py_False; #endif - Py_INCREF(have_zlibng); - PyModule_AddObject(m, "HAVE_ZLIBNG", have_zlibng); + if (PyModule_AddObjectRef(m, "HAVE_ZLIBNG", have_zlibng) < 0) { + return -1; + } #ifdef HAVE_LIBTIFF { @@ -4391,8 +4411,9 @@ setup_module(PyObject *m) { #else have_xcb = Py_False; #endif - Py_INCREF(have_xcb); - PyModule_AddObject(m, "HAVE_XCB", have_xcb); + if (PyModule_AddObjectRef(m, "HAVE_XCB", have_xcb) < 0) { + return -1; + } PyObject *pillow_version = PyUnicode_FromString(version); PyDict_SetItemString( diff --git a/src/_imagingcms.c b/src/_imagingcms.c index ad3b27896..469be05f4 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -558,7 +558,13 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!imOut) { + return NULL; + } return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform)); } @@ -1447,14 +1453,14 @@ setup_module(PyObject *m) { int vn; /* Ready object types */ - PyType_Ready(&CmsProfile_Type); - PyType_Ready(&CmsTransform_Type); + if (PyType_Ready(&CmsProfile_Type) < 0 || PyType_Ready(&CmsTransform_Type) < 0) { + return -1; + } - Py_INCREF(&CmsProfile_Type); - PyModule_AddObject(m, "CmsProfile", (PyObject *)&CmsProfile_Type); - - Py_INCREF(&CmsTransform_Type); - PyModule_AddObject(m, "CmsTransform", (PyObject *)&CmsTransform_Type); + if (PyModule_AddObjectRef(m, "CmsProfile", (PyObject *)&CmsProfile_Type) < 0 || + PyModule_AddObjectRef(m, "CmsTransform", (PyObject *)&CmsTransform_Type) < 0) { + return -1; + } d = PyModule_GetDict(m); diff --git a/src/_imagingft.c b/src/_imagingft.c index a371173d6..5d91eaad6 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -941,8 +941,18 @@ font_render(FontObject *self, PyObject *args) { return NULL; } PyObject *imagePtr = PyObject_GetAttrString(image, "ptr"); + if (!imagePtr) { + PyMem_Del(glyph_info); + Py_DECREF(image); + return NULL; + } im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); - Py_XDECREF(imagePtr); + Py_DECREF(imagePtr); + if (!im) { + PyMem_Del(glyph_info); + Py_DECREF(image); + return NULL; + } x_offset = round(x_offset - stroke_width); y_offset = round(y_offset - stroke_width); @@ -1042,130 +1052,99 @@ font_render(FontObject *self, PyObject *args) { yy = -(py + glyph_slot->bitmap_top); } - // Null buffer, is dereferenced in FT_Bitmap_Convert - if (!bitmap.buffer && bitmap.rows) { - PyErr_SetString(PyExc_OSError, "Bitmap missing for glyph"); - goto glyph_error; - } - - /* convert non-8bpp bitmaps */ - switch (bitmap.pixel_mode) { - case FT_PIXEL_MODE_MONO: - convert_scale = 255; - break; - case FT_PIXEL_MODE_GRAY2: - convert_scale = 255 / 3; - break; - case FT_PIXEL_MODE_GRAY4: - convert_scale = 255 / 15; - break; - default: - convert_scale = 1; - } - switch (bitmap.pixel_mode) { - case FT_PIXEL_MODE_MONO: - case FT_PIXEL_MODE_GRAY2: - case FT_PIXEL_MODE_GRAY4: - if (!bitmap_converted_ready) { - FT_Bitmap_Init(&bitmap_converted); - bitmap_converted_ready = 1; - } - error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1); - if (error) { - geterror(error); - goto glyph_error; - } - bitmap = bitmap_converted; - /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ - case FT_PIXEL_MODE_GRAY: - break; - case FT_PIXEL_MODE_BGRA: - if (color) { + if (bitmap.buffer) { + /* convert non-8bpp bitmaps */ + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + convert_scale = 255; break; - } - /* we didn't ask for color, fall through to default */ - default: - PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); - goto glyph_error; - } - - /* clip glyph bitmap width to target image bounds */ - x0 = 0; - x1 = bitmap.width; - if (xx < 0) { - x0 = -xx; - } - if (xx + x1 > im->xsize) { - x1 = im->xsize - xx; - } - - source = (unsigned char *)bitmap.buffer; - for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { - /* clip glyph bitmap height to target image bounds */ - if (yy >= 0 && yy < im->ysize) { - /* blend this glyph into the buffer */ - int k; - unsigned char *target; - unsigned int tmp; - if (color) { - /* target[RGB] returns the color, target[A] returns the mask */ - /* target bands get split again in ImageDraw.text */ - target = (unsigned char *)im->image[yy] + xx * 4; - } else { - target = im->image8[yy] + xx; - } - if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { - /* paste color glyph */ - for (k = x0; k < x1; k++) { - unsigned int src_alpha = source[k * 4 + 3]; - - /* paste only if source has data */ - if (src_alpha > 0) { - /* unpremultiply BGRa */ - int src_red = - CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha); - int src_green = - CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha); - int src_blue = - CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha); - - /* blend required if target has data */ - if (target[k * 4 + 3] > 0) { - /* blend RGBA colors */ - target[k * 4 + 0] = - BLEND(src_alpha, target[k * 4 + 0], src_red, tmp); - target[k * 4 + 1] = - BLEND(src_alpha, target[k * 4 + 1], src_green, tmp); - target[k * 4 + 2] = - BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp); - target[k * 4 + 3] = CLIP8( - src_alpha + - MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp) - ); - } else { - /* paste unpremultiplied RGBA values */ - target[k * 4 + 0] = src_red; - target[k * 4 + 1] = src_green; - target[k * 4 + 2] = src_blue; - target[k * 4 + 3] = src_alpha; - } - } + case FT_PIXEL_MODE_GRAY2: + convert_scale = 255 / 3; + break; + case FT_PIXEL_MODE_GRAY4: + convert_scale = 255 / 15; + break; + default: + convert_scale = 1; + } + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + case FT_PIXEL_MODE_GRAY2: + case FT_PIXEL_MODE_GRAY4: + if (!bitmap_converted_ready) { + FT_Bitmap_Init(&bitmap_converted); + bitmap_converted_ready = 1; } - } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1); + if (error) { + geterror(error); + goto glyph_error; + } + bitmap = bitmap_converted; + /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ + case FT_PIXEL_MODE_GRAY: + break; + case FT_PIXEL_MODE_BGRA: if (color) { - unsigned char *ink = (unsigned char *)&foreground_ink; + break; + } + /* we didn't ask for color, fall through to default */ + default: + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); + goto glyph_error; + } + + /* clip glyph bitmap width to target image bounds */ + x0 = 0; + x1 = bitmap.width; + if (xx < 0) { + x0 = -xx; + } + if (xx + x1 > im->xsize) { + x1 = im->xsize - xx; + } + + source = (unsigned char *)bitmap.buffer; + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { + /* clip glyph bitmap height to target image bounds */ + if (yy >= 0 && yy < im->ysize) { + /* blend this glyph into the buffer */ + int k; + unsigned char *target; + unsigned int tmp; + if (color) { + /* target[RGB] returns the color, target[A] returns the mask */ + /* target bands get split again in ImageDraw.text */ + target = (unsigned char *)im->image[yy] + xx * 4; + } else { + target = im->image8[yy] + xx; + } + if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { + /* paste color glyph */ for (k = x0; k < x1; k++) { - unsigned int src_alpha = source[k] * convert_scale; + unsigned int src_alpha = source[k * 4 + 3]; + + /* paste only if source has data */ if (src_alpha > 0) { + /* unpremultiply BGRa */ + int src_red = + CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha); + int src_green = + CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha); + int src_blue = + CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha); + + /* blend required if target has data */ if (target[k * 4 + 3] > 0) { + /* blend RGBA colors */ target[k * 4 + 0] = BLEND( - src_alpha, target[k * 4 + 0], ink[0], tmp + src_alpha, target[k * 4 + 0], src_red, tmp ); target[k * 4 + 1] = BLEND( - src_alpha, target[k * 4 + 1], ink[1], tmp + src_alpha, target[k * 4 + 1], src_green, tmp ); target[k * 4 + 2] = BLEND( - src_alpha, target[k * 4 + 2], ink[2], tmp + src_alpha, target[k * 4 + 2], src_blue, tmp ); target[k * 4 + 3] = CLIP8( src_alpha + @@ -1174,35 +1153,68 @@ font_render(FontObject *self, PyObject *args) { ) ); } else { - target[k * 4 + 0] = ink[0]; - target[k * 4 + 1] = ink[1]; - target[k * 4 + 2] = ink[2]; + /* paste unpremultiplied RGBA values */ + target[k * 4 + 0] = src_red; + target[k * 4 + 1] = src_green; + target[k * 4 + 2] = src_blue; target[k * 4 + 3] = src_alpha; } } } - } else { - for (k = x0; k < x1; k++) { - unsigned int src_alpha = source[k] * convert_scale; - if (src_alpha > 0) { - target[k] = - target[k] > 0 - ? CLIP8( - src_alpha + - MULDIV255( - target[k], (255 - src_alpha), tmp + } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + if (color) { + unsigned char *ink = (unsigned char *)&foreground_ink; + for (k = x0; k < x1; k++) { + unsigned int src_alpha = source[k] * convert_scale; + if (src_alpha > 0) { + if (target[k * 4 + 3] > 0) { + target[k * 4 + 0] = BLEND( + src_alpha, target[k * 4 + 0], ink[0], tmp + ); + target[k * 4 + 1] = BLEND( + src_alpha, target[k * 4 + 1], ink[1], tmp + ); + target[k * 4 + 2] = BLEND( + src_alpha, target[k * 4 + 2], ink[2], tmp + ); + target[k * 4 + 3] = CLIP8( + src_alpha + MULDIV255( + target[k * 4 + 3], + (255 - src_alpha), + tmp + ) + ); + } else { + target[k * 4 + 0] = ink[0]; + target[k * 4 + 1] = ink[1]; + target[k * 4 + 2] = ink[2]; + target[k * 4 + 3] = src_alpha; + } + } + } + } else { + for (k = x0; k < x1; k++) { + unsigned int src_alpha = source[k] * convert_scale; + if (src_alpha > 0) { + target[k] = + target[k] > 0 + ? CLIP8( + src_alpha + + MULDIV255( + target[k], (255 - src_alpha), tmp + ) ) - ) - : src_alpha; + : src_alpha; + } } } + } else { + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); + goto glyph_error; } - } else { - PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); - goto glyph_error; } + source += bitmap.pitch; } - source += bitmap.pitch; } x += glyph_info[i].x_advance; y += glyph_info[i].y_advance; @@ -1543,7 +1555,9 @@ setup_module(PyObject *m) { d = PyModule_GetDict(m); /* Ready object type */ - PyType_Ready(&Font_Type); + if (PyType_Ready(&Font_Type) < 0) { + return -1; + } if (FT_Init_FreeType(&library)) { return 0; /* leave it uninitialized */ diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 48c395900..c04361468 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -187,8 +187,17 @@ _unop(PyObject *self, PyObject *args) { } unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC); + if (!unop) { + return NULL; + } out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!out) { + return NULL; + } im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!im1) { + return NULL; + } unop(out, im1); @@ -219,9 +228,21 @@ _binop(PyObject *self, PyObject *args) { } binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC); + if (!binop) { + return NULL; + } out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!out) { + return NULL; + } im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!im1) { + return NULL; + } im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC); + if (!im2) { + return NULL; + } binop(out, im1, im2); diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 5995f9d42..b6f307c84 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -53,7 +53,13 @@ apply(PyObject *self, PyObject *args) { } imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!imgin) { + return NULL; + } imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!imgout) { + return NULL; + } width = imgin->xsize; height = imgin->ysize; @@ -143,6 +149,9 @@ match(PyObject *self, PyObject *args) { } imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!imgin) { + return NULL; + } if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); @@ -185,6 +194,10 @@ match(PyObject *self, PyObject *args) { (b6 << 6) | (b7 << 7) | (b8 << 8)); if (lut[lut_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + if (!coordObj) { + Py_DECREF(ret); + return NULL; + } PyList_Append(ret, coordObj); Py_XDECREF(coordObj); } @@ -216,6 +229,9 @@ get_on_pixels(PyObject *self, PyObject *args) { } img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!img) { + return NULL; + } rows = img->image8; width = img->xsize; height = img->ysize; @@ -230,6 +246,10 @@ get_on_pixels(PyObject *self, PyObject *args) { for (col_idx = 0; col_idx < width; col_idx++) { if (row[col_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + if (!coordObj) { + Py_DECREF(ret); + return NULL; + } PyList_Append(ret, coordObj); Py_XDECREF(coordObj); } diff --git a/src/_webp.c b/src/_webp.c index d065e329c..115141273 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -219,6 +219,7 @@ _anim_encoder_dealloc(PyObject *self) { WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPPictureFree(&(encp->frame)); WebPAnimEncoderDelete(encp->enc); + Py_TYPE(self)->tp_free(self); } PyObject * @@ -261,6 +262,9 @@ _anim_encoder_add(PyObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } // Setup config for this frame if (!WebPConfigInit(&config)) { @@ -441,6 +445,7 @@ _anim_decoder_dealloc(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPDataClear(&(decp->data)); WebPAnimDecoderDelete(decp->dec); + Py_TYPE(self)->tp_free(self); } PyObject * @@ -503,6 +508,9 @@ _anim_decoder_get_next(PyObject *self) { bytes = PyBytes_FromStringAndSize( (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height ); + if (!bytes) { + return NULL; + } ret = Py_BuildValue("Si", bytes, timestamp); @@ -605,6 +613,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } // Setup config for this frame if (!WebPConfigInit(&config)) { diff --git a/src/decode.c b/src/decode.c index 7ec461c0e..cda4ce702 100644 --- a/src/decode.c +++ b/src/decode.c @@ -163,7 +163,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) { + if (!PyArg_ParseTuple(args, "O(iiii)", &op, &x0, &y0, &x1, &y1)) { return NULL; } im = PyImaging_AsImaging(op); @@ -176,15 +176,10 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { state = &decoder->state; /* Setup decoding tile extent */ - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; - } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; - } + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; if (state->xoff < 0 || state->xsize <= 0 || state->xsize + state->xoff > (int)im->xsize || state->yoff < 0 || @@ -910,6 +905,7 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { + PyErr_SetString(PyExc_ValueError, "unknown codec format"); return NULL; } diff --git a/src/display.c b/src/display.c index 5b5853a3c..944c60b70 100644 --- a/src/display.c +++ b/src/display.c @@ -480,6 +480,9 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { GlobalUnlock(handle); CloseClipboard(); + if (!result) { + return NULL; + } return Py_BuildValue("zN", format_names[format], result); } diff --git a/src/encode.c b/src/encode.c index 06e4a0893..1fc31404d 100644 --- a/src/encode.c +++ b/src/encode.c @@ -232,27 +232,26 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) { + if (!PyArg_ParseTuple(args, "O(nnnn)", &op, &x0, &y0, &x1, &y1)) { return NULL; } im = PyImaging_AsImaging(op); if (!im) { return NULL; } + if (im->xsize == 0 || im->ysize == 0) { + PyErr_SetString(PyExc_ValueError, "cannot write empty image"); + return NULL; + } encoder->im = im; state = &encoder->state; - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; - } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; - } + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; if (state->xoff < 0 || state->xsize <= 0 || state->xsize + state->xoff > im->xsize || state->yoff < 0 || @@ -728,6 +727,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { const RawModeID rawmode = findRawModeID(rawmode_name); if (get_packer(encoder, mode, rawmode) < 0) { + Py_DECREF(encoder); return NULL; } @@ -743,6 +743,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { for (pos = 0; pos < tags_size; pos++) { item = PyList_GetItemRef(tags, pos); if (item == NULL) { + Py_DECREF(encoder); return NULL; } @@ -767,6 +768,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { if (!is_core_tag) { PyObject *tag_type; if (PyDict_GetItemRef(types, key, &tag_type) < 0) { + Py_DECREF(encoder); return NULL; // Exception has been already set } if (tag_type) { @@ -838,6 +840,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { if (key_int == TIFFTAG_COLORMAP) { int stride = 256; if (len != 768) { + Py_DECREF(encoder); PyErr_SetString( PyExc_ValueError, "Requiring 768 items for Colormap" ); @@ -999,8 +1002,9 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, PyBytes_AsString(value) ); - } else if (type == TIFF_DOUBLE || type == TIFF_SRATIONAL || - type == TIFF_RATIONAL) { + } else if ( + type == TIFF_DOUBLE || type == TIFF_SRATIONAL || type == TIFF_RATIONAL + ) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) ); @@ -1343,11 +1347,10 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (strcmp(format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - } else if (strcmp(format, "jpt") == 0) { - codec_format = OPJ_CODEC_JPT; } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { + PyErr_SetString(PyExc_ValueError, "unknown codec format"); return NULL; } @@ -1362,6 +1365,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(progression, "CPRL") == 0) { prog_order = OPJ_CPRL; } else { + PyErr_SetString(PyExc_ValueError, "unknown progression"); return NULL; } @@ -1374,6 +1378,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(cinema_mode, "cinema4k-24") == 0) { cine_mode = OPJ_CINEMA4K_24; } else { + PyErr_SetString(PyExc_ValueError, "unknown cinema mode"); return NULL; } diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index d2ed10f0a..de4d3568e 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -170,16 +170,17 @@ export_named_type(struct ArrowSchema *schema, char *format, const char *name) { strncpy(formatp, format, format_len); strncpy(namep, name, name_len); - *schema = (struct ArrowSchema){// Type description - .format = formatp, - .name = namep, - .metadata = NULL, - .flags = 0, - .n_children = 0, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &ReleaseExportedSchema + *schema = (struct ArrowSchema){ + // Type description + .format = formatp, + .name = namep, + .metadata = NULL, + .flags = 0, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &ReleaseExportedSchema }; return 0; } @@ -287,17 +288,18 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) { im->refcount++; MUTEX_UNLOCK(&im->mutex); // Initialize primitive fields - *array = (struct ArrowArray){// Data description - .length = length, - .offset = 0, - .null_count = 0, - .n_buffers = 2, - .n_children = 0, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &release_const_array, - .private_data = im + *array = (struct ArrowArray){ + // Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im }; // Allocate list of buffers @@ -332,17 +334,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { // Initialize primitive fields // Fixed length arrays are 1 buffer of validity, and the length in pixels. // Data is in a child array. - *array = (struct ArrowArray){// Data description - .length = length, - .offset = 0, - .null_count = 0, - .n_buffers = 1, - .n_children = 1, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &release_const_array, - .private_data = im + *array = (struct ArrowArray){ + // Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 1, + .n_children = 1, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im }; // Allocate list of buffers @@ -367,17 +370,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { MUTEX_LOCK(&im->mutex); im->refcount++; MUTEX_UNLOCK(&im->mutex); - *array->children[0] = (struct ArrowArray){// Data description - .length = length * 4, - .offset = 0, - .null_count = 0, - .n_buffers = 2, - .n_children = 0, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &release_const_array, - .private_data = im + *array->children[0] = (struct ArrowArray){ + // Data description + .length = length * 4, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im }; array->children[0]->buffers = diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 330e5325c..002497c32 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1695,16 +1695,20 @@ ImagingConvertTransparent(Imaging imIn, const ModeID mode, int r, int g, int b) if (mode == IMAGING_MODE_RGBa) { premultiplied = 1; } - } else if (imIn->mode == IMAGING_MODE_RGB && - (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)) { + } else if ( + imIn->mode == IMAGING_MODE_RGB && + (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La) + ) { convert = rgb2la; source_transparency = 1; if (mode == IMAGING_MODE_La) { premultiplied = 1; } - } else if ((imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I || - imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) && - (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA)) { + } else if ( + (imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I || + imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) && + (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA) + ) { if (imIn->mode == IMAGING_MODE_1) { convert = bit2rgb; } else if (imIn->mode == IMAGING_MODE_I) { diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index d28980432..0d28069f0 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -537,8 +537,9 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if ((ymin == current->ymin || ymin == current->ymax) && - current->dx != 0) { + } else if ( + (ymin == current->ymin || ymin == current->ymax) && current->dx != 0 + ) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; @@ -570,8 +571,10 @@ polygon_generic( adjacent_line_x, adjacent_line_x_other_edge )) + 1; - } else if (xx[j - 1] < adjacent_line_x - 1 && - xx[j - 1] < adjacent_line_x_other_edge - 1) { + } else if ( + xx[j - 1] < adjacent_line_x - 1 && + xx[j - 1] < adjacent_line_x_other_edge - 1 + ) { xx[j - 1] = roundf(fmin( adjacent_line_x, adjacent_line_x_other_edge diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index d336121d5..7a57f6894 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -89,10 +89,12 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if (alpha_only && - (im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || - im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || - im->mode == IMAGING_MODE_PA)) { + } else if ( + alpha_only && + (im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || + im->mode == IMAGING_MODE_PA) + ) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 1b496f45e..1123d7bc9 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -601,6 +601,7 @@ j2ku_sycca_rgba( static const struct j2k_decode_unpacker j2k_unpackers[] = { {IMAGING_MODE_L, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, {IMAGING_MODE_P, OPJ_CLRSPC_SRGB, 1, 0, j2ku_gray_l}, + {IMAGING_MODE_P, OPJ_CLRSPC_CMYK, 1, 0, j2ku_gray_l}, {IMAGING_MODE_PA, OPJ_CLRSPC_SRGB, 2, 0, j2ku_graya_la}, {IMAGING_MODE_I_16, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, {IMAGING_MODE_I_16B, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index fdfbde2d7..3012783a2 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -330,8 +330,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { components = 4; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgba; -#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ - (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2) +#if ( \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2 \ +) } else if (im->mode == IMAGING_MODE_CMYK) { components = 4; color_space = OPJ_CLRSPC_CMYK; diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index ae3274456..05cb37554 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -206,8 +206,10 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by context->cinfo.out_color_space = JCS_EXT_RGBX; } #endif - else if (context->rawmode == IMAGING_RAWMODE_CMYK || - context->rawmode == IMAGING_RAWMODE_CMYK_I) { + else if ( + context->rawmode == IMAGING_RAWMODE_CMYK || + context->rawmode == IMAGING_RAWMODE_CMYK_I + ) { context->cinfo.out_color_space = JCS_CMYK; } else if (context->rawmode == IMAGING_RAWMODE_YCbCr) { context->cinfo.out_color_space = JCS_YCbCr; diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index d28e04edf..acd59ba7f 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -46,8 +46,9 @@ ImagingConvertMatrix(Imaging im, const ModeID mode, float m[]) { } } ImagingSectionLeave(&cookie); - } else if (mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || - mode == IMAGING_MODE_RGB) { + } else if ( + mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || mode == IMAGING_MODE_RGB + ) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); if (!imOut) { return NULL; diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index fdf5a72aa..161d82f2e 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -325,6 +325,19 @@ ImagingPackXBGR(UINT8 *out, const UINT8 *in, int pixels) { } } +void +ImagingPackCMYK2RGB(UINT8 *out, const UINT8 *in, int xsize) { + int x, nk, tmp; + for (x = 0; x < xsize; x++) { + nk = 255 - in[3]; + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); + out += 3; + in += 4; + } +} + void ImagingPackBGRA(UINT8 *out, const UINT8 *in, int pixels) { int i; @@ -605,6 +618,7 @@ static struct { {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_RGB, 24, ImagingPackCMYK2RGB}, /* video (YCbCr) */ {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingPackRGB}, diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 371ba644b..b2dacf656 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -27,7 +27,8 @@ ImagingPaletteNew(const ModeID mode) { int i; ImagingPalette palette; - if (mode != IMAGING_MODE_RGB && mode != IMAGING_MODE_RGBA) { + if (mode != IMAGING_MODE_RGB && mode != IMAGING_MODE_RGBA && + mode != IMAGING_MODE_CMYK) { return (ImagingPalette)ImagingError_ModeError(); } diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 25941ab3d..f01bce933 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -352,16 +352,16 @@ ImagingPaste( static inline void fill( - Imaging imOut, const void *ink_, int dx, int dy, int xsize, int ysize, int pixelsize + Imaging imOut, const UINT8 *ink, int dx, int dy, int xsize, int ysize, int pixelsize ) { /* fill opaque region */ - int x, y; + int x, y, i; UINT8 ink8 = 0; INT32 ink32 = 0L; - memcpy(&ink32, ink_, pixelsize); - memcpy(&ink8, ink_, sizeof(ink8)); + memcpy(&ink32, ink, pixelsize); + memcpy(&ink8, ink, sizeof(ink8)); if (imOut->image8 || ink32 == 0L) { dx *= pixelsize; @@ -371,12 +371,24 @@ fill( } } else { +#if defined _WIN32 && !defined _WIN64 + dx *= pixelsize; + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; + for (x = 0; x < xsize; x++) { + for (i = 0; i < pixelsize; i++) { + *out++ = ink[i]; + } + } + } +#else for (y = 0; y < ysize; y++) { INT32 *out = imOut->image32[y + dy] + dx; for (x = 0; x < xsize; x++) { out[x] = ink32; } } +#endif } } diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index a562f582c..2f5268b80 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -175,8 +175,15 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t /* Get all data from File descriptor */ c = (SGISTATE *)state->context; - _imaging_seek_pyFd(state->fd, 0L, SEEK_END); + if (_imaging_seek_pyFd(state->fd, 0L, SEEK_END) == -1) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } c->bufsize = _imaging_tell_pyFd(state->fd); + if (c->bufsize == -1) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } c->bufsize -= SGI_HEADER_SIZE; c->tablen = im->bands * im->ysize; @@ -194,8 +201,8 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t state->errcode = IMAGING_CODEC_MEMORY; return -1; } - _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); - if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { + if (_imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET) == -1 || + _imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { free(ptr); state->errcode = IMAGING_CODEC_UNKNOWN; return -1; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index c09062c92..c8175612e 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -677,9 +677,15 @@ ImagingNewArrow( Imaging im; struct ArrowSchema *schema = (struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema"); + if (!schema) { + return NULL; + } struct ArrowArray *external_array = (struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array"); + if (!external_array) { + return NULL; + } if (xsize < 0 || ysize < 0) { return (Imaging)ImagingError_ValueError("bad image size"); diff --git a/src/libImaging/codec_fd.c b/src/libImaging/codec_fd.c index 526168110..c5614e603 100644 --- a/src/libImaging/codec_fd.c +++ b/src/libImaging/codec_fd.c @@ -12,6 +12,9 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { int bytes_result; result = PyObject_CallMethod(fd, "read", "n", bytes); + if (result == NULL) { + goto err; + } bytes_result = PyBytes_AsStringAndSize(result, &buffer, &length); if (bytes_result == -1) { @@ -28,7 +31,7 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { return length; err: - Py_DECREF(result); + Py_XDECREF(result); return -1; } @@ -38,9 +41,16 @@ _imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) { PyObject *byteObj; byteObj = PyBytes_FromStringAndSize(src, bytes); + if (!byteObj) { + return -1; + } result = PyObject_CallMethod(fd, "write", "O", byteObj); Py_DECREF(byteObj); + if (result == NULL) { + return -1; + } + Py_DECREF(result); return bytes; @@ -51,6 +61,9 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence) { PyObject *result; result = PyObject_CallMethod(fd, "seek", "ni", offset, whence); + if (result == NULL) { + return -1; + } Py_DECREF(result); return 0; @@ -62,6 +75,9 @@ _imaging_tell_pyFd(PyObject *fd) { Py_ssize_t location; result = PyObject_CallMethod(fd, "tell", NULL); + if (result == NULL) { + return -1; + } location = PyLong_AsSsize_t(result); Py_DECREF(result); diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index bd9bd06b6..466cca176 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,14 +114,14 @@ ARCHITECTURES = { V = { "BROTLI": "1.2.0", - "FREETYPE": "2.14.1", + "FREETYPE": "2.14.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "12.3.2", + "HARFBUZZ": "13.2.1", "JPEGTURBO": "3.1.3", "LCMS2": "2.18", - "LIBAVIF": "1.3.0", + "LIBAVIF": "1.4.1", "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.54", + "LIBPNG": "1.6.56", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", @@ -542,14 +542,11 @@ def write_script( def get_footer(dep: dict[str, Any]) -> list[str]: - lines = [] - for out in dep.get("headers", []): - lines.append(cmd_copy(out, "{inc_dir}")) - for out in dep.get("libs", []): - lines.append(cmd_copy(out, "{lib_dir}")) - for out in dep.get("bins", []): - lines.append(cmd_copy(out, "{bin_dir}")) - return lines + return ( + [cmd_copy(out, "{inc_dir}") for out in dep.get("headers", [])] + + [cmd_copy(out, "{lib_dir}") for out in dep.get("libs", [])] + + [cmd_copy(out, "{bin_dir}") for out in dep.get("bins", [])] + ) def build_env(prefs: dict[str, str], verbose: bool) -> None: