diff --git a/.appveyor.yml b/.appveyor.yml index d4dd2dc95..60132a9a3 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,34 +13,34 @@ environment: - PYTHON: C:/Python311 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - - PYTHON: C:/Python37-x64 + - PYTHON: C:/Python38-x64 ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 install: - '%PYTHON%\%EXECUTABLE% --version' +- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-depends.zip -oc:\ - 7z x pillow-test-images.zip -oc:\ - mv c:\pillow-depends-main c:\pillow-depends - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images -- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ -- ..\pillow-depends\gs1000w32.exe /S -- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% +- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\ +- choco install ghostscript --version=10.0.0.20230317 +- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | - c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ + c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\pillow\winbuild\build\build_dep_all.cmd $host.SetShouldExit(0) - path C:\pillow\winbuild\build\bin;%PATH% build_script: -- ps: | - c:\pillow\winbuild\build\build_pillow.cmd install - $host.SetShouldExit(0) - cd c:\pillow +- winbuild\build\build_env.cmd +- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .' - '%PYTHON%\%EXECUTABLE% selftest.py --installed' test_script: @@ -52,8 +52,8 @@ test_script: #- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? after_test: -- python -m pip install codecov -- codecov --file coverage.xml --name %PYTHON% --flags AppVeyor +- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe +- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor matrix: fast_finish: true @@ -62,18 +62,15 @@ cache: - '%LOCALAPPDATA%\pip\Cache' artifacts: -- path: pillow\dist\*.egg +- path: pillow\*.egg name: egg -- path: pillow\dist\*.wheel +- path: pillow\*.whl name: wheel before_deploy: - cd c:\pillow - - '%PYTHON%\%EXECUTABLE% -m pip install wheel' - - cd c:\pillow\winbuild\ - - c:\pillow\winbuild\build\build_pillow.cmd bdist_wheel - - cd c:\pillow - - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + - '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .' + - ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } deploy: provider: S3 diff --git a/.ci/after_success.sh b/.ci/after_success.sh index 23a6fcd4d..c71546f00 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -1,7 +1,7 @@ #!/bin/bash # gather the coverage data -python3 -m pip install codecov +python3 -m pip install coverage if [[ $MATRIX_DOCKER ]]; then python3 -m coverage xml --ignore-errors else diff --git a/.ci/install.sh b/.ci/install.sh index 6aa122cc5..d5cbd8248 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -22,7 +22,8 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ - cmake meson imagemagick libharfbuzz-dev libfribidi-dev + cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ + sway wl-clipboard fi python3 -m pip install --upgrade pip @@ -42,7 +43,7 @@ if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 + sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi diff --git a/.editorconfig b/.editorconfig index 449530717..d74549fe2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,10 +13,6 @@ indent_style = space trim_trailing_whitespace = true -[*.rst] -# Four-space indentation -indent_size = 4 - [*.yml] # Two-space indentation indent_size = 2 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ba2b7d8ed..d03fcf0d9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,6 +19,7 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Follow PEP 8. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. - Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. +- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged. ## Reporting Issues diff --git a/.github/mergify.yml b/.github/mergify.yml index 8dfa07f4e..3c2066137 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -7,7 +7,7 @@ pull_request_rules: - status-success=Test Successful - status-success=Docker Test Successful - status-success=Windows Test Successful - - status-success=MinGW Test Successful + - status-success=MinGW - status-success=Cygwin Test Successful - status-success=continuous-integration/appveyor/pr actions: diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index db0307046..560d6c7df 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -3,10 +3,12 @@ name: CIFuzz on: push: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" workflow_dispatch: @@ -14,7 +16,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..81ba8ef15 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,55 @@ +name: Docs + +on: + push: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + build: + + runs-on: ubuntu-latest + name: Docs + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: ".ci/*.sh" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Install Linux dependencies + run: | + .ci/install.sh + env: + GHA_PYTHON_VERSION: "3.x" + + - name: Build + run: | + .ci/build.sh + + - name: Docs + run: | + make doccheck diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8c210bc90..24b8f85d1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v7 + uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 451181434..2ba5e741e 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -1,6 +1,15 @@ name: Test Cygwin -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -30,7 +39,7 @@ jobs: uses: actions/checkout@v3 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v3 + uses: cygwin/cygwin-install-action@v4 with: platform: x86_64 packages: > @@ -58,7 +67,6 @@ jobs: python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools wget xorg-server-extra zlib-devel @@ -68,13 +76,23 @@ jobs: with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' + - name: Select Python version + run: | + ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + + - name: Get latest NumPy version + id: latest-numpy + shell: bash.exe -eo pipefail -o igncr "{0}" + run: | + python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT + - name: pip cache uses: actions/cache@v3 with: path: 'C:\cygwin\home\runneradmin\.cache\pip' - key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} + key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }} restore-keys: | - ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- + ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}- - name: Build system information run: | @@ -84,15 +102,15 @@ jobs: run: | bash.exe .ci/install.sh - - name: Install a different NumPy + - name: Install latest NumPy shell: dash.exe -l "{0}" run: | - python3 -m pip install -U 'numpy!=1.21.*' + python3 -m pip install -U numpy - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" run: | - SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh + .ci/build.sh - name: Test run: | diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 7d2b20d65..36d9c131d 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -1,6 +1,15 @@ name: Test Docker -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -24,16 +33,17 @@ jobs: # Then run the remainder alpine, amazon-2-amd64, + amazon-2023-amd64, arch, centos-7-amd64, centos-stream-8-amd64, centos-stream-9-amd64, - debian-10-buster-x86, - debian-11-bullseye-x86, - fedora-36-amd64, + debian-11-bullseye-amd64, + debian-12-bookworm-x86, + debian-12-bookworm-amd64, fedora-37-amd64, + fedora-38-amd64, gentoo, - ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, ] diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 737da7b94..36bb38cd7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -1,6 +1,15 @@ name: Test MinGW -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -12,27 +21,16 @@ concurrency: jobs: build: runs-on: windows-latest - strategy: - fail-fast: false - matrix: - mingw: ["MINGW32", "MINGW64"] - include: - - mingw: "MINGW32" - name: "MSYS2 MinGW 32-bit" - package: "mingw-w64-i686" - - mingw: "MINGW64" - name: "MSYS2 MinGW 64-bit" - package: "mingw-w64-x86_64" defaults: run: shell: bash.exe --login -eo pipefail "{0}" env: - MSYSTEM: ${{ matrix.mingw }} + MSYSTEM: MINGW64 CHERE_INVOKING: 1 timeout-minutes: 30 - name: ${{ matrix.name }} + name: "MinGW" steps: - name: Checkout Pillow @@ -45,33 +43,29 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-freetype \ - ${{ matrix.package }}-gcc \ - ${{ matrix.package }}-ghostscript \ - ${{ matrix.package }}-lcms2 \ - ${{ matrix.package }}-libimagequant \ - ${{ matrix.package }}-libjpeg-turbo \ - ${{ matrix.package }}-libraqm \ - ${{ matrix.package }}-libtiff \ - ${{ matrix.package }}-libwebp \ - ${{ matrix.package }}-openjpeg2 \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools - - if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then - pacman -S --noconfirm \ - ${{ matrix.package }}-python-pyqt6 - fi + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-ghostscript \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-python3-cffi \ + mingw-w64-x86_64-python3-numpy \ + mingw-w64-x86_64-python3-olefile \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools \ + mingw-w64-x86_64-python-pyqt6 python3 -m pip install pyroma pytest pytest-cov pytest-timeout pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . + run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . - name: Test Pillow run: | @@ -84,14 +78,4 @@ jobs: with: file: ./coverage.xml flags: GHA_Windows - name: ${{ matrix.name }} - - success: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: MinGW Test Successful - steps: - - name: Success - run: echo MinGW Test Successful + name: "MSYS2 MinGW" diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index f8b050f76..6fab0ecd2 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -5,10 +5,12 @@ name: Test Valgrind on: push: paths: + - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" workflow_dispatch: @@ -16,7 +18,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 306e34ca9..70afbab24 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -1,6 +1,15 @@ name: Test Windows -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -15,18 +24,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] - architecture: ["x86", "x64"] - include: - # PyPy 7.3.4+ only ships 64-bit binaries for Windows - - python-version: "pypy3.8" - architecture: "x64" - - python-version: "pypy3.9" - architecture: "x64" + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] timeout-minutes: 30 - name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} + name: Python ${{ matrix.python-version }} steps: - name: Checkout Pillow @@ -49,24 +51,23 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - architecture: ${{ matrix.architecture }} cache: pip cache-dependency-path: ".github/workflows/test-windows.yml" - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install run: | - 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" - echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH + 7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\" + echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH - winbuild\depends\gs1000w32.exe /S - echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.0.0.20230317 + echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images @@ -88,7 +89,7 @@ jobs: - name: Prepare build if: steps.build-cache.outputs.cache-hit != 'true' run: | - & python.exe winbuild\build_prepare.py -v --python=$env:pythonLocation --srcdir + & python.exe winbuild\build_prepare.py -v shell: pwsh - name: Build dependencies / libjpeg-turbo @@ -156,9 +157,9 @@ jobs: - name: Build Pillow run: | - $FLAGS="" - if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" } - & winbuild\build\build_pillow.cmd $FLAGS install + $FLAGS="-C raqm=vendor -C fribidi=vendor" + if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" } + cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh @@ -197,14 +198,14 @@ jobs: with: file: ./coverage.xml flags: GHA_Windows - name: ${{ runner.os }} Python ${{ matrix.python-version }} ${{ matrix.architecture }} + name: ${{ runner.os }} Python ${{ matrix.python-version }} - name: Build wheel id: wheel if: "github.event_name != 'pull_request'" run: | - mkdir fribidi\${{ matrix.architecture }} - copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }} + mkdir fribidi + copy winbuild\build\bin\fribidi* fribidi setlocal EnableDelayedExpansion for %%f in (winbuild\build\license\*) do ( set x=%%~nf @@ -222,7 +223,8 @@ jobs: ) ) for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT% - winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel + call winbuild\\build\\build_env.cmd + %pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable . shell: cmd - name: Upload wheel @@ -230,7 +232,7 @@ jobs: if: "github.event_name != 'pull_request'" with: name: ${{ steps.wheel.outputs.dist }} - path: dist\*.whl + path: "*.whl" - name: Upload fribidi.dll if: "github.event_name != 'pull_request' && matrix.python-version == 3.11" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33958bea8..893c0d12c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,15 @@ name: Test -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -20,17 +29,16 @@ jobs: "ubuntu-latest", ] python-version: [ + "pypy3.10", "pypy3.9", - "pypy3.8", "3.12-dev", "3.11", "3.10", "3.9", "3.8", - "3.7", ] include: - - python-version: "3.7" + - python-version: "3.9" PYTHONOPTIMIZE: 1 REVERSE: "--reverse" - python-version: "3.8" @@ -76,7 +84,9 @@ jobs: python3 -m pip install pytest-reverse fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh + xvfb-run -s '-screen 0 1024x768x24' sway& + export WAYLAND_DISPLAY=wayland-1 + .ci/test.sh else .ci/test.sh fi @@ -96,11 +106,6 @@ jobs: name: errors path: Tests/errors - - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11 - run: | - make doccheck - - name: After success run: | .ci/after_success.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45c1f3c5f..5354509d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,9 @@ repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.7.0 hooks: - id: black - args: [--target-version=py37] - # Only .py files, until https://github.com/psf/black/issues/402 resolved - files: \.py$ - types: [] + args: [--target-version=py38] - repo: https://github.com/PyCQA/isort rev: 5.12.0 @@ -14,25 +11,25 @@ repos: - id: isort - repo: https://github.com/PyCQA/bandit - rev: 1.7.4 + rev: 1.7.5 hooks: - id: bandit args: [--severity-level=high] files: ^src/ - repo: https://github.com/asottile/yesqa - rev: v1.4.0 + rev: v1.5.0 hooks: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.4.2 + rev: v1.5.3 hooks: - id: remove-tabs - exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: @@ -49,6 +46,7 @@ repos: hooks: - id: check-merge-conflict - id: check-json + - id: check-toml - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -56,8 +54,18 @@ repos: hooks: - id: sphinx-lint + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 0.13.0 + hooks: + - id: pyproject-fmt + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.13 + hooks: + - id: validate-pyproject + - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.6.1 + rev: 1.3.1 hooks: - id: tox-ini-fmt diff --git a/.readthedocs.yml b/.readthedocs.yml index 0f581ebba..bda03d944 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,12 @@ version: 2 +formats: [pdf] + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + python: install: - method: pip diff --git a/CHANGES.rst b/CHANGES.rst index fe0230c34..4a7b5ba5e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,234 @@ Changelog (Pillow) ================== -9.5.0 (unreleased) +10.1.0 (unreleased) +------------------- + +- Allow getpixel() to accept a list #7355 + [radarhere, homm] + +- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336 + [radarhere] + +- Expand JPEG buffer size when saving optimized or progressive #7345 + [radarhere] + +- Added session type check for Linux in ImageGrab.grabclipboard() #7332 + [TheNooB2706, radarhere, hugovk] + +- Read WebP duration after opening #7311 + [k128, radarhere] + +- Allow "loop=None" when saving GIF images #7329 + [radarhere] + +- Fixed transparency when saving P mode images to PDF #7323 + [radarhere] + +- Added saving LA images as PDFs #7299 + [radarhere] + +- Set SMaskInData to 1 for PDFs with alpha #7316, #7317 + [radarhere] + +- Changed Image mode property to be read-only by default #7307 + [radarhere] + +- Silence exceptions in _repr_jpeg_ and _repr_png_ #7266 + [mtreinish, radarhere] + +- Do not use transparency when saving GIF if it has been removed when normalizing mode #7284 + [radarhere] + +- Fix missing symbols when libtiff depends on libjpeg #7270 + [heitbaum] + +10.0.0 (2023-07-01) +------------------- + +- Fixed deallocating mask images #7246 + [radarhere] + +- Added ImageFont.MAX_STRING_LENGTH #7244 + [radarhere, hugovk] + +- Fix Windows build with pyproject.toml #7230 + [hugovk, nulano, radarhere] + +- Do not close provided file handles with libtiff #7199 + [radarhere] + +- Convert to HSV if mode is HSV in getcolor() #7226 + [radarhere] + +- Added alpha_only argument to getbbox() #7123 + [radarhere. hugovk] + +- Prioritise speed in _repr_png_ #7242 + [radarhere] + +- Do not use CFFI access by default on PyPy #7236 + [radarhere] + +- Limit size even if one dimension is zero in decompression bomb check #7235 + [radarhere] + +- Use --config-settings instead of deprecated --global-option #7171 + [radarhere] + +- Better C integer definitions #6645 + [Yay295, hugovk] + +- Fixed finding dependencies on Cygwin #7175 + [radarhere] + +- Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219 + [abey79, radarhere] + +- Added in_place argument to ImageOps.exif_transpose() #7092 + [radarhere] + +- Fixed calling putpalette() on L and LA images before load() #7187 + [radarhere] + +- Fixed saving TIFF multiframe images with LONG8 tag types #7078 + [radarhere] + +- Fixed combining single duration across duplicate APNG frames #7146 + [radarhere] + +- Remove temporary file when error is raised #7148 + [radarhere] + +- Do not use temporary file when grabbing clipboard on Linux #7200 + [radarhere] + +- If the clipboard fails to open on Windows, wait and try again #7141 + [radarhere] + +- Fixed saving multiple 1 mode frames to GIF #7181 + [radarhere] + +- Replaced absolute PIL import with relative import #7173 + [radarhere] + +- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192 + [radarhere] + +- Improved wl-paste mimetype handling in ImageGrab #7094 + [rrcgat, radarhere] + +- Added _repr_jpeg_() for IPython display_jpeg #7135 + [n3011, radarhere, nulano] + +- Use "/sbin/ldconfig" if ldconfig is not found #7068 + [radarhere] + +- Prefer screenshots using XCB over gnome-screenshot #7143 + [nulano, radarhere] + +- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 + [radarhere] + +- Support reading signed 8-bit TIFF images #7111 + [radarhere] + +- Added width argument to ImageDraw regular_polygon #7132 + [radarhere] + +- Support I mode for ImageFilter.BuiltinFilter #7108 + [radarhere] + +- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112 + [radarhere] + +- Added unpacker from I;16B to I;16 #7125 + [radarhere] + +- Support float font sizes #7107 + [radarhere] + +- Use later value for duplicate xref entries in PdfParser #7102 + [radarhere] + +- Load before getting size in __getstate__ #7105 + [bigcat88, radarhere] + +- Fixed type handling for include and lib directories #7069 + [adisbladis, radarhere] + +- Remove deprecations for Pillow 10.0.0 #7059, #7080 + [hugovk, radarhere] + +- Drop support for soon-EOL Python 3.7 #7058 + [hugovk, radarhere] + +9.5.0 (2023-04-01) ------------------ +- Added ImageSourceData to TAGS_V2 #7053 + [radarhere] + +- Clear PPM half token after use #7052 + [radarhere] + +- Removed absolute path to ldconfig #7044 + [radarhere] + +- Support custom comments and PLT markers when saving JPEG2000 images #6903 + [joshware, radarhere, hugovk] + +- Load before getting size in __array_interface__ #7034 + [radarhere] + +- Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 #7010 + [radarhere] + +- Consider transparency when applying APNG blend mask #7018 + [radarhere] + +- Round duration when saving animated WebP images #6996 + [radarhere] + +- Added reading of JPEG2000 comments #6909 + [radarhere] + +- Decrement reference count #7003 + [radarhere, nulano] + +- Allow libtiff_support_custom_tags to be missing #7020 + [radarhere] + +- Improved I;16N support #6834 + [radarhere] + +- Added QOI reading #6852 + [radarhere, hugovk] + +- Added saving RGBA images as PDFs #6925 + [radarhere] + +- Do not raise an error if os.environ does not contain PATH #6935 + [radarhere, hugovk] + +- Close OleFileIO instance when closing or exiting FPX or MIC #7005 + [radarhere] + +- Added __int__ to IFDRational for Python >= 3.11 #6998 + [radarhere] + +- Added memoryview support to Dib.frombytes() #6988 + [radarhere, nulano] + +- Close file pointer copy in the libtiff encoder if still open #6986 + [fcarron, radarhere] + +- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 + [radarhere] + +- Added "corners" argument to ImageDraw rounded_rectangle() #6954 + [radarhere] + - Added memoryview support to frombytes() #6974 [radarhere] diff --git a/MANIFEST.in b/MANIFEST.in index f51551303..606e7e074 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,6 +15,7 @@ graft src graft depends graft winbuild graft docs +graft _custom_build # build/src control detritus exclude .appveyor.yml diff --git a/Makefile b/Makefile index a2545b54e..57d756b47 100644 --- a/Makefile +++ b/Makefile @@ -16,10 +16,16 @@ coverage: python3 -m coverage report .PHONY: doc -doc: +.PHONY: html +doc html: python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html +.PHONY: htmlview +htmlview: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs htmlview + .PHONY: doccheck doccheck: $(MAKE) doc @@ -38,8 +44,8 @@ help: @echo " coverage run coverage test (in progress)" @echo " doc make HTML docs" @echo " docserve run an HTTP server on the docs directory" - @echo " html to make standalone HTML files" - @echo " inplace make inplace extension" + @echo " html make HTML docs" + @echo " htmlview open the index page built by the html target in your browser" @echo " install make and install" @echo " install-coverage make and install with C coverage" @echo " lint run the lint checks" @@ -47,10 +53,6 @@ help: @echo " release-test run code and package tests before release" @echo " test run tests on installed Pillow" -.PHONY: inplace -inplace: clean - python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" . - .PHONY: install install: python3 -m pip -v install . @@ -58,7 +60,7 @@ install: .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" . + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . python3 selftest.py .PHONY: debug @@ -67,10 +69,11 @@ debug: # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip -v install --global-option="build_ext" . > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null .PHONY: release-test release-test: + python3 Tests/check_release_notes.py python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests @@ -116,5 +119,5 @@ lint: lint-fix: python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort - python3 -m black --target-version py37 . + python3 -m black --target-version py38 . python3 -m isort . diff --git a/RELEASING.md b/RELEASING.md index c203a9c12..604bb1b8c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -11,14 +11,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Develop and prepare release in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions. -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: ```bash git branch 5.2.x git tag 5.2.0 - git push --all git push --tags ``` * [ ] Create and check source distribution: @@ -32,8 +31,11 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. python3 -m twine upload dist/Pillow-5.2.0* ``` * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` - +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), + increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: + ```bash + git push --all + ``` ## Point Release Released as needed for security, installation or critical bug fixes. @@ -45,16 +47,12 @@ Released as needed for security, installation or critical bug fixes. git checkout -t remotes/origin/5.2.x ``` * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. - - - * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: ```bash git tag 5.2.1 - git push git push --tags ``` * [ ] Create and check source distribution: @@ -67,7 +65,10 @@ Released as needed for security, installation or critical bug fixes. python3 -m twine check --strict dist/* python3 -m twine upload dist/Pillow-5.2.1* ``` -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: + ```bash + git push + ``` ## Embargoed Release @@ -83,7 +84,6 @@ Released as needed privately to individual vendors for critical security-related ```bash git checkout 2.5.x git tag 2.5.3 - git push origin 2.5.x git push origin --tags ``` * [ ] Create and check source distribution: @@ -91,15 +91,14 @@ Released as needed privately to individual vendors for critical security-related make sdist ``` * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: + ```bash + git push origin 2.5.x + ``` ## Binary Distributions -### Windows -* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) - and copy into `dist/` - -### Mac and Linux +### macOS and Linux * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): ```bash git clone https://github.com/python-pillow/pillow-wheels @@ -107,7 +106,18 @@ Released as needed privately to individual vendors for critical security-related ./update-pillow-tag.sh [[release tag]] ``` * [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases) - and copy into `dist/` + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo: + ```bash + gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels + ``` + +### Windows +* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): + ```bash + gh run download --dir dist + # select dist-x.y.z + ``` ## Publicize Release diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 87cad699d..69ebef9b4 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -27,25 +27,19 @@ def timer(func, label, *args): for x in range(iterations): func(*args) if time.time() - starttime > 10: - print( - "{}: breaking at {} iterations, {:.6f} per iteration".format( - label, x + 1, (time.time() - starttime) / (x + 1.0) - ) - ) break - if x == iterations - 1: - endtime = time.time() - print( - "{}: {:.4f} s {:.6f} per iteration".format( - label, endtime - starttime, (endtime - starttime) / (x + 1.0) - ) + endtime = time.time() + print( + "{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format( + label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0) ) + ) def test_direct(): im = hopper() im.load() - # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) + # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) caccess = im.im.pixel_access(False) access = PyAccess.new(im, False) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index ab8d77719..940c0b00d 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -75,43 +75,42 @@ post-patch: """ -def test_qtables_leak(): +standard_l_qtable = ( + # fmt: off + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, + 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, + 72, 92, 95, 98, 112, 100, 103, 99, + # fmt: on +) + +standard_chrominance_qtable = ( + # fmt: off + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + # fmt: on +) + + +@pytest.mark.parametrize( + "qtables", + ( + (standard_l_qtable, standard_chrominance_qtable), + [standard_l_qtable, standard_chrominance_qtable], + ), +) +def test_qtables_leak(qtables): im = hopper("RGB") - - standard_l_qtable = [ - int(s) - for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split( - None - ) - ] - - standard_chrominance_qtable = [ - int(s) - for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split( - None - ) - ] - - qtables = [standard_l_qtable, standard_chrominance_qtable] - for _ in range(iterations): test_output = BytesIO() im.save(test_output, "JPEG", qtables=qtables) diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py new file mode 100644 index 000000000..0a9a898d7 --- /dev/null +++ b/Tests/check_release_notes.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +for rst in Path("docs/releasenotes").glob("[1-9]*.rst"): + if "TODO" in open(rst).read(): + sys.exit(f"Error: remove TODO from {rst}") diff --git a/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf b/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf new file mode 100644 index 000000000..fe200842e Binary files /dev/null and b/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf differ diff --git a/Tests/helper.py b/Tests/helper.py index 0d1d03ac8..69246bfcf 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) HAS_UPLOADER = False -if os.environ.get("SHOW_ERRORS", None): +if os.environ.get("SHOW_ERRORS"): # local img.show for errors. HAS_UPLOADER = True @@ -271,7 +271,7 @@ def netpbm_available(): def magick_command(): if sys.platform == "win32": - magickhome = os.environ.get("MAGICK_HOME", "") + magickhome = os.environ.get("MAGICK_HOME") if magickhome: imagemagick = [os.path.join(magickhome, "convert.exe")] graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] diff --git a/Tests/images/8bit.s.tif b/Tests/images/8bit.s.tif new file mode 100644 index 000000000..043cba6af Binary files /dev/null and b/Tests/images/8bit.s.tif differ diff --git a/Tests/images/blend_transparency.png b/Tests/images/blend_transparency.png new file mode 100644 index 000000000..cef0a16de Binary files /dev/null and b/Tests/images/blend_transparency.png differ diff --git a/Tests/images/comment.jp2 b/Tests/images/comment.jp2 new file mode 100644 index 000000000..4bdf91760 Binary files /dev/null and b/Tests/images/comment.jp2 differ diff --git a/Tests/images/duplicate_xref_entry.pdf b/Tests/images/duplicate_xref_entry.pdf new file mode 100644 index 000000000..f57a57d61 Binary files /dev/null and b/Tests/images/duplicate_xref_entry.pdf differ diff --git a/Tests/images/hopper.qoi b/Tests/images/hopper.qoi new file mode 100644 index 000000000..6b255aba1 Binary files /dev/null and b/Tests/images/hopper.qoi differ diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png new file mode 100644 index 000000000..f4dab388f Binary files /dev/null and b/Tests/images/hopper_emboss_I.png differ diff --git a/Tests/images/hopper_emboss_more_I.png b/Tests/images/hopper_emboss_more_I.png new file mode 100644 index 000000000..c417c915f Binary files /dev/null and b/Tests/images/hopper_emboss_more_I.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png index 11a1be6fa..5e3cf22b4 100644 Binary files a/Tests/images/imagedraw_ellipse_various_sizes.png and b/Tests/images/imagedraw_ellipse_various_sizes.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes_filled.png b/Tests/images/imagedraw_ellipse_various_sizes_filled.png index d71e175b8..dd2f641f1 100644 Binary files a/Tests/images/imagedraw_ellipse_various_sizes_filled.png and b/Tests/images/imagedraw_ellipse_various_sizes_filled.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png new file mode 100644 index 000000000..3e79e21ae Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnny.png new file mode 100644 index 000000000..7fa09a3c0 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png new file mode 100644 index 000000000..d825ad263 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png new file mode 100644 index 000000000..c19da698e Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png new file mode 100644 index 000000000..f3e95d487 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png new file mode 100644 index 000000000..274d27984 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png new file mode 100644 index 000000000..c5f40bfdb Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png new file mode 100644 index 000000000..01bfd1750 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png new file mode 100644 index 000000000..efd27be4f Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png new file mode 100644 index 000000000..d3acd01ab Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png new file mode 100644 index 000000000..55ddbc033 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png new file mode 100644 index 000000000..c000b26e9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png new file mode 100644 index 000000000..7056b4fd9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyny.png new file mode 100644 index 000000000..5eca030b9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png new file mode 100644 index 000000000..7f1f00344 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png new file mode 100644 index 000000000..2e815f4ad Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_x_odd.png b/Tests/images/imagedraw_rounded_rectangle_x_odd.png new file mode 100644 index 000000000..f23f1945e Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_x_odd.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_y_odd.png b/Tests/images/imagedraw_rounded_rectangle_y_odd.png new file mode 100644 index 000000000..96441bc72 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_y_odd.png differ diff --git a/Tests/images/imagedraw_triangle_width.png b/Tests/images/imagedraw_triangle_width.png new file mode 100644 index 000000000..3d35326e7 Binary files /dev/null and b/Tests/images/imagedraw_triangle_width.png differ diff --git a/Tests/images/orientation_rectangle.jpg b/Tests/images/orientation_rectangle.jpg new file mode 100644 index 000000000..85cfbd0a8 Binary files /dev/null and b/Tests/images/orientation_rectangle.jpg differ diff --git a/Tests/images/pil123rgba.qoi b/Tests/images/pil123rgba.qoi new file mode 100644 index 000000000..1e46036c7 Binary files /dev/null and b/Tests/images/pil123rgba.qoi differ diff --git a/Tests/images/zero_width.gif b/Tests/images/zero_width.gif new file mode 100644 index 000000000..da6823b60 Binary files /dev/null and b/Tests/images/zero_width.gif differ diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index 7e9098f53..37fad7bc8 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -20,7 +20,7 @@ python3 setup.py build --build-base=/tmp/build install # Build fuzzers in $OUT. for fuzzer in $(find $SRC -name 'fuzz_*.py'); do compile_python_fuzzer $fuzzer \ - --add-binary /usr/local/lib/libjpeg.so.62.3.0:. \ + --add-binary /usr/local/lib/libjpeg.so.62.4.0:. \ --add-binary /usr/local/lib/libfreetype.so.6:. \ --add-binary /usr/local/lib/liblcms2.so.2:. \ --add-binary /usr/local/lib/libopenjp2.so.7:. \ diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index dc111c38b..0526f550e 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -6,6 +6,7 @@ import packaging import pytest from PIL import Image, features +from Tests.helper import skip_unless_feature if sys.platform.startswith("win32"): pytest.skip("Fuzzer is linux only", allow_module_level=True) @@ -48,6 +49,7 @@ def test_fuzz_images(path): fuzzers.disable_decompressionbomb_error() +@skip_unless_feature("freetype2") @pytest.mark.parametrize( "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") ) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 3bdd5177d..745364ddc 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -22,7 +22,7 @@ def test_imageops_box_blur(): def box_blur(image, radius=1, n=1): - return image._new(image.im.box_blur(radius, n)) + return image._new(image.im.box_blur((radius, radius), n)) def assert_image(im, data, delta=0): diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 385192a3c..9021a9fb3 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -177,13 +177,14 @@ class TestEnvVars: Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) assert Image.core.get_block_size() == 2 * 1024 * 1024 - def test_warnings(self): - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"} - ) + @pytest.mark.parametrize( + "var", + ( + {"PILLOW_ALIGNMENT": "15"}, + {"PILLOW_BLOCK_SIZE": "1024"}, + {"PILLOW_BLOCKS_MAX": "wat"}, + ), + ) + def test_warnings(self, var): + with pytest.warns(UserWarning): + Image._apply_env_variables(var) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 63071b78c..87681a0b5 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -36,12 +36,10 @@ class TestDecompressionBomb: Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 - def open(): + with pytest.warns(Image.DecompressionBombWarning): with Image.open(TEST_FILE): pass - pytest.warns(Image.DecompressionBombWarning, open) - def test_exception(self): # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 @@ -66,6 +64,15 @@ class TestDecompressionBomb: with pytest.raises(Image.DecompressionBombError): im.seek(1) + def test_exception_gif_zero_width(self): + # Set limit to trigger exception on the test file + Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 + assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 + + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/zero_width.gif"): + pass + def test_exception_bmp(self): with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/bmp/b/reallybig.bmp"): @@ -87,7 +94,8 @@ class TestDecompressionCrop: # same decompression bomb warnings on them. with hopper() as src: box = (0, 0, src.width * 2, src.height * 2) - pytest.warns(Image.DecompressionBombWarning, src.crop, box) + with pytest.warns(Image.DecompressionBombWarning): + src.crop(box) def test_crop_decompression_checks(self): im = Image.new("RGB", (100, 100)) @@ -95,7 +103,8 @@ class TestDecompressionCrop: for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): assert im.crop(value).size == (9, 9) - pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99)) + with pytest.warns(Image.DecompressionBombWarning): + im.crop((-160, -160, 99, 99)) with pytest.raises(Image.DecompressionBombError): im.crop((-99909, -99990, 99999, 99999)) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 3375eb6b2..f175b90af 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -6,11 +6,6 @@ from PIL import _deprecate @pytest.mark.parametrize( "version, expected", [ - ( - 10, - "Old thing is deprecated and will be removed in Pillow 10 " - r"\(2023-07-01\)\. Use new thing instead\.", - ), ( 11, "Old thing is deprecated and will be removed in Pillow 11 " @@ -29,7 +24,7 @@ def test_version(version, expected): def test_unknown_version(): - expected = r"Unknown removal version, update PIL\._deprecate\?" + expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" with pytest.raises(ValueError, match=expected): _deprecate.deprecate("Old thing", 12345, "new thing") @@ -57,18 +52,18 @@ def test_old_version(deprecated, plural, expected): def test_plural(): expected = ( - r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Use new thing instead\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old things", 10, "new thing", plural=True) + _deprecate.deprecate("Old things", 11, "new thing", plural=True) def test_replacement_and_action(): expected = "Use only one of 'replacement' and 'action'" with pytest.raises(ValueError, match=expected): _deprecate.deprecate( - "Old thing", 10, replacement="new thing", action="Upgrade to new thing" + "Old thing", 11, replacement="new thing", action="Upgrade to new thing" ) @@ -81,16 +76,16 @@ def test_replacement_and_action(): ) def test_action(action): expected = ( - r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 10, action=action) + _deprecate.deprecate("Old thing", 11, action=action) def test_no_replacement_or_action(): expected = ( - r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)" + r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 10) + _deprecate.deprecate("Old thing", 11) diff --git a/Tests/test_deprecated_imageqt.py b/Tests/test_deprecated_imageqt.py deleted file mode 100644 index 2528ff3f7..000000000 --- a/Tests/test_deprecated_imageqt.py +++ /dev/null @@ -1,18 +0,0 @@ -import warnings - -with warnings.catch_warnings(record=True) as w: - # Arrange: cause all warnings to always be triggered - warnings.simplefilter("always") - - # Act: trigger a warning with Qt5 - from PIL import ImageQt - - -def test_deprecated(): - # Assert - if ImageQt.qt_version in ("5", "side2"): - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "deprecated" in str(w[0].message) - else: - assert len(w) == 0 diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 51637c786..8cb9a814e 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -163,6 +163,12 @@ def test_apng_blend(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) +def test_apng_blend_transparency(): + with Image.open("Tests/images/blend_transparency.png") as im: + im.seek(1) + assert im.getpixel((0, 0)) == (255, 0, 0) + + def test_apng_chunk_order(): with Image.open("Tests/images/apng/fctl_actl.png") as im: im.seek(im.n_frames - 1) @@ -263,13 +269,11 @@ def test_apng_chunk_errors(): with Image.open("Tests/images/apng/chunk_no_actl.png") as im: assert not im.is_animated - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() assert not im.is_animated - pytest.warns(UserWarning, open) - with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: assert not im.is_animated @@ -287,21 +291,17 @@ def test_apng_chunk_errors(): def test_apng_syntax_errors(): - def open_frames_zero(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert not im.is_animated with pytest.raises(OSError): im.load() - pytest.warns(UserWarning, open_frames_zero) - - def open_frames_zero_default(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open_frames_zero_default) - # we can handle this case gracefully exception = None with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: @@ -316,13 +316,11 @@ def test_apng_syntax_errors(): im.seek(im.n_frames - 1) im.load() - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open) - @pytest.mark.parametrize( "test_file", @@ -376,6 +374,20 @@ def test_apng_save(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) +def test_apng_save_alpha(tmp_path): + test_file = str(tmp_path / "temp.png") + + im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) + im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) + im.save(test_file, save_all=True, append_images=[im2]) + + with Image.open(test_file) as reloaded: + assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255) + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) + + def test_apng_save_split_fdat(tmp_path): # test to make sure we do not generate sequence errors when writing # frames with image data spanning multiple fdAT chunks (in this case @@ -449,6 +461,17 @@ def test_apng_save_duration_loop(tmp_path): assert im.info.get("duration") == 750 +def test_apng_save_duplicate_duration(tmp_path): + test_file = str(tmp_path / "temp.png") + frame = Image.new("RGB", (1, 1)) + + # Test a single duration is correctly combined across duplicate frames + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + + def test_apng_save_disposal(tmp_path): test_file = str(tmp_path / "temp.png") size = (128, 64) @@ -657,13 +680,3 @@ def test_different_modes_in_later_frames(mode, tmp_path): im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))]) with Image.open(test_file) as reloaded: assert reloaded.mode == mode - - -def test_constants_deprecation(): - for enum, prefix in { - PngImagePlugin.Disposal: "APNG_DISPOSE_", - PngImagePlugin.Blend: "APNG_BLEND_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(PngImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index ba2781820..8b1355b62 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,6 +1,6 @@ import pytest -from PIL import BlpImagePlugin, Image +from PIL import Image from .helper import ( assert_image_equal, @@ -72,14 +72,3 @@ def test_crashes(test_file): with Image.open(f) as im: with pytest.raises(OSError): im.load() - - -def test_constants_deprecation(): - for enum, prefix in { - BlpImagePlugin.Format: "BLP_FORMAT_", - BlpImagePlugin.Encoding: "BLP_ENCODING_", - BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(BlpImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 76f185b9a..a7714c92c 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -56,6 +56,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index ef378b24a..22686af34 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -28,7 +28,8 @@ def test_unclosed_file(): im = Image.open(TEST_FILE) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index ac6e84447..26adfff87 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -28,34 +28,65 @@ FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" # EPS test files with binary preview FILE3 = "Tests/images/binary_preview_map.eps" +# Three unsigned 32bit little-endian values: +# 0xC6D3D0C5 magic number +# byte position of start of postscript section (12) +# byte length of postscript section (0) +# this byte length isn't valid, but we don't read it +simple_binary_header = b"\xc5\xd0\xd3\xc6\x0c\x00\x00\x00\x00\x00\x00\x00" + +# taken from page 8 of the specification +# https://web.archive.org/web/20220120164601/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf +simple_eps_file = ( + b"%!PS-Adobe-3.0 EPSF-3.0", + b"%%BoundingBox: 5 5 105 105", + b"10 setlinewidth", + b"10 10 moveto", + b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", + b"stroke", +) +simple_eps_file_with_comments = ( + simple_eps_file[:1] + + ( + b"%%Comment1: Some Value", + b"%%SecondComment: Another Value", + ) + + simple_eps_file[1:] +) +simple_eps_file_without_version = simple_eps_file[1:] +simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] +simple_eps_file_with_invalid_boundingbox = ( + simple_eps_file[:1] + (b"%%BoundingBox: a b c d",) + simple_eps_file[2:] +) +simple_eps_file_with_invalid_boundingbox_valid_imagedata = ( + simple_eps_file_with_invalid_boundingbox + (b"%ImageData: 100 100 8 3",) +) +simple_eps_file_with_long_ascii_comment = ( + simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:] +) +simple_eps_file_with_long_binary_data = ( + simple_eps_file[:2] + + ( + b"%%BeginBinary: 300", + b"\0" * 300, + b"%%EndBinary", + ) + + simple_eps_file[2:] +) + @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_sanity(): - # Regular scale - with Image.open(FILE1) as image1: - image1.load() - assert image1.mode == "RGB" - assert image1.size == (460, 352) - assert image1.format == "EPS" - - with Image.open(FILE2) as image2: - image2.load() - assert image2.mode == "RGB" - assert image2.size == (360, 252) - assert image2.format == "EPS" - - # Double scale - with Image.open(FILE1) as image1_scale2: - image1_scale2.load(scale=2) - assert image1_scale2.mode == "RGB" - assert image1_scale2.size == (920, 704) - assert image1_scale2.format == "EPS" - - with Image.open(FILE2) as image2_scale2: - image2_scale2.load(scale=2) - assert image2_scale2.mode == "RGB" - assert image2_scale2.size == (720, 504) - assert image2_scale2.format == "EPS" +@pytest.mark.parametrize( + ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) +) +@pytest.mark.parametrize("scale", (1, 2)) +def test_sanity(filename, size, scale): + expected_size = tuple(s * scale for s in size) + with Image.open(filename) as image: + image.load(scale=scale) + assert image.mode == "RGB" + assert image.size == expected_size + assert image.format == "EPS" @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -69,11 +100,72 @@ def test_load(): def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(invalid_file) +def test_binary_header_only(): + data = io.BytesIO(simple_binary_header) + with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_version_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) + with pytest.raises(SyntaxError): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_boundingbox_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) + with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) + with pytest.raises(OSError, match="cannot determine EPS bounding box"): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): + data = io.BytesIO( + prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) + ) + with Image.open(data) as img: + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_ascii_comment_too_long(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) + with pytest.raises(SyntaxError, match="not an EPS file"): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_long_binary_data(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_load_long_binary_data(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + with Image.open(data) as img: + img.load() + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @@ -100,7 +192,7 @@ def test_showpage(): with Image.open("Tests/images/reqd_showpage.png") as target: # should not crash/hang plot_image.load() - # fonts could be slightly different + # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -111,7 +203,7 @@ def test_transparency(): assert plot_image.mode == "RGBA" with Image.open("Tests/images/reqd_showpage_transparency.png") as target: - # fonts could be slightly different + # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -206,7 +298,6 @@ def test_resize(filename): @pytest.mark.parametrize("filename", (FILE1, FILE2)) def test_thumbnail(filename): # Issue #619 - # Arrange with Image.open(filename) as im: new_size = (100, 100) im.thumbnail(new_size) @@ -220,7 +311,7 @@ def test_read_binary_preview(): pass -def test_readline(tmp_path): +def test_readline_psfile(tmp_path): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' line_endings = ["\r\n", "\n", "\n\r", "\r"] @@ -237,7 +328,8 @@ def test_readline(tmp_path): def _test_readline_io_psfile(test_string, ending): f = io.BytesIO(test_string.encode("latin-1")) - t = EpsImagePlugin.PSFile(f) + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) def _test_readline_file_psfile(test_string, ending): @@ -246,7 +338,8 @@ def test_readline(tmp_path): w.write(test_string.encode("latin-1")) with open(f, "rb") as r: - t = EpsImagePlugin.PSFile(r) + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(r) _test_readline(t, ending) for ending in line_endings: @@ -255,6 +348,25 @@ def test_readline(tmp_path): _test_readline_file_psfile(s, ending) +def test_psfile_deprecation(): + with pytest.warns(DeprecationWarning): + EpsImagePlugin.PSFile(None) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +@pytest.mark.parametrize( + "line_ending", + (b"\r\n", b"\n", b"\n\r", b"\r"), +) +def test_readline(prefix, line_ending): + simple_file = prefix + line_ending.join(simple_eps_file_with_comments) + data = io.BytesIO(simple_file) + test_file = EpsImagePlugin.EpsImageFile(data) + assert test_file.info["Comment1"] == "Some Value" + assert test_file.info["SecondComment"] == "Another Value" + assert test_file.size == (100, 100) + + @pytest.mark.parametrize( "filename", ( diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 3048827e0..68b3eb567 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -2,7 +2,7 @@ from io import BytesIO import pytest -from PIL import FitsImagePlugin, FitsStubImagePlugin, Image +from PIL import FitsImagePlugin, Image from .helper import assert_image_equal, hopper @@ -48,38 +48,3 @@ def test_comment(): image_data = b"SIMPLE = T / comment string" with pytest.raises(OSError): FitsImagePlugin.FitsImageFile(BytesIO(image_data)) - - -def test_stub_deprecated(): - class Handler: - opened = False - loaded = False - - def open(self, im): - self.opened = True - - def load(self, im): - self.loaded = True - return Image.new("RGB", (1, 1)) - - handler = Handler() - with pytest.warns(DeprecationWarning): - FitsStubImagePlugin.register_handler(handler) - - with Image.open(TEST_FILE) as im: - assert im.format == "FITS" - assert im.size == (128, 128) - assert im.mode == "L" - - assert handler.opened - assert not handler.loaded - - im.load() - assert handler.loaded - - FitsStubImagePlugin._handler = None - Image.register_open( - FitsImagePlugin.FitsImageFile.format, - FitsImagePlugin.FitsImageFile, - FitsImagePlugin._accept, - ) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 70d4d76db..f96afdc95 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -36,7 +36,8 @@ def test_unclosed_file(): im = Image.open(static_test_file) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index fa22e90f6..9a1784d31 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -18,6 +18,16 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") +def test_close(): + with Image.open("Tests/images/input_bw_one_band.fpx") as im: + pass + assert im.ole.fp.closed + + im = Image.open("Tests/images/input_bw_one_band.fpx") + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index cae20fa46..ac6253db0 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -21,12 +21,3 @@ def test_invalid_file(): with pytest.raises(SyntaxError): FtexImagePlugin.FtexImageFile(invalid_file) - - -def test_constants_deprecation(): - for enum, prefix in { - FtexImagePlugin.Format: "FORMAT_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(FtexImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index bce72d192..d571692b1 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -36,7 +36,8 @@ def test_unclosed_file(): im = Image.open(TEST_GIF) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -251,6 +252,19 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 +def test_roundtrip_save_all_1(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == 255 + + @pytest.mark.parametrize( "path, mode", ( @@ -861,6 +875,14 @@ def test_identical_frames_to_single_frame(duration, tmp_path): assert reread.info["duration"] == 8500 +def test_loop_none(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.save(out, loop=None) + with Image.open(out) as reread: + assert "loop" not in reread.info + + def test_number_of_loops(tmp_path): number_of_loops = 2 @@ -1072,6 +1094,21 @@ def test_transparent_optimize(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) +def test_removed_transparency(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("RGB", (256, 1)) + + for x in range(256): + im.putpixel((x, 0), (x, 0, 0)) + + im.info["transparency"] = (255, 255, 255) + with pytest.warns(UserWarning): + im.save(out) + + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info + + def test_rgb_transparency(tmp_path): out = str(tmp_path / "temp.gif") @@ -1087,7 +1124,8 @@ def test_rgb_transparency(tmp_path): im = Image.new("RGB", (1, 1)) im.info["transparency"] = b"" ims = [Image.new("RGB", (1, 1))] - pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims) + with pytest.warns(UserWarning): + im.save(out, save_all=True, append_images=ims) with Image.open(out) as reloaded: assert "transparency" not in reloaded.info @@ -1115,6 +1153,18 @@ def test_bbox(tmp_path): assert reread.n_frames == 2 +def test_bbox_alpha(tmp_path): + out = str(tmp_path / "temp.gif") + + im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) + im.putpixel((0, 1), (255, 0, 0, 0)) + im2 = Image.new("RGBA", (1, 2), (255, 0, 0, 0)) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reread: + assert reread.n_frames == 2 + + def test_palette_save_L(tmp_path): # Generate an L mode image with a separate palette diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 768ac12bd..dd1c5e7d2 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -56,6 +56,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 98dc5443c..7ca10fac5 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -57,6 +57,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 9c1c3cf17..4e6dbe6ed 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -212,12 +212,10 @@ def test_save_append_images(tmp_path): def test_unexpected_size(): # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/hopper_unexpected.ico") as im: assert im.size == (16, 16) - pytest.warns(UserWarning, open) - def test_draw_reloaded(tmp_path): with Image.open(TEST_ICO_FILE) as im: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 425e690d6..fd00f260e 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -32,7 +32,8 @@ def test_unclosed_file(): im = Image.open(TEST_IM) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 4981e15af..b62132613 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -214,13 +214,20 @@ class TestFileJpeg: # Should not raise OSError for image with icc larger than image size. im.save( f, - format="JPEG", progressive=True, quality=95, icc_profile=icc_profile, optimize=True, ) + with Image.open("Tests/images/flower2.jpg") as im: + f = str(tmp_path / "temp2.jpg") + im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955) + + with Image.open("Tests/images/flower2.jpg") as im: + f = str(tmp_path / "temp3.jpg") + im.save(f, progressive=True, quality=94, exif=b" " * 43668) + def test_optimize(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), optimize=0) @@ -636,12 +643,6 @@ class TestFileJpeg: assert max(im2.quantization[0]) <= 255 assert max(im2.quantization[1]) <= 255 - def test_convert_dict_qtables_deprecation(self): - with pytest.warns(DeprecationWarning): - qtable = {0: [1, 2, 3, 4]} - qtable2 = JpegImagePlugin.convert_dict_qtables(qtable) - assert qtable == qtable2 - @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self): with Image.open(TEST_FILE) as img: @@ -928,6 +929,18 @@ class TestFileJpeg: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False + def test_repr_jpeg(self): + im = hopper() + + with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: + assert repr_jpeg.format == "JPEG" + assert_image_similar(im, repr_jpeg, 17) + + def test_repr_jpeg_error_returns_none(self): + im = hopper("F") + + assert im._repr_jpeg_() is None + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index de622c478..99df26fc9 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -4,13 +4,21 @@ from io import BytesIO import pytest -from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features +from PIL import ( + Image, + ImageFile, + Jpeg2KImagePlugin, + UnidentifiedImageError, + _binary, + features, +) from .helper import ( assert_image_equal, assert_image_similar, assert_image_similar_tofile, skip_unless_feature, + skip_unless_feature_version, ) EXTRA_DIR = "Tests/images/jpeg2000" @@ -266,17 +274,15 @@ def test_sgnd(tmp_path): assert reloaded_signed.getpixel((0, 0)) == 128 -def test_rgba(): +@pytest.mark.parametrize("ext", (".j2k", ".jp2")) +def test_rgba(ext): # Arrange - with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: - with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Act - j2k.load() - jp2.load() + with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: + # Act + im.load() - # Assert - assert j2k.mode == "RGBA" - assert jp2.mode == "RGBA" + # Assert + assert im.mode == "RGBA" @pytest.mark.parametrize("ext", (".j2k", ".jp2")) @@ -353,6 +359,35 @@ def test_subsampling_decode(name): assert_image_similar(im, expected, epsilon) +def test_comment(): + with Image.open("Tests/images/comment.jp2") as im: + assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" + + # Test an image that is truncated partway through a codestream + with open("Tests/images/comment.jp2", "rb") as fp: + b = BytesIO(fp.read(130)) + with Image.open(b) as im: + pass + + +def test_save_comment(): + for comment in ("Created by Pillow", b"Created by Pillow"): + out = BytesIO() + test_card.save(out, "JPEG2000", comment=comment) + + with Image.open(out) as im: + assert im.info["comment"] == b"Created by Pillow" + + out = BytesIO() + long_comment = b" " * 65531 + test_card.save(out, "JPEG2000", comment=long_comment) + with Image.open(out) as im: + assert im.info["comment"] == long_comment + + with pytest.raises(ValueError): + test_card.save(out, "JPEG2000", comment=long_comment + b" ") + + @pytest.mark.parametrize( "test_file", [ @@ -370,3 +405,29 @@ def test_crashes(test_file): im.load() except OSError: pass + + +@skip_unless_feature_version("jpg_2000", "2.4.0") +def test_plt_marker(): + # Search the start of the codesteam for PLT + out = BytesIO() + test_card.save(out, "JPEG2000", no_jp2=True, plt=True) + out.seek(0) + while True: + marker = out.read(2) + if not marker: + assert False, "End of stream without PLT" + + jp2_boxid = _binary.i16be(marker) + if jp2_boxid == 0xFF4F: + # SOC has no length + continue + elif jp2_boxid == 0xFF58: + # PLT + return + elif jp2_boxid == 0xFF93: + assert False, "SOD without finding PLT first" + + hdr = out.read(2) + length = _binary.i16be(hdr) + out.seek(length - 2, os.SEEK_CUR) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index f886d3aae..ac78b0869 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -668,6 +668,16 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) + def test_exif_ifd(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert im.tag_v2[34665] == 125456 + im.save(outfile) + + with Image.open(outfile) as reloaded: + if Image.core.libtiff_support_custom_tags: + assert reloaded.tag_v2[34665] == 125456 + def test_crashing_metadata(self, tmp_path): # issue 1597 with Image.open("Tests/images/rdf.tif") as im: @@ -984,6 +994,36 @@ class TestFileLibTiff(LibTiffTestCase): ) as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + @pytest.mark.parametrize( + "file_name, mode, size, tile", + [ + ( + "tiff_wrong_bits_per_sample.tiff", + "RGBA", + (52, 53), + [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_2.tiff", + "RGB", + (16, 16), + [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_3.tiff", + "RGBA", + (512, 256), + [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], + ), + ], + ) + def test_wrong_bits_per_sample(self, file_name, mode, size, tile): + with Image.open("Tests/images/" + file_name) as im: + assert im.mode == mode + assert im.size == size + assert im.tile == tile + im.load() + def test_no_rows_per_strip(self): # This image does not have a RowsPerStrip TIFF tag infile = "Tests/images/no_rows_per_strip.tif" @@ -1065,3 +1105,27 @@ class TestFileLibTiff(LibTiffTestCase): out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): im.save(out, compression=compression) + + def test_save_many_compressed(self, tmp_path): + im = hopper() + out = str(tmp_path / "temp.tif") + for _ in range(10000): + im.save(out, compression="jpeg") + + @pytest.mark.parametrize( + "path, sizes", + ( + ("Tests/images/hopper.tif", ()), + ("Tests/images/child_ifd.tiff", (16, 8)), + ("Tests/images/child_ifd_jpeg.tiff", (20,)), + ), + ) + def test_get_child_images(self, path, sizes): + with Image.open(path) as im: + ims = im.get_child_images() + + assert len(ims) == len(sizes) + for i, im in enumerate(ims): + w = sizes[i] + expected = Image.new("RGB", (w, w), "#f00") + assert_image_similar(im, expected, 1) diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 464d138e2..2588d3a05 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -51,6 +51,16 @@ def test_seek(): assert im.tell() == 0 +def test_close(): + with Image.open(TEST_FILE) as im: + pass + assert im.ole.fp.closed + + im = Image.open(TEST_FILE) + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index f0dedc2de..2e921e467 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -42,7 +42,8 @@ def test_unclosed_file(): im = Image.open(test_files[0]) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 2afec9960..ffc392d6b 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -8,7 +8,7 @@ import pytest from PIL import Image, PdfParser, features -from .helper import hopper, mark_if_feature_version +from .helper import hopper, mark_if_feature_version, skip_unless_feature def helper_save_as_pdf(tmp_path, mode, **kwargs): @@ -42,6 +42,28 @@ def test_save(tmp_path, mode): helper_save_as_pdf(tmp_path, mode) +@skip_unless_feature("jpg_2000") +@pytest.mark.parametrize("mode", ("LA", "RGBA")) +def test_save_alpha(tmp_path, mode): + helper_save_as_pdf(tmp_path, mode) + + +def test_p_alpha(tmp_path): + # Arrange + outfile = str(tmp_path / "temp.pdf") + with Image.open("Tests/images/pil123p.png") as im: + assert im.mode == "P" + assert isinstance(im.info["transparency"], bytes) + + # Act + im.save(outfile) + + # Assert + with open(outfile, "rb") as fp: + contents = fp.read() + assert b"\n/SMask " in contents + + def test_monochrome(tmp_path): # Arrange mode = "1" @@ -52,8 +74,8 @@ def test_monochrome(tmp_path): def test_unsupported_mode(tmp_path): - im = hopper("LA") - outfile = str(tmp_path / "temp_LA.pdf") + im = hopper("PA") + outfile = str(tmp_path / "temp_PA.pdf") with pytest.raises(ValueError): im.save(outfile) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c4db97905..f8df88d67 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -79,7 +79,7 @@ class TestFilePng: def test_sanity(self, tmp_path): # internal version number - assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) + assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) test_file = str(tmp_path / "temp.png") @@ -532,11 +532,10 @@ class TestFilePng: assert repr_png.format == "PNG" assert_image_equal(im, repr_png) - def test_repr_png_error(self): + def test_repr_png_error_returns_none(self): im = hopper("F") - with pytest.raises(ValueError): - im._repr_png_() + assert im._repr_png_() is None def test_chunk_order(self, tmp_path): with Image.open("Tests/images/icc_profile.png") as im: diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fbcbea6c6..292642ca9 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -256,6 +256,16 @@ def test_truncated_file(tmp_path): im.load() +def test_not_enough_image_data(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P2 1 2 255 255") + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + @pytest.mark.parametrize("maxval", (b"0", b"65536")) def test_invalid_maxval(maxval, tmp_path): path = str(tmp_path / "temp.ppm") diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 036cb9d4b..e405834b5 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -27,7 +27,8 @@ def test_unclosed_file(): im = Image.open(test_file) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py new file mode 100644 index 000000000..f33eada61 --- /dev/null +++ b/Tests/test_file_qoi.py @@ -0,0 +1,28 @@ +import pytest + +from PIL import Image, QoiImagePlugin + +from .helper import assert_image_equal_tofile, assert_image_similar_tofile + + +def test_sanity(): + with Image.open("Tests/images/hopper.qoi") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "QOI" + + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + with Image.open("Tests/images/pil123rgba.qoi") as im: + assert im.mode == "RGBA" + assert im.size == (162, 150) + assert im.format == "QOI" + + assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03) + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + QoiImagePlugin.QoiImageFile(invalid_file) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 011e208d8..09f1ef8e4 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -25,7 +25,8 @@ def test_unclosed_file(): im = Image.open(TEST_FILE) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 799c243d6..b27fa25f3 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -29,11 +29,9 @@ def test_sanity(codec, test_path, format): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - pytest.warns(ResourceWarning, open) - def test_close(): with warnings.catch_warnings(): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bac00e855..1a5730f49 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -163,7 +163,9 @@ def test_save_id_section(tmp_path): # Save with custom id section greater than 255 characters id_section = b"Test content" * 25 - pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section)) + with pytest.warns(UserWarning): + im.save(out, id_section=id_section) + with Image.open(out) as test_im: assert test_im.info["id_section"] == id_section[:255] diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 70142747c..f13436ce8 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -61,7 +61,8 @@ class TestFileTiff: im = Image.open("Tests/images/multipage.tiff") im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(self): with warnings.catch_warnings(): @@ -83,24 +84,6 @@ class TestFileTiff: with Image.open("Tests/images/multipage.tiff") as im: im.load() - @pytest.mark.parametrize( - "path, sizes", - ( - ("Tests/images/hopper.tif", ()), - ("Tests/images/child_ifd.tiff", (16, 8)), - ("Tests/images/child_ifd_jpeg.tiff", (20,)), - ), - ) - def test_get_child_images(self, path, sizes): - with Image.open(path) as im: - ims = im.get_child_images() - - assert len(ims) == len(sizes) - for i, im in enumerate(ims): - w = sizes[i] - expected = Image.new("RGB", (w, w), "#f00") - assert_image_similar(im, expected, 1) - def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] @@ -113,39 +96,16 @@ class TestFileTiff: assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self): + def test_bigtiff(self, tmp_path): with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") - @pytest.mark.parametrize( - "file_name,mode,size,tile", - [ - ( - "tiff_wrong_bits_per_sample.tiff", - "RGBA", - (52, 53), - [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_2.tiff", - "RGB", - (16, 16), - [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_3.tiff", - "RGBA", - (512, 256), - [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], - ), - ], - ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile): - with Image.open("Tests/images/" + file_name) as im: - assert im.mode == mode - assert im.size == size - assert im.tile == tile - im.load() + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + # multistrip support not yet implemented + del im.tag_v2[273] + + outfile = str(tmp_path / "temp.tif") + im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() @@ -231,7 +191,8 @@ class TestFileTiff: def test_bad_exif(self): with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. - pytest.warns(UserWarning, i._getexif) + with pytest.warns(UserWarning): + i._getexif() def test_save_rgba(self, tmp_path): im = hopper("RGBA") @@ -244,6 +205,12 @@ class TestFileTiff: with pytest.raises(OSError): im.save(outfile) + def test_8bit_s(self): + with Image.open("Tests/images/8bit.s.tif") as im: + im.load() + assert im.mode == "L" + assert im.getpixel((50, 50)) == 184 + def test_little_endian(self): with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index fdabae3a3..b7d100e7a 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -252,7 +252,8 @@ def test_empty_metadata(): head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) # Should not raise struct.error. - pytest.warns(UserWarning, info.load, f) + with pytest.warns(UserWarning): + info.load(f) def test_iccprofile(tmp_path): @@ -418,11 +419,12 @@ def test_too_many_entries(): ifd = TiffImagePlugin.ImageFileDirectory_v2() # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack("hh", 4, 4) + ifd._tagdata[277] = struct.pack("dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -581,7 +610,7 @@ def test_polygon_kite(mode): expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + draw.polygon(kite_points, fill="blue", outline="yellow") # Assert assert_image_equal_tofile(im, expected) @@ -628,7 +657,7 @@ def test_polygon_translucent(): assert_image_equal_tofile(im, expected) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -655,63 +684,68 @@ def test_big_rectangle(): assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) -def test_rectangle_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width.png" # Act - draw.rectangle(BBOX1, outline="green", width=5) + draw.rectangle(bbox, outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width_fill.png" # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=5) + draw.rectangle(bbox, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=0) + draw.rectangle(bbox, fill="blue", outline="green", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") -def test_rectangle_I16(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_I16(bbox): # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="black", outline="green") + draw.rectangle(bbox, fill="black", outline="green") # Assert assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") -def test_rectangle_translucent_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_translucent_outline(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") # Act - draw.rectangle(BBOX1, fill="black", outline=(0, 255, 0, 127), width=5) + draw.rectangle(bbox, fill="black", outline=(0, 255, 0, 127), width=5) # Assert assert_image_equal_tofile( @@ -735,6 +769,36 @@ def test_rounded_rectangle(xy): assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") +@pytest.mark.parametrize("top_left", (True, False)) +@pytest.mark.parametrize("top_right", (True, False)) +@pytest.mark.parametrize("bottom_right", (True, False)) +@pytest.mark.parametrize("bottom_left", (True, False)) +def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_left): + corners = (top_left, top_right, bottom_right, bottom_left) + + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle( + (10, 20, 190, 180), 30, fill="red", outline="green", width=5, corners=corners + ) + + # Assert + suffix = "".join( + ( + ("y" if top_left else "n"), + ("y" if top_right else "n"), + ("y" if bottom_right else "n"), + ("y" if bottom_left else "n"), + ) + ) + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_corners_" + suffix + ".png" + ) + + @pytest.mark.parametrize( "xy, radius, type", [ @@ -758,13 +822,14 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type): ) -def test_rounded_rectangle_zero_radius(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rounded_rectangle_zero_radius(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5) + draw.rounded_rectangle(bbox, 0, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") @@ -774,7 +839,9 @@ def test_rounded_rectangle_zero_radius(): "xy, suffix", [ ((20, 10, 80, 90), "x"), + ((20, 10, 81, 90), "x_odd"), ((10, 20, 90, 80), "y"), + ((10, 20, 90, 81), "y_odd"), ((20, 20, 80, 80), "both"), ], ) @@ -794,14 +861,15 @@ def test_rounded_rectangle_translucent(xy, suffix): ) -def test_floodfill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill(bbox): red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -826,13 +894,14 @@ def test_floodfill(): assert_image_equal(im, Image.new("RGB", (1, 1), red)) -def test_floodfill_border(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_border(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -847,13 +916,14 @@ def test_floodfill_border(): assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") -def test_floodfill_thresh(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_thresh(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="darkgreen", fill="green") + draw.rectangle(bbox, outline="darkgreen", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -902,9 +972,6 @@ def test_square(): img, draw = create_base_image_draw((10, 10)) draw.rectangle((2, 2, 7, 7), BLACK) assert_image_equal_tofile(img, expected, "square as normal rectangle failed") - img, draw = create_base_image_draw((10, 10)) - draw.rectangle((7, 7, 2, 2), BLACK) - assert_image_equal_tofile(img, expected, "square as inverted rectangle failed") def test_triangle_right(): @@ -1191,21 +1258,6 @@ def test_textbbox_stroke(): assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) -def test_textsize_deprecation(): - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - with pytest.warns(DeprecationWarning) as log: - draw.textsize("Hello") - assert len(log) == 1 - with pytest.warns(DeprecationWarning) as log: - draw.textsize("Hello\nWorld") - assert len(log) == 1 - with pytest.warns(DeprecationWarning) as log: - draw.multiline_textsize("Hello\nWorld") - assert len(log) == 1 - - @skip_unless_feature("freetype2") def test_stroke(): for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): @@ -1274,6 +1326,7 @@ def test_stroke_multiline(): assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) +@skip_unless_feature("freetype2") def test_setting_default_font(): # Arrange im = Image.new("RGB", (100, 250)) @@ -1291,7 +1344,8 @@ def test_setting_default_font(): assert isinstance(draw.getfont(), ImageFont.ImageFont) -def test_same_color_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_same_color_outline(bbox): # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1307,12 +1361,12 @@ def test_same_color_outline(): for mode in ["RGB", "L"]: for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: for operation, args in { - "chord": [BBOX1, 0, 180], - "ellipse": [BBOX1], + "chord": [bbox, 0, 180], + "ellipse": [bbox], "shape": [s], - "pieslice": [BBOX1, -90, 45], + "pieslice": [bbox, -90, 45], "polygon": [[(18, 30), (85, 30), (60, 72)]], - "rectangle": [BBOX1], + "rectangle": [bbox], }.items(): # Arrange im = Image.new(mode, (W, H)) @@ -1329,20 +1383,20 @@ def test_same_color_outline(): @pytest.mark.parametrize( - "n_sides, rotation, polygon_name", - [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], + "n_sides, polygon_name, args", + [ + (4, "square", {}), + (8, "regular_octagon", {}), + (4, "square_rotate_45", {"rotation": 45}), + (3, "triangle_width", {"width": 5, "outline": "yellow"}), + ], ) -def test_draw_regular_polygon(n_sides, rotation, polygon_name): +def test_draw_regular_polygon(n_sides, polygon_name, args): im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) - filename_base = f"Tests/images/imagedraw_{polygon_name}" - filename = ( - f"{filename_base}.png" - if rotation == 0 - else f"{filename_base}_rotate_{rotation}.png" - ) + filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) bounding_circle = ((W // 2, H // 2), 25) - draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") + draw.regular_polygon(bounding_circle, n_sides, fill="red", **args) assert_image_equal_tofile(im, filename) @@ -1469,3 +1523,21 @@ def test_polygon2(): draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") expected = "Tests/images/imagedraw_outline_polygon_RGB.png" assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) +def test_incorrectly_ordered_coordinates(xy): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with pytest.raises(ValueError): + draw.arc(xy, 10, 260) + with pytest.raises(ValueError): + draw.chord(xy, 10, 260) + with pytest.raises(ValueError): + draw.ellipse(xy) + with pytest.raises(ValueError): + draw.pieslice(xy, 10, 260) + with pytest.raises(ValueError): + draw.rectangle(xy) + with pytest.raises(ValueError): + draw.rounded_rectangle(xy) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 6fc829f1a..a2c2fa1f0 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -2,7 +2,7 @@ import os.path import pytest -from PIL import Image, ImageDraw, ImageDraw2 +from PIL import Image, ImageDraw, ImageDraw2, features from .helper import ( assert_image_equal, @@ -27,15 +27,16 @@ X1 = int(X0 * 3) Y0 = int(H / 4) Y1 = int(X0 * 3) -# Two kinds of bounding box -BBOX1 = [(X0, Y0), (X1, Y1)] -BBOX2 = [X0, Y0, X1, Y1] +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) -# Two kinds of coordinate sequences -POINTS1 = [(10, 10), (20, 40), (30, 30)] -POINTS2 = [10, 10, 20, 40, 30, 30] - -KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +# Coordinate sequences +POINTS = ( + ((10, 10), (20, 40), (30, 30)), + [(10, 10), (20, 40), (30, 30)], + (10, 10, 20, 40, 30, 30), + [10, 10, 20, 40, 30, 30], +) FONT_PATH = "Tests/fonts/FreeMono.ttf" @@ -52,7 +53,7 @@ def test_sanity(): draw.line(list(range(10)), pen) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_ellipse(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -80,7 +81,7 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -94,7 +95,8 @@ def test_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_line_pen_as_brush(): +@pytest.mark.parametrize("points", POINTS) +def test_line_pen_as_brush(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -103,13 +105,13 @@ def test_line_pen_as_brush(): # Act # Pass in the pen as the brush parameter - draw.line(POINTS1, pen, brush) + draw.line(points, pen, brush) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) @@ -124,7 +126,7 @@ def test_polygon(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -171,19 +173,18 @@ def test_text(): @skip_unless_feature("freetype2") -def test_textsize(): +def test_textbbox(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) font = ImageDraw2.Font("white", FONT_PATH) # Act - with pytest.warns(DeprecationWarning) as log: - size = draw.textsize("ImageDraw2", font) - assert len(log) == 1 + bbox = draw.textbbox((0, 0), "ImageDraw2", font) # Assert - assert size[1] == 12 + right = 72 if features.check_feature("raqm") else 70 + assert bbox == (0, 2, right, 12) @skip_unless_feature("freetype2") diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 412bc10d9..ff75b8c2a 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -136,7 +136,7 @@ class TestImageFile: class DummyImageFile(ImageFile.ImageFile): def _open(self): - self.mode = "RGB" + self._mode = "RGB" self._size = (1, 1) im = DummyImageFile(buf) @@ -217,7 +217,7 @@ xoff, yoff, xsize, ysize = 10, 20, 100, 100 class MockImageFile(ImageFile.ImageFile): def _open(self): self.rawmode = "RGBA" - self.mode = "RGBA" + self._mode = "RGBA" self._size = (200, 200) self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 306a2f1bf..02622e721 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -191,6 +191,16 @@ def test_getlength( assert length == length_raqm +def test_float_size(): + lengths = [] + for size in (48, 48.5, 49): + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine + ) + lengths.append(f.getlength("text")) + assert lengths[0] != lengths[1] != lengths[2] + + def test_render_multiline(font): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -251,27 +261,6 @@ def test_draw_align(font): draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_size(font): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - with pytest.warns(DeprecationWarning) as log: - # Test that textsize() correctly connects to multiline_textsize() - assert draw.textsize(TEST_TEXT, font=font) == draw.multiline_textsize( - TEST_TEXT, font=font - ) - - # Test that multiline_textsize corresponds to ImageFont.textsize() - # for single line text - assert font.getsize("A") == draw.multiline_textsize("A", font=font) - - # Test that textsize() can pass on additional arguments - # to multiline_textsize() - draw.textsize(TEST_TEXT, font=font, spacing=4) - draw.textsize(TEST_TEXT, font, 4) - assert len(log) == 6 - - def test_multiline_bbox(font): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -298,12 +287,6 @@ def test_multiline_width(font): draw.textbbox((0, 0), "longest line", font=font)[2] == draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2] ) - with pytest.warns(DeprecationWarning) as log: - assert ( - draw.textsize("longest line", font=font)[0] - == draw.multiline_textsize("longest line\nline", font=font)[0] - ) - assert len(log) == 2 def test_multiline_spacing(font): @@ -326,32 +309,27 @@ def test_rotated_transposed_font(font, orientation): # Original font draw.font = font - with pytest.warns(DeprecationWarning) as log: - box_size_a = draw.textsize(word) - assert box_size_a == font.getsize(word) - assert len(log) == 2 bbox_a = draw.textbbox((10, 10), word) # Rotated font draw.font = transposed_font - with pytest.warns(DeprecationWarning) as log: - box_size_b = draw.textsize(word) - assert box_size_b == transposed_font.getsize(word) - assert len(log) == 2 bbox_b = draw.textbbox((20, 20), word) - # Check (w,h) of box a is (h,w) of box b - assert box_size_a[0] == box_size_b[1] - assert box_size_a[1] == box_size_b[0] + # Check (w, h) of box a is (h, w) of box b + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( + bbox_b[3] - bbox_b[1], + bbox_b[2] - bbox_b[0], + ) - # Check bbox b is (20, 20, 20 + h, 20 + w) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] - assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) # text length is undefined for vertical text - pytest.raises(ValueError, draw.textlength, word) + with pytest.raises(ValueError): + draw.textlength(word) @pytest.mark.parametrize( @@ -372,28 +350,25 @@ def test_unrotated_transposed_font(font, orientation): # Original font draw.font = font - with pytest.warns(DeprecationWarning) as log: - box_size_a = draw.textsize(word) - assert len(log) == 1 bbox_a = draw.textbbox((10, 10), word) length_a = draw.textlength(word) # Rotated font draw.font = transposed_font - with pytest.warns(DeprecationWarning) as log: - box_size_b = draw.textsize(word) - assert len(log) == 1 bbox_b = draw.textbbox((20, 20), word) length_b = draw.textlength(word) # Check boxes a and b are same size - assert box_size_a == box_size_b + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( + bbox_b[2] - bbox_b[0], + bbox_b[3] - bbox_b[1], + ) - # Check bbox b is (20, 20, 20 + w, 20 + h) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] - assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) assert length_a == length_b @@ -446,19 +421,6 @@ def test_free_type_font_get_metrics(font): assert (ascent, descent) == (16, 4) -def test_free_type_font_get_offset(font): - # Arrange - text = "offset this" - - # Act - with pytest.warns(DeprecationWarning) as log: - offset = font.getoffset(text) - - # Assert - assert len(log) == 1 - assert offset == (0, 3) - - def test_free_type_font_get_mask(font): # Arrange text = "mask this" @@ -501,6 +463,11 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") +@pytest.mark.parametrize("mode", (None, "1", "RGBA")) +def test_getbbox(font, mode): + assert (0, 4, 12, 16) == font.getbbox("A", mode) + + def test_getbbox_empty(font): # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") @@ -617,19 +584,6 @@ def test_imagefont_getters(font): assert font.getlength("M") == 12 assert font.getlength("y") == 12 assert font.getlength("a") == 12 - with pytest.warns(DeprecationWarning) as log: - assert font.getsize("A") == (12, 16) - assert font.getsize("AB") == (24, 16) - assert font.getsize("M") == (12, 16) - assert font.getsize("y") == (12, 20) - assert font.getsize("a") == (12, 16) - assert font.getsize_multiline("A") == (12, 16) - assert font.getsize_multiline("AB") == (24, 16) - assert font.getsize_multiline("a") == (12, 16) - assert font.getsize_multiline("ABC\n") == (36, 36) - assert font.getsize_multiline("ABC\nA") == (36, 36) - assert font.getsize_multiline("ABC\nAaaa") == (48, 36) - assert len(log) == 11 @pytest.mark.parametrize("stroke_width", (0, 2)) @@ -640,16 +594,6 @@ def test_getsize_stroke(font, stroke_width): 12 + stroke_width, 16 + stroke_width, ) - with pytest.warns(DeprecationWarning) as log: - assert font.getsize("A", stroke_width=stroke_width) == ( - 12 + stroke_width * 2, - 16 + stroke_width * 2, - ) - assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( - 48 + stroke_width * 2, - 36 + stroke_width * 4, - ) - assert len(log) == 2 def test_complex_font_settings(): @@ -780,11 +724,8 @@ def test_textbbox_non_freetypefont(): im = Image.new("RGB", (200, 200)) d = ImageDraw.Draw(im) default_font = ImageFont.load_default() - with pytest.warns(DeprecationWarning) as log: - width, height = d.textsize("test", font=default_font) - assert len(log) == 1 - assert d.textlength("test", font=default_font) == width - assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) + assert d.textlength("test", font=default_font) == 24 + assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) @pytest.mark.parametrize( @@ -872,25 +813,23 @@ def test_anchor_invalid(font): d.font = font for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: - pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)) - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor) + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor) + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) for anchor in ["lt", "lb"]: - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) @@ -1084,14 +1023,6 @@ def test_woff2(layout_engine): assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) -def test_fill_deprecation(font): - with pytest.warns(DeprecationWarning): - font.getmask2("Hello world", fill=Image.core.fill) - with pytest.warns(DeprecationWarning): - with pytest.raises(TypeError): - font.getmask2("Hello world", fill=None) - - def test_render_mono_size(): # issue 4177 @@ -1107,10 +1038,30 @@ def test_render_mono_size(): assert_image_equal_tofile(im, "Tests/images/text_mono.gif") +def test_too_many_characters(font): + with pytest.raises(ValueError): + font.getlength("A" * 1_000_001) + with pytest.raises(ValueError): + font.getbbox("A" * 1_000_001) + with pytest.raises(ValueError): + font.getmask2("A" * 1_000_001) + + transposed_font = ImageFont.TransposedFont(font) + with pytest.raises(ValueError): + transposed_font.getlength("A" * 1_000_001) + + default_font = ImageFont.load_default() + with pytest.raises(ValueError): + default_font.getlength("A" * 1_000_001) + with pytest.raises(ValueError): + default_font.getbbox("A" * 1_000_001) + + @pytest.mark.parametrize( "test_file", [ "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", + "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) def test_oom(test_file): @@ -1131,12 +1082,3 @@ def test_raqm_missing_warning(monkeypatch): "Raqm layout was requested, but Raqm is not available. " "Falling back to basic layout." ) - - -def test_constants_deprecation(): - for enum, prefix in { - ImageFont.Layout: "LAYOUT_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(ImageFont, prefix + name) == enum[name] diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index cf039e86e..6099b04e4 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -360,37 +360,20 @@ def test_anchor_invalid_ttb(): d.font = font for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: - pytest.raises( - ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, lambda: font.getbbox("hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, - lambda: d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb"), - ) - pytest.raises( - ValueError, - lambda: d.multiline_text( - (0, 0), "foo\nbar", anchor=anchor, direction="ttb" - ), - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox( - (0, 0), "foo\nbar", anchor=anchor, direction="ttb" - ), - ) + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") # ttb multiline text does not support anchors at all - pytest.raises( - ValueError, - lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"), - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb"), - ) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb") diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index fa88065f4..f8059eca4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -98,3 +98,18 @@ $ms = new-object System.IO.MemoryStream(, $bytes) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, "Tests/images/hopper.png") + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize("ext", ("gif", "png", "ico")) + def test_grabclipboard_wl_clipboard(self, ext): + image_path = "Tests/images/hopper." + ext + with open(image_path, "rb") as fp: + subprocess.call(["wl-copy"], stdin=fp) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index d390f3c1e..b05785be0 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -404,6 +404,18 @@ def test_exif_transpose(): assert 0x0112 not in transposed_im.getexif() +def test_exif_transpose_in_place(): + with Image.open("Tests/images/orientation_rectangle.jpg") as im: + assert im.size == (2, 1) + assert im.getexif()[0x0112] == 8 + expected = im.rotate(90, expand=True) + + ImageOps.exif_transpose(im, in_place=True) + assert im.size == (1, 2) + assert 0x0112 not in im.getexif() + assert_image_equal(im, expected) + + def test_autocontrast_cutoff(): # Test the cutoff argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index ac99ef381..baa698bb4 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -9,10 +9,6 @@ def test_sanity(): palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError): - ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10) - def test_reload(): with Image.open("Tests/images/hopper.gif") as im: diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8f8a9f449..c112cfd87 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -28,7 +28,7 @@ def test_path(): (6.0, 7.0), (8.0, 9.0), ] - assert p.tolist(1) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + assert p.tolist(True) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] assert p.getbbox() == (0.0, 1.0, 8.0, 9.0) @@ -38,48 +38,65 @@ def test_path(): p.transform((1, 0, 1, 0, 1, 1)) assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] - # alternative constructors - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0.0, 1.0]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([(0, 1)]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(0)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(1)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(array.array("f", [0, 1])) - assert list(p) == [(0.0, 1.0)] - arr = array.array("f", [0, 1]) - p = ImagePath.Path(arr.tobytes()) +@pytest.mark.parametrize( + "coords", + ( + (0, 1), + [0, 1], + (0.0, 1.0), + [0.0, 1.0], + ((0, 1),), + [(0, 1)], + ((0.0, 1.0),), + [(0.0, 1.0)], + array.array("f", [0, 1]), + array.array("f", [0, 1]).tobytes(), + ImagePath.Path((0, 1)), + ), +) +def test_path_constructors(coords): + # Arrange / Act + p = ImagePath.Path(coords) + + # Assert assert list(p) == [(0.0, 1.0)] -def test_invalid_coords(): - # Arrange - coords = ["a", "b"] - - # Act / Assert +@pytest.mark.parametrize( + "coords", + ( + ("a", "b"), + ([0, 1],), + [[0, 1]], + ([0.0, 1.0],), + [[0.0, 1.0]], + ), +) +def test_invalid_path_constructors(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "incorrect coordinate type" -def test_path_odd_number_of_coordinates(): - # Arrange - coords = [0] - - # Act / Assert +@pytest.mark.parametrize( + "coords", + ( + (0,), + [0], + (0, 1, 2), + [0, 1, 2], + ), +) +def test_path_odd_number_of_coordinates(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "wrong number of coordinates" diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 2f2b07918..2c73a2094 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -2,13 +2,10 @@ import warnings import pytest +from PIL import ImageQt + from .helper import assert_image_similar, hopper -with warnings.catch_warnings() as w: - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt - - pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" ) @@ -26,10 +23,6 @@ def test_rgb(): from PyQt6.QtGui import qRgb elif ImageQt.qt_version == "side6": from PySide6.QtGui import qRgb - elif ImageQt.qt_version == "5": - from PyQt5.QtGui import qRgb - elif ImageQt.qt_version == "side2": - from PySide2.QtGui import qRgb assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 3e147a9ef..e54372b60 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -55,8 +55,8 @@ def test_show_without_viewers(): viewers = ImageShow._viewers ImageShow._viewers = [] - im = hopper() - assert not ImageShow.show(im) + with hopper() as im: + assert not ImageShow.show(im) ImageShow._viewers = viewers @@ -89,20 +89,3 @@ def test_ipythonviewer(): im = hopper() assert test_viewer.show(im) == 1 - - -@pytest.mark.skipif( - not on_ci() or is_win32(), - reason="Only run on CIs; hangs on Windows CIs", -) -@pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_file_deprecated(tmp_path, viewer): - f = str(tmp_path / "temp.jpg") - hopper().save(f) - with pytest.warns(DeprecationWarning): - try: - viewer.show_file(file=f) - except NotImplementedError: - pass - with pytest.raises(TypeError): - viewer.show_file() diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index 995d0ee1f..a0c9574ba 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -89,13 +89,6 @@ def test_photoimage_blank(mode): assert_image_equal(reloaded.convert(mode), im) -def test_box_deprecation(): - im = hopper() - im_tk = ImageTk.PhotoImage(im) - with pytest.warns(DeprecationWarning): - im_tk.paste(im, (0, 0, 128, 128)) - - def test_bitmapimage(): im = hopper("1") diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 9d64d17a3..5e489284f 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -100,8 +100,11 @@ class TestImageWinDib: # Act # Make one the same as the using tobytes()/frombytes() test_buffer = dib1.tobytes() - dib2.frombytes(test_buffer) + for datatype in ("bytes", "memoryview"): + if datatype == "memoryview": + test_buffer = memoryview(test_buffer) + dib2.frombytes(test_buffer) - # Assert - # Confirm they're the same - assert dib1.tobytes() == dib2.tobytes() + # Assert + # Confirm they're the same + assert dib1.tobytes() == dib2.tobytes() diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 979806cae..f7812f62b 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -207,6 +207,9 @@ class TestLibPack: 0x01000083, ) + def test_I16(self): + self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + def test_F_float(self): self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) @@ -754,6 +757,7 @@ class TestLibUnpack: def test_I16(self): self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) @@ -761,10 +765,12 @@ class TestLibUnpack: self.assert_unpack("I;16", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16B", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16L", "I;16N", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) else: self.assert_unpack("I;16", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) + self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) def test_CMYK16(self): self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index dcdee3d41..1786dba38 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -88,10 +88,7 @@ def test_tobytes(): def test_convert(): im = original.copy() - verify(im.convert("I;16")) - verify(im.convert("I;16").convert("L")) - verify(im.convert("I;16").convert("I")) - - verify(im.convert("I;16B")) - verify(im.convert("I;16B").convert("L")) - verify(im.convert("I;16B").convert("I")) + for mode in ("I;16", "I;16B", "I;16N"): + verify(im.convert(mode)) + verify(im.convert(mode).convert("L")) + verify(im.convert(mode).convert("I")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index a8bbcbdb8..147f94a71 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -4,7 +4,7 @@ import pytest from PIL import Image -from .helper import assert_deep_equal, assert_image, hopper +from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature numpy = pytest.importorskip("numpy", reason="NumPy not installed") @@ -219,6 +219,13 @@ def test_zero_size(): assert im.size == (0, 0) +@skip_unless_feature("libtiff") +def test_load_first(): + with Image.open("Tests/images/g4_orientation_5.tif") as im: + a = numpy.array(im) + assert a.shape == (88, 590) + + def test_bool(): # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index 43e244c7b..105a838d9 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -117,3 +117,9 @@ def test_pdf_repr(): assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" + + +def test_duplicate_xref_entry(): + pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") + assert pdf.xref_table.existing_entries[6][0] == 1197 + pdf.close() diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 2f6d05888..1c5d482bd 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -75,13 +75,13 @@ def test_pickle_la_mode_with_palette(tmp_path): # Act / Assert for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - im.mode = "LA" + im._mode = "LA" with open(filename, "wb") as f: pickle.dump(im, f, protocol) with open(filename, "rb") as f: loaded_im = pickle.load(f) - im.mode = "PA" + im._mode = "PA" assert im == loaded_im @@ -112,6 +112,7 @@ def helper_assert_pickled_font_images(font1, font2): assert_image_equal(im1, im2) +@skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_string(protocol): # Arrange @@ -125,6 +126,7 @@ def test_pickle_font_string(protocol): helper_assert_pickled_font_images(font, unpickled_font) +@skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_file(tmp_path, protocol): # Arrange diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 4929fa933..5d2e41212 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,10 +1,6 @@ -import warnings - import pytest -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt +from PIL import ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -19,14 +15,6 @@ if ImageQt.qt_is_installed: from PySide6.QtCore import QPoint from PySide6.QtGui import QImage, QPainter, QRegion from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - elif ImageQt.qt_version == "5": - from PyQt5.QtCore import QPoint - from PyQt5.QtGui import QImage, QPainter, QRegion - from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - elif ImageQt.qt_version == "side2": - from PySide2.QtCore import QPoint - from PySide2.QtGui import QImage, QPainter, QRegion - from PySide2.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget class Example(QWidget): def __init__(self): diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index c1983031a..95c13ba75 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,10 +1,6 @@ -import warnings - import pytest -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt +from PIL import ImageQt from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -32,7 +28,7 @@ def test_sanity(mode, tmp_path): assert_image_equal(rt, src) if mode == "1": - # BW appears to not save correctly on QT5 + # BW appears to not save correctly on Qt # kicks out errors on console: # libpng warning: Invalid color type/bit depth combination # in IHDR diff --git a/_custom_build/backend.py b/_custom_build/backend.py new file mode 100755 index 000000000..9b3265a94 --- /dev/null +++ b/_custom_build/backend.py @@ -0,0 +1,56 @@ +import sys + +from setuptools.build_meta import * # noqa: F401, F403 +from setuptools.build_meta import build_wheel + +backend_class = build_wheel.__self__.__class__ + + +class _CustomBuildMetaBackend(backend_class): + def run_setup(self, setup_script="setup.py"): + if self.config_settings: + + def config_has(key, value): + settings = self.config_settings.get(key) + if settings: + if not isinstance(settings, list): + settings = [settings] + return value in settings + + flags = [] + for dependency in ( + "zlib", + "jpeg", + "tiff", + "freetype", + "raqm", + "lcms", + "webp", + "webpmux", + "jpeg2000", + "imagequant", + "xcb", + ): + if config_has(dependency, "enable"): + flags.append("--enable-" + dependency) + elif config_has(dependency, "disable"): + flags.append("--disable-" + dependency) + for dependency in ("raqm", "fribidi"): + if config_has(dependency, "vendor"): + flags.append("--vendor-" + dependency) + if self.config_settings.get("platform-guessing") == "disable": + flags.append("--disable-platform-guessing") + if self.config_settings.get("debug") == "true": + flags.append("--debug") + if flags: + sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:] + return super().run_setup(setup_script) + + def build_wheel( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + self.config_settings = config_settings + return super().build_wheel(wheel_directory, config_settings, metadata_directory) + + +build_wheel = _CustomBuildMetaBackend().build_wheel diff --git a/codecov.yml b/codecov.yml index f3afccc1c..1ea7974eb 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,9 @@ -# Documentation: https://docs.codecov.io/docs/codecov-yaml +# Documentation: https://docs.codecov.com/docs/codecov-yaml codecov: # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" # https://github.com/codecov/support/issues/363 - # https://docs.codecov.io/docs/comparing-commits + # https://docs.codecov.com/docs/comparing-commits allow_coverage_offsets: true comment: false @@ -12,7 +12,7 @@ coverage: status: project: default: - threshold: 0.01% + threshold: 0.1% # Matches 'omit:' in .coveragerc ignore: diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 8b847b894..fd6000ee1 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.1.0 +archive=libimagequant-4.2.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index d1b31cfa5..24c1f9c30 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=libraqm-0.10.0 +archive=libraqm-0.10.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/depends/install_webp.sh b/depends/install_webp.sh index f8b985a7a..4636aab43 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.3.0 +archive=libwebp-1.3.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/Guardfile b/docs/Guardfile index b689b079a..6cbf07b06 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -2,7 +2,7 @@ from livereload.compiler import shell from livereload.task import Task -Task.add('*.rst', shell('make html')) -Task.add('*/*.rst', shell('make html')) -Task.add('Makefile', shell('make html')) -Task.add('conf.py', shell('make html')) +Task.add("*.rst", shell("make html")) +Task.add("*/*.rst", shell("make html")) +Task.add("Makefile", shell("make html")) +Task.add("conf.py", shell("make html")) diff --git a/docs/Makefile b/docs/Makefile index d32d25a3c..3b4deb9bf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -19,6 +19,7 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" + @echo " htmlview to open the index page built by the html target in your browser" @echo " serve to start a local server for viewing docs" @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @@ -45,7 +46,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph + $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-removed-in sphinxext-opengraph .PHONY: html html: @@ -196,6 +197,10 @@ doctest: @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." +.PHONY: htmlview +htmlview: html + $(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" + .PHONY: livehtml livehtml: html livereload $(BUILDDIR)/html -p 33233 diff --git a/docs/conf.py b/docs/conf.py index e1ffa49b8..a2c825292 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,11 +28,11 @@ needs_sphinx = "2.4" # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_inline_tabs", - "sphinx_issues", "sphinx_removed_in", "sphinxext.opengraph", ] @@ -317,8 +317,28 @@ def setup(app): app.add_css_file("css/dark.css") -# GitHub repo for sphinx-issues -issues_github_path = "python-pillow/Pillow" +linkcheck_allowed_redirects = { + r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501 + r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501 + r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501 + r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501 + r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", + r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501 + r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501 + r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501 +} + +# sphinx.ext.extlinks +# This config is a dictionary of external sites, +# mapping unique short aliases to a base URL and a prefix. +# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html +_repo = "https://github.com/python-pillow/Pillow/" +extlinks = { + "cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"), + "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), + "issue": (_repo + "issues/%s", "#%s"), + "pr": (_repo + "pull/%s", "#%s"), +} # sphinxext.opengraph ogp_image = ( diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a..ce956cade 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,22 +12,50 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. +PSFile +~~~~~~ + +.. deprecated:: 9.5.0 + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + +PyAccess and Image.USE_CFFI_ACCESS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.0.0 + +Since Pillow's C API is now faster than PyAccess on PyPy, +:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow +11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. + +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is +similarly deprecated. + +Removed features +---------------- + +Deprecated features are only removed in major releases after an appropriate +period of deprecation has passed. + Tk/Tcl 8.4 ~~~~~~~~~~ .. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -when Tk/Tcl 8.5 will be the minimum supported. +Support for Tk/Tcl 8.4 was removed in Pillow 10.0.0 (2023-07-01). Categories ~~~~~~~~~~ .. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and -``Image.CONTAINER`` attributes. +``im.category`` was removed along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. To determine if an image has multiple frames or not, ``getattr(im, "is_animated", False)`` can be used instead. @@ -36,43 +64,40 @@ JpegImagePlugin.convert_dict_qtables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 8.3.0 +.. versionremoved:: 10.0.0 -JPEG ``quantization`` is now automatically converted, but still returned as a -dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer -performs any operations on the data given to it, has been deprecated and will be -removed in Pillow 10.0.0 (2023-07-01). +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. ImagePalette size parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 8.4.0 - -The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). +.. versionremoved:: 10.0.0 Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by -default, and the size parameter could be used to override that. Pillow 8.3.0 removed -the default required length, also removing the need for the size parameter. +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. ImageShow.Viewer.show_file file argument ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been -deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by -``path``. +removed and replaced by ``path``. In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. -``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest -``viewer.show_file(path="test.jpg")`` instead. Constants ~~~~~~~~~ .. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 -A number of constants have been deprecated and will be removed in Pillow 10.0.0 -(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. +A number of constants have been removed. +Instead, ``enum.IntEnum`` classes have been added. .. note:: @@ -81,7 +106,7 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 See :ref:`restored-image-constants` ===================================================== ============================================================ -Deprecated Use instead +Removed Use instead ===================================================== ============================================================ ``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` ``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` @@ -115,71 +140,31 @@ FitsStubImagePlugin ~~~~~~~~~~~~~~~~~~~ .. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 -The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in -Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through -:mod:`~PIL.FitsImagePlugin` instead. - -FreeTypeFont.getmask2 fill parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been -deprecated and will be removed in Pillow 10 (2023-07-01). - -PhotoImage.paste box parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). - -PyQt5 and PySide2 -~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed -in Pillow 10 (2023-07-01). Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. - -Image.coerce_e -~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -This undocumented method has been deprecated and will be removed in Pillow 10 -(2023-07-01). - -.. _Font size and offset methods: +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. Font size and offset methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 -Several functions for computing the size and offset of rendered text -have been deprecated and will be removed in Pillow 10 (2023-07-01): +Several functions for computing the size and offset of rendered text have been removed: -=========================================================================== ============================================================================================================= -Deprecated Use instead -=========================================================================== ============================================================================================================= -:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` -:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` -:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` -:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` -:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` -:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` -=========================================================================== ============================================================================================================= +=============================================================== ============================================================================================================= +Removed Use instead +=============================================================== ============================================================================================================= +``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` +``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=============================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -194,9 +179,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont @@ -211,11 +194,43 @@ Use instead: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") width, height = right - left, bottom - top -Removed features ----------------- +FreeTypeFont.getmask2 fill parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Deprecated features are only removed in major releases after an appropriate -period of deprecation has passed. +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + +PhotoImage.paste box parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The ``box`` parameter was unused and has been removed. + +PyQt5 and PySide2 +~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +This undocumented method has been removed. PILLOW_VERSION constant ~~~~~~~~~~~~~~~~~~~~~~~ @@ -265,7 +280,7 @@ FreeType 2.7 Support for FreeType 2.7 has been removed. We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _FreeType: https://freetype.org/ @@ -336,16 +351,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 26451533e..61690410b 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -225,7 +225,7 @@ class DdsImageFile(ImageFile.ImageFile): flags, height, width = struct.unpack("<3I", header.read(12)) self._size = (width, height) - self.mode = "RGBA" + self._mode = "RGBA" pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) struct.unpack("<11I", header.read(44)) # reserved diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 0aa2f1119..e0975a121 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -62,7 +62,6 @@ Pillow also provides limited support for a few additional modes, including: * ``BGR;15`` (15-bit reversed true colour) * ``BGR;16`` (16-bit reversed true colour) * ``BGR;24`` (24-bit reversed true colour) - * ``BGR;32`` (32-bit reversed true colour) Premultiplied alpha is where the values for each other channel have been multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` @@ -96,9 +95,8 @@ in the upper left corner. Note that the coordinates refer to the implied pixel corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5). Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles -are represented as 4-tuples, with the upper left corner given first. For -example, a rectangle covering all of an 800x600 pixel image is written as (0, -0, 800, 600). +are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given +first. Palette ------- diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 6058149cb..2a42bdacb 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -253,7 +253,7 @@ their :py:attr:`~PIL.Image.Image.info` values. **loop** Integer number of times the GIF should loop. 0 means that it will loop - forever. By default, the image will not loop. + forever. If omitted or ``None``, the image will not loop. **comment** A comment about the image. @@ -589,6 +589,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.1.0 +**comment** + Adds a custom comment to the file, replacing the default + "Created by OpenJPEG version" comment. + + .. versionadded:: 9.5.0 + +**plt** + If ``True`` and OpenJPEG 2.4.0 or later is available, then include a PLT + (packet length, tile-part header) marker in the produced file. + Defaults to ``False``. + + .. versionadded:: 9.5.0 + .. note:: To enable JPEG 2000 support, you need to build and install the OpenJPEG @@ -848,6 +861,10 @@ PPM Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or ``RGB`` data. +"Raw" (P4 to P6) formats can be read, and are used when writing. + +Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. + SGI ^^^ @@ -1367,6 +1384,12 @@ PSD Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. +QOI +^^^ + +.. versionadded:: 9.5.0 + +Pillow identifies and reads images in Quite OK Image format. SUN ^^^ @@ -1385,7 +1408,7 @@ the open function in the :py:mod:`~PIL.WalImageFile` module to read files in this format. By default, a Quake2 standard palette is attached to the texture. To override -the palette, use the putpalette method. +the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. WMF, EMF ^^^^^^^^ @@ -1393,9 +1416,7 @@ WMF, EMF Pillow can identify WMF and EMF files. On Windows, it can read WMF and EMF files. By default, it will load the image -at 72 dpi. To load it at another resolution: - -.. code-block:: python +at 72 dpi. To load it at another resolution:: from PIL import Image @@ -1404,9 +1425,7 @@ at 72 dpi. To load it at another resolution: To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF -handler. - -.. code-block:: python +handler. :: from PIL import Image from PIL import WmfImagePlugin @@ -1461,8 +1480,13 @@ PDF ^^^ Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4 -files, using either JPEG or HEX encoding depending on the image mode (and -whether JPEG support is available or not). +files. Different encoding methods are used, depending on the image mode. + +* 1 mode images are saved using TIFF encoding, or JPEG encoding if libtiff support is + unavailable +* L, RGB and CMYK mode images use JPEG encoding +* P mode images use HEX encoding +* LA and RGBA mode images use JPEG2000 encoding .. _pdf-saving: diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst index 0aecd3483..3a9572ab2 100644 --- a/docs/handbook/text-anchors.rst +++ b/docs/handbook/text-anchors.rst @@ -29,7 +29,7 @@ For example, in the following image, the text is ``ms`` (middle-baseline) aligne :alt: ms (middle-baseline) aligned text. :align: left -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 59dfac588..ca16fccda 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -72,11 +72,11 @@ true color. # mode setting bits = int(header[3]) if bits == 1: - self.mode = "1" + self._mode = "1" elif bits == 8: - self.mode = "L" + self._mode = "L" elif bits == 24: - self.mode = "RGB" + self._mode = "RGB" else: msg = "unknown number of bits" raise SyntaxError(msg) @@ -108,9 +108,7 @@ Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. -Once the plugin has been imported, it can be used: - -.. code-block:: python +Once the plugin has been imported, it can be used:: from PIL import Image import SpamImagePlugin @@ -169,9 +167,7 @@ The raw decoder The ``raw`` decoder is used to read uncompressed data from an image file. It can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.frombytes` function, use the following syntax: - -.. code-block:: python +:py:func:`PIL.Image.frombytes` function, use the following syntax:: image = Image.frombytes( mode, size, data, "raw", @@ -281,9 +277,7 @@ decoder that can be used to read various packed formats into a floating point image memory. To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use -the following syntax: - -.. code-block:: python +the following syntax:: image = Image.frombytes( mode, size, data, "bit", diff --git a/docs/installation.rst b/docs/installation.rst index 1b5719a8e..37739c8c0 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -68,13 +68,24 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow + While we provide binaries for both x86-64 and arm64, we do not provide universal2 + binaries. However, it is simple to combine our current binaries to create one:: + + python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow + python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow + python3 -m pip install delocate + + Then, with the names of the downloaded wheels, use Python to combine them:: + + from delocate.fuse import fuse_wheels + fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') + .. tab:: Windows We provide Pillow binaries for Windows compiled for the matrix of - supported Pythons in both 32 and 64-bit versions in the wheel format. - These binaries include support for all optional libraries except - libimagequant and libxcb. Raqm support requires - FriBiDi to be installed separately:: + supported Pythons in 64-bit versions in the wheel format. These binaries include + support for all optional libraries except libimagequant and libxcb. Raqm support + requires FriBiDi to be installed separately:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow @@ -143,14 +154,14 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5.1** * **libfreetype** provides type related services * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.14**. + above uses liblcms2. Tested with **1.19** and **2.7-2.15**. * **libwebp** provides the WebP format. @@ -169,7 +180,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.1** + * Pillow has been tested with libimagequant **2.6-4.2** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. @@ -186,8 +197,8 @@ Many of Pillow's features require external libraries: * Pillow wheels since version 8.2.0 include a modified version of libraqm that loads libfribidi at runtime if it is installed. On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` - into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs) - `_ + into a directory listed in the `Dynamic-link library search order (Microsoft Learn) + `_ (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). See `Build Options`_ to see how to build this version. * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. @@ -300,6 +311,11 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libraqm + https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with + MSYS2. To workaround this, before installing Pillow you must run:: + + export SETUPTOOLS_USE_DISTUTILS=stdlib + .. tab:: FreeBSD .. Note:: Only FreeBSD 10 and 11 tested @@ -368,40 +384,40 @@ Build Options using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. -* Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``, - ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``, - ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``. +* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, + ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, + ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, + ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. Disable building the corresponding feature even if the development libraries are present on the building machine. -* Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``, - ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``, - ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``. +* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, + ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, + ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, + ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Build flags: ``--vendor-raqm``, ``--vendor-fribidi``. +* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. These flags are used to compile a modified version of libraqm and a shim that dynamically loads libfribidi at runtime. These are used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``--disable-platform-guessing``. Skips all of the +* Build flag: ``-C platform-guessing=disable``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). -* Build flag: ``--debug``. Adds a debugging flag to the include and +* Build flag: ``-C debug=true``. Adds a debugging flag to the include and library search process to dump all paths searched for and found to stdout. Sample usage:: - python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" + python3 -m pip install --upgrade Pillow -C [feature]=enable Platform Support ---------------- @@ -422,7 +438,9 @@ These platforms are built and tested for every change. +==================================+============================+=====================+ | Alpine | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2 | 3.7 | x86-64 | +| Amazon Linux 2 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2023 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Arch | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -432,35 +450,35 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 10 Buster | 3.7 | x86 | +| Debian 11 Bullseye | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 11 Bullseye | 3.9 | x86 | -+----------------------------------+----------------------------+---------------------+ -| Fedora 36 | 3.10 | x86-64 | +| Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 38 | 3.11 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.10 | arm64v8, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2016 | 3.7 | x86-64 | +| Windows Server 2016 | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | +| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86, x86-64 | +| | 3.11 | x86 | +| +----------------------------+---------------------+ +| | 3.9 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.8, 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -480,7 +498,9 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |arm | +| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.0 |arm | +| +---------------------------+------------------+ | +| | 3.7 | 9.5.0 | | +----------------------------------+---------------------------+------------------+--------------+ | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ diff --git a/docs/make.bat b/docs/make.bat index c943319ad..0ed5ee1a5 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -19,6 +19,7 @@ if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files + echo. htmlview to open the index page built by the html target in your browser echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files @@ -44,12 +45,23 @@ if "%1" == "clean" ( goto end ) -if "%1" == "html" ( +set html=false +if "%1%" == "html" set html=true +if "%1%" == "htmlview" set html=true +if "%html%" == "true" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end + + if "%1" == "htmlview" ( + if EXIST "%BUILDDIR%\html\index.html" ( + echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... + start "" "%BUILDDIR%\html\index.html" + ) + ) + + goto end ) if "%1" == "dirhtml" ( diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index ed2369259..1457d59de 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,6 +1,8 @@ -Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,, -Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, -Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, -Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, -Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes +Python,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow >= 10.1,Yes,Yes,Yes,Yes,Yes,,, +Pillow 10.0,,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,, +Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,, +Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes, +Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes, +Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes \ No newline at end of file diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ad0abbbd9..41d3b8fce 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -17,9 +17,7 @@ Open, rotate, and display an image (using the default viewer) The following script loads an image, rotates it 45 degrees, and displays it using an external viewer (usually xv on Unix, and the Paint program on -Windows). - -.. code-block:: python +Windows). :: from PIL import Image with Image.open("hopper.jpg") as im: @@ -29,9 +27,7 @@ Create thumbnails ^^^^^^^^^^^^^^^^^ The following script creates nice thumbnails of all JPEG images in the -current directory preserving aspect ratios with 128x128 max resolution. - -.. code-block:: python +current directory preserving aspect ratios with 128x128 max resolution. :: from PIL import Image import glob, os @@ -127,9 +123,7 @@ methods. Unless otherwise stated, all methods return a new instance of the .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to -ITU-R 709, using the D65 luminant) to the CIE XYZ color space: - -.. code-block:: python +ITU-R 709, using the D65 luminant) to the CIE XYZ color space:: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, @@ -140,9 +134,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop -This crops the input image with the provided coordinates: - -.. code-block:: python +This crops the input image with the provided coordinates:: from PIL import Image @@ -162,9 +154,7 @@ This crops the input image with the provided coordinates: .. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter -This blurs the input image using a filter from the ``ImageFilter`` module: - -.. code-block:: python +This blurs the input image using a filter from the ``ImageFilter`` module:: from PIL import Image, ImageFilter @@ -176,9 +166,7 @@ This blurs the input image using a filter from the ``ImageFilter`` module: .. automethod:: PIL.Image.Image.frombytes .. automethod:: PIL.Image.Image.getbands -This helps to get the bands of the input image: - -.. code-block:: python +This helps to get the bands of the input image:: from PIL import Image @@ -187,9 +175,7 @@ This helps to get the bands of the input image: .. automethod:: PIL.Image.Image.getbbox -This helps to get the bounding box coordinates of the input image: - -.. code-block:: python +This helps to get the bounding box coordinates of the input image:: from PIL import Image @@ -217,9 +203,7 @@ This helps to get the bounding box coordinates of the input image: .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.resize -This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: - -.. code-block:: python +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``:: from PIL import Image @@ -231,9 +215,7 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` .. automethod:: PIL.Image.Image.rotate -This rotates the input image by ``theta`` degrees counter clockwise: - -.. code-block:: python +This rotates the input image by ``theta`` degrees counter clockwise:: from PIL import Image @@ -256,9 +238,7 @@ This rotates the input image by ``theta`` degrees counter clockwise: .. automethod:: PIL.Image.Image.transpose This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` -method. - -.. code-block:: python +method. :: from PIL import Image @@ -432,18 +412,6 @@ See :ref:`concept-filters` for details. :undoc-members: :noindex: -Some deprecated filters are also available under the following names: - -.. data:: NONE - :noindex: - :value: Resampling.NEAREST -.. data:: LINEAR - :value: Resampling.BILINEAR -.. data:: CUBIC - :value: Resampling.BICUBIC -.. data:: ANTIALIAS - :value: Resampling.LANCZOS - Dither modes ^^^^^^^^^^^^ @@ -471,7 +439,7 @@ Used to specify the dithering method to use for the Palettes ^^^^^^^^ -Used to specify the pallete to use for the :meth:`~Image.convert` method. +Used to specify the palette to use for the :meth:`~Image.convert` method. .. autoclass:: Palette :members: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9aa26916a..95a40007b 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -16,7 +16,7 @@ For a more advanced drawing library for PIL, see the `aggdraw module`_. Example: Draw a gray cross over an image ---------------------------------------- -.. code-block:: python +:: import sys from PIL import Image, ImageDraw @@ -78,7 +78,7 @@ libraries, and may not available in all PIL builds. Example: Draw Partial Opacity Text ---------------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -105,7 +105,7 @@ Example: Draw Partial Opacity Text Example: Draw Multiline Text ---------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -243,6 +243,7 @@ Methods .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the ``xy`` list. + The coordinate pixels are included in the drawn line. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. @@ -287,7 +288,7 @@ Methods The polygon outline consists of straight lines between the given coordinates, plus a straight line between the last and the first - coordinate. + coordinate. The coordinate pixels are included in the drawn polygon. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. @@ -296,7 +297,7 @@ Methods :param width: The line width, in pixels. -.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1) Draws a regular polygon inscribed in ``bounding_circle``, with ``n_sides``, and rotation of ``rotation`` degrees. @@ -311,6 +312,7 @@ Methods (e.g. ``rotation=90``, applies a 90 degree rotation). :param fill: Color to use for the fill. :param outline: Color to use for the outline. + :param width: The line width, in pixels. .. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) @@ -318,25 +320,28 @@ Methods Draws a rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. - :param outline: Color to use for the outline. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. .. versionadded:: 5.3.0 -.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1) +.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1, corners=None) Draws a rounded rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param radius: Radius of the corners. - :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. + :param corners: A tuple of whether to round each corner, + ``(top_left, top_right, bottom_right, bottom_left)``. + Keyword-only argument. .. versionadded:: 8.2.0 @@ -472,116 +477,6 @@ Methods .. versionadded:: 8.0.0 -.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) - - .. deprecated:: 9.2.0 - - See :ref:`deprecations ` for more information. - - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. - - Return the size of the given string, in pixels. - - .. note:: For historical reasons this function measures text height from - the ascender line instead of the top, see :ref:`text-anchors`. - If you wish to measure text height from the top, it is recommended - to use :meth:`textbbox` with ``anchor='lt'`` instead. - - :param text: Text to be measured. If it contains any newline characters, - the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: If the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`, - the number of pixels between lines. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - -.. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) - - .. deprecated:: 9.2.0 - - See :ref:`deprecations ` for more information. - - Use :py:meth:`.multiline_textbbox` instead. - - Return the size of the given string, in pixels. - - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. - - .. note:: For historical reasons this function measures text height as the - distance between the top ascender line and bottom descender line, - not the top and bottom of the text, see :ref:`text-anchors`. - If you wish to measure text height from the top to the bottom of text, - it is recommended to use :meth:`multiline_textbbox` instead. - - :param text: Text to be measured. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: The number of pixels between lines. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - .. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False) Returns length (in pixels with 1/64 precision) of given text when rendered @@ -597,18 +492,14 @@ Methods string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = draw.textlength("Hello", font) world = draw.textlength("World", font) hello_world = hello + world # not adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # may fail - use - - .. code-block:: python + use :: hello = draw.textlength("HelloW", font) - draw.textlength( "W", font @@ -617,9 +508,7 @@ Methods hello_world = hello + world # adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) @@ -649,7 +538,7 @@ Methods It should be a `BCP 47 language code`_. Requires libraqm. :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - :return: Width for horizontal, height for vertical text. + :return: Either width for horizontal text, or height for vertical text. .. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 29ceee314..457f0d4df 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -10,7 +10,7 @@ for image enhancement. Example: Vary the sharpness of an image --------------------------------------- -.. code-block:: python +:: from PIL import ImageEnhance @@ -29,6 +29,8 @@ Classes All enhancement classes implement a common interface, containing a single method: +.. _enhancement-factor: + .. py:class:: _Enhance .. py:method:: enhance(factor) @@ -45,31 +47,35 @@ method: Adjust image color balance. - This class can be used to adjust the colour balance of an image, in - a manner similar to the controls on a colour TV set. An enhancement - factor of 0.0 gives a black and white image. A factor of 1.0 gives - the original image. + This class can be used to adjust the colour balance of an image, in a + manner similar to the controls on a colour TV set. An + :ref:`enhancement factor ` of 0.0 gives a black and + white image. A factor of 1.0 gives the original image. .. py:class:: Contrast(image) Adjust image contrast. - This class can be used to control the contrast of an image, similar - to the contrast control on a TV set. An enhancement factor of 0.0 - gives a solid grey image. A factor of 1.0 gives the original image. + This class can be used to control the contrast of an image, similar to the + contrast control on a TV set. An + :ref:`enhancement factor ` of 0.0 gives a solid grey + image, a factor of 1.0 gives the original image, and greater values + increase the contrast of the image. .. py:class:: Brightness(image) Adjust image brightness. - This class can be used to control the brightness of an image. An - enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the - original image. + This class can be used to control the brightness of an image. An + :ref:`enhancement factor ` of 0.0 gives a black image, + a factor of 1.0 gives the original image, and greater values increase the + brightness of the image. .. py:class:: Sharpness(image) Adjust image sharpness. This class can be used to adjust the sharpness of an image. An - enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the - original image, and a factor of 2.0 gives a sharpened image. + :ref:`enhancement factor ` of 0.0 gives a blurred + image, a factor of 1.0 gives the original image, and a factor of 2.0 gives + a sharpened image. diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 3cf59c610..047990f1c 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -15,7 +15,7 @@ and **xmllib** modules. Example: Parse an image ----------------------- -.. code-block:: python +:: from PIL import ImageFile diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index c85da4fb5..044aede62 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -11,7 +11,7 @@ filters, which can be be used with the :py:meth:`Image.filter() Example: Filter an image ------------------------ -.. code-block:: python +:: from PIL import ImageFilter diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 516fa63a7..2abfa0cc9 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -18,10 +18,19 @@ OpenType fonts (as well as other font formats supported by the FreeType library). For earlier versions, TrueType support is only available as part of the imToolkit package. +.. warning:: + To protect against potential DOS attacks when using arbitrary strings as + text input, Pillow will raise a ``ValueError`` if the number of characters + is over a certain limit, :py:data:`MAX_STRING_LENGTH`. + + This threshold can be changed by setting + :py:data:`MAX_STRING_LENGTH`. It can be disabled by setting + ``ImageFont.MAX_STRING_LENGTH = None``. + Example ------- -.. code-block:: python +:: from PIL import ImageFont, ImageDraw @@ -73,3 +82,12 @@ Constants Requires Raqm, you can check support using :py:func:`PIL.features.check_feature` with ``feature="raqm"``. + +Constants +--------- + +.. data:: MAX_STRING_LENGTH + + Set to 1,000,000, to protect against potential DOS attacks. Pillow will + raise a ``ValueError`` if the number of characters is over this limit. The + check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 3086ba8c3..0b94032d5 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -15,8 +15,9 @@ or the clipboard to a PIL image memory. returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, the entire screen is copied. - On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it - is installed. To capture the default X11 display instead, pass ``xdisplay=""``. + On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return + a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is + installed. To disable this behaviour, pass ``xdisplay=""`` instead. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) @@ -39,9 +40,11 @@ or the clipboard to a PIL image memory. .. py:function:: grabclipboard() - Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported. + Take a snapshot of the clipboard image, if any. - .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS) + On Linux, ``wl-paste`` or ``xclip`` is required. + + .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) :return: On Windows, an image, a list of filenames, or None if the clipboard does not contain image data or filenames. @@ -49,3 +52,5 @@ or the clipboard to a PIL image memory. On Mac, an image, or None if the clipboard does not contain image data. + + On Linux, an image. diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 63f88fddd..118d988d6 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -11,7 +11,7 @@ an expression string and one or more images. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- -.. code-block:: python +:: from PIL import Image, ImageMath diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index b9bdfc507..500096ef7 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -48,7 +48,7 @@ vector data. Path objects can be passed to the methods on the Maps the path through a function. -.. py:method:: PIL.ImagePath.Path.tolist(flat=0) +.. py:method:: PIL.ImagePath.Path.tolist(flat=False) Converts the path to a Python list [(x, y), …]. @@ -60,9 +60,7 @@ vector data. Path objects can be passed to the methods on the .. py:method:: PIL.ImagePath.Path.transform(matrix) Transforms the path in place, using an affine transform. The matrix is a - 6-tuple (a, b, c, d, e, f), and each point is mapped as follows: - - .. code-block:: python + 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:: xOut = xIn * a + yIn * b + c yOut = xIn * d + yIn * e + f diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 15d052d1c..7e67a44d3 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -4,16 +4,8 @@ :py:mod:`~PIL.ImageQt` Module ============================= -The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5 -or PySide2 QImage objects from PIL images. - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed -in Pillow 10 (2023-07-01). Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. +The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6 +QImage objects from PIL images. .. versionadded:: 1.1.6 @@ -22,7 +14,7 @@ in Pillow 10 (2023-07-01). Upgrade to Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt6/PySide6/PyQt5/PySide2 API functions and methods. + to PyQt6/PySide6 API functions and methods. This operation is currently supported for mode 1, L, P, RGB, and RGBA images. To handle other modes, you need to convert the image first. diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index f2e7d9edd..a27b2fb4e 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -10,7 +10,7 @@ iterate over the frames of an image sequence. Extracting frames from an animation ----------------------------------- -.. code-block:: python +:: from PIL import Image, ImageSequence diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 2ee3cadb7..4151be4a7 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -9,9 +9,7 @@ Windows. ImageWin can be used with PythonWin and other user interface toolkits that provide access to Windows device contexts or window handles. For example, -Tkinter makes the window handle available via the winfo_id method: - -.. code-block:: python +Tkinter makes the window handle available via the winfo_id method:: from PIL import ImageWin diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index b234b7b4e..04d6f5dcd 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -18,9 +18,7 @@ Example ------- The following script loads an image, accesses one pixel from it, then -changes it. - -.. code-block:: python +changes it. :: from PIL import Image @@ -35,9 +33,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index f9eb9b524..ed58ca3a5 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -17,9 +17,7 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the Example ------- -The following script loads an image, accesses one pixel from it, then changes it. - -.. code-block:: python +The following script loads an image, accesses one pixel from it, then changes it. :: from PIL import Image @@ -34,9 +32,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index dc4c2bf94..5e8586905 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -10,19 +10,13 @@ distributions. - ``python3-dbg`` package for the gdb extensions and python symbols - ``gdb`` and ``valgrind`` -- Potentially debug symbols for libraries. On ubuntu they're shipped - in package-dbgsym packages, from a different repo. +- Potentially debug symbols for libraries. On Ubuntu you can follow those + instructions to install the corresponding packages: `Debug Symbol Packages `_ -:: +Then ``sudo apt-get install libtiff5-dbgsym`` - deb http://ddebs.ubuntu.com focal main restricted universe multiverse - deb http://ddebs.ubuntu.com focal-updates main restricted universe multiverse - deb http://ddebs.ubuntu.com focal-proposed main restricted universe multiverse - -Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym`` - -- There's a bug with the dbg package for at least python 3.8 on ubuntu - 20.04, and you need to add a new link or two to make it autoload when +- There's a bug with the ``python3-dbg`` package for at least Python 3.8 on + Ubuntu 20.04, and you need to add a new link or two to make it autoload when running python: :: diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 6bfd50588..f31941c9a 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -61,9 +61,7 @@ Image Lifecycle * ``Image.Image.close()`` Closes the file and destroys the core image object. The Pillow context manager will also close the file, but will not destroy - the core image object. e.g.: - -.. code-block:: python + the core image object. e.g.:: with Image.open("test.jpg") as img: img.load() diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst new file mode 100644 index 000000000..06acfc7af --- /dev/null +++ b/docs/releasenotes/10.0.0.rst @@ -0,0 +1,209 @@ +10.0.0 +------ + +Backwards Incompatible Changes +============================== + +Categories +^^^^^^^^^^ + +``im.category`` has been removed, along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +Tk/Tcl 8.4 +^^^^^^^^^^ + +Support for Tk/Tcl 8.4 has been removed. + +JpegImagePlugin.convert_dict_qtables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. + +ImagePalette size parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. + +ImageShow.Viewer.show_file file argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +removed and replaced by ``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. + +Constants +^^^^^^^^^ + +A number of constants have been removed. +Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Removed Use instead +===================================================== ============================================================ +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +FitsStubImagePlugin +^^^^^^^^^^^^^^^^^^^ + +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. + +Font size and offset methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several functions for computing the size and offset of rendered text have been removed: + +=============================================================== ============================================================================================================= +Removed Use instead +=============================================================== ============================================================================================================= +``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` +``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=============================================================== ============================================================================================================= + +FreeTypeFont.getmask2 fill parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + +PhotoImage.paste box parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``box`` parameter was unused and has been removed. + +PyQt5 and PySide2 +^^^^^^^^^^^^^^^^^ + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +Image.coerce_e +^^^^^^^^^^^^^^ + +This undocumented method has been removed. + +Deprecations +============ + +PyAccess and Image.USE_CFFI_ACCESS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since Pillow's C API is now faster than PyAccess on PyPy, +:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow +11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. + +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is +similarly deprecated. + +API Changes +=========== + +Added line width parameter to ImageDraw regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to +``ImageDraw.Draw.regular_polygon``. + +API Additions +============= + +Added ``alpha_only`` argument to ``getbbox()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getbbox` now accepts a keyword argument of +``alpha_only``. This is an optional flag, defaulting to ``True``. If ``True`` +and the image has an alpha channel, trim transparent pixels. Otherwise, trim +pixels when all channels are zero. + +Security +======== + +Limit size even if one dimension is zero +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When performing decompression bomb checks, Pillow did not reject images with +excessive width and zero height, or zero width and excessive height. That has +now been fixed. + +This effectively dates to the PIL fork, since problem images would still have +been processed before Pillow started checking for decompression bombs. + +Added ImageFont.MAX_STRING_LENGTH +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using arbitrary strings as text +input, Pillow will now raise a ``ValueError`` if the number of characters +passed into ImageFont methods is over a certain limit, +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. + +This threshold can be changed by setting +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting +``ImageFont.MAX_STRING_LENGTH = None``. + +Other Changes +============= + +32-bit wheels +^^^^^^^^^^^^^ + +32-bit wheels are no longer provided. + +Support display_jpeg() in IPython +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to ``display()`` and ``display_png``, ``display_jpeg()`` can now +also be used to display images in IPython:: + + from PIL import Image + from IPython.display import display_jpeg + + im = Image.new("RGB", (100, 100), (255, 0, 0)) + display_jpeg(im) + +Support reading signed 8-bit TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TIFF images with signed integer data, 8 bits per sample and a photometric +interpretation of BlackIsZero can now be read. diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst new file mode 100644 index 000000000..da5153cce --- /dev/null +++ b/docs/releasenotes/10.1.0.rst @@ -0,0 +1,54 @@ +10.1.0 +------ + +Backwards Incompatible Changes +============================== + +Setting image mode +^^^^^^^^^^^^^^^^^^ + +If you attempt to set the mode of an image directly, e.g. +``im.mode = "RGBA"``, you will now receive an ``AttributeError``. This is +not about removing existing functionality, but instead about raising an +explicit error to prevent later consequences. The ``convert`` method is the +correct way to change an image's mode. + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/3.1.1.rst b/docs/releasenotes/3.1.1.rst index 38118ea39..5d60e116c 100644 --- a/docs/releasenotes/3.1.1.rst +++ b/docs/releasenotes/3.1.1.rst @@ -6,7 +6,7 @@ CVE-2016-0740 -- Buffer overflow in TiffDecode.c ------------------------------------------------ Pillow 3.1.0 and earlier when linked against libtiff >= 4.0.0 on x64 -may overflow a buffer when reading a specially crafted tiff file (:cve:`CVE-2016-0740`). +may overflow a buffer when reading a specially crafted tiff file (:cve:`2016-0740`). Specifically, libtiff >= 4.0.0 changed the return type of ``TIFFScanlineSize`` from ``int32`` to machine dependent @@ -24,7 +24,7 @@ CVE-2016-0775 -- Buffer overflow in FliDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, FliDecode.c has a buffer overflow error (:cve:`CVE-2016-0775`). +release, FliDecode.c has a buffer overflow error (:cve:`2016-0775`). Around line 192: @@ -53,7 +53,7 @@ CVE-2016-2533 -- Buffer overflow in PcdDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, ``PcdDecode.c`` has a buffer overflow error (:cve:`CVE-2016-2533`). +release, ``PcdDecode.c`` has a buffer overflow error (:cve:`2016-2533`). The ``state.buffer`` for ``PcdDecode.c`` is allocated based on a 3 bytes per pixel sizing, where ``PcdDecode.c`` wrote into the buffer diff --git a/docs/releasenotes/3.1.2.rst b/docs/releasenotes/3.1.2.rst index b5f7cfe99..04325ad86 100644 --- a/docs/releasenotes/3.1.2.rst +++ b/docs/releasenotes/3.1.2.rst @@ -7,7 +7,7 @@ CVE-2016-3076 -- Buffer overflow in Jpeg2KEncode.c Pillow between 2.5.0 and 3.1.1 may overflow a buffer when writing large Jpeg2000 files, allowing for code execution or other memory -corruption (:cve:`CVE-2016-3076`). +corruption (:cve:`2016-3076`). This occurs specifically in the function ``j2k_encode_entry``, at the line: diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index eb4304843..76e13b061 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -13,16 +13,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been dep Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: - -.. code-block:: python +Deprecated:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") @@ -79,9 +75,7 @@ Image quality for JPEG compressed TIFF The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG -encoder. The default is 75. For example: - -.. code-block:: python +encoder. The default is 75. For example:: im.save("out.tif", compression="jpeg", quality=85) diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 20a009cc1..7daac1b19 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -10,9 +10,7 @@ Text stroking ``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing operations. They allow text to be outlined, setting the width of the stroke and and the color respectively. If not provided, ``stroke_fill`` will default to -the ``fill`` parameter. - -.. code-block:: python +the ``fill`` parameter. :: from PIL import Image, ImageDraw, ImageFont @@ -28,9 +26,7 @@ the ``fill`` parameter. draw.multiline_text((10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0") -For example, - -.. code-block:: python +For example, :: from PIL import Image, ImageDraw, ImageFont @@ -73,7 +69,7 @@ Security ======== This release catches several buffer overruns, as well as addressing -:cve:`CVE-2019-16865`. The CVE is regarding DOS problems, such as consuming large +:cve:`2019-16865`. The CVE is regarding DOS problems, such as consuming large amounts of memory, or taking a large amount of time to process an image. In RawDecode.c, an error is now thrown if skip is calculated to be less than diff --git a/docs/releasenotes/6.2.2.rst b/docs/releasenotes/6.2.2.rst index 79d4b88aa..47692a3de 100644 --- a/docs/releasenotes/6.2.2.rst +++ b/docs/releasenotes/6.2.2.rst @@ -6,13 +6,13 @@ Security This release addresses several security problems. -:cve:`CVE-2019-19911` is regarding FPX images. If an image reports that it has a large +:cve:`2019-19911` is regarding FPX images. If an image reports that it has a large number of bands, a large amount of resources will be used when trying to process the image. This is fixed by limiting the number of bands to those usable by Pillow. -Buffer overruns were found when processing an SGI (:cve:`CVE-2020-5311`), -PCX (:cve:`CVE-2020-5312`) or FLI image (:cve:`CVE-2020-5313`). Checks have been added +Buffer overruns were found when processing an SGI (:cve:`2020-5311`), +PCX (:cve:`2020-5312`) or FLI image (:cve:`2020-5313`). Checks have been added to prevent this. -:cve:`CVE-2020-5310`: Overflow checks have been added when calculating the size of a +:cve:`2020-5310`: Overflow checks have been added when calculating the size of a memory block to be reallocated in the processing of a TIFF image. diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 80002b0ce..f2e235289 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -118,9 +118,7 @@ Loading WMF images at a given DPI ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ On Windows, Pillow can read WMF files, with a default DPI of 72. An image can -now also be loaded at another resolution: - -.. code-block:: python +now also be loaded at another resolution:: from PIL import Image with Image.open("drawing.wmf") as im: @@ -136,16 +134,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0024a537d..6e231464e 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -10,9 +10,7 @@ Allow saving of zero quality JPEG images If no quality was specified when saving a JPEG, Pillow internally used a value of zero to indicate that the default quality should be used. However, this removed the ability to actually save a JPEG with zero quality. This has now -been resolved. - -.. code-block:: python +been resolved. :: from PIL import Image im = Image.open("hopper.jpg") @@ -74,11 +72,11 @@ Security This release includes security fixes. -* :cve:`CVE-2020-10177` Fix multiple out-of-bounds reads in FLI decoding -* :cve:`CVE-2020-10378` Fix bounds overflow in PCX decoding -* :cve:`CVE-2020-10379` Fix two buffer overflows in TIFF decoding -* :cve:`CVE-2020-10994` Fix bounds overflow in JPEG 2000 decoding -* :cve:`CVE-2020-11538` Fix buffer overflow in SGI-RLE decoding +* :cve:`2020-10177` Fix multiple out-of-bounds reads in FLI decoding +* :cve:`2020-10378` Fix bounds overflow in PCX decoding +* :cve:`2020-10379` Fix two buffer overflows in TIFF decoding +* :cve:`2020-10994` Fix bounds overflow in JPEG 2000 decoding +* :cve:`2020-11538` Fix buffer overflow in SGI-RLE decoding Other Changes ============= diff --git a/docs/releasenotes/8.0.1.rst b/docs/releasenotes/8.0.1.rst index 3584a5d72..f7a1cea65 100644 --- a/docs/releasenotes/8.0.1.rst +++ b/docs/releasenotes/8.0.1.rst @@ -4,7 +4,7 @@ Security ======== -Update FreeType used in binary wheels to `2.10.4`_ to fix :cve:`CVE-2020-15999`: +Update FreeType used in binary wheels to `2.10.4`_ to fix :cve:`2020-15999`: - A heap buffer overflow has been found in the handling of embedded PNG bitmaps, introduced in FreeType version 2.6. diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst index 8ed1d9d85..69726e628 100644 --- a/docs/releasenotes/8.1.0.rst +++ b/docs/releasenotes/8.1.0.rst @@ -11,7 +11,7 @@ Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022 when FreeType 2.8 will be the minimum supported. We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ @@ -40,13 +40,13 @@ This release includes security fixes. * An out-of-bounds read when saving TIFFs with custom metadata through LibTIFF * An out-of-bounds read when saving a GIF of 1px width -* :cve:`CVE-2020-35653` Buffer read overrun in PCX decoding +* :cve:`2020-35653` Buffer read overrun in PCX decoding The PCX image decoder used the reported image stride to calculate the row buffer, rather than calculating it from the image size. This issue dates back to the PIL fork. Thanks to Google's `OSS-Fuzz`_ project for finding this. -* :cve:`CVE-2020-35654` Fix TIFF out-of-bounds write error +* :cve:`2020-35654` Fix TIFF out-of-bounds write error Out-of-bounds write in ``TiffDecode.c`` when reading corrupt YCbCr files in some LibTIFF versions (4.1.0/Ubuntu 20.04, but not 4.0.9/Ubuntu 18.04). In some cases @@ -55,7 +55,7 @@ an out-of-bounds write in ``TiffDecode.c``. This potentially affects Pillow vers from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through `Tidelift`_. -* :cve:`CVE-2020-35655` Fix for SGI Decode buffer overrun +* :cve:`2020-35655` Fix for SGI Decode buffer overrun 4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly checking the offsets and length tables. Independently reported through `Tidelift`_ and Google's diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst index 4081c49ca..18d0a33f1 100644 --- a/docs/releasenotes/8.1.1.rst +++ b/docs/releasenotes/8.1.1.rst @@ -4,19 +4,19 @@ Security ======== -:cve:`CVE-2021-25289`: The previous fix for :cve:`CVE-2020-35654` was insufficient +:cve:`2021-25289`: The previous fix for :cve:`2020-35654` was insufficient due to incorrect error checking in ``TiffDecode.c``. -:cve:`CVE-2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` +:cve:`2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` with an invalid size. -:cve:`CVE-2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to +:cve:`2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to an out-of-bounds read in ``TIFFReadRGBATile``. -:cve:`CVE-2021-25292`: The PDF parser has a catastrophic backtracking regex +:cve:`2021-25292`: The PDF parser has a catastrophic backtracking regex that could be used as a DOS attack. -:cve:`CVE-2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, +:cve:`2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, since Pillow 4.3.0. diff --git a/docs/releasenotes/8.1.2.rst b/docs/releasenotes/8.1.2.rst index 50d132f33..de50a3f1d 100644 --- a/docs/releasenotes/8.1.2.rst +++ b/docs/releasenotes/8.1.2.rst @@ -4,8 +4,8 @@ Security ======== -There is an exhaustion of memory DOS in the BLP (:cve:`CVE-2021-27921`), -ICNS (:cve:`CVE-2021-27922`) and ICO (:cve:`CVE-2021-27923`) container formats +There is an exhaustion of memory DOS in the BLP (:cve:`2021-27921`), +ICNS (:cve:`2021-27922`) and ICO (:cve:`2021-27923`) container formats where Pillow did not properly check the reported size of the contained image. These images could cause arbitrarily large memory allocations. This was reported by Jiayi Lin, Luke Shaffer, Xinran Xie, and Akshay Ajayan of diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index c902ccf71..452077f1a 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -76,9 +76,7 @@ ImageDraw.rounded_rectangle Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as :py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` argument. ``radius`` is limited to half of the width or the height, so that users can -create a circle, but not any other ellipse. - -.. code-block:: python +create a circle, but not any other ellipse. :: from PIL import Image, ImageDraw im = Image.new("RGB", (200, 200)) @@ -131,15 +129,15 @@ Security These were all found with `OSS-Fuzz`_. -:cve:`CVE-2021-25287`, :cve:`CVE-2021-25288`: Fix OOB read in Jpeg2KDecode -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-25287`, :cve:`2021-25288`: Fix OOB read in Jpeg2KDecode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * For J2k images with multiple bands, it's legal to have different widths for each band, e.g. 1 byte for ``L``, 4 bytes for ``A``. * This dates to Pillow 2.4.0. -:cve:`CVE-2021-28675`: Fix DOS in PsdImagePlugin -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28675`: Fix DOS in PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input layers with regard to the size of the data block, this could lead to a @@ -147,15 +145,15 @@ These were all found with `OSS-Fuzz`_. :py:meth:`~PIL.Image.Image.load`. * This dates to the PIL fork. -:cve:`CVE-2021-28676`: Fix FLI DOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28676`: Fix FLI DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * ``FliDecode.c`` did not properly check that the block advance was non-zero, potentially leading to an infinite loop on load. * This dates to the PIL fork. -:cve:`CVE-2021-28677`: Fix EPS DOS on _open -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28677`: Fix EPS DOS on _open +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line endings. It accidentally used a quadratic method of accumulating lines while looking @@ -164,8 +162,8 @@ These were all found with `OSS-Fuzz`_. open phase, before an image was accepted for opening. * This dates to the PIL fork. -:cve:`CVE-2021-28678`: Fix BLP DOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28678`: Fix BLP DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets returned data. This could lead to a denial-of-service where the decoder could be run a diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index 0bfead144..e74880f6f 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -8,7 +8,7 @@ JpegImagePlugin.convert_dict_qtables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ JPEG ``quantization`` is now automatically converted, but still returned as a -dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer +dictionary. The ``convert_dict_qtables`` method no longer performs any operations on the data given to it, has been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). @@ -85,7 +85,7 @@ Security Buffer overflow ^^^^^^^^^^^^^^^ -This release addresses :cve:`CVE-2021-34552`. PIL since 1.1.4 and Pillow since 1.0 +This release addresses :cve:`2021-34552`. PIL since 1.1.4 and Pillow since 1.0 allowed parameters passed into a convert function to trigger buffer overflow in Convert.c. diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst index 6b5c759fc..3333d63a1 100644 --- a/docs/releasenotes/8.3.2.rst +++ b/docs/releasenotes/8.3.2.rst @@ -4,7 +4,7 @@ Security ======== -* :cve:`CVE-2021-23437`: Avoid a potential ReDoS (regular expression denial of service) +* :cve:`2021-23437`: Avoid a potential ReDoS (regular expression denial of service) in :py:class:`~PIL.ImageColor`'s :py:meth:`~PIL.ImageColor.getrgb` by raising :py:exc:`ValueError` if the color specifier is too long. Present since Pillow 5.2.0. diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index 9becf9146..e61471e72 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -24,9 +24,7 @@ Added "transparency" argument for loading EPS images This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", generating an RGBA image with a transparent background instead of an RGB image with a -white background. - -.. code-block:: python +white background. :: with Image.open("sample.eps") as im: im.load(transparency=True) diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index a19da361a..73e77ad3e 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -43,7 +43,7 @@ FreeType 2.7 Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported. We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _FreeType: https://freetype.org/ @@ -119,7 +119,7 @@ Google's `OSS-Fuzz`_ project for finding this issue. Restrict builtins available to ImageMath.eval ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:cve:`CVE-2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow +:cve:`2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow will now restrict the builtins available to :py:meth:`PIL.ImageMath.eval`. This will help prevent problems arising if users evaluate arbitrary expressions, such as ``ImageMath.eval("exec(exit())")``. @@ -127,7 +127,7 @@ help prevent problems arising if users evaluate arbitrary expressions, such as Fixed ImagePath.Path array handling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:cve:`CVE-2022-22815` (:cwe:`CWE-126`) and :cve:`CVE-2022-22816` (:cwe:`CWE-665`) were +:cve:`2022-22815` (:cwe:`126`) and :cve:`2022-22816` (:cwe:`665`) were found when initializing ``ImagePath.Path``. .. _OSS-Fuzz: https://github.com/google/oss-fuzz @@ -155,9 +155,7 @@ altered slightly with this change. Added support for pickling TrueType fonts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TrueType fonts may now be pickled and unpickled. For example: - -.. code-block:: python +TrueType fonts may now be pickled and unpickled. For example:: import pickle from PIL import ImageFont diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst index c1feee088..acb92dc41 100644 --- a/docs/releasenotes/9.0.1.rst +++ b/docs/releasenotes/9.0.1.rst @@ -6,12 +6,12 @@ Security This release addresses several security problems. -:cve:`CVE-2022-24303`: If the path to the temporary directory on Linux or macOS +:cve:`2022-24303`: If the path to the temporary directory on Linux or macOS contained a space, this would break removal of the temporary image file after ``im.show()`` (and related actions), and potentially remove an unrelated file. This has been present since PIL. -:cve:`CVE-2022-22817`: While Pillow 9.0 restricted top-level builtins available to +:cve:`2022-22817`: While Pillow 9.0 restricted top-level builtins available to :py:meth:`PIL.ImageMath.eval`, it did not prevent builtins available to lambda expressions. These are now also restricted. diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index e97b58a41..19690ca59 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -182,17 +182,13 @@ GifImagePlugin loading strategy Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as -well. - -.. code-block:: python +well. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS Or subsequent frames can be kept in ``P`` mode as long as there is only a single -palette. - -.. code-block:: python +palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY diff --git a/docs/releasenotes/9.1.1.rst b/docs/releasenotes/9.1.1.rst index f8b155f3d..bab70f8f9 100644 --- a/docs/releasenotes/9.1.1.rst +++ b/docs/releasenotes/9.1.1.rst @@ -6,7 +6,7 @@ Security This release addresses several security problems. -:cve:`CVE-2022-30595`: When reading a TGA file with RLE packets that cross scan lines, +:cve:`2022-30595`: When reading a TGA file with RLE packets that cross scan lines, Pillow reads the information past the end of the first line without deducting that from the length of the remaining file data. This vulnerability was introduced in Pillow 9.1.0, and can cause a heap buffer overflow. diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6dbfa2702..b875edf8e 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -15,7 +15,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed in Pillow 10 (2023-07-01). Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. FreeTypeFont.getmask2 fill parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -48,20 +48,18 @@ Font size and offset methods Several functions for computing the size and offset of rendered text have been deprecated and will be removed in Pillow 10 (2023-07-01): -=========================================================================== ============================================================================================================= -Deprecated Use instead -=========================================================================== ============================================================================================================= -:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` -:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` -:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` -:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` -:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` -:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` -=========================================================================== ============================================================================================================= +=============================================================== ============================================================================================================= +Deprecated Use instead +=============================================================== ============================================================================================================= +``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` +``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=============================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -76,9 +74,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst new file mode 100644 index 000000000..b1e982fcc --- /dev/null +++ b/docs/releasenotes/9.5.0.rst @@ -0,0 +1,96 @@ +9.5.0 +----- + +Deprecations +============ + +PSFile +^^^^^^ + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + +API Additions +============= + +QOI file format +^^^^^^^^^^^^^^^ + +Pillow can now read images in Quite OK Image format. + +Added ``dpi`` argument when saving PDFs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving a PDF, resolution could already be specified using the +``resolution`` argument. Now, a tuple of ``(x_resolution, y_resolution)`` can +be provided as ``dpi``. If both are provided, ``dpi`` will override +``resolution``. + +Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`.ImageDraw.rounded_rectangle` now accepts a keyword argument of +``corners``. This a tuple of Booleans, specifying whether to round each corner, +``(top_left, top_right, bottom_right, bottom_left)``. + +JPEG2000 comments and PLT marker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG2000 image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info`. The ``comment`` keyword argument can be used +to save it back again. + +If OpenJPEG 2.4.0 or later is available and the ``plt`` keyword argument +is present and true when saving JPEG2000 images, tell the encoder to generate +PLT markers. + +Security +======== + +Clear PPM half token after use +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Image files that are small on disk are often prevented from expanding to be +big images consuming a large amount of resources simply because they lack the +data to populate those resources. + +PpmImagePlugin might hold onto the last data read for a pixel value in case the +pixel value has not been finished yet. However, that data was not being cleared +afterwards, meaning that infinite data could be available to fill any image +size. This has been present since Pillow 9.2.0. + +That data is now cleared after use. + +Saving TIFF tag ImageSourceData +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If Pillow incorrectly saved the TIFF tag ImageSourceData as ASCII instead of +UNDEFINED, a segmentation fault was triggered. + +The correct tag type will now be used by default instead. + +Other Changes +============= + +Added support for saving PDFs in RGBA mode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Using the JPXDecode filter, PDFs can now be saved in RGBA mode. + +Improved I;16N support +^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for I;16N access, packing and unpacking. Conversion to +and from L mode has also been added. + +BGR;* modes +^^^^^^^^^^^ + +It is now possible to create new BGR;15, BGR;16 and BGR;24 images. Conversely, BGR;32 +has been removed from ImageMode and its associated methods, dropping the little support +Pillow had for the mode. + +With that, all modes listed under :ref:`concept-modes` can now be used to create a new +image. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index a2b588696..a843ddd72 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,9 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.1.0 + 10.0.0 + 9.5.0 9.4.0 9.3.0 9.2.0 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index f7271ae2b..440d04b1c 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -1,5 +1,5 @@ -x.y.z ------ +xx.y.z +------ Backwards Incompatible Changes ============================== diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..fd9c05f92 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +build-backend = "backend" +requires = [ + "setuptools>=67.8", +] +backend-path = [ + "_custom_build", +] diff --git a/setup.cfg b/setup.cfg index 824cae088..e560f9516 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,11 +12,11 @@ classifiers = License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND) Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Multimedia :: Graphics @@ -36,7 +36,7 @@ project_urls = [options] packages = PIL -python_requires = >=3.7 +python_requires = >=3.8 include_package_data = True package_dir = = src @@ -48,7 +48,6 @@ docs = sphinx>=2.4 sphinx-copybutton sphinx-inline-tabs - sphinx-issues>=3.0.1 sphinx-removed-in sphinxext-opengraph tests = diff --git a/setup.py b/setup.py index 8f7f223f8..935166716 100755 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ import os import re +import shutil import struct import subprocess import sys @@ -38,7 +39,7 @@ TIFF_ROOT = None ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 12): +if sys.platform == "win32" and sys.version_info >= (3, 13): import atexit atexit.register( @@ -136,7 +137,6 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -PLATFORM_PYPY = hasattr(sys, "pypy_version_info") def _dbg(s, tp=None): @@ -150,6 +150,7 @@ def _dbg(s, tp=None): def _find_library_dirs_ldconfig(): # Based on ctypes.util from Python 2 + ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" @@ -166,14 +167,14 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache - args = ["/sbin/ldconfig", "-p"] + args = [ldconfig, "-p"] expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" elif sys.platform.startswith("freebsd"): - args = ["/sbin/ldconfig", "-r"] + args = [ldconfig, "-r"] expr = r".* => (.*)" env = {} @@ -242,7 +243,9 @@ def _find_include_dir(self, dirname, include): return subdir -def _cmd_exists(cmd): +def _cmd_exists(cmd: str) -> bool: + if "PATH" not in os.environ: + return False return any( os.access(os.path.join(path, cmd), os.X_OK) for path in os.environ["PATH"].split(os.pathsep) @@ -271,9 +274,7 @@ def _pkg_config(name): )[::2][1:] cflags = re.split( r"(^|\s+)-I", - subprocess.check_output(command_cflags, stderr=stderr) - .decode("utf8") - .strip(), + subprocess.check_output(command_cflags).decode("utf8").strip(), )[::2][1:] return libs, cflags except Exception: @@ -473,9 +474,13 @@ class pil_build_ext(build_ext): lib_root = include_root = root if lib_root is not None: + if not isinstance(lib_root, (tuple, list)): + lib_root = (lib_root,) for lib_dir in lib_root: _add_directory(library_dirs, lib_dir) if include_root is not None: + if not isinstance(include_root, (tuple, list)): + include_root = (include_root,) for include_dir in include_root: _add_directory(include_dirs, include_dir) @@ -509,6 +514,7 @@ class pil_build_ext(build_ext): elif sys.platform == "cygwin": # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory + self.compiler.shared_lib_extension = ".dll.a" _add_directory( library_dirs, os.path.join( @@ -570,9 +576,7 @@ class pil_build_ext(build_ext): ): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) - if sys.platform.startswith("linux") and os.environ.get( - "ANDROID_ROOT", None - ): + if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): # termux support for android. # system libraries (zlib) are installed in /system/lib # headers are at $PREFIX/include @@ -683,10 +687,6 @@ class pil_build_ext(build_ext): # Add the directory to the include path so we can include # rather than having to cope with the versioned # include path - # FIXME (melvyn-sopacua): - # At this point it's possible that best_path is already in - # self.compiler.include_dirs. Should investigate how that is - # possible. _add_directory(self.compiler.include_dirs, best_path, 0) feature.jpeg2000 = "openjp2" feature.openjpeg_version = ".".join(str(x) for x in best_version) @@ -816,6 +816,15 @@ class pil_build_ext(build_ext): libs = self.add_imaging_libs.split() defs = [] + if feature.tiff: + libs.append(feature.tiff) + defs.append(("HAVE_LIBTIFF", None)) + if sys.platform == "win32": + # This define needs to be defined if-and-only-if it was defined + # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, + # so we have to guess; by default it is defined in all Windows builds. + # See #4237, #5243, #5359 for more information. + defs.append(("USE_WIN32_FILEIO", None)) if feature.jpeg: libs.append(feature.jpeg) defs.append(("HAVE_LIBJPEG", None)) @@ -830,15 +839,6 @@ class pil_build_ext(build_ext): if feature.imagequant: libs.append(feature.imagequant) defs.append(("HAVE_LIBIMAGEQUANT", None)) - if feature.tiff: - libs.append(feature.tiff) - defs.append(("HAVE_LIBTIFF", None)) - if sys.platform == "win32": - # This define needs to be defined if-and-only-if it was defined - # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, - # so we have to guess; by default it is defined in all Windows builds. - # See #4237, #5243, #5359 for more information. - defs.append(("USE_WIN32_FILEIO", None)) if feature.xcb: libs.append(feature.xcb) defs.append(("HAVE_XCB", None)) @@ -847,14 +847,7 @@ class pil_build_ext(build_ext): if struct.unpack("h", b"\0\1")[0] == 1: defs.append(("WORDS_BIGENDIAN", None)) - if ( - sys.platform == "win32" - and sys.version_info < (3, 9) - and not (PLATFORM_PYPY or PLATFORM_MINGW) - ): - defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) - else: - defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) + defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) self._update_extension("PIL._imaging", libs, defs) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index e0dd4dede..075d46290 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,27 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - [x, y, l, d] = [int(p) for p in props["BBX"].split()] - [dx, dy] = [int(p) for p in props["DWIDTH"].split()] + # The word BBX + # followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBxoff0, BByoff0) + # of the lower left corner from the origin of the character. + width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y) + # The word DWIDTH + # followed by the width in x and y of the character in device pixels. + dwx, dwy = [int(p) for p in props["DWIDTH"].split()] + + bbox = ( + (dwx, dwy), + (x_disp, -y_disp - height, width + x_disp, -y_disp), + (0, 0, width, height), + ) try: - im = Image.frombytes("1", (x, y), bitmap, "hex", "1") + im = Image.frombytes("1", (width, height), bitmap, "hex", "1") except ValueError: # deal with zero-width characters - im = Image.new("1", (x, y)) + im = Image.new("1", (width, height)) return id, int(props["ENCODING"]), bbox, im diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 1cc0d4b3c..a5cfad5f4 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -35,7 +35,6 @@ from enum import IntEnum from io import BytesIO from . import Image, ImageFile -from ._deprecate import deprecate class Format(IntEnum): @@ -54,21 +53,6 @@ class AlphaEncoding(IntEnum): DXT5 = 7 -def __getattr__(name): - for enum, prefix in { - Format: "BLP_FORMAT_", - Encoding: "BLP_ENCODING_", - AlphaEncoding: "BLP_ALPHA_ENCODING_", - }.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - def unpack_565(i): return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 @@ -282,7 +266,7 @@ class BlpImageFile(ImageFile.ImageFile): msg = f"Bad BLP magic {repr(self.magic)}" raise BLPFormatError(msg) - self.mode = "RGBA" if self._blp_alpha_depth else "RGB" + self._mode = "RGBA" if self._blp_alpha_depth else "RGB" self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 9fe4be544..3e4b5eac1 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -163,7 +163,7 @@ class BmpImageFile(ImageFile.ImageFile): offset += 4 * file_info["colors"] # ---------------------- Check bit depth for unusual unsupported values - self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) + self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) if self.mode is None: msg = f"Unsupported BMP pixel depth ({file_info['bits']})" raise OSError(msg) @@ -200,7 +200,7 @@ class BmpImageFile(ImageFile.ImageFile): and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] ): raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] - self.mode = "RGBA" if "A" in raw_mode else self.mode + self._mode = "RGBA" if "A" in raw_mode else self.mode elif ( file_info["bits"] in (24, 16) and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] @@ -215,7 +215,7 @@ class BmpImageFile(ImageFile.ImageFile): elif file_info["compression"] == self.RAW: try: if file_info["bits"] == 32 and self.alpha: - raw_mode, self.mode = "BGRA", "RGBA" + raw_mode, self._mode = "BGRA", "RGBA" except AttributeError: pass elif file_info["compression"] in (self.RLE8, self.RLE4): @@ -248,10 +248,10 @@ class BmpImageFile(ImageFile.ImageFile): # ------- If all colors are grey, white or black, ditch palette if greyscale: - self.mode = "1" if file_info["colors"] == 2 else "L" + self._mode = "1" if file_info["colors"] == 2 else "L" raw_mode = self.mode else: - self.mode = "P" + self._mode = "P" self.palette = ImagePalette.raw( "BGRX" if padding == 4 else "BGR", palette ) diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 0425bbd75..eef25aa14 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -46,7 +46,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): self.fp.seek(offset) # make something up - self.mode = "F" + self._mode = "F" self._size = 1, 1 loader = self._load() diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index a946daeaa..1368ae24e 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -128,7 +128,7 @@ class DdsImageFile(ImageFile.ImageFile): flags, height, width = struct.unpack("<3I", header.read(12)) self._size = (width, height) - self.mode = "RGBA" + self._mode = "RGBA" pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) struct.unpack("<11I", header.read(44)) # reserved @@ -141,9 +141,9 @@ class DdsImageFile(ImageFile.ImageFile): if pfflags & DDPF_LUMINANCE: # Texture contains uncompressed L or LA data if pfflags & DDPF_ALPHAPIXELS: - self.mode = "LA" + self._mode = "LA" else: - self.mode = "L" + self._mode = "L" self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))] elif pfflags & DDPF_RGB: @@ -153,7 +153,7 @@ class DdsImageFile(ImageFile.ImageFile): if pfflags & DDPF_ALPHAPIXELS: rawmode += masks[0xFF000000] else: - self.mode = "RGB" + self._mode = "RGB" rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF] self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))] @@ -172,15 +172,15 @@ class DdsImageFile(ImageFile.ImageFile): elif fourcc == b"ATI1": self.pixel_format = "BC4" n = 4 - self.mode = "L" + self._mode = "L" elif fourcc == b"ATI2": self.pixel_format = "BC5" n = 5 - self.mode = "RGB" + self._mode = "RGB" elif fourcc == b"BC5S": self.pixel_format = "BC5S" n = 5 - self.mode = "RGB" + self._mode = "RGB" elif fourcc == b"DX10": data_start += 20 # ignoring flags which pertain to volume textures and cubemaps @@ -189,19 +189,19 @@ class DdsImageFile(ImageFile.ImageFile): if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM): self.pixel_format = "BC5" n = 5 - self.mode = "RGB" + self._mode = "RGB" elif dxgi_format == DXGI_FORMAT_BC5_SNORM: self.pixel_format = "BC5S" n = 5 - self.mode = "RGB" + self._mode = "RGB" elif dxgi_format == DXGI_FORMAT_BC6H_UF16: self.pixel_format = "BC6H" n = 6 - self.mode = "RGB" + self._mode = "RGB" elif dxgi_format == DXGI_FORMAT_BC6H_SF16: self.pixel_format = "BC6HS" n = 6 - self.mode = "RGB" + self._mode = "RGB" elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM): self.pixel_format = "BC7" n = 7 diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 60cb46df9..b96ce9603 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -29,10 +29,11 @@ import tempfile from . import Image, ImageFile from ._binary import i32le as i32 +from ._deprecate import deprecate -# # -------------------------------------------------------------------- + split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") @@ -133,6 +134,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): if gs_windows_binary is not None: if not gs_windows_binary: + try: + os.unlink(outfile) + if infile_temp: + os.unlink(infile_temp) + except OSError: + pass + msg = "Unable to locate Ghostscript on paths" raise OSError(msg) command[0] = gs_windows_binary @@ -162,9 +170,16 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. + This class is no longer used internally, but kept for backwards compatibility. """ def __init__(self, fp): + deprecate( + "PSFile", + 11, + action="If you need the functionality of this class " + "you will need to implement it yourself.", + ) self.fp = fp self.char = None @@ -173,13 +188,11 @@ class PSFile: self.fp.seek(offset, whence) def readline(self): - s = [] - if self.char: - s.append(self.char) - self.char = None + s = [self.char or b""] + self.char = None c = self.fp.read(1) - while (c not in b"\r\n") and len(c) and len(b"".join(s).strip(b"\r\n")) <= 255: + while (c not in b"\r\n") and len(c): s.append(c) c = self.fp.read(1) @@ -196,7 +209,7 @@ def _accept(prefix): ## -# Image plugin for Encapsulated PostScript. This plugin supports only +# Image plugin for Encapsulated PostScript. This plugin supports only # a few variants of this format. @@ -211,29 +224,69 @@ class EpsImageFile(ImageFile.ImageFile): def _open(self): (length, offset) = self._find_offset(self.fp) - # Rewrap the open file pointer in something that will - # convert line endings and decode to latin-1. - fp = PSFile(self.fp) - # go to offset - start of "%!PS" - fp.seek(offset) + self.fp.seek(offset) - box = None + self._mode = "RGB" + self._size = None - self.mode = "RGB" - self._size = 1, 1 # FIXME: huh? + byte_arr = bytearray(255) + bytes_mv = memoryview(byte_arr) + bytes_read = 0 + reading_comments = True - # - # Load EPS header + def check_required_header_comments(): + if "PS-Adobe" not in self.info: + msg = 'EPS header missing "%!PS-Adobe" comment' + raise SyntaxError(msg) + if "BoundingBox" not in self.info: + msg = 'EPS header missing "%%BoundingBox" comment' + raise SyntaxError(msg) - s_raw = fp.readline() - s = s_raw.strip("\r\n") + while True: + byte = self.fp.read(1) + if byte == b"": + # if we didn't read a byte we must be at the end of the file + if bytes_read == 0: + break + elif byte in b"\r\n": + # if we read a line ending character, ignore it and parse what + # we have already read. if we haven't read any other characters, + # continue reading + if bytes_read == 0: + continue + else: + # ASCII/hexadecimal lines in an EPS file must not exceed + # 255 characters, not including line ending characters + if bytes_read >= 255: + # only enforce this for lines starting with a "%", + # otherwise assume it's binary data + if byte_arr[0] == ord("%"): + msg = "not an EPS file" + raise SyntaxError(msg) + else: + if reading_comments: + check_required_header_comments() + reading_comments = False + # reset bytes_read so we can keep reading + # data until the end of the line + bytes_read = 0 + byte_arr[bytes_read] = byte[0] + bytes_read += 1 + continue - while s_raw: - if s: - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) + if reading_comments: + # Load EPS header + + # if this line doesn't start with a "%", + # or does start with "%%EndComments", + # then we've reached the end of the header/comments + if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": + check_required_header_comments() + reading_comments = False + continue + + s = str(bytes_mv[:bytes_read], "latin-1") try: m = split.match(s) @@ -256,16 +309,12 @@ class EpsImageFile(ImageFile.ImageFile): ] except Exception: pass - else: m = field.match(s) if m: k = m.group(1) - - if k == "EndComments": - break if k[:8] == "PS-Adobe": - self.info[k[:8]] = k[9:] + self.info["PS-Adobe"] = k[9:] else: self.info[k] = "" elif s[0] == "%": @@ -275,43 +324,43 @@ class EpsImageFile(ImageFile.ImageFile): else: msg = "bad EPS header" raise OSError(msg) + elif bytes_mv[:11] == b"%ImageData:": + # Check for an "ImageData" descriptor + # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 - s_raw = fp.readline() - s = s_raw.strip("\r\n") + # Values: + # columns + # rows + # bit depth (1 or 8) + # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) + # number of padding channels + # block size (number of bytes per row per channel) + # binary/ascii (1: binary, 2: ascii) + # data start identifier (the image data follows after a single line + # consisting only of this quoted value) + image_data_values = byte_arr[11:bytes_read].split(None, 7) + columns, rows, bit_depth, mode_id = [ + int(value) for value in image_data_values[:4] + ] - if s and s[:1] != "%": - break - - # - # Scan for an "ImageData" descriptor - - while s[:1] == "%": - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) - - if s[:11] == "%ImageData:": - # Encoded bitmapped image. - x, y, bi, mo = s[11:].split(None, 7)[:4] - - if int(bi) == 1: - self.mode = "1" - elif int(bi) == 8: + if bit_depth == 1: + self._mode = "1" + elif bit_depth == 8: try: - self.mode = self.mode_map[int(mo)] + self._mode = self.mode_map[mode_id] except ValueError: break else: break - self._size = int(x), int(y) + self._size = columns, rows return - s = fp.readline().strip("\r\n") - if not s: - break + bytes_read = 0 - if not box: + check_required_header_comments() + + if not self._size: msg = "cannot determine EPS bounding box" raise OSError(msg) @@ -342,7 +391,7 @@ class EpsImageFile(ImageFile.ImageFile): # Load EPS via Ghostscript if self.tile: self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) - self.mode = self.im.mode + self._mode = self.im.mode self._size = self.im.size self.tile = [] return Image.Image.load(self) @@ -353,18 +402,15 @@ class EpsImageFile(ImageFile.ImageFile): pass -# # -------------------------------------------------------------------- def _save(im, fp, filename, eps=1): """EPS Writer for the Python Imaging Library.""" - # # make sure image data is available im.load() - # # determine PostScript image mode if im.mode == "L": operator = (8, 1, b"image") @@ -377,7 +423,6 @@ def _save(im, fp, filename, eps=1): raise ValueError(msg) if eps: - # # write EPS header fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") @@ -389,7 +434,6 @@ def _save(im, fp, filename, eps=1): fp.write(b"%%ImageData: %d %d " % im.size) fp.write(b'%d %d 0 1 1 "%s"\n' % operator) - # # image header fp.write(b"gsave\n") fp.write(b"10 dict begin\n") @@ -410,7 +454,6 @@ def _save(im, fp, filename, eps=1): fp.flush() -# # -------------------------------------------------------------------- diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 1359aeb12..e0e51aaac 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -51,14 +51,14 @@ class FitsImageFile(ImageFile.ImageFile): number_of_bits = int(headers[b"BITPIX"]) if number_of_bits == 8: - self.mode = "L" + self._mode = "L" elif number_of_bits == 16: - self.mode = "I" + self._mode = "I" # rawmode = "I;16S" elif number_of_bits == 32: - self.mode = "I" + self._mode = "I" elif number_of_bits in (-32, -64): - self.mode = "F" + self._mode = "F" # rawmode = "F" if number_of_bits == -32 else "F;64F" offset = math.ceil(self.fp.tell() / 2880) * 2880 diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py deleted file mode 100644 index 50948ec42..000000000 --- a/src/PIL/FitsStubImagePlugin.py +++ /dev/null @@ -1,76 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# FITS stub adapter -# -# Copyright (c) 1998-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from . import FitsImagePlugin, Image, ImageFile -from ._deprecate import deprecate - -_handler = None - - -def register_handler(handler): - """ - Install application-specific FITS image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - deprecate( - "FitsStubImagePlugin", - 10, - action="FITS images can now be read without " - "a handler through FitsImagePlugin instead", - ) - - # Override FitsImagePlugin with this handler - # for backwards compatibility - try: - Image.ID.remove(FITSStubImageFile.format) - except ValueError: - pass - - Image.register_open( - FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept - ) - - -class FITSStubImageFile(ImageFile.StubImageFile): - format = FitsImagePlugin.FitsImageFile.format - format_description = FitsImagePlugin.FitsImageFile.format_description - - def _open(self): - offset = self.fp.tell() - - im = FitsImagePlugin.FitsImageFile(self.fp) - self._size = im.size - self.mode = im.mode - self.tile = [] - - self.fp.seek(offset) - - loader = self._load() - if loader: - loader.open(self) - - def _load(self): - return _handler - - -def _save(im, fp, filename): - msg = "FITS save handler not installed" - raise OSError(msg) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_save(FITSStubImageFile.format, _save) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index f4e89a03e..8f641ece9 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -56,7 +56,7 @@ class FliImageFile(ImageFile.ImageFile): self.is_animated = self.n_frames > 1 # image characteristics - self.mode = "P" + self._mode = "P" self._size = i16(s, 8), i16(s, 10) # animation speed diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index d145d01f7..a878cbfd2 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -106,7 +106,7 @@ class FpxImageFile(ImageFile.ImageFile): # note: for now, we ignore the "uncalibrated" flag colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) - self.mode, self.rawmode = MODES[tuple(colors)] + self._mode, self.rawmode = MODES[tuple(colors)] # load JPEG tables, if any self.jpeg = {} @@ -235,6 +235,14 @@ class FpxImageFile(ImageFile.ImageFile): return ImageFile.ImageFile.load(self) + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index c7c32252b..c2e4ead71 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -56,7 +56,6 @@ from enum import IntEnum from io import BytesIO from . import Image, ImageFile -from ._deprecate import deprecate MAGIC = b"FTEX" @@ -66,17 +65,6 @@ class Format(IntEnum): UNCOMPRESSED = 1 -def __getattr__(name): - for enum, prefix in {Format: "FORMAT_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - class FtexImageFile(ImageFile.ImageFile): format = "FTEX" format_description = "Texture File Format (IW2:EOC)" @@ -89,7 +77,7 @@ class FtexImageFile(ImageFile.ImageFile): self._size = struct.unpack("<2i", self.fp.read(8)) mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8)) - self.mode = "RGB" + self._mode = "RGB" # Only support single-format files. # I don't know of any multi-format file. @@ -102,7 +90,7 @@ class FtexImageFile(ImageFile.ImageFile): data = self.fp.read(mipmap_size) if format == Format.DXT1: - self.mode = "RGBA" + self._mode = "RGBA" self.tile = [("bcn", (0, 0) + self.size, 0, 1)] elif format == Format.UNCOMPRESSED: self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 994a6e8eb..ec6e9de6e 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -73,9 +73,9 @@ class GbrImageFile(ImageFile.ImageFile): comment = self.fp.read(comment_length)[:-1] if color_depth == 1: - self.mode = "L" + self._mode = "L" else: - self.mode = "RGBA" + self._mode = "RGBA" self._size = width, height diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 7dda4f143..3599994a8 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -47,11 +47,11 @@ class GdImageFile(ImageFile.ImageFile): # Header s = self.fp.read(1037) - if not i16(s) in [65534, 65535]: + if i16(s) not in [65534, 65535]: msg = "Not a valid GD 2.x .gd file" raise SyntaxError(msg) - self.mode = "L" # FIXME: "P" + self._mode = "L" # FIXME: "P" self._size = i16(s, 2), i16(s, 4) true_color = s[6] diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index eadee1560..92074b0d4 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -304,11 +304,11 @@ class GifImageFile(ImageFile.ImageFile): if frame == 0: if self._frame_palette: if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: - self.mode = "RGBA" if frame_transparency is not None else "RGB" + self._mode = "RGBA" if frame_transparency is not None else "RGB" else: - self.mode = "P" + self._mode = "P" else: - self.mode = "L" + self._mode = "L" if not palette and self.global_palette: from copy import copy @@ -325,10 +325,10 @@ class GifImageFile(ImageFile.ImageFile): if "transparency" in self.info: self.im.putpalettealpha(self.info["transparency"], 0) self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) - self.mode = "RGBA" + self._mode = "RGBA" del self.info["transparency"] else: - self.mode = "RGB" + self._mode = "RGB" self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) def _rgb(color): @@ -424,7 +424,7 @@ class GifImageFile(ImageFile.ImageFile): self.im.putpalette(*self._frame_palette.getdata()) else: self.im = None - self.mode = temp_mode + self._mode = temp_mode self._frame_palette = None super().load_prepare() @@ -434,9 +434,9 @@ class GifImageFile(ImageFile.ImageFile): if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self._frame_transparency is not None: self.im.putpalettealpha(self._frame_transparency, 0) - self.mode = "RGBA" + self._mode = "RGBA" else: - self.mode = "RGB" + self._mode = "RGB" self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) return if not self._prev_im: @@ -449,7 +449,7 @@ class GifImageFile(ImageFile.ImageFile): frame_im = self._crop(frame_im, self.dispose_extent) self.im = self._prev_im - self.mode = self.im.mode + self._mode = self.im.mode if frame_im.mode == "RGBA": self.im.paste(frame_im, self.dispose_extent, frame_im) else: @@ -569,9 +569,9 @@ def _getbbox(base_im, im_frame): delta = ImageChops.subtract_modulo(im_frame, base_im) else: delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_im.convert("RGB") + im_frame.convert("RGBA"), base_im.convert("RGBA") ) - return delta.getbbox() + return delta.getbbox(alpha_only=False) def _write_multiple_frames(im, fp, palette): @@ -683,11 +683,7 @@ def get_interlace(im): def _write_local_header(fp, im, offset, flags): transparent_color_exists = False try: - if "transparency" in im.encoderinfo: - transparency = im.encoderinfo["transparency"] - else: - transparency = im.info["transparency"] - transparency = int(transparency) + transparency = int(im.encoderinfo["transparency"]) except (KeyError, ValueError): pass else: @@ -879,7 +875,7 @@ def _get_palette_bytes(im): :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return im.palette.palette + return im.palette.palette if im.palette else b"" def _get_background(im, info_background): @@ -916,7 +912,7 @@ def _get_global_header(im, info): info and ( "transparency" in info - or "loop" in info + or info.get("loop") is not None or info.get("duration") or info.get("comment") ) @@ -941,7 +937,7 @@ def _get_global_header(im, info): # Global Color Table _get_header_palette(palette_bytes), ] - if "loop" in info: + if info.get("loop") is not None: header.append( b"!" + o8(255) # extension intro diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 8a799f19c..c1c71da08 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -46,7 +46,7 @@ class GribStubImageFile(ImageFile.StubImageFile): self.fp.seek(offset) # make something up - self.mode = "F" + self._mode = "F" self._size = 1, 1 loader = self._load() diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index bba05ed65..c26b480ac 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -46,7 +46,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile): self.fp.seek(offset) # make something up - self.mode = "F" + self._mode = "F" self._size = 1, 1 loader = self._load() diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index c2f050edd..0aa4f7a84 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -22,11 +22,11 @@ import os import struct import sys -from PIL import Image, ImageFile, PngImagePlugin, features +from . import Image, ImageFile, PngImagePlugin, features enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: - from PIL import Jpeg2KImagePlugin + from . import Jpeg2KImagePlugin MAGIC = b"icns" HEADERSIZE = 8 @@ -253,7 +253,7 @@ class IcnsImageFile(ImageFile.ImageFile): def _open(self): self.icns = IcnsFile(self.fp) - self.mode = "RGBA" + self._mode = "RGBA" self.info["sizes"] = self.icns.itersizes() self.best_size = self.icns.bestsize() self.size = ( @@ -305,7 +305,7 @@ class IcnsImageFile(ImageFile.ImageFile): px = im.load() self.im = im.im - self.mode = im.mode + self._mode = im.mode self.size = im.size return px diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index a188f8fdc..0445a2ab2 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -330,7 +330,7 @@ class IcoImageFile(ImageFile.ImageFile): im.load() self.im = im.im self.pyaccess = None - self.mode = im.mode + self._mode = im.mode if im.size != self.size: warnings.warn("Image was not the expected size") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 746743f65..b42ba7cac 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -205,7 +205,7 @@ class ImImageFile(ImageFile.ImageFile): # Basic attributes self._size = self.info[SIZE] - self.mode = self.info[MODE] + self._mode = self.info[MODE] # Skip forward to start of image data while s and s[:1] != b"\x1A": @@ -231,9 +231,9 @@ class ImImageFile(ImageFile.ImageFile): self.lut = list(palette[:256]) else: if self.mode in ["L", "P"]: - self.mode = self.rawmode = "P" + self._mode = self.rawmode = "P" elif self.mode in ["LA", "PA"]: - self.mode = "PA" + self._mode = "PA" self.rawmode = "PA;L" self.palette = ImagePalette.raw("RGB;L", palette) elif self.mode == "RGB": diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 63bad83a1..6ea711b56 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -56,29 +56,8 @@ from . import ( _plugins, ) from ._binary import i32le, o32be, o32le -from ._deprecate import deprecate from ._util import DeferredError, is_path - -def __getattr__(name): - categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} - if name in categories: - deprecate("Image categories", 10, "is_animated", plural=True) - return categories[name] - old_resampling = { - "LINEAR": "BILINEAR", - "CUBIC": "BICUBIC", - "ANTIALIAS": "LANCZOS", - } - if name in old_resampling: - deprecate( - name, 10, f"{old_resampling[name]} or Resampling.{old_resampling[name]}" - ) - return Resampling[old_resampling[name]] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - logger = logging.getLogger(__name__) @@ -128,8 +107,7 @@ except ImportError as v: raise -# works everywhere, win for pypy, not cpython -USE_CFFI_ACCESS = hasattr(sys, "pypy_version_info") +USE_CFFI_ACCESS = False try: import cffi except ImportError: @@ -441,26 +419,18 @@ def _getencoder(mode, encoder_name, args, extra=()): # Simple expression analyzer -def coerce_e(value): - deprecate("coerce_e", 10) - return value if isinstance(value, _E) else _E(1, value) - - -# _E(scale, offset) represents the affine transformation scale * x + offset. -# The "data" field is named for compatibility with the old implementation, -# and should be renamed once coerce_e is removed. class _E: - def __init__(self, scale, data): + def __init__(self, scale, offset): self.scale = scale - self.data = data + self.offset = offset def __neg__(self): - return _E(-self.scale, -self.data) + return _E(-self.scale, -self.offset) def __add__(self, other): if isinstance(other, _E): - return _E(self.scale + other.scale, self.data + other.data) - return _E(self.scale, self.data + other) + return _E(self.scale + other.scale, self.offset + other.offset) + return _E(self.scale, self.offset + other) __radd__ = __add__ @@ -473,19 +443,19 @@ class _E: def __mul__(self, other): if isinstance(other, _E): return NotImplemented - return _E(self.scale * other, self.data * other) + return _E(self.scale * other, self.offset * other) __rmul__ = __mul__ def __truediv__(self, other): if isinstance(other, _E): return NotImplemented - return _E(self.scale / other, self.data / other) + return _E(self.scale / other, self.offset / other) def _getscaleoffset(expr): a = expr(_E(1, 0)) - return (a.scale, a.data) if isinstance(a, _E) else (0, a) + return (a.scale, a.offset) if isinstance(a, _E) else (0, a) # -------------------------------------------------------------------- @@ -512,21 +482,14 @@ class Image: # FIXME: take "new" parameters / other image? # FIXME: turn mode and size into delegating properties? self.im = None - self.mode = "" + self._mode = "" self._size = (0, 0) self.palette = None self.info = {} - self._category = 0 self.readonly = 0 self.pyaccess = None self._exif = None - def __getattr__(self, name): - if name == "category": - deprecate("Image categories", 10, "is_animated", plural=True) - return self._category - raise AttributeError(name) - @property def width(self): return self.size[0] @@ -539,10 +502,14 @@ class Image: def size(self): return self._size + @property + def mode(self): + return self._mode + def _new(self, im): new = Image() new.im = im - new.mode = im.mode + new._mode = im.mode new._size = im.size if im.mode in ("P", "PA"): if self.palette: @@ -639,7 +606,6 @@ class Image: and self.mode == other.mode and self.size == other.size and self.info == other.info - and self._category == other._category and self.getpalette() == other.getpalette() and self.tobytes() == other.tobytes() ) @@ -670,27 +636,37 @@ class Image: ) ) - def _repr_png_(self): - """iPython display hook support + def _repr_image(self, image_format, **kwargs): + """Helper function for iPython display hook. - :returns: png version of the image as bytes + :param image_format: Image format. + :returns: image as bytes, saved into the given format. """ b = io.BytesIO() try: - self.save(b, "PNG") - except Exception as e: - msg = "Could not save to PNG for display" - raise ValueError(msg) from e + self.save(b, image_format, **kwargs) + except Exception: + return None return b.getvalue() + def _repr_png_(self): + """iPython display hook support for PNG format. + + :returns: PNG version of the image as bytes + """ + return self._repr_image("PNG", compress_level=1) + + def _repr_jpeg_(self): + """iPython display hook support for JPEG format. + + :returns: JPEG version of the image as bytes + """ + return self._repr_image("JPEG") + @property def __array_interface__(self): # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new["shape"] = shape - new["typestr"] = typestr - new["version"] = 3 + new = {"version": 3} try: if self.mode == "1": # Binary images need to be extended from bits to bytes @@ -709,16 +685,18 @@ class Image: if parse_version(numpy.__version__) < parse_version("1.23"): warnings.warn(e) raise + new["shape"], new["typestr"] = _conv_type_shape(self) return new def __getstate__(self): - return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] + im_data = self.tobytes() # load image first + return [self.info, self.mode, self.size, self.getpalette(), im_data] def __setstate__(self, state): Image.__init__(self) info, mode, size, palette, data = state self.info = info - self.mode = mode + self._mode = mode self._size = size self.im = core.new(mode, size) if mode in ("L", "LA", "P", "PA") and palette: @@ -765,17 +743,17 @@ class Image: bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - data = [] + output = [] while True: - l, s, d = e.encode(bufsize) - data.append(d) - if s: + bytes_consumed, errcode, data = e.encode(bufsize) + output.append(data) + if errcode: break - if s < 0: - msg = f"encoder error {s} in tobytes" + if errcode < 0: + msg = f"encoder error {errcode} in tobytes" raise RuntimeError(msg) - return b"".join(data) + return b"".join(output) def tobitmap(self, name="image"): """ @@ -1147,7 +1125,6 @@ class Image: Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` (default). :returns: A new image - """ self.load() @@ -1279,7 +1256,7 @@ class Image: if ymargin is None: ymargin = xmargin self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) + return self._new(self.im.expand(xmargin, ymargin)) def filter(self, filter): """ @@ -1318,11 +1295,15 @@ class Image: """ return ImageMode.getmode(self.mode).bands - def getbbox(self): + def getbbox(self, *, alpha_only=True): """ Calculates the bounding box of the non-zero regions in the image. + :param alpha_only: Optional flag, defaulting to ``True``. + If ``True`` and the image has an alpha channel, trim transparent pixels. + Otherwise, trim pixels when all channels are zero. + Keyword-only argument. :returns: The bounding box is returned as a 4-tuple defining the left, upper, right, and lower pixel coordinate. See :ref:`coordinate-system`. If the image is completely empty, this @@ -1331,7 +1312,7 @@ class Image: """ self.load() - return self.im.getbbox() + return self.im.getbbox(alpha_only) def getcolors(self, maxcolors=256): """ @@ -1458,12 +1439,12 @@ class Image: self._exif.load(exif_info) # XMP tags - if 0x0112 not in self._exif: + if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") if xmp_tags: match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: - self._exif[0x0112] = int(match[2]) + self._exif[ExifTags.Base.Orientation] = int(match[2]) return self._exif @@ -1584,7 +1565,7 @@ class Image: self.load() if self.pyaccess: return self.pyaccess.getpixel(xy) - return self.im.getpixel(xy) + return self.im.getpixel(tuple(xy)) def getprojection(self): """ @@ -1756,7 +1737,7 @@ class Image: if not isinstance(dest, (list, tuple)): msg = "Destination must be a tuple" raise ValueError(msg) - if not len(source) in (2, 4): + if len(source) not in (2, 4): msg = "Source must be a 2 or 4-tuple" raise ValueError(msg) if not len(dest) == 2: @@ -1862,7 +1843,7 @@ class Image: raise ValueError from e # sanity check self.im = im self.pyaccess = None - self.mode = self.im.mode + self._mode = self.im.mode except KeyError as e: msg = "illegal image mode" raise ValueError(msg) from e @@ -1940,7 +1921,7 @@ class Image: if not isinstance(data, bytes): data = bytes(data) palette = ImagePalette.raw(rawmode, data) - self.mode = "PA" if "A" in self.mode else "P" + self._mode = "PA" if "A" in self.mode else "P" self.palette = palette self.palette.mode = "RGB" self.load() # install new palette @@ -2048,7 +2029,7 @@ class Image: mapping_palette = bytearray(new_positions) m_im = self.copy() - m_im.mode = "P" + m_im._mode = "P" m_im.palette = ImagePalette.ImagePalette( palette_mode, palette=mapping_palette * bands @@ -2476,8 +2457,8 @@ class Image: The image is first saved to a temporary file. By default, it will be in PNG format. - On Unix, the image is then opened using the **display**, **eog** or - **xv** utility, depending on which one can be found. + On Unix, the image is then opened using the **xdg-open**, **display**, + **gm**, **eog** or **xv** utility, depending on which one can be found. On macOS, the image is opened with the native Preview application. @@ -2623,7 +2604,7 @@ class Image: self.im = im.im self._size = size - self.mode = self.im.mode + self._mode = self.im.mode self.readonly = 0 self.pyaccess = None @@ -2907,7 +2888,7 @@ def new(mode, size, color=0): :param color: What color to use for the image. Default is black. If given, this should be a single integer or floating point value for single-band modes, and a tuple for multi-band modes (one value - per band). When creating RGB images, you can also use color + per band). When creating RGB or HSV images, you can also use color strings as supported by the ImageColor module. If the color is None, the image is not initialised. :returns: An :py:class:`~PIL.Image.Image` object. @@ -3034,21 +3015,29 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): def fromarray(obj, mode=None): """ Creates an image memory from an object exporting the array interface - (using the buffer protocol). + (using the buffer protocol):: + + from PIL import Image + import numpy as np + a = np.zeros((5, 5)) + im = Image.fromarray(a) If ``obj`` is not contiguous, then the ``tobytes`` method is called and :py:func:`~PIL.Image.frombuffer` is used. - If you have an image in NumPy:: + In the case of NumPy, be aware that Pillow modes do not always correspond + to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, + 32-bit signed integer pixels, and 32-bit floating point pixels. + + Pillow images can also be converted to arrays:: from PIL import Image import numpy as np im = Image.open("hopper.jpg") a = np.asarray(im) - Then this can be used to convert it to a Pillow image:: - - im = Image.fromarray(a) + When converting Pillow images to arrays however, only pixel values are + transferred. This means that P and PA mode images will lose their palette. :param obj: Object with array interface :param mode: Optional mode to use when reading ``obj``. Will be determined from @@ -3158,7 +3147,7 @@ def _decompression_bomb_check(size): if MAX_IMAGE_PIXELS is None: return - pixels = size[0] * size[1] + pixels = max(1, size[0]) * max(1, size[1]) if pixels > 2 * MAX_IMAGE_PIXELS: msg = ( @@ -3188,7 +3177,8 @@ def open(fp, mode="r", formats=None): :param fp: A filename (string), pathlib.Path object or a file object. The file object must implement ``file.read``, ``file.seek``, and ``file.tell`` methods, - and be opened in binary mode. + and be opened in binary mode. The file object will also seek to zero + before reading. :param mode: The mode. If given, this argument must be "r". :param formats: A list or tuple of formats to attempt to load the file in. This can be used to restrict the set of formats checked. diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index fec4694b2..701200317 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -38,9 +38,7 @@ def duplicate(image): def invert(image): """ - Invert an image (channel). - - .. code-block:: python + Invert an image (channel). :: out = MAX - image @@ -54,9 +52,7 @@ def invert(image): def lighter(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. - - .. code-block:: python + the lighter values. :: out = max(image1, image2) @@ -71,9 +67,7 @@ def lighter(image1, image2): def darker(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the darker values. - - .. code-block:: python + the darker values. :: out = min(image1, image2) @@ -88,9 +82,7 @@ def darker(image1, image2): def difference(image1, image2): """ Returns the absolute value of the pixel-by-pixel difference between the two - images. - - .. code-block:: python + images. :: out = abs(image1 - image2) @@ -107,9 +99,7 @@ def multiply(image1, image2): Superimposes two images on top of each other. If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. - - .. code-block:: python + you multiply with a solid white image, the image is unaffected. :: out = image1 * image2 / MAX @@ -123,9 +113,7 @@ def multiply(image1, image2): def screen(image1, image2): """ - Superimposes two inverted images on top of each other. - - .. code-block:: python + Superimposes two inverted images on top of each other. :: out = MAX - ((MAX - image1) * (MAX - image2) / MAX) @@ -176,9 +164,7 @@ def overlay(image1, image2): def add(image1, image2, scale=1.0, offset=0): """ Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 + image2) / scale + offset) @@ -193,9 +179,7 @@ def add(image1, image2, scale=1.0, offset=0): def subtract(image1, image2, scale=1.0, offset=0): """ Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 - image2) / scale + offset) @@ -208,9 +192,7 @@ def subtract(image1, image2, scale=1.0, offset=0): def add_modulo(image1, image2): - """Add two images, without clipping the result. - - .. code-block:: python + """Add two images, without clipping the result. :: out = ((image1 + image2) % MAX) @@ -223,9 +205,7 @@ def add_modulo(image1, image2): def subtract_modulo(image1, image2): - """Subtract two images, without clipping the result. - - .. code-block:: python + """Subtract two images, without clipping the result. :: out = ((image1 - image2) % MAX) @@ -243,9 +223,7 @@ def logical_and(image1, image2): Both of the images must have mode "1". If you would like to perform a logical AND on an image with a mode other than "1", try :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. - - .. code-block:: python + as the second image. :: out = ((image1 and image2) % MAX) @@ -260,9 +238,7 @@ def logical_and(image1, image2): def logical_or(image1, image2): """Logical OR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((image1 or image2) % MAX) @@ -277,9 +253,7 @@ def logical_or(image1, image2): def logical_xor(image1, image2): """Logical XOR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((bool(image1) != bool(image2)) % MAX) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index f87849680..3a337f9f2 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -18,12 +18,10 @@ import sys from enum import IntEnum -from PIL import Image - -from ._deprecate import deprecate +from . import Image try: - from PIL import _imagingcms + from . import _imagingcms except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. @@ -117,17 +115,6 @@ class Direction(IntEnum): PROOF = 2 -def __getattr__(name): - for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # # flags @@ -198,12 +185,8 @@ class ImageCmsProfile: def _set(self, profile, filename=None): self.profile = profile self.filename = filename - if profile: - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info - else: - self.product_name = None - self.product_info = None + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info def tobytes(self): """ @@ -288,7 +271,7 @@ def get_display_profile(handle=None): if sys.platform != "win32": return None - from PIL import ImageWin + from . import ImageWin if isinstance(handle, ImageWin.HDC): profile = core.get_display_profile_win32(handle, 1) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index e184ed68d..befc1fd1d 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -122,9 +122,11 @@ def getrgb(color): def getcolor(color, mode): """ - Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a - greyscale value if ``mode`` is not color or a palette image. If the string - cannot be parsed, this function raises a :py:exc:`ValueError` exception. + Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if + ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is + not color or a palette image, converts the RGB value to a greyscale value. + If the string cannot be parsed, this function raises a :py:exc:`ValueError` + exception. .. versionadded:: 1.1.4 @@ -137,7 +139,13 @@ def getcolor(color, mode): if len(color) == 4: color, alpha = color[:3], color[3] - if Image.getmodebase(mode) == "L": + if mode == "HSV": + from colorsys import rgb_to_hsv + + r, g, b = color + h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) + return int(h * 255), int(s * 255), int(v * 255) + elif Image.getmodebase(mode) == "L": r, g, b = color # ITU-R Recommendation 601-2 for nonlinear RGB # scaled to 24 bits to match the convert's implementation. diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 163828d31..7d1790faa 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -32,10 +32,8 @@ import math import numbers -import warnings from . import Image, ImageColor -from ._deprecate import deprecate """ A simple 2D drawing interface for PIL images. @@ -281,11 +279,11 @@ class ImageDraw: self.im.paste(im.im, (0, 0) + im.size, mask.im) def regular_polygon( - self, bounding_circle, n_sides, rotation=0, fill=None, outline=None + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 ): """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - self.polygon(xy, fill, outline) + self.polygon(xy, fill, outline, width) def rectangle(self, xy, fill=None, outline=None, width=1): """Draw a rectangle.""" @@ -295,29 +293,43 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_rectangle(xy, ink, 0, width) - def rounded_rectangle(self, xy, radius=0, fill=None, outline=None, width=1): + def rounded_rectangle( + self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None + ): """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): (x0, y0), (x1, y1) = xy else: x0, y0, x1, y1 = xy + if x1 < x0: + msg = "x1 must be greater than or equal to x0" + raise ValueError(msg) + if y1 < y0: + msg = "y1 must be greater than or equal to y0" + raise ValueError(msg) + if corners is None: + corners = (True, True, True, True) d = radius * 2 - full_x = d >= x1 - x0 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) + full_x, full_y = False, False + if all(corners): + full_x = d >= x1 - x0 - 1 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 - 1 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) - if d == 0: - # If the corners have no curve, that is a rectangle + if d == 0 or not any(corners): + # If the corners have no curve, + # or there are no corners, + # that is a rectangle return self.rectangle(xy, fill, outline, width) r = d // 2 @@ -338,12 +350,17 @@ class ImageDraw: ) else: # Draw four separate corners - parts = ( - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ((x0, y0, x0 + d, y0 + d), 180, 270), - ) + parts = [] + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) + ): + if corners[i]: + parts.append(part) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -358,28 +375,52 @@ class ImageDraw: else: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) if not full_x and not full_y: - self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) - self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) + left = [x0, y0, x0 + r, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, fill, 1) + + right = [x1 - r, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, fill, 1) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: - self.draw.draw_rectangle( - (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 - ) + top = [x0, y0, x1, y0 + width - 1] + if corners[0]: + top[0] += r + 1 + if corners[1]: + top[2] -= r + 1 + self.draw.draw_rectangle(top, ink, 1) + + bottom = [x0, y1 - width + 1, x1, y1] + if corners[3]: + bottom[0] += r + 1 + if corners[2]: + bottom[2] -= r + 1 + self.draw.draw_rectangle(bottom, ink, 1) if not full_y: - self.draw.draw_rectangle( - (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 - ) + left = [x0, y0, x0 + width - 1, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, ink, 1) + + right = [x1 - width + 1, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text): - """Draw text.""" split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text @@ -390,17 +431,11 @@ class ImageDraw: return text.split(split_character) def _multiline_spacing(self, font, spacing, stroke_width): - # this can be replaced with self.textbbox(...)[3] when textsize is removed - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return ( - self.textsize( - "A", - font=font, - stroke_width=stroke_width, - )[1] - + spacing - ) + return ( + self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + + stroke_width + + spacing + ) def text( self, @@ -420,6 +455,7 @@ class ImageDraw: *args, **kwargs, ): + """Draw text.""" if self._multiline_check(text): return self.multiline_text( xy, @@ -601,72 +637,6 @@ class ImageDraw: ) top += line_spacing - def textsize( - self, - text, - font=None, - spacing=4, - direction=None, - features=None, - language=None, - stroke_width=0, - ): - """Get the size of a given string, in pixels.""" - deprecate("textsize", 10, "textbbox or textlength") - if self._multiline_check(text): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return self.multiline_textsize( - text, - font, - spacing, - direction, - features, - language, - stroke_width, - ) - - if font is None: - font = self.getfont() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return font.getsize( - text, - direction, - features, - language, - stroke_width, - ) - - def multiline_textsize( - self, - text, - font=None, - spacing=4, - direction=None, - features=None, - language=None, - stroke_width=0, - ): - deprecate("multiline_textsize", 10, "multiline_textbbox") - max_width = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - for line in lines: - line_width, line_height = self.textsize( - line, - font, - spacing, - direction, - features, - language, - stroke_width, - ) - max_width = max(max_width, line_width) - return max_width, len(lines) * line_spacing - spacing - def textlength( self, text, @@ -687,22 +657,7 @@ class ImageDraw: if font is None: font = self.getfont() mode = "RGBA" if embedded_color else self.fontmode - try: - return font.getlength(text, mode, direction, features, language) - except AttributeError: - deprecate("textlength support for fonts without getlength", 10) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - size = self.textsize( - text, - font, - direction=direction, - features=features, - language=language, - ) - if direction == "ttb": - return size[1] - return size[0] + return font.getlength(text, mode, direction, features, language) def textbbox( self, diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 2667b77dd..7ce0224a6 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -24,10 +24,7 @@ """ -import warnings - from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath -from ._deprecate import deprecate class Pen: @@ -173,19 +170,6 @@ class Draw: xy.transform(self.transform) self.draw.text(xy, text, font=font.font, fill=font.color) - def textsize(self, text, font): - """ - .. deprecated:: 9.2.0 - - Return the size of the given string, in pixels. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize` - """ - deprecate("textsize", 10, "textbbox or textlength") - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return self.draw.textsize(text, font=font.font) - def textbbox(self, xy, text, font): """ Returns bounding box (in pixels) of given text. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 132490a8e..8e4f7dfb2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -530,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - l, s = encoder.encode_to_pyfd() + errcode = encoder.encode_to_pyfd()[1] else: if exc: # compress to Python file-compatible object while True: - l, s, d = encoder.encode(bufsize) - fp.write(d) - if s: + errcode, data = encoder.encode(bufsize)[1:] + fp.write(data) + if errcode: break else: # slight speedup: compress to real file object - s = encoder.encode_to_file(fh, bufsize) - if s < 0: - msg = f"encoder error {s} when writing image file" + errcode = encoder.encode_to_file(fh, bufsize) + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 63d6dcf5c..57268b8f5 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -35,7 +35,7 @@ class BuiltinFilter(MultibandFilter): class Kernel(BuiltinFilter): """ - Create a convolution kernel. The current version only + Create a convolution kernel. The current version only supports 3x3 and 5x5 integer and floating point kernels. In the current version, kernels can only be applied to @@ -43,9 +43,10 @@ class Kernel(BuiltinFilter): :param size: Kernel size, given as (width, height). In the current version, this must be (3,3) or (5,5). - :param kernel: A sequence containing kernel weights. + :param kernel: A sequence containing kernel weights. The kernel will + be flipped vertically before being applied to the image. :param scale: Scale factor. If given, the result for each pixel is - divided by this value. The default is the sum of the + divided by this value. The default is the sum of the kernel weights. :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. @@ -156,7 +157,8 @@ class GaussianBlur(MultibandFilter): approximates a Gaussian kernel. For details on accuracy see - :param radius: Standard deviation of the Gaussian kernel. + :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two + numbers for x and y, or a single number for both. """ name = "GaussianBlur" @@ -165,7 +167,12 @@ class GaussianBlur(MultibandFilter): self.radius = radius def filter(self, image): - return image.gaussian_blur(self.radius) + xy = self.radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + if xy == (0, 0): + return image.copy() + return image.gaussian_blur(xy) class BoxBlur(MultibandFilter): @@ -175,21 +182,31 @@ class BoxBlur(MultibandFilter): which runs in linear time relative to the size of the image for any radius value. - :param radius: Size of the box in one direction. Radius 0 does not blur, - returns an identical image. Radius 1 takes 1 pixel - in each direction, i.e. 9 pixels in total. + :param radius: Size of the box in a direction. Either a sequence of two numbers for + x and y, or a single number for both. + + Radius 0 does not blur, returns an identical image. + Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. """ name = "BoxBlur" def __init__(self, radius): - if radius < 0: + xy = radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + if xy[0] < 0 or xy[1] < 0: msg = "radius must be >= 0" raise ValueError(msg) self.radius = radius def filter(self, image): - return image.box_blur(self.radius) + xy = self.radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + if xy == (0, 0): + return image.copy() + return image.box_blur(xy) class UnsharpMask(MultibandFilter): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index bd13c391e..b77dc6ab1 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,7 +26,6 @@ # import base64 -import math import os import sys import warnings @@ -34,7 +33,6 @@ from enum import IntEnum from io import BytesIO from . import Image -from ._deprecate import deprecate from ._util import is_directory, is_path @@ -43,31 +41,21 @@ class Layout(IntEnum): RAQM = 1 -def __getattr__(name): - for enum, prefix in {Layout: "LAYOUT_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - -class _ImagingFtNotInstalled: - # module placeholder - def __getattr__(self, id): - msg = "The _imagingft C module is not installed" - raise ImportError(msg) +MAX_STRING_LENGTH = 1_000_000 try: from . import _imagingft as core -except ImportError: - core = _ImagingFtNotInstalled() +except ImportError as ex: + from ._util import DeferredError + + core = DeferredError(ex) -_UNSPECIFIED = object() +def _string_length_check(text): + if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: + msg = "too many characters in string" + raise ValueError(msg) # FIXME: add support for pilfont2 format (see FontFile.py) @@ -139,23 +127,6 @@ class ImageFont: self.font = Image.core.font(image.im, data) - def getsize(self, text, *args, **kwargs): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text. - - :param text: Text to measure. - - :return: (width, height) - """ - deprecate("getsize", 10, "getbbox or getlength") - return self.font.getsize(text) - def getmask(self, text, mode="", *args, **kwargs): """ Create a bitmap for the text. @@ -190,6 +161,7 @@ class ImageFont: :return: ``(left, top, right, bottom)`` bounding box """ + _string_length_check(text) width, height = self.font.getsize(text) return 0, 0, width, height @@ -200,6 +172,7 @@ class ImageFont: .. versionadded:: 9.2.0 """ + _string_length_check(text) width, height = self.font.getsize(text) return width @@ -263,10 +236,6 @@ class FreeTypeFont: path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def _multiline_split(self, text): - split_character = "\n" if isinstance(text, str) else b"\n" - return text.split(split_character) - def getname(self): """ :return: A tuple of the font family (e.g. Helvetica) and the font style @@ -297,27 +266,21 @@ class FreeTypeFont: string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = font.getlength("Hello") world = font.getlength("World") hello_world = hello + world # not adjusted for kerning assert hello_world == font.getlength("HelloWorld") # may fail - use - - .. code-block:: python + use :: hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning world = font.getlength("World") hello_world = hello + world # adjusted for kerning assert hello_world == font.getlength("HelloWorld") # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) @@ -355,8 +318,9 @@ class FreeTypeFont: `_ Requires libraqm. - :return: Width for horizontal, height for vertical text. + :return: Either width for horizontal text, or height for vertical text. """ + _string_length_check(text) return self.font.getlength(text, mode, direction, features, language) / 64 def getbbox( @@ -416,6 +380,7 @@ class FreeTypeFont: :return: ``(left, top, right, bottom)`` bounding box """ + _string_length_check(text) size, offset = self.font.getsize( text, mode, direction, features, language, anchor ) @@ -423,165 +388,6 @@ class FreeTypeFont: width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width return left, top, left + width, top + height - def getsize( - self, - text, - direction=None, - features=None, - language=None, - stroke_width=0, - ): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`getlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text if rendered in font with - provided direction, features, and language. - - .. note:: For historical reasons this function measures text height from - the ascender line instead of the top, see :ref:`text-anchors`. - If you wish to measure text height from the top, it is recommended - to use the bottom value of :meth:`getbbox` with ``anchor='lt'`` instead. - - :param text: Text to measure. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - """ - deprecate("getsize", 10, "getbbox or getlength") - # vertical offset is added for historical reasons - # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 - size, offset = self.font.getsize(text, "L", direction, features, language) - return ( - size[0] + stroke_width * 2, - size[1] + stroke_width * 2 + offset[1], - ) - - def getsize_multiline( - self, - text, - direction=None, - spacing=4, - features=None, - language=None, - stroke_width=0, - ): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.ImageDraw.multiline_textbbox` instead. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text if rendered in font - with provided direction, features, and language, while respecting - newline characters. - - :param text: Text to measure. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param spacing: The vertical gap between lines, defaulting to 4 pixels. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - """ - deprecate("getsize_multiline", 10, "ImageDraw.multiline_textbbox") - max_width = 0 - lines = self._multiline_split(text) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing - for line in lines: - line_width, line_height = self.getsize( - line, direction, features, language, stroke_width - ) - max_width = max(max_width, line_width) - - return max_width, len(lines) * line_spacing - spacing - - def getoffset(self, text): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` instead. - - See :ref:`deprecations ` for more information. - - Returns the offset of given text. This is the gap between the - starting coordinate and the first marking. Note that this gap is - included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`. - - :param text: Text to measure. - - :return: A tuple of the x and y offset - """ - deprecate("getoffset", 10, "getbbox") - return self.font.getsize(text)[1] - def getmask( self, text, @@ -676,7 +482,6 @@ class FreeTypeFont: self, text, mode="", - fill=_UNSPECIFIED, direction=None, features=None, language=None, @@ -702,12 +507,6 @@ class FreeTypeFont: .. versionadded:: 1.1.5 - :param fill: Optional fill function. By default, an internal Pillow function - will be used. - - Deprecated. This parameter will be removed in Pillow 10 - (2023-07-01). - :param direction: Direction of the text. It can be 'rtl' (right to left), 'ltr' (left to right) or 'ttb' (top to bottom). Requires libraqm. @@ -760,31 +559,32 @@ class FreeTypeFont: :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - if fill is _UNSPECIFIED: - fill = Image.core.fill - else: - deprecate("fill", 10) - size, offset = self.font.getsize( - text, mode, direction, features, language, anchor - ) + _string_length_check(text) if start is None: start = (0, 0) - size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) - offset = offset[0] - stroke_width, offset[1] - stroke_width - Image._decompression_bomb_check(size) - im = fill("RGBA" if mode == "RGBA" else "L", size, 0) - self.font.render( + im = None + + def fill(mode, size): + nonlocal im + + im = Image.core.fill(mode, size) + return im + + size, offset = self.font.render( text, - im.id, + fill, mode, direction, features, language, stroke_width, + anchor, ink, start[0], start[1], + Image.MAX_IMAGE_PIXELS, ) + Image._decompression_bomb_check(size) return im, offset def font_variant( @@ -886,22 +686,6 @@ class TransposedFont: self.font = font self.orientation = orientation # any 'transpose' argument, or None - def getsize(self, text, *args, **kwargs): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. - - See :ref:`deprecations ` for more information. - """ - deprecate("getsize", 10, "getbbox or getlength") - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w, h = self.font.getsize(text) - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - return h, w - return w, h - def getmask(self, text, mode="", *args, **kwargs): im = self.font.getmask(text, mode, *args, **kwargs) if self.orientation is not None: @@ -922,6 +706,7 @@ class TransposedFont: if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): msg = "text length is undefined for text rotated by 90 or 270 degrees" raise ValueError(msg) + _string_length_check(text) return self.font.getlength(text, *args, **kwargs) @@ -1018,7 +803,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): if windir: dirs.append(os.path.join(windir, "fonts")) elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS", "") + lindirs = os.environ.get("XDG_DATA_DIRS") if not lindirs: # According to the freedesktop spec, XDG_DATA_DIRS should # default to /usr/share diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 982f77f20..43019f74a 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,6 +15,7 @@ # See the README file for information on usage and redistribution. # +import io import os import shutil import subprocess @@ -61,7 +62,17 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N left, top, right, bottom = bbox im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im - elif shutil.which("gnome-screenshot"): + try: + if not Image.core.HAVE_XCB: + msg = "Pillow was built without XCB support" + raise OSError(msg) + size, data = Image.core.grabscreen_x11(xdisplay) + except OSError: + if ( + xdisplay is None + and sys.platform not in ("darwin", "win32") + and shutil.which("gnome-screenshot") + ): fh, filepath = tempfile.mkstemp(".png") os.close(fh) subprocess.call(["gnome-screenshot", "-f", filepath]) @@ -73,27 +84,25 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N im.close() return im_cropped return im - # use xdisplay=None for default display on non-win32/macOS systems - if not Image.core.HAVE_XCB: - msg = "Pillow was built without XCB support" - raise OSError(msg) - size, data = Image.core.grabscreen_x11(xdisplay) - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im + else: + raise + else: + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im def grabclipboard(): if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".jpg") + fh, filepath = tempfile.mkstemp(".png") os.close(fh) commands = [ 'set theFile to (open for access POSIX file "' + filepath + '" with write permission)', "try", - " write (the clipboard as JPEG picture) to theFile", + " write (the clipboard as «class PNGf») to theFile", "end try", "close access theFile", ] @@ -120,8 +129,6 @@ def grabclipboard(): files = data[o:].decode("mbcs").split("\0") return files[: files.index("")] if isinstance(data, bytes): - import io - data = io.BytesIO(data) if fmt == "png": from . import PngImagePlugin @@ -133,17 +140,38 @@ def grabclipboard(): return BmpImagePlugin.DibImageFile(data) return None else: - if shutil.which("wl-paste"): + if os.getenv("WAYLAND_DISPLAY"): + session_type = "wayland" + elif os.getenv("DISPLAY"): + session_type = "x11" + else: # Session type check failed + session_type = None + + if shutil.which("wl-paste") and session_type in ("wayland", None): + output = subprocess.check_output(["wl-paste", "-l"]).decode() + mimetypes = output.splitlines() + if "image/png" in mimetypes: + mimetype = "image/png" + elif mimetypes: + mimetype = mimetypes[0] + else: + mimetype = None + args = ["wl-paste"] - elif shutil.which("xclip"): + if mimetype: + args.extend(["-t", mimetype]) + elif shutil.which("xclip") and session_type in ("x11", None): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) - fh, filepath = tempfile.mkstemp() - subprocess.call(args, stdout=fh) - os.close(fh) - im = Image.open(filepath) + + p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + err = p.stderr + if err: + msg = f"{args[0]} error: {err.strip().decode()}" + raise ChildProcessError(msg) + data = io.BytesIO(p.stdout) + im = Image.open(data) im.load() - os.unlink(filepath) return im diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index ac7d36b69..eb6bbe6c6 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -247,7 +247,7 @@ def eval(expression, _dict={}, **kw): def scan(code): for const in code.co_consts: - if type(const) == type(compiled_code): + if type(const) is type(compiled_code): scan(const) for name in code.co_names: diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 0973536c9..a0b335142 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -58,10 +58,9 @@ def getmode(mode): "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), # extra experimental modes "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), endian + "u2"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), endian + "u2"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), endian + "u3"), - "BGR;32": ("RGB", "L", ("B", "G", "R"), endian + "u4"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), "LA": ("L", "L", ("L", "A"), "|u1"), "La": ("L", "L", ("L", "a"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"), diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 301c593c7..17702778c 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,7 +21,7 @@ import functools import operator import re -from . import Image, ImagePalette +from . import ExifTags, Image, ImagePalette # # helpers @@ -576,19 +576,20 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image): +def exif_transpose(image, *, in_place=False): """ - If an image has an EXIF Orientation tag, other than 1, return a new image - that is transposed accordingly. The new image will have the orientation - data removed. - - Otherwise, return a copy of the image. + If an image has an EXIF Orientation tag, other than 1, transpose the image + accordingly, and remove the orientation data. :param image: The image to transpose. - :return: An image. + :param in_place: Boolean. Keyword-only argument. + If ``True``, the original image is modified in-place, and ``None`` is returned. + If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned + with the transposition applied. If there is no transposition, a copy of the + image will be returned. """ - exif = image.getexif() - orientation = exif.get(0x0112) + image_exif = image.getexif() + orientation = image_exif.get(ExifTags.Base.Orientation) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -600,22 +601,28 @@ def exif_transpose(image): }.get(orientation) if method is not None: transposed_image = image.transpose(method) - transposed_exif = transposed_image.getexif() - if 0x0112 in transposed_exif: - del transposed_exif[0x0112] - if "exif" in transposed_image.info: - transposed_image.info["exif"] = transposed_exif.tobytes() - elif "Raw profile type exif" in transposed_image.info: - transposed_image.info[ - "Raw profile type exif" - ] = transposed_exif.tobytes().hex() - elif "XML:com.adobe.xmp" in transposed_image.info: + if in_place: + image.im = transposed_image.im + image.pyaccess = None + image._size = transposed_image._size + exif_image = image if in_place else transposed_image + + exif = exif_image.getexif() + if ExifTags.Base.Orientation in exif: + del exif[ExifTags.Base.Orientation] + if "exif" in exif_image.info: + exif_image.info["exif"] = exif.tobytes() + elif "Raw profile type exif" in exif_image.info: + exif_image.info["Raw profile type exif"] = exif.tobytes().hex() + elif "XML:com.adobe.xmp" in exif_image.info: for pattern in ( r'tiff:Orientation="([0-9])"', r"([0-9])", ): - transposed_image.info["XML:com.adobe.xmp"] = re.sub( - pattern, "", transposed_image.info["XML:com.adobe.xmp"] + exif_image.info["XML:com.adobe.xmp"] = re.sub( + pattern, "", exif_image.info["XML:com.adobe.xmp"] ) - return transposed_image - return image.copy() + if not in_place: + return transposed_image + elif not in_place: + return image.copy() diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index e455c0459..f0c094708 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -19,7 +19,6 @@ import array from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile -from ._deprecate import deprecate class ImagePalette: @@ -34,16 +33,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None, size=0): + def __init__(self, mode="RGB", palette=None): self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() self.dirty = None - if size != 0: - deprecate("The size parameter", 10, None) - if size != len(self.palette): - msg = "wrong palette size" - raise ValueError(msg) @property def palette(self): diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index ad607a97b..9b7245454 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -20,14 +20,11 @@ import sys from io import BytesIO from . import Image -from ._deprecate import deprecate from ._util import is_path qt_versions = [ ["6", "PyQt6"], ["side6", "PySide6"], - ["5", "PyQt5"], - ["side2", "PySide2"], ] # If a version has already been imported, attempt it first @@ -40,16 +37,6 @@ for qt_version, qt_module in qt_versions: elif qt_module == "PySide6": from PySide6.QtCore import QBuffer, QIODevice from PySide6.QtGui import QImage, QPixmap, qRgba - elif qt_module == "PyQt5": - from PyQt5.QtCore import QBuffer, QIODevice - from PyQt5.QtGui import QImage, QPixmap, qRgba - - deprecate("Support for PyQt5", 10, "PyQt6 or PySide6") - elif qt_module == "PySide2": - from PySide2.QtCore import QBuffer, QIODevice - from PySide2.QtGui import QImage, QPixmap, qRgba - - deprecate("Support for PySide2", 10, "PyQt6 or PySide6") except (ImportError, RuntimeError): continue qt_is_installed = True diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index f0e73fb90..8b1c3f8bb 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -17,9 +17,7 @@ import subprocess import sys from shlex import quote -from PIL import Image - -from ._deprecate import deprecate +from . import Image _viewers = [] @@ -111,21 +109,10 @@ class Viewer: """Display the given image.""" return self.show_file(self.save_image(image), **options) - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) os.system(self.get_command(path, **options)) # nosec return 1 @@ -164,21 +151,10 @@ class MacViewer(Viewer): command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" return command - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.call(["open", "-a", "Preview.app", path]) executable = sys.executable or shutil.which("python3") if executable: @@ -215,21 +191,10 @@ class XDGViewer(UnixViewer): command = executable = "xdg-open" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["xdg-open", path]) return 1 @@ -246,20 +211,10 @@ class DisplayViewer(UnixViewer): command += f" -title {quote(title)}" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) args = ["display"] title = options.get("title") if title: @@ -278,20 +233,10 @@ class GmDisplayViewer(UnixViewer): command = "gm display" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["gm", "display", path]) return 1 @@ -304,20 +249,10 @@ class EogViewer(UnixViewer): command = "eog -n" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["eog", "-n", path]) return 1 @@ -336,20 +271,10 @@ class XVViewer(UnixViewer): command += f" -name {quote(title)}" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) args = ["xv"] title = options.get("title") if title: diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index ef569ed2e..bf98eb2c8 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -29,7 +29,6 @@ import tkinter from io import BytesIO from . import Image -from ._deprecate import deprecate # -------------------------------------------------------------------- # Check for Tkinter interface hooks @@ -162,7 +161,7 @@ class PhotoImage: """ return self.__size[1] - def paste(self, im, box=None): + def paste(self, im): """ Paste a PIL image into the photo image. Note that this can be very slow if the photo image is displayed. @@ -170,13 +169,7 @@ class PhotoImage: :param im: A PIL image. The size must match the target region. If the mode does not match, the image is converted to the mode of the bitmap image. - :param box: Deprecated. This parameter will be removed in Pillow 10 - (2023-07-01). """ - - if box is not None: - deprecate("The box parameter", 10, None) - # convert to blittable im.load() image = im.im diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index ac267457b..d409fcd59 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -89,7 +89,7 @@ class ImtImageFile(ImageFile.ImageFile): ysize = int(v) self._size = xsize, ysize elif k == b"pixel" and v == b"n8": - self.mode = "L" + self._mode = "L" # diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 4c47b55c1..6ce4975c0 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -109,11 +109,11 @@ class IptcImageFile(ImageFile.ImageFile): else: id = 0 if layers == 1 and not component: - self.mode = "L" + self._mode = "L" elif layers == 3 and component: - self.mode = "RGB"[id] + self._mode = "RGB"[id] elif layers == 4 and component: - self.mode = "CMYK"[id] + self._mode = "CMYK"[id] # size self._size = self.getint((3, 20)), self.getint((3, 30)) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 7457874c1..963d6c1a3 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -17,7 +17,7 @@ import io import os import struct -from . import Image, ImageFile +from . import Image, ImageFile, _binary class BoxReader: @@ -99,7 +99,7 @@ def _parse_codestream(fp): count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" hdr = fp.read(2) - lsiz = struct.unpack(">H", hdr)[0] + lsiz = _binary.i16be(hdr) siz = hdr + fp.read(lsiz - 2) lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( ">HHIIIIIIIIH", siz @@ -208,16 +208,18 @@ class Jpeg2KImageFile(ImageFile.ImageFile): sig = self.fp.read(4) if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" - self._size, self.mode = _parse_codestream(self.fp) + self._size, self._mode = _parse_codestream(self.fp) else: sig = sig + self.fp.read(8) if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": self.codec = "jp2" header = _parse_jp2_header(self.fp) - self._size, self.mode, self.custom_mimetype, dpi = header + self._size, self._mode, self.custom_mimetype, dpi = header if dpi is not None: self.info["dpi"] = dpi + if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + self._parse_comment() else: msg = "not a JPEG 2000 file" raise SyntaxError(msg) @@ -254,6 +256,28 @@ class Jpeg2KImageFile(ImageFile.ImageFile): ) ] + def _parse_comment(self): + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + self.fp.seek(length - 2, os.SEEK_CUR) + + while True: + marker = self.fp.read(2) + if not marker: + break + typ = marker[1] + if typ in (0x90, 0xD9): + # Start of tile or end of codestream + break + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + if typ == 0x64: + # Comment + self.info["comment"] = self.fp.read(length - 2)[2:] + break + else: + self.fp.seek(length - 2, os.SEEK_CUR) + @property def reduce(self): # https://github.com/python-pillow/Pillow/issues/4343 found that the @@ -327,8 +351,12 @@ def _save(im, fp, filename): cinema_mode = info.get("cinema_mode", "no") mct = info.get("mct", 0) signed = info.get("signed", False) - fd = -1 + comment = info.get("comment") + if isinstance(comment, str): + comment = comment.encode() + plt = info.get("plt", False) + fd = -1 if hasattr(fp, "fileno"): try: fd = fp.fileno() @@ -350,6 +378,8 @@ def _save(im, fp, filename): mct, signed, fd, + comment, + plt, ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 71ae84c04..74130854f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -46,7 +46,6 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 -from ._deprecate import deprecate from .JpegPresets import presets # @@ -209,11 +208,11 @@ def SOF(self, marker): self.layers = s[5] if self.layers == 1: - self.mode = "L" + self._mode = "L" elif self.layers == 3: - self.mode = "RGB" + self._mode = "RGB" elif self.layers == 4: - self.mode = "CMYK" + self._mode = "CMYK" else: msg = f"cannot handle {self.layers}-layer images" raise SyntaxError(msg) @@ -427,7 +426,7 @@ class JpegImageFile(ImageFile.ImageFile): original_size = self.size if a[0] == "RGB" and mode in ["L", "YCbCr"]: - self.mode = mode + self._mode = mode a = mode, "" if size: @@ -458,6 +457,11 @@ class JpegImageFile(ImageFile.ImageFile): if os.path.exists(self.filename): subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: + try: + os.unlink(path) + except OSError: + pass + msg = "Invalid Filename" raise ValueError(msg) @@ -471,7 +475,7 @@ class JpegImageFile(ImageFile.ImageFile): except OSError: pass - self.mode = self.im.mode + self._mode = self.im.mode self._size = self.im.size self.tile = [] @@ -612,11 +616,6 @@ samplings = { # fmt: on -def convert_dict_qtables(qtables): - deprecate("convert_dict_qtables", 10, action="Conversion is no longer needed") - return qtables - - def get_sampling(im): # There's no subsampling when images have only 1 layer # (grayscale images) or when they are CMYK (4 layers), @@ -798,10 +797,14 @@ def _save(im, fp, filename): bufsize = 2 * im.size[0] * im.size[1] else: bufsize = im.size[0] * im.size[1] - - # The EXIF info needs to be written as one block, + APP1, + one spare byte. - # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(ImageFile.MAXBLOCK, bufsize, len(exif) + 5, len(extra) + 1) + if exif: + bufsize += len(exif) + 5 + if extra: + bufsize += len(extra) + 1 + else: + # The EXIF info needs to be written as one block, + APP1, + one spare byte. + # Ensure that our buffer is big enough. Same with the icc_profile block. + bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 17c008b9a..bb79e71de 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -58,7 +58,7 @@ class McIdasImageFile(ImageFile.ImageFile): msg = "unsupported McIdas format" raise SyntaxError(msg) - self.mode = mode + self._mode = mode self._size = w[10], w[9] offset = w[34] + w[15] diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 8dd9f2909..801318930 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -66,9 +66,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 - if len(self.images) > 1: - self._category = Image.CONTAINER - self.seek(0) def seek(self, frame): @@ -89,6 +86,14 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def tell(self): return self.frame + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index d96d3a11c..bfa88fe99 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -68,7 +68,7 @@ class MpegImageFile(ImageFile.ImageFile): msg = "not an MPEG file" raise SyntaxError(msg) - self.mode = "RGB" + self._mode = "RGB" self._size = s.read(12), s.read(12) diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index c6567b2ae..3f3609f1c 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -62,7 +62,7 @@ class MspImageFile(ImageFile.ImageFile): msg = "bad MSP checksum" raise SyntaxError(msg) - self.mode = "1" + self._mode = "1" self._size = i16(s, 4), i16(s, 6) if s[:4] == b"DanM": diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index e390f3fe5..c7cbca8c5 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -43,7 +43,7 @@ class PcdImageFile(ImageFile.ImageFile): elif orientation == 3: self.tile_post_rotate = -90 - self.mode = "RGB" + self._mode = "RGB" self._size = 768, 512 # FIXME: not correct for rotated images! self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index d5f510f03..8db5822fe 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,9 +86,22 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - x, y, l, r, w, a, d, f = metrics[ix] - glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix] - self.glyph[ch] = glyph + ( + xsize, + ysize, + left, + right, + width, + ascent, + descent, + attributes, + ) = metrics[ix] + self.glyph[ch] = ( + (width, 0), + (left, descent - ysize, xsize + left, descent), + (0, 0, xsize, ysize), + bitmaps[ix], + ) def _getformat(self, tag): format, size, offset = self.toc[tag] @@ -206,9 +219,11 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y, l, r, w, a, d, f = metrics[i] - b, e = offsets[i], offsets[i + 1] - bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) + xsize, ysize = metrics[i][:2] + b, e = offsets[i : i + 2] + bitmaps.append( + Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) + ) return bitmaps diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index f42c2456b..854d9e83e 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -108,7 +108,7 @@ class PcxImageFile(ImageFile.ImageFile): msg = "unknown PCX mode" raise OSError(msg) - self.mode = mode + self._mode = mode self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] # Don't trust the passed in stride. diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 4fa1998ba..09fc0c7e6 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -46,6 +46,132 @@ def _save_all(im, fp, filename): # (Internal) Image save plugin for the PDF format. +def _write_image(im, filename, existing_pdf, image_refs): + # FIXME: Should replace ASCIIHexDecode with RunLengthDecode + # (packbits) or LZWDecode (tiff/lzw compression). Note that + # PDF 1.2 also supports Flatedecode (zip compression). + + params = None + decode = None + + # + # Get image characteristics + + width, height = im.size + + dict_obj = {"BitsPerComponent": 8} + if im.mode == "1": + if features.check("libtiff"): + filter = "CCITTFaxDecode" + dict_obj["BitsPerComponent"] = 1 + params = PdfParser.PdfArray( + [ + PdfParser.PdfDict( + { + "K": -1, + "BlackIs1": True, + "Columns": width, + "Rows": height, + } + ) + ] + ) + else: + filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "L": + filter = "DCTDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "LA": + filter = "JPXDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + procset = "ImageB" # grayscale + dict_obj["SMaskInData"] = 1 + elif im.mode == "P": + filter = "ASCIIHexDecode" + palette = im.getpalette() + dict_obj["ColorSpace"] = [ + PdfParser.PdfName("Indexed"), + PdfParser.PdfName("DeviceRGB"), + 255, + PdfParser.PdfBinary(palette), + ] + procset = "ImageI" # indexed color + + if "transparency" in im.info: + smask = im.convert("LA").getchannel("A") + smask.encoderinfo = {} + + image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] + dict_obj["SMask"] = image_ref + elif im.mode == "RGB": + filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") + procset = "ImageC" # color images + elif im.mode == "RGBA": + filter = "JPXDecode" + procset = "ImageC" # color images + dict_obj["SMaskInData"] = 1 + elif im.mode == "CMYK": + filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") + procset = "ImageC" # color images + decode = [1, 0, 1, 0, 1, 0, 1, 0] + else: + msg = f"cannot save mode {im.mode}" + raise ValueError(msg) + + # + # image + + op = io.BytesIO() + + if filter == "ASCIIHexDecode": + ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + elif filter == "CCITTFaxDecode": + im.save( + op, + "TIFF", + compression="group4", + # use a single strip + strip_size=math.ceil(width / 8) * height, + ) + elif filter == "DCTDecode": + Image.SAVE["JPEG"](im, op, filename) + elif filter == "JPXDecode": + del dict_obj["BitsPerComponent"] + Image.SAVE["JPEG2000"](im, op, filename) + else: + msg = f"unsupported PDF filter ({filter})" + raise ValueError(msg) + + stream = op.getvalue() + if filter == "CCITTFaxDecode": + stream = stream[8:] + filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + else: + filter = PdfParser.PdfName(filter) + + image_ref = image_refs.pop(0) + existing_pdf.write_obj( + image_ref, + stream=stream, + Type=PdfParser.PdfName("XObject"), + Subtype=PdfParser.PdfName("Image"), + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, + Filter=filter, + Decode=decode, + DecodeParms=params, + **dict_obj, + ) + + return image_ref, procset + + def _save(im, fp, filename, save_all=False): is_appending = im.encoderinfo.get("append", False) if is_appending: @@ -109,6 +235,9 @@ def _save(im, fp, filename, save_all=False): number_of_pages += im_number_of_pages for i in range(im_number_of_pages): image_refs.append(existing_pdf.next_object_id(0)) + if im.mode == "P" and "transparency" in im.info: + image_refs.append(existing_pdf.next_object_id(0)) + page_refs.append(existing_pdf.next_object_id(0)) contents_refs.append(existing_pdf.next_object_id(0)) existing_pdf.pages.append(page_refs[-1]) @@ -121,112 +250,7 @@ def _save(im, fp, filename, save_all=False): for im_sequence in ims: im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] for im in im_pages: - # FIXME: Should replace ASCIIHexDecode with RunLengthDecode - # (packbits) or LZWDecode (tiff/lzw compression). Note that - # PDF 1.2 also supports Flatedecode (zip compression). - - bits = 8 - params = None - decode = None - - # - # Get image characteristics - - width, height = im.size - - if im.mode == "1": - if features.check("libtiff"): - filter = "CCITTFaxDecode" - bits = 1 - params = PdfParser.PdfArray( - [ - PdfParser.PdfDict( - { - "K": -1, - "BlackIs1": True, - "Columns": width, - "Rows": height, - } - ) - ] - ) - else: - filter = "DCTDecode" - colorspace = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "L": - filter = "DCTDecode" - # params = f"<< /Predictor 15 /Columns {width-2} >>" - colorspace = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "P": - filter = "ASCIIHexDecode" - palette = im.getpalette() - colorspace = [ - PdfParser.PdfName("Indexed"), - PdfParser.PdfName("DeviceRGB"), - 255, - PdfParser.PdfBinary(palette), - ] - procset = "ImageI" # indexed color - elif im.mode == "RGB": - filter = "DCTDecode" - colorspace = PdfParser.PdfName("DeviceRGB") - procset = "ImageC" # color images - elif im.mode == "CMYK": - filter = "DCTDecode" - colorspace = PdfParser.PdfName("DeviceCMYK") - procset = "ImageC" # color images - decode = [1, 0, 1, 0, 1, 0, 1, 0] - else: - msg = f"cannot save mode {im.mode}" - raise ValueError(msg) - - # - # image - - op = io.BytesIO() - - if filter == "ASCIIHexDecode": - ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) - elif filter == "CCITTFaxDecode": - im.save( - op, - "TIFF", - compression="group4", - # use a single strip - strip_size=math.ceil(im.width / 8) * im.height, - ) - elif filter == "DCTDecode": - Image.SAVE["JPEG"](im, op, filename) - elif filter == "FlateDecode": - ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) - elif filter == "RunLengthDecode": - ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) - else: - msg = f"unsupported PDF filter ({filter})" - raise ValueError(msg) - - stream = op.getvalue() - if filter == "CCITTFaxDecode": - stream = stream[8:] - filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) - else: - filter = PdfParser.PdfName(filter) - - existing_pdf.write_obj( - image_refs[page_number], - stream=stream, - Type=PdfParser.PdfName("XObject"), - Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / x_resolution, - Height=height, # * 72.0 / y_resolution, - Filter=filter, - BitsPerComponent=bits, - Decode=decode, - DecodeParms=params, - ColorSpace=colorspace, - ) + image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) # # page @@ -235,13 +259,13 @@ def _save(im, fp, filename, save_all=False): page_refs[page_number], Resources=PdfParser.PdfDict( ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], - XObject=PdfParser.PdfDict(image=image_refs[page_number]), + XObject=PdfParser.PdfDict(image=image_ref), ), MediaBox=[ 0, 0, - width * 72.0 / x_resolution, - height * 72.0 / y_resolution, + im.width * 72.0 / x_resolution, + im.height * 72.0 / y_resolution, ], Contents=contents_refs[page_number], ) @@ -250,8 +274,8 @@ def _save(im, fp, filename, save_all=False): # page contents page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - width * 72.0 / x_resolution, - height * 72.0 / y_resolution, + im.width * 72.0 / x_resolution, + im.height * 72.0 / y_resolution, ) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 1b3cb52a2..dc1012f54 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -957,14 +957,11 @@ class PdfParser: check_format_condition(m, "xref entry not found") offset = m.end() is_free = m.group(3) == b"f" - generation = int(m.group(2)) if not is_free: + generation = int(m.group(2)) new_entry = (int(m.group(1)), generation) - check_format_condition( - i not in self.xref_table or self.xref_table[i] == new_entry, - "xref entry duplicated (and not identical)", - ) - self.xref_table[i] = new_entry + if i not in self.xref_table: + self.xref_table[i] = new_entry return offset def read_indirect(self, ref, max_nesting=-1): diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 7eb82228a..850272311 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -54,7 +54,7 @@ class PixarImageFile(ImageFile.ImageFile): mode = i16(s, 424), i16(s, 426) if mode == (14, 2): - self.mode = "RGB" + self._mode = "RGB" # FIXME: to be continued... # create tile descriptor (assuming "dumped") diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 9078957dc..2ed182d32 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -45,7 +45,6 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 -from ._deprecate import deprecate logger = logging.getLogger(__name__) @@ -131,17 +130,6 @@ class Blend(IntEnum): """ -def __getattr__(name): - for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - def _safe_zlib_decompress(s): dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) @@ -749,7 +737,7 @@ class PngImageFile(ImageFile.ImageFile): # difficult to break if things go wrong in the decoder... # (believe me, I've tried ;-) - self.mode = self.png.im_mode + self._mode = self.png.im_mode self._size = self.png.im_size self.info = self.png.im_info self._text = None @@ -1003,9 +991,13 @@ class PngImageFile(ImageFile.ImageFile): else: if self._prev_im and self.blend_op == Blend.OP_OVER: updated = self._crop(self.im, self.dispose_extent) - self._prev_im.paste( - updated, self.dispose_extent, updated.convert("RGBA") - ) + if self.im.mode == "RGB" and "transparency" in self.info: + mask = updated.convert_transparent( + "RGBA", self.info["transparency"] + ) + else: + mask = updated.convert("RGBA") + self._prev_im.paste(updated, self.dispose_extent, mask) self.im = self._prev_im if self.pyaccess: self.pyaccess = None @@ -1146,19 +1138,22 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) else: base_im = previous["im"] delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_im.convert("RGB") + im_frame.convert("RGBA"), base_im.convert("RGBA") ) - bbox = delta.getbbox() + bbox = delta.getbbox(alpha_only=False) if ( not bbox and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") ): - if isinstance(duration, (list, tuple)): - previous["encoderinfo"]["duration"] += encoderinfo["duration"] + previous["encoderinfo"]["duration"] += encoderinfo.get( + "duration", duration + ) continue else: bbox = None + if "duration" not in encoderinfo: + encoderinfo["duration"] = duration im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) # animation control @@ -1183,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data["encoderinfo"] - frame_duration = int(round(encoderinfo.get("duration", duration))) + frame_duration = int(round(encoderinfo["duration"])) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 5aa418044..e480ab055 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -116,18 +116,18 @@ class PpmImageFile(ImageFile.ImageFile): elif ix == 1: # token is the y size ysize = token if mode == "1": - self.mode = "1" + self._mode = "1" rawmode = "1;I" break else: - self.mode = rawmode = mode + self._mode = rawmode = mode elif ix == 2: # token is maxval maxval = token if not 0 < maxval < 65536: msg = "maxval must be greater than 0 and less than 65536" raise ValueError(msg) if maxval > 255 and mode == "L": - self.mode = "I" + self._mode = "I" if decoder_name != "ppm_plain": # If maxval matches a bit depth, use the raw decoder directly @@ -237,6 +237,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): if half_token: block = half_token + block # stitch half_token to new block + half_token = False tokens = block.split() diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 5a5d60d56..2f019bb8c 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -79,7 +79,7 @@ class PsdImageFile(ImageFile.ImageFile): mode = "RGBA" channels = 4 - self.mode = mode + self._mode = mode self._size = i32(s, 18), i32(s, 14) # @@ -146,7 +146,7 @@ class PsdImageFile(ImageFile.ImageFile): # seek to given layer (1..max) try: name, mode, bbox, tile = self.layers[layer - 1] - self.mode = mode + self._mode = mode self.tile = tile self.frame = layer self.fp = self._fp diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index e9cb34ced..99b46a4a6 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -22,6 +22,8 @@ import logging import sys +from ._deprecate import deprecate + try: from cffi import FFI @@ -47,6 +49,7 @@ logger = logging.getLogger(__name__) class PyAccess: def __init__(self, img, readonly=False): + deprecate("PyAccess", 11) vals = dict(img.im.unsafe_ptrs) self.readonly = readonly self.image8 = ffi.cast("unsigned char **", vals["image8"]) @@ -320,6 +323,7 @@ mode_map = { "1": _PyAccess8, "L": _PyAccess8, "P": _PyAccess8, + "I;16N": _PyAccessI16_N, "LA": _PyAccess32_2, "La": _PyAccess32_2, "PA": _PyAccess32_2, diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py new file mode 100644 index 000000000..5c3407503 --- /dev/null +++ b/src/PIL/QoiImagePlugin.py @@ -0,0 +1,105 @@ +# +# The Python Imaging Library. +# +# QOI support for PIL +# +# See the README file for information on usage and redistribution. +# + +import os + +from . import Image, ImageFile +from ._binary import i32be as i32 +from ._binary import o8 + + +def _accept(prefix): + return prefix[:4] == b"qoif" + + +class QoiImageFile(ImageFile.ImageFile): + format = "QOI" + format_description = "Quite OK Image" + + def _open(self): + if not _accept(self.fp.read(4)): + msg = "not a QOI file" + raise SyntaxError(msg) + + self._size = tuple(i32(self.fp.read(4)) for i in range(2)) + + channels = self.fp.read(1)[0] + self._mode = "RGB" if channels == 3 else "RGBA" + + self.fp.seek(1, os.SEEK_CUR) # colorspace + self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)] + + +class QoiDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def _add_to_previous_pixels(self, value): + self._previous_pixel = value + + r, g, b, a = value + hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + self._previously_seen_pixels[hash_value] = value + + def decode(self, buffer): + self._previously_seen_pixels = {} + self._previous_pixel = None + self._add_to_previous_pixels(b"".join(o8(i) for i in (0, 0, 0, 255))) + + data = bytearray() + bands = Image.getmodebands(self.mode) + while len(data) < self.state.xsize * self.state.ysize * bands: + byte = self.fd.read(1)[0] + if byte == 0b11111110: # QOI_OP_RGB + value = self.fd.read(3) + o8(255) + elif byte == 0b11111111: # QOI_OP_RGBA + value = self.fd.read(4) + else: + op = byte >> 6 + if op == 0: # QOI_OP_INDEX + op_index = byte & 0b00111111 + value = self._previously_seen_pixels.get(op_index, (0, 0, 0, 0)) + elif op == 1: # QOI_OP_DIFF + value = ( + (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) + % 256, + (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2) + % 256, + (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256, + ) + value += (self._previous_pixel[3],) + elif op == 2: # QOI_OP_LUMA + second_byte = self.fd.read(1)[0] + diff_green = (byte & 0b00111111) - 32 + diff_red = ((second_byte & 0b11110000) >> 4) - 8 + diff_blue = (second_byte & 0b00001111) - 8 + + value = tuple( + (self._previous_pixel[i] + diff_green + diff) % 256 + for i, diff in enumerate((diff_red, 0, diff_blue)) + ) + value += (self._previous_pixel[3],) + elif op == 3: # QOI_OP_RUN + run_length = (byte & 0b00111111) + 1 + value = self._previous_pixel + if bands == 3: + value = value[:3] + data += value * run_length + continue + value = b"".join(o8(i) for i in value) + self._add_to_previous_pixels(value) + + if bands == 3: + value = value[:3] + data += value + self.set_as_raw(bytes(data)) + return -1, 0 + + +Image.register_open(QoiImageFile.format, QoiImageFile, _accept) +Image.register_decoder("qoi", QoiDecoder) +Image.register_extension(QoiImageFile.format, ".qoi") diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 3662ffd15..acb9ce5a3 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -94,7 +94,7 @@ class SgiImageFile(ImageFile.ImageFile): raise ValueError(msg) self._size = xsize, ysize - self.mode = rawmode.split(";")[0] + self._mode = rawmode.split(";")[0] if self.mode == "RGB": self.custom_mimetype = "image/rgb" diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index eac27e679..408b982b5 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -36,7 +36,7 @@ import os import struct import sys -from PIL import Image, ImageFile +from . import Image, ImageFile def isInt(f): @@ -149,7 +149,7 @@ class SpiderImageFile(ImageFile.ImageFile): self.rawmode = "F;32BF" else: self.rawmode = "F;32F" - self.mode = "F" + self._mode = "F" self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] self._fp = self.fp # FIXME: hack @@ -191,7 +191,7 @@ class SpiderImageFile(ImageFile.ImageFile): # returns a ImageTk.PhotoImage object, after rescaling to 0..255 def tkPhotoImage(self): - from PIL import ImageTk + from . import ImageTk return ImageTk.PhotoImage(self.convert2byte(), palette=256) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 6712583d7..6a8d5d86b 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -66,21 +66,21 @@ class SunImageFile(ImageFile.ImageFile): palette_length = i32(s, 28) if depth == 1: - self.mode, rawmode = "1", "1;I" + self._mode, rawmode = "1", "1;I" elif depth == 4: - self.mode, rawmode = "L", "L;4" + self._mode, rawmode = "L", "L;4" elif depth == 8: - self.mode = rawmode = "L" + self._mode = rawmode = "L" elif depth == 24: if file_type == 3: - self.mode, rawmode = "RGB", "RGB" + self._mode, rawmode = "RGB", "RGB" else: - self.mode, rawmode = "RGB", "BGR" + self._mode, rawmode = "RGB", "BGR" elif depth == 32: if file_type == 3: - self.mode, rawmode = "RGB", "RGBX" + self._mode, rawmode = "RGB", "RGBX" else: - self.mode, rawmode = "RGB", "BGRX" + self._mode, rawmode = "RGB", "BGRX" else: msg = "Unsupported Mode/Bit Depth" raise SyntaxError(msg) @@ -97,7 +97,7 @@ class SunImageFile(ImageFile.ImageFile): offset = offset + palette_length self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) if self.mode == "L": - self.mode = "P" + self._mode = "P" rawmode = rawmode.replace("L", "P") # 16 bit boundaries on stride diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 67dfc3d3c..f24ee4f5c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -76,17 +76,17 @@ class TgaImageFile(ImageFile.ImageFile): # image mode if imagetype in (3, 11): - self.mode = "L" + self._mode = "L" if depth == 1: - self.mode = "1" # ??? + self._mode = "1" # ??? elif depth == 16: - self.mode = "LA" + self._mode = "LA" elif imagetype in (1, 9): - self.mode = "P" + self._mode = "P" elif imagetype in (2, 10): - self.mode = "RGB" + self._mode = "RGB" if depth == 32: - self.mode = "RGBA" + self._mode = "RGBA" else: msg = "unknown TGA mode" raise SyntaxError(msg) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index aaaf8fcb9..99c23ad4b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -49,7 +49,7 @@ from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational -from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags +from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 @@ -170,6 +170,8 @@ OPEN_INFO = { (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (II, 1, (1,), 1, (8,), ()): ("L", "L"), (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (2,), 1, (8,), ()): ("L", "L"), + (MM, 1, (2,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), @@ -425,6 +427,9 @@ class IFDRational(Rational): __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") + # Python >= 3.11 + if hasattr(Fraction, "__int__"): + __int__ = _delegate("__int__") class ImageFileDirectory_v2(MutableMapping): @@ -1180,7 +1185,7 @@ class TiffImageFile(ImageFile.ImageFile): :returns: Photoshop "Image Resource Blocks" in a dictionary. """ blocks = {} - val = self.tag_v2.get(0x8649) + val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: while val[:4] == b"8BIM": id = i16(val[4:6]) @@ -1248,9 +1253,8 @@ class TiffImageFile(ImageFile.ImageFile): # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading # into a string in python. - # libtiff closes the file descriptor, so pass in a dup. try: - fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) + fp = hasattr(self.fp, "fileno") and self.fp.fileno() # flush the file descriptor, prevents error on pypy 2.4+ # should also eliminate the need for fp.tell # in _seek @@ -1300,18 +1304,11 @@ class TiffImageFile(ImageFile.ImageFile): # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) - if fp: - try: - os.close(fp) - except OSError: - pass - self.tile = [] self.readonly = 0 self.load_end() - # libtiff closed the fp in a, we need to close self.fp, if possible if close_self_fp: self.fp.close() self.fp = None # might be shared @@ -1412,7 +1409,7 @@ class TiffImageFile(ImageFile.ImageFile): ) logger.debug(f"format key: {key}") try: - self.mode, rawmode = OPEN_INFO[key] + self._mode, rawmode = OPEN_INFO[key] except KeyError as e: logger.debug("- unsupported format") msg = "unknown pixel mode" @@ -1464,7 +1461,7 @@ class TiffImageFile(ImageFile.ImageFile): # this should always work, since all the # fillorder==2 modes have a corresponding # fillorder=1 mode - self.mode, rawmode = OPEN_INFO[key] + self._mode, rawmode = OPEN_INFO[key] # libtiff always returns the bytes in native order. # we're expecting image byte order. So, if the rawmode # contains I;16, we need to convert from native to image @@ -1545,7 +1542,7 @@ class TiffImageFile(ImageFile.ImageFile): palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) - self._tile_orientation = self.tag_v2.get(0x0112) + self._tile_orientation = self.tag_v2.get(ExifTags.Base.Orientation) # @@ -1804,7 +1801,7 @@ def _save(im, fp, filename): # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - if not Image.core.libtiff_support_custom_tags: + if not getattr(Image.core, "libtiff_support_custom_tags", False): continue if tag in ifd.tagtype: @@ -1845,13 +1842,18 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16 * 1024) + errcode, data = e.encode(16 * 1024)[1:] if not _fp: - fp.write(d) - if s: + fp.write(data) + if errcode: break - if s < 0: - msg = f"encoder error {s} when writing image file" + if _fp: + try: + os.close(_fp) + except OSError: + pass + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) else: @@ -1884,6 +1886,10 @@ class AppendingTiffWriter: 8, # srational 4, # float 8, # double + 4, # ifd + 2, # unicode + 4, # complex + 8, # long8 ] # StripOffsets = 273 diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index ac048ba56..30b05e4e1 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -195,6 +195,7 @@ TAGS_V2 = { 34675: ("ICCProfile", UNDEFINED, 1), 34853: ("GPSInfoIFD", LONG, 1), 36864: ("ExifVersion", UNDEFINED, 1), + 37724: ("ImageSourceData", UNDEFINED, 1), 40965: ("InteroperabilityIFD", LONG, 1), 41730: ("CFAPattern", UNDEFINED, 1), # MPInfo diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index e4f47aa04..3d9f97f84 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -32,7 +32,7 @@ class WalImageFile(ImageFile.ImageFile): format_description = "Quake2 Texture" def _open(self): - self.mode = "P" + self._mode = "P" # read header fields header = self.fp.read(32 + 24 + 32 + 12) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index d060dd4b8..a6e1a2a00 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -43,7 +43,7 @@ class WebPImageFile(ImageFile.ImageFile): def _open(self): if not _webp.HAVE_WEBPANIM: # Legacy mode - data, width, height, self.mode, icc_profile, exif = _webp.WebPDecode( + data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( self.fp.read() ) if icc_profile: @@ -74,7 +74,10 @@ class WebPImageFile(ImageFile.ImageFile): self.info["background"] = (bg_r, bg_g, bg_b, bg_a) self.n_frames = frame_count self.is_animated = self.n_frames > 1 - self.mode = "RGB" if mode == "RGBX" else mode + ret = self._decoder.get_next() + if ret is not None: + self.info["duration"] = ret[1] + self._mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode self.tile = [] @@ -90,7 +93,7 @@ class WebPImageFile(ImageFile.ImageFile): self.info["xmp"] = xmp # Initialize seek state - self._reset(reset=False) + self._reset() def _getexif(self): if "exif" not in self.info: @@ -113,9 +116,8 @@ class WebPImageFile(ImageFile.ImageFile): # Set logical frame to requested position self.__logical_frame = frame - def _reset(self, reset=True): - if reset: - self._decoder.reset() + def _reset(self): + self._decoder.reset() self.__physical_frame = 0 self.__loaded = -1 self.__timestamp = 0 @@ -285,7 +287,7 @@ def _save_all(im, fp, filename): # Append the frame to the animation encoder enc.add( frame.tobytes("raw", rawmode), - timestamp, + round(timestamp), frame.size[0], frame.size[1], rawmode, @@ -305,7 +307,7 @@ def _save_all(im, fp, filename): im.seek(cur_idx) # Force encoder to flush frames - enc.add(None, timestamp, 0, 0, "", lossless, quality, 0) + enc.add(None, round(timestamp), 0, 0, "", lossless, quality, 0) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 0ecab56a8..3e5fb0151 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -42,7 +42,7 @@ if hasattr(Image.core, "drawwmf"): class WmfHandler: def open(self, im): - im.mode = "RGB" + im._mode = "RGB" self.bbox = im.info["wmf_bbox"] def load(self, im): @@ -139,7 +139,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): msg = "Unsupported file format" raise SyntaxError(msg) - self.mode = "RGB" + self._mode = "RGB" self._size = size loader = self._load() diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index aa4a01f4e..eda60c5c5 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -65,7 +65,7 @@ class XVThumbImageFile(ImageFile.ImageFile): # parse header line (already read) s = s.strip().split() - self.mode = "P" + self._mode = "P" self._size = int(s[0]), int(s[1]) self.palette = ImagePalette.raw("RGB", PALETTE) diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 3c12564c9..71cd57d74 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -60,7 +60,7 @@ class XbmImageFile(ImageFile.ImageFile): if m.group("hotspot"): self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) - self.mode = "1" + self._mode = "1" self._size = xsize, ysize self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 5d5bdc3ed..8491d3b7e 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -98,7 +98,7 @@ class XpmImageFile(ImageFile.ImageFile): msg = "cannot read this XPM file" raise ValueError(msg) - self.mode = "P" + self._mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index bb245de6d..4e56da1b4 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -32,7 +32,6 @@ _plugins = [ "DdsImagePlugin", "EpsImagePlugin", "FitsImagePlugin", - "FitsStubImagePlugin", "FliImagePlugin", "FpxImagePlugin", "FtexImagePlugin", @@ -60,6 +59,7 @@ _plugins = [ "PngImagePlugin", "PpmImagePlugin", "PsdImagePlugin", + "QoiImagePlugin", "SgiImagePlugin", "SpiderImagePlugin", "SunImagePlugin", diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index fa6e1d00c..2f2a3df13 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -45,12 +45,10 @@ def deprecate( elif when <= int(__version__.split(".")[0]): msg = f"{deprecated} {is_} deprecated and should be removed." raise RuntimeError(msg) - elif when == 10: - removed = "Pillow 10 (2023-07-01)" elif when == 11: removed = "Pillow 11 (2024-10-15)" else: - msg = f"Unknown removal version, update {__name__}?" + msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg) if replacement and action: diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 5cd7e9b1f..597c21b5e 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -4,8 +4,6 @@ import sys import tkinter from tkinter import _tkinter as tk -from ._deprecate import deprecate - try: if hasattr(sys, "pypy_find_executable"): TKINTER_LIB = tk.tklib_cffi.__file__ @@ -17,7 +15,3 @@ except AttributeError: TKINTER_LIB = None tk_version = str(tkinter.TkVersion) -if tk_version == "8.4": - deprecate( - "Support for Tk/Tcl 8.4", 10, action="Please upgrade to Tk/Tcl 8.5 or newer" - ) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 7baa9fb6c..cf5019e80 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.5.0.dev0" +__version__ = "10.1.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 6f9d99e76..f14e60cf5 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -24,7 +24,7 @@ def check_module(feature): :returns: ``True`` if available, ``False`` otherwise. :raises ValueError: If the module is not defined in this version of Pillow. """ - if not (feature in modules): + if feature not in modules: msg = f"Unknown module {feature}" raise ValueError(msg) @@ -33,7 +33,10 @@ def check_module(feature): try: __import__(module) return True - except ImportError: + except ModuleNotFoundError: + return False + except ImportError as ex: + warnings.warn(str(ex)) return False @@ -145,7 +148,10 @@ def check_feature(feature): try: imported_module = __import__(module, fromlist=["PIL"]) return getattr(imported_module, flag) - except ImportError: + except ModuleNotFoundError: + return None + except ImportError as ex: + warnings.warn(str(ex)) return None diff --git a/src/Tk/_tkmini.h b/src/Tk/_tkmini.h index 9852fc9d6..68247bc47 100644 --- a/src/Tk/_tkmini.h +++ b/src/Tk/_tkmini.h @@ -119,17 +119,7 @@ typedef struct Tk_PhotoImageBlock { } Tk_PhotoImageBlock; /* Typedefs derived from function signatures in Tk header */ -/* Tk_PhotoPutBlock for Tk <= 8.4 */ -typedef void (*Tk_PhotoPutBlock_84_t)( - Tk_PhotoHandle handle, - Tk_PhotoImageBlock *blockPtr, - int x, - int y, - int width, - int height, - int compRule); -/* Tk_PhotoPutBlock for Tk >= 8.5 */ -typedef int (*Tk_PhotoPutBlock_85_t)( +typedef int (*Tk_PhotoPutBlock_t)( Tcl_Interp *interp, Tk_PhotoHandle handle, Tk_PhotoImageBlock *blockPtr, @@ -138,8 +128,6 @@ typedef int (*Tk_PhotoPutBlock_85_t)( int width, int height, int compRule); -/* Tk_PhotoSetSize for Tk <= 8.4 */ -typedef void (*Tk_PhotoSetSize_84_t)(Tk_PhotoHandle handle, int width, int height); /* Tk_FindPhoto */ typedef Tk_PhotoHandle (*Tk_FindPhoto_t)(Tcl_Interp *interp, const char *imageName); /* Tk_PhotoGetImage */ diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index ad503baec..bd3cafe95 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -48,14 +48,11 @@ * Global vars for Tcl / Tk functions. We load these symbols from the tkinter * extension module or loaded Tcl / Tk libraries at run-time. */ -static int TK_LT_85 = 0; static Tcl_CreateCommand_t TCL_CREATE_COMMAND; static Tcl_AppendResult_t TCL_APPEND_RESULT; static Tk_FindPhoto_t TK_FIND_PHOTO; static Tk_PhotoGetImage_t TK_PHOTO_GET_IMAGE; -static Tk_PhotoPutBlock_84_t TK_PHOTO_PUT_BLOCK_84; -static Tk_PhotoSetSize_84_t TK_PHOTO_SET_SIZE_84; -static Tk_PhotoPutBlock_85_t TK_PHOTO_PUT_BLOCK_85; +static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; static Imaging ImagingFind(const char *name) { @@ -130,26 +127,15 @@ PyImagingPhotoPut( block.pitch = im->linesize; block.pixelPtr = (unsigned char *)im->block; - if (TK_LT_85) { /* Tk 8.4 */ - TK_PHOTO_PUT_BLOCK_84( - photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); - if (strcmp(im->mode, "RGBA") == 0) { - /* Tk workaround: we need apply ToggleComplexAlphaIfNeeded */ - /* (fixed in Tk 8.5a3) */ - TK_PHOTO_SET_SIZE_84(photo, block.width, block.height); - } - } else { - /* Tk >=8.5 */ - TK_PHOTO_PUT_BLOCK_85( - interp, - photo, - &block, - 0, - 0, - block.width, - block.height, - TK_PHOTO_COMPOSITE_SET); - } + TK_PHOTO_PUT_BLOCK( + interp, + photo, + &block, + 0, + 0, + block.width, + block.height, + TK_PHOTO_COMPOSITE_SET); return TCL_OK; } @@ -290,16 +276,7 @@ get_tk(HMODULE hMod) { if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(hMod, "Tk_FindPhoto")) == NULL) { return -1; }; - TK_LT_85 = GetProcAddress(hMod, "Tk_PhotoPutBlock_Panic") == NULL; - /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ - if (TK_LT_85) { - TK_PHOTO_PUT_BLOCK_84 = (Tk_PhotoPutBlock_84_t)func; - return ((TK_PHOTO_SET_SIZE_84 = - (Tk_PhotoSetSize_84_t)_dfunc(hMod, "Tk_PhotoSetSize")) == NULL) - ? -1 - : 1; - } - TK_PHOTO_PUT_BLOCK_85 = (Tk_PhotoPutBlock_85_t)func; + TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)func; return 1; } @@ -422,18 +399,9 @@ _func_loader(void *lib) { if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(lib, "Tk_FindPhoto")) == NULL) { return 1; } - /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ - TK_LT_85 = (dlsym(lib, "Tk_PhotoPutBlock_Panic") == NULL); - if (TK_LT_85) { - return ( - ((TK_PHOTO_PUT_BLOCK_84 = - (Tk_PhotoPutBlock_84_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL) || - ((TK_PHOTO_SET_SIZE_84 = - (Tk_PhotoSetSize_84_t)_dfunc(lib, "Tk_PhotoSetSize")) == NULL)); - } return ( - (TK_PHOTO_PUT_BLOCK_85 = - (Tk_PhotoPutBlock_85_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); + (TK_PHOTO_PUT_BLOCK = + (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); } int diff --git a/src/_imaging.c b/src/_imaging.c index cece2e93a..95da2772d 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -251,6 +251,10 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { static const char *must_be_sequence = "argument must be a sequence"; static const char *must_be_two_coordinates = "coordinate list must contain exactly 2 coordinates"; +static const char *incorrectly_ordered_x_coordinate = + "x1 must be greater than or equal to x0"; +static const char *incorrectly_ordered_y_coordinate = + "y1 must be greater than or equal to y0"; static const char *wrong_mode = "unrecognized image mode"; static const char *wrong_raw_mode = "unrecognized raw mode"; static const char *outside_image = "image index out of range"; @@ -487,7 +491,7 @@ getink(PyObject *color, Imaging im, char *ink) { int g = 0, b = 0, a = 0; double f = 0; /* Windows 64 bit longs are 32 bits, and 0xFFFFFFFF (white) is a - python long (not int) that raises an overflow error when trying + Python long (not int) that raises an overflow error when trying to return it into a 32 bit C long */ PY_LONG_LONG r = 0; @@ -498,7 +502,8 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - if (PyTuple_Check(color) && PyTuple_GET_SIZE(color) == 1) { + int tupleSize = PyTuple_Check(color) ? PyTuple_GET_SIZE(color) : -1; + if (tupleSize == 1) { color = PyTuple_GetItem(color, 0); } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || @@ -509,15 +514,13 @@ getink(PyObject *color, Imaging im, char *ink) { return NULL; } rIsInt = 1; - } else if (im->type == IMAGING_TYPE_UINT8) { - if (!PyTuple_Check(color)) { - PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); - return NULL; - } - } else { + } else if (im->bands == 1) { PyErr_SetString( PyExc_TypeError, "color must be int or single-element tuple"); return NULL; + } else if (tupleSize == -1) { + PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); + return NULL; } } @@ -527,7 +530,7 @@ getink(PyObject *color, Imaging im, char *ink) { if (im->bands == 1) { /* unsigned integer, single layer */ if (rIsInt != 1) { - if (PyTuple_GET_SIZE(color) != 1) { + if (tupleSize != 1) { PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); return NULL; } else if (!PyArg_ParseTuple(color, "L", &r)) { @@ -537,7 +540,6 @@ getink(PyObject *color, Imaging im, char *ink) { ink[0] = (char)CLIP8(r); ink[1] = ink[2] = ink[3] = 0; } else { - a = 255; if (rIsInt) { /* compatibility: ABGR */ a = (UINT8)(r >> 24); @@ -545,7 +547,7 @@ getink(PyObject *color, Imaging im, char *ink) { g = (UINT8)(r >> 8); r = (UINT8)r; } else { - int tupleSize = PyTuple_GET_SIZE(color); + a = 255; if (im->bands == 2) { if (tupleSize != 1 && tupleSize != 2) { PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements"); @@ -589,6 +591,41 @@ getink(PyObject *color, Imaging im, char *ink) { ink[1] = (UINT8)(r >> 8); ink[2] = ink[3] = 0; return ink; + } else { + if (rIsInt) { + b = (UINT8)(r >> 16); + g = (UINT8)(r >> 8); + r = (UINT8)r; + } else if (tupleSize != 3) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or three elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "Lii", &r, &g, &b)) { + return NULL; + } + if (!strcmp(im->mode, "BGR;15")) { + UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + + ((((UINT16)g) << 2) & 0x03e0) + + ((((UINT16)b) >> 3) & 0x001f); + + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;16")) { + UINT16 v = ((((UINT16)r) << 8) & 0xf800) + + ((((UINT16)g) << 3) & 0x07e0) + + ((((UINT16)b) >> 3) & 0x001f); + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;24")) { + ink[0] = (UINT8)b; + ink[1] = (UINT8)g; + ink[2] = (UINT8)r; + ink[3] = 0; + return ink; + } } } @@ -990,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) { static PyObject * _expand_image(ImagingObject *self, PyObject *args) { int x, y; - int mode = 0; - if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) { + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return NULL; } - return PyImagingNew(ImagingExpand(self->image, x, y, mode)); + return PyImagingNew(ImagingExpand(self->image, x, y)); } static PyObject * @@ -1039,9 +1075,9 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; - float radius = 0; + float xradius, yradius; int passes = 3; - if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) { + if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &passes)) { return NULL; } @@ -1051,7 +1087,7 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { return NULL; } - if (!ImagingGaussianBlur(imOut, imIn, radius, passes)) { + if (!ImagingGaussianBlur(imOut, imIn, xradius, yradius, passes)) { ImagingDelete(imOut); return NULL; } @@ -1245,6 +1281,10 @@ _histogram(ImagingObject *self, PyObject *args) { /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); + if (list == NULL) { + ImagingHistogramDelete(h); + return NULL; + } for (i = 0; i < h->bands * 256; i++) { PyObject *item; item = PyLong_FromLong(h->histogram[i]); @@ -2091,9 +2131,9 @@ _box_blur(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; - float radius; + float xradius, yradius; int n = 1; - if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) { + if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &n)) { return NULL; } @@ -2103,7 +2143,7 @@ _box_blur(ImagingObject *self, PyObject *args) { return NULL; } - if (!ImagingBoxBlur(imOut, imIn, radius, n)) { + if (!ImagingBoxBlur(imOut, imIn, xradius, yradius, n)) { ImagingDelete(imOut); return NULL; } @@ -2119,9 +2159,15 @@ _isblock(ImagingObject *self) { } static PyObject * -_getbbox(ImagingObject *self) { +_getbbox(ImagingObject *self, PyObject *args) { int bbox[4]; - if (!ImagingGetBBox(self->image, bbox)) { + + int alpha_only = 1; + if (!PyArg_ParseTuple(args, "|i", &alpha_only)) { + return NULL; + } + + if (!ImagingGetBBox(self->image, bbox, alpha_only)) { Py_INCREF(Py_None); return Py_None; } @@ -2150,6 +2196,10 @@ _getcolors(ImagingObject *self, PyObject *args) { Py_INCREF(out); } else { out = PyList_New(colors); + if (out == NULL) { + free(items); + return NULL; + } for (i = 0; i < colors; i++) { ImagingColorItem *v = &items[i]; PyObject *item = Py_BuildValue( @@ -2805,6 +2855,16 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawArc( self->image->image, @@ -2886,6 +2946,16 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawChord( self->image->image, @@ -2932,6 +3002,16 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawEllipse( self->image->image, @@ -3101,6 +3181,16 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawPieslice( self->image->image, @@ -3197,6 +3287,16 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawRectangle( self->image->image, @@ -3479,7 +3579,7 @@ static struct PyMethodDef methods[] = { {"isblock", (PyCFunction)_isblock, METH_NOARGS}, - {"getbbox", (PyCFunction)_getbbox, METH_NOARGS}, + {"getbbox", (PyCFunction)_getbbox, METH_VARARGS}, {"getcolors", (PyCFunction)_getcolors, METH_VARARGS}, {"getextrema", (PyCFunction)_getextrema, METH_NOARGS}, {"getprojection", (PyCFunction)_getprojection, METH_NOARGS}, @@ -3625,18 +3725,18 @@ static PySequenceMethods image_as_sequence = { static PyTypeObject Imaging_Type = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingCore", /*tp_name*/ - sizeof(ImagingObject), /*tp_size*/ + sizeof(ImagingObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - &image_as_sequence, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + &image_as_sequence, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -3660,18 +3760,18 @@ static PyTypeObject Imaging_Type = { static PyTypeObject ImagingFont_Type = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ - sizeof(ImagingFontObject), /*tp_size*/ + sizeof(ImagingFontObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_font_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -3693,18 +3793,18 @@ static PyTypeObject ImagingFont_Type = { static PyTypeObject ImagingDraw_Type = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingDraw", /*tp_name*/ - sizeof(ImagingDrawObject), /*tp_size*/ + sizeof(ImagingDrawObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_draw_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -3735,19 +3835,19 @@ static PyMappingMethods pixel_access_as_mapping = { /* type description */ static PyTypeObject PixelAccess_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", - sizeof(PixelAccessObject), - 0, + PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", /*tp_name*/ + sizeof(PixelAccessObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ /* methods */ (destructor)pixel_access_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - &pixel_access_as_mapping, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + &pixel_access_as_mapping, /*tp_as_mapping*/ 0 /*tp_hash*/ }; @@ -3756,6 +3856,7 @@ static PyTypeObject PixelAccess_Type = { static PyObject * _get_stats(PyObject *self, PyObject *args) { PyObject *d; + PyObject *v; ImagingMemoryArena arena = &ImagingDefaultArena; if (!PyArg_ParseTuple(args, ":get_stats")) { @@ -3766,15 +3867,29 @@ _get_stats(PyObject *self, PyObject *args) { if (!d) { return NULL; } - PyDict_SetItemString(d, "new_count", PyLong_FromLong(arena->stats_new_count)); - PyDict_SetItemString( - d, "allocated_blocks", PyLong_FromLong(arena->stats_allocated_blocks)); - PyDict_SetItemString( - d, "reused_blocks", PyLong_FromLong(arena->stats_reused_blocks)); - PyDict_SetItemString( - d, "reallocated_blocks", PyLong_FromLong(arena->stats_reallocated_blocks)); - PyDict_SetItemString(d, "freed_blocks", PyLong_FromLong(arena->stats_freed_blocks)); - PyDict_SetItemString(d, "blocks_cached", PyLong_FromLong(arena->blocks_cached)); + v = PyLong_FromLong(arena->stats_new_count); + PyDict_SetItemString(d, "new_count", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_allocated_blocks); + PyDict_SetItemString(d, "allocated_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_reused_blocks); + PyDict_SetItemString(d, "reused_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_reallocated_blocks); + PyDict_SetItemString(d, "reallocated_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_freed_blocks); + PyDict_SetItemString(d, "freed_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->blocks_cached); + PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); + Py_XDECREF(v); return d; } @@ -4143,28 +4258,33 @@ setup_module(PyObject *m) { #ifdef HAVE_LIBJPEG { extern const char *ImagingJpegVersion(void); - PyDict_SetItemString( - d, "jpeglib_version", PyUnicode_FromString(ImagingJpegVersion())); + PyObject *v = PyUnicode_FromString(ImagingJpegVersion()); + PyDict_SetItemString(d, "jpeglib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif #ifdef HAVE_OPENJPEG { extern const char *ImagingJpeg2KVersion(void); - PyDict_SetItemString( - d, "jp2klib_version", PyUnicode_FromString(ImagingJpeg2KVersion())); + PyObject *v = PyUnicode_FromString(ImagingJpeg2KVersion()); + PyDict_SetItemString(d, "jp2klib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif PyObject *have_libjpegturbo; #ifdef LIBJPEG_TURBO_VERSION have_libjpegturbo = Py_True; + { #define tostr1(a) #a #define tostr(a) tostr1(a) - PyDict_SetItemString( - d, "libjpeg_turbo_version", PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION))); + PyObject *v = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); + PyDict_SetItemString(d, "libjpeg_turbo_version", v ? v : Py_None); + Py_XDECREF(v); #undef tostr #undef tostr1 + } #else have_libjpegturbo = Py_False; #endif @@ -4176,8 +4296,9 @@ setup_module(PyObject *m) { have_libimagequant = Py_True; { extern const char *ImagingImageQuantVersion(void); - PyDict_SetItemString( - d, "imagequant_version", PyUnicode_FromString(ImagingImageQuantVersion())); + PyObject *v = PyUnicode_FromString(ImagingImageQuantVersion()); + PyDict_SetItemString(d, "imagequant_version", v ? v : Py_None); + Py_XDECREF(v); } #else have_libimagequant = Py_False; @@ -4194,16 +4315,18 @@ setup_module(PyObject *m) { PyModule_AddIntConstant(m, "FIXED", Z_FIXED); { extern const char *ImagingZipVersion(void); - PyDict_SetItemString( - d, "zlib_version", PyUnicode_FromString(ImagingZipVersion())); + PyObject *v = PyUnicode_FromString(ImagingZipVersion()); + PyDict_SetItemString(d, "zlib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); - PyDict_SetItemString( - d, "libtiff_version", PyUnicode_FromString(ImagingTiffVersion())); + PyObject *v = PyUnicode_FromString(ImagingTiffVersion()); + PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None); + Py_XDECREF(v); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 PyObject *support_custom_tags; @@ -4226,7 +4349,9 @@ setup_module(PyObject *m) { Py_INCREF(have_xcb); PyModule_AddObject(m, "HAVE_XCB", have_xcb); - PyDict_SetItemString(d, "PILLOW_VERSION", PyUnicode_FromString(version)); + PyObject *pillow_version = PyUnicode_FromString(version); + PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); + Py_XDECREF(pillow_version); return 0; } @@ -4246,6 +4371,7 @@ PyInit__imaging(void) { m = PyModule_Create(&module_def); if (setup_module(m) < 0) { + Py_DECREF(m); return NULL; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 9b5a121d7..56d5d73f8 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -116,7 +116,7 @@ cms_profile_open(PyObject *self, PyObject *args) { } static PyObject * -cms_profile_fromstring(PyObject *self, PyObject *args) { +cms_profile_frombytes(PyObject *self, PyObject *args) { cmsHPROFILE hProfile; char *pProfile; @@ -950,6 +950,8 @@ _is_intent_supported(CmsProfileObject *self, int clut) { return Py_None; } PyDict_SetItem(result, id, entry); + Py_DECREF(id); + Py_DECREF(entry); } return result; } @@ -960,8 +962,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { static PyMethodDef pyCMSdll_methods[] = { {"profile_open", cms_profile_open, METH_VARARGS}, - {"profile_frombytes", cms_profile_fromstring, METH_VARARGS}, - {"profile_fromstring", cms_profile_fromstring, METH_VARARGS}, + {"profile_frombytes", cms_profile_frombytes, METH_VARARGS}, {"profile_tobytes", cms_profile_tobytes, METH_VARARGS}, /* profile and transform functions */ @@ -1420,19 +1421,19 @@ static struct PyGetSetDef cms_profile_getsetters[] = { {NULL}}; static PyTypeObject CmsProfile_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL._imagingcms.CmsProfile", /*tp_name */ - sizeof(CmsProfileObject), - 0, /*tp_basicsize, tp_itemsize */ + PyVarObject_HEAD_INIT(NULL, 0) "PIL._imagingcms.CmsProfile", /*tp_name*/ + sizeof(CmsProfileObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ /* methods */ (destructor)cms_profile_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -1472,19 +1473,19 @@ static struct PyGetSetDef cms_transform_getsetters[] = { {NULL}}; static PyTypeObject CmsTransform_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "CmsTransform", - sizeof(CmsTransformObject), - 0, + PyVarObject_HEAD_INIT(NULL, 0) "CmsTransform", /*tp_name*/ + sizeof(CmsTransformObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ /* methods */ (destructor)cms_transform_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -1532,7 +1533,8 @@ setup_module(PyObject *m) { } else { v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); } - PyDict_SetItemString(d, "littlecms_version", v); + PyDict_SetItemString(d, "littlecms_version", v ? v : Py_None); + Py_XDECREF(v); return 0; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 0db17a5a6..2165fbc7a 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -33,12 +33,6 @@ #include FT_COLOR_H #endif -#define KEEP_PY_UNICODE - -#if !defined(FT_LOAD_TARGET_MONO) -#define FT_LOAD_TARGET_MONO FT_LOAD_MONOCHROME -#endif - /* -------------------------------------------------------------------- */ /* error table */ @@ -122,7 +116,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { int error = 0; char *filename = NULL; - Py_ssize_t size; + float size; + FT_Size_RequestRec req; + FT_Long width; Py_ssize_t index = 0; Py_ssize_t layout_engine = 0; unsigned char *encoding; @@ -136,10 +132,31 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { return NULL; } +#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11 + PyConfig config; + PyConfig_InitPythonConfig(&config); if (!PyArg_ParseTupleAndKeywords( args, kw, - "etn|nsy#n", + "etf|nsy#n", + kwlist, + config.filesystem_encoding, + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size, + &layout_engine)) { + PyConfig_Clear(&config); + return NULL; + } + PyConfig_Clear(&config); +#else + if (!PyArg_ParseTupleAndKeywords( + args, + kw, + "etf|nsy#n", kwlist, Py_FileSystemDefaultEncoding, &filename, @@ -151,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &layout_engine)) { return NULL; } +#endif self = PyObject_New(FontObject, &Font_Type); if (!self) { @@ -171,7 +189,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { /* Don't free this before FT_Done_Face */ self->font_bytes = PyMem_Malloc(font_bytes_size); if (!self->font_bytes) { - error = 65; // Out of Memory in Freetype. + error = FT_Err_Out_Of_Memory; } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); @@ -185,7 +203,13 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { } if (!error) { - error = FT_Set_Pixel_Sizes(self->face, 0, size); + width = size * 64; + req.type = FT_SIZE_REQUEST_TYPE_NOMINAL; + req.width = width; + req.height = width; + req.horiResolution = 0; + req.vertResolution = 0; + error = FT_Request_Size(self->face, &req); } if (!error && encoding && strlen((char *)encoding) == 4) { @@ -230,9 +254,7 @@ text_layout_raqm( const char *dir, PyObject *features, const char *lang, - GlyphInfo **glyph_info, - int mask, - int color) { + GlyphInfo **glyph_info) { size_t i = 0, count = 0, start = 0; raqm_t *rq; raqm_glyph_t *glyphs = NULL; @@ -420,11 +442,9 @@ text_layout_fallback( if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif for (i = 0; font_getchar(string, i, &ch); i++) { (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); @@ -471,7 +491,7 @@ text_layout( #ifdef HAVE_RAQM if (have_raqm && self->layout_engine == LAYOUT_RAQM) { count = text_layout_raqm( - string, self, dir, features, lang, glyph_info, mask, color); + string, self, dir, features, lang, glyph_info); } else #endif { @@ -529,75 +549,25 @@ font_getlength(FontObject *self, PyObject *args) { return PyLong_FromLong(length); } -static PyObject * -font_getsize(FontObject *self, PyObject *args) { +static int +bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) { int position; /* pen position along primary axis, in 26.6 precision */ int advanced; /* pen position along primary axis, in pixels */ int px, py; /* position of current glyph, in pixels */ int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ - int load_flags; /* FreeType load_flags parameter */ int error; - FT_Face face; FT_Glyph glyph; - FT_BBox bbox; /* glyph bounding box */ - GlyphInfo *glyph_info = NULL; /* computed text layout */ - size_t i, count; /* glyph_info index and length */ - int horizontal_dir; /* is primary axis horizontal? */ - int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ - int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; - const char *dir = NULL; - const char *lang = NULL; - const char *anchor = NULL; - PyObject *features = Py_None; - PyObject *string; - - /* calculate size and bearing for a given string */ - - if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { - return NULL; - } - - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; - - if (anchor == NULL) { - anchor = horizontal_dir ? "la" : "lt"; - } - if (strlen(anchor) != 2) { - goto bad_anchor; - } - - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); - if (PyErr_Occurred()) { - return NULL; - } - - load_flags = FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } -#ifdef FT_LOAD_COLOR - if (color) { - load_flags |= FT_LOAD_COLOR; - } -#endif - + FT_BBox bbox; /* glyph bounding box */ + size_t i; /* glyph_info index */ /* * text bounds are given by: * - bounding boxes of individual glyphs * - pen line, i.e. 0 to `advanced` along primary axis * this means point (0, 0) is part of the text bounding box */ - face = NULL; position = x_min = x_max = y_min = y_max = 0; for (i = 0; i < count; i++) { - face = self->face; - if (horizontal_dir) { px = PIXEL(position + glyph_info[i].x_offset); py = PIXEL(glyph_info[i].y_offset); @@ -620,12 +590,14 @@ font_getsize(FontObject *self, PyObject *args) { error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + return 1; } error = FT_Get_Glyph(face->glyph, &glyph); if (error) { - return geterror(error); + geterror(error); + return 1; } FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); @@ -649,13 +621,15 @@ font_getsize(FontObject *self, PyObject *args) { FT_Done_Glyph(glyph); } - if (glyph_info) { - PyMem_Free(glyph_info); - glyph_info = NULL; + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; } x_anchor = y_anchor = 0; - if (face) { + if (count) { if (horizontal_dir) { switch (anchor[0]) { case 'l': // left @@ -673,15 +647,15 @@ font_getsize(FontObject *self, PyObject *args) { } switch (anchor[1]) { case 'a': // ascender - y_anchor = PIXEL(self->face->size->metrics.ascender); + y_anchor = PIXEL(face->size->metrics.ascender); break; case 't': // top y_anchor = y_max; break; case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( - (self->face->size->metrics.ascender + - self->face->size->metrics.descender) / + (face->size->metrics.ascender + + face->size->metrics.descender) / 2); break; case 's': // horizontal baseline @@ -691,7 +665,7 @@ font_getsize(FontObject *self, PyObject *args) { y_anchor = y_min; break; case 'd': // descender - y_anchor = PIXEL(self->face->size->metrics.descender); + y_anchor = PIXEL(face->size->metrics.descender); break; default: goto bad_anchor; @@ -731,17 +705,74 @@ font_getsize(FontObject *self, PyObject *args) { } } } - - return Py_BuildValue( - "(ii)(ii)", - (x_max - x_min), - (y_max - y_min), - (-x_anchor + x_min), - -(-y_anchor + y_max)); + *width = x_max - x_min; + *height = y_max - y_min; + *x_offset = -x_anchor + x_min; + *y_offset = -(-y_anchor + y_max); + return 0; bad_anchor: PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); - return NULL; + return 1; +} + +static PyObject * +font_getsize(FontObject *self, PyObject *args) { + int width, height, x_offset, y_offset; + int load_flags; /* FreeType load_flags parameter */ + int error; + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t count; /* glyph_info length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple( + args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + return NULL; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + if (error) { + return NULL; + } + + return Py_BuildValue( + "(ii)(ii)", + width, + height, + x_offset, + y_offset); } static PyObject * @@ -765,6 +796,7 @@ font_render(FontObject *self, PyObject *args) { unsigned int bitmap_y; /* glyph bitmap y index */ unsigned char *source; /* glyph bitmap source buffer */ unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ + PyObject *image; Imaging im; Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ @@ -775,27 +807,34 @@ font_render(FontObject *self, PyObject *args) { const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; + const char *anchor = NULL; PyObject *features = Py_None; PyObject *string; + PyObject *fill; float x_start = 0; float y_start = 0; + int width, height, x_offset, y_offset; + int horizontal_dir; /* is primary axis horizontal? */ + PyObject *max_image_pixels = Py_None; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ if (!PyArg_ParseTuple( args, - "On|zzOziLff:render", + "OO|zzOzizLffO:render", &string, - &id, + &fill, &mode, &dir, &features, &lang, &stroke_width, + &anchor, &foreground_ink_long, &x_start, - &y_start)) { + &y_start, + &max_image_pixels)) { return NULL; } @@ -821,14 +860,52 @@ font_render(FontObject *self, PyObject *args) { if (PyErr_Occurred()) { return NULL; } - if (count == 0) { - Py_RETURN_NONE; + + load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (error) { + PyMem_Del(glyph_info); + return NULL; + } + + width += stroke_width * 2 + ceil(x_start); + height += stroke_width * 2 + ceil(y_start); + if (max_image_pixels != Py_None) { + if ((long long)(width > 1 ? width : 1) * (height > 1 ? height : 1) > PyLong_AsLongLong(max_image_pixels) * 2) { + PyMem_Del(glyph_info); + return Py_BuildValue("(ii)(ii)", width, height, 0, 0); + } + } + + image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height); + if (image == NULL) { + PyMem_Del(glyph_info); + return NULL; + } + id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); + im = (Imaging)id; + + x_offset -= stroke_width; + y_offset -= stroke_width; + if (count == 0 || width == 0 || height == 0) { + PyMem_Del(glyph_info); + return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset); } if (stroke_width) { error = FT_Stroker_New(library, &stroker); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } FT_Stroker_Set( @@ -839,17 +916,6 @@ font_render(FontObject *self, PyObject *args) { 0); } - im = (Imaging)id; - load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } -#ifdef FT_LOAD_COLOR - if (color) { - load_flags |= FT_LOAD_COLOR; - } -#endif - /* * calculate x_min and y_max * must match font_getsize or there may be clipping! @@ -862,7 +928,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } glyph_slot = self->face->glyph; @@ -893,7 +960,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } glyph_slot = self->face->glyph; @@ -907,7 +975,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); } if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } bitmap_glyph = (FT_BitmapGlyph)glyph; @@ -958,13 +1027,11 @@ font_render(FontObject *self, PyObject *args) { /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ case FT_PIXEL_MODE_GRAY: break; -#ifdef FT_LOAD_COLOR case FT_PIXEL_MODE_BGRA: if (color) { break; } /* we didn't ask for color, fall through to default */ -#endif default: PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; @@ -995,7 +1062,6 @@ font_render(FontObject *self, PyObject *args) { } else { target = im->image8[yy] + xx; } -#ifdef FT_LOAD_COLOR if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { /* paste color glyph */ for (k = x0; k < x1; k++) { @@ -1010,9 +1076,7 @@ font_render(FontObject *self, PyObject *args) { target[k * 4 + 3] = source[k * 4 + 3]; } } - } else -#endif - if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { if (color) { unsigned char *ink = (unsigned char *)&foreground_ink; for (k = x0; k < x1; k++) { @@ -1049,11 +1113,18 @@ font_render(FontObject *self, PyObject *args) { if (bitmap_converted_ready) { FT_Bitmap_Done(library, &bitmap_converted); } + Py_DECREF(image); FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - Py_RETURN_NONE; + return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset); glyph_error: + if (im->destroy) { + im->destroy(im); + } + if (im->image) { + free(im->image); + } if (stroker != NULL) { FT_Done_Glyph(glyph); } @@ -1082,11 +1153,17 @@ font_getvarnames(FontObject *self) { num_namedstyles = master->num_namedstyles; list_names = PyList_New(num_namedstyles); + if (list_names == NULL) { + FT_Done_MM_Var(library, master); + return NULL; + } name_count = FT_Get_Sfnt_Name_Count(self->face); for (i = 0; i < name_count; i++) { error = FT_Get_Sfnt_Name(self->face, i, &name); if (error) { + Py_DECREF(list_names); + FT_Done_MM_Var(library, master); return geterror(error); } @@ -1125,25 +1202,44 @@ font_getvaraxes(FontObject *self) { name_count = FT_Get_Sfnt_Name_Count(self->face); list_axes = PyList_New(num_axis); + if (list_axes == NULL) { + FT_Done_MM_Var(library, master); + return NULL; + } for (i = 0; i < num_axis; i++) { axis = master->axis[i]; list_axis = PyDict_New(); - PyDict_SetItemString( - list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536)); - PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536)); - PyDict_SetItemString( - list_axis, "maximum", PyLong_FromLong(axis.maximum / 65536)); + if (list_axis == NULL) { + Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); + return NULL; + } + PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); + PyDict_SetItemString(list_axis, "minimum", minimum ? minimum : Py_None); + Py_XDECREF(minimum); + + PyObject *def = PyLong_FromLong(axis.def / 65536); + PyDict_SetItemString(list_axis, "default", def ? def : Py_None); + Py_XDECREF(def); + + PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); + PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); + Py_XDECREF(maximum); for (j = 0; j < name_count; j++) { error = FT_Get_Sfnt_Name(self->face, j, &name); if (error) { + Py_DECREF(list_axis); + Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); return geterror(error); } if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString(list_axis, "name", axis_name); + PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None); + Py_XDECREF(axis_name); break; } } @@ -1305,19 +1401,19 @@ static struct PyGetSetDef font_getsetters[] = { {NULL}}; static PyTypeObject Font_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "Font", - sizeof(FontObject), - 0, + PyVarObject_HEAD_INIT(NULL, 0) "Font", /*tp_name*/ + sizeof(FontObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ /* methods */ - (destructor)font_dealloc, /* tp_dealloc */ - 0, /* tp_print */ + (destructor)font_dealloc, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -1358,7 +1454,8 @@ setup_module(PyObject *m) { FT_Library_Version(library, &major, &minor, &patch); v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); - PyDict_SetItemString(d, "freetype2_version", v); + PyDict_SetItemString(d, "freetype2_version", v ? v : Py_None); + Py_XDECREF(v); #ifdef HAVE_RAQM #if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) @@ -1376,35 +1473,34 @@ setup_module(PyObject *m) { PyDict_SetItemString(d, "HAVE_RAQM", v); PyDict_SetItemString(d, "HAVE_FRIBIDI", v); PyDict_SetItemString(d, "HAVE_HARFBUZZ", v); + Py_DECREF(v); if (have_raqm) { + v = NULL; #ifdef RAQM_VERSION_MAJOR v = PyUnicode_FromString(raqm_version_string()); -#else - v = Py_None; #endif - PyDict_SetItemString(d, "raqm_version", v); + PyDict_SetItemString(d, "raqm_version", v ? v : Py_None); + Py_XDECREF(v); + v = NULL; #ifdef FRIBIDI_MAJOR_VERSION { const char *a = strchr(fribidi_version_info, ')'); const char *b = strchr(fribidi_version_info, '\n'); if (a && b && a + 2 < b) { v = PyUnicode_FromStringAndSize(a + 2, b - (a + 2)); - } else { - v = Py_None; } } -#else - v = Py_None; #endif - PyDict_SetItemString(d, "fribidi_version", v); + PyDict_SetItemString(d, "fribidi_version", v ? v : Py_None); + Py_XDECREF(v); + v = NULL; #ifdef HB_VERSION_STRING v = PyUnicode_FromString(hb_version_string()); -#else - v = Py_None; #endif - PyDict_SetItemString(d, "harfbuzz_version", v); + PyDict_SetItemString(d, "harfbuzz_version", v ? v : Py_None); + Py_XDECREF(v); } return 0; diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c0644b616..8815c2b7e 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -136,13 +136,18 @@ match(PyObject *self, PyObject *args) { int row_idx, col_idx; UINT8 **inrows; PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } if (!PyBytes_Check(py_lut)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); return NULL; } @@ -150,6 +155,7 @@ match(PyObject *self, PyObject *args) { lut_len = PyBytes_Size(py_lut); if (lut_len < LUT_SIZE) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } @@ -158,6 +164,7 @@ match(PyObject *self, PyObject *args) { imgin = (Imaging)i0; if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } @@ -194,6 +201,7 @@ match(PyObject *self, PyObject *args) { if (lut[lut_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); } } } @@ -213,10 +221,13 @@ get_on_pixels(PyObject *self, PyObject *args) { int row_idx, col_idx; int width, height; PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } if (!PyArg_ParseTuple(args, "n", &i0)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; } img = (Imaging)i0; @@ -230,21 +241,13 @@ get_on_pixels(PyObject *self, PyObject *args) { if (row[col_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); } } } return ret; } -static int -setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); - - PyDict_SetItemString(d, "__version", PyUnicode_FromString("0.1")); - - return 0; -} - static PyMethodDef functions[] = { /* Functions */ {"apply", (PyCFunction)apply, METH_VARARGS, NULL}, @@ -266,9 +269,5 @@ PyInit__imagingmorph(void) { m = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - return NULL; - } - return m; } diff --git a/src/_imagingtk.c b/src/_imagingtk.c index b9273b0b8..efa7fc1b6 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -58,5 +58,9 @@ PyInit__imagingtk(void) { }; PyObject *m; m = PyModule_Create(&module_def); - return (load_tkinter_funcs() == 0) ? m : NULL; + if (load_tkinter_funcs() != 0) { + Py_DECREF(m); + return NULL; + } + return m; } diff --git a/src/_webp.c b/src/_webp.c index 493e0709c..a1b4dbc1a 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -487,18 +487,18 @@ static struct PyMethodDef _anim_encoder_methods[] = { // WebPAnimEncoder type definition static PyTypeObject WebPAnimEncoder_Type = { PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimEncoder", /*tp_name */ - sizeof(WebPAnimEncoderObject), /*tp_size */ + sizeof(WebPAnimEncoderObject), /*tp_basicsize */ 0, /*tp_itemsize */ /* methods */ (destructor)_anim_encoder_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -530,18 +530,18 @@ static struct PyMethodDef _anim_decoder_methods[] = { // WebPAnimDecoder type definition static PyTypeObject WebPAnimDecoder_Type = { PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimDecoder", /*tp_name */ - sizeof(WebPAnimDecoderObject), /*tp_size */ + sizeof(WebPAnimDecoderObject), /*tp_basicsize */ 0, /*tp_itemsize */ /* methods */ (destructor)_anim_decoder_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -949,20 +949,14 @@ addAnimFlagToModule(PyObject *m) { void addTransparencyFlagToModule(PyObject *m) { - PyModule_AddObject( - m, "HAVE_TRANSPARENCY", PyBool_FromLong(!WebPDecoderBuggyAlpha())); + PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); + if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { + Py_DECREF(have_transparency); + } } static int setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); - addMuxFlagToModule(m); - addAnimFlagToModule(m); - addTransparencyFlagToModule(m); - - PyDict_SetItemString( - d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); - #ifdef HAVE_WEBPANIM /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || @@ -970,6 +964,15 @@ setup_module(PyObject *m) { return -1; } #endif + PyObject *d = PyModule_GetDict(m); + addMuxFlagToModule(m); + addAnimFlagToModule(m); + addTransparencyFlagToModule(m); + + PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); + PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); + Py_XDECREF(v); + return 0; } @@ -987,6 +990,7 @@ PyInit__webp(void) { m = PyModule_Create(&module_def); if (setup_module(m) < 0) { + Py_DECREF(m); return NULL; } diff --git a/src/decode.c b/src/decode.c index 82a3af832..ea2f3af80 100644 --- a/src/decode.c +++ b/src/decode.c @@ -134,6 +134,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionLeave(&cookie); } + PyBuffer_Release(&buffer); return Py_BuildValue("ii", status, decoder->state.errcode); } @@ -257,18 +258,18 @@ static struct PyGetSetDef getseters[] = { static PyTypeObject ImagingDecoderType = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingDecoder", /*tp_name*/ - sizeof(ImagingDecoderObject), /*tp_size*/ + sizeof(ImagingDecoderObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/display.c b/src/display.c index a50fc3e24..ef2ff3754 100644 --- a/src/display.c +++ b/src/display.c @@ -195,20 +195,21 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) { static PyObject * _frombytes(ImagingDisplayObject *display, PyObject *args) { - char *ptr; - Py_ssize_t bytes; + Py_buffer buffer; - if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) { + if (!PyArg_ParseTuple(args, "y*:frombytes", &buffer)) { return NULL; } - if (display->dib->ysize * display->dib->linesize != bytes) { + if (display->dib->ysize * display->dib->linesize != buffer.len) { + PyBuffer_Release(&buffer); PyErr_SetString(PyExc_ValueError, "wrong size"); return NULL; } - memcpy(display->dib->bits, ptr, bytes); + memcpy(display->dib->bits, buffer.buf, buffer.len); + PyBuffer_Release(&buffer); Py_INCREF(Py_None); return Py_None; } @@ -250,18 +251,18 @@ static struct PyGetSetDef getsetters[] = { static PyTypeObject ImagingDisplayType = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingDisplay", /*tp_name*/ - sizeof(ImagingDisplayObject), /*tp_size*/ + sizeof(ImagingDisplayObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_delete, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -436,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; if (!OpenClipboard(NULL)) { - PyErr_SetString(PyExc_OSError, "failed to open clipboard"); - return NULL; + // Maybe the clipboard is temporarily in use by another process. + // Wait and try again + Sleep(500); + + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } } // find best format as set by clipboard owner diff --git a/src/encode.c b/src/encode.c index 21c42d915..08544aede 100644 --- a/src/encode.c +++ b/src/encode.c @@ -323,18 +323,18 @@ static struct PyGetSetDef getseters[] = { static PyTypeObject ImagingEncoderType = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingEncoder", /*tp_name*/ - sizeof(ImagingEncoderObject), /*tp_size*/ + sizeof(ImagingEncoderObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -904,7 +904,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value)); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)); + &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value)); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (INT16)PyLong_AsLong(value)); @@ -1214,10 +1214,13 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; + char *comment; + Py_ssize_t comment_size; + int plt = 0; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbn", + "ss|OOOsOnOOOssbbnz#p", &mode, &format, &offset, @@ -1233,7 +1236,10 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &cinema_mode, &mct, &sgnd, - &fd)) { + &fd, + &comment, + &comment_size, + &plt)) { return NULL; } @@ -1315,6 +1321,26 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } } + if (comment && comment_size > 0) { + /* Size is stored as as an uint16, subtract 4 bytes for the header */ + if (comment_size >= 65532) { + PyErr_SetString( + PyExc_ValueError, + "JPEG 2000 comment is too long"); + Py_DECREF(encoder); + return NULL; + } + + char *p = malloc(comment_size + 1); + if (!p) { + Py_DECREF(encoder); + return ImagingError_MemoryError(); + } + memcpy(p, comment, comment_size); + p[comment_size] = '\0'; + context->comment = p; + } + if (quality_layers && PySequence_Check(quality_layers)) { context->quality_is_in_db = strcmp(quality_mode, "dB") == 0; context->quality_layers = quality_layers; @@ -1332,6 +1358,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->cinema_mode = cine_mode; context->mct = mct; context->sgnd = sgnd; + context->plt = plt; return (PyObject *)encoder; } diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 83860c38a..dd0418696 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -13,7 +13,7 @@ /* use make_hash.py from the pillow-scripts repository to calculate these values */ #define ACCESS_TABLE_SIZE 27 -#define ACCESS_TABLE_HASH 3078 +#define ACCESS_TABLE_HASH 33051 static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; @@ -46,22 +46,11 @@ add_item(const char *mode) { /* fetch individual pixel */ static void -get_pixel(Imaging im, int x, int y, void *color) { +get_pixel_32_2bands(Imaging im, int x, int y, void *color) { char *out = color; - - /* generic pixel access*/ - - if (im->image8) { - out[0] = im->image8[y][x]; - } else { - UINT8 *p = (UINT8 *)&im->image32[y][x]; - if (im->type == IMAGING_TYPE_UINT8 && im->bands == 2) { - out[0] = p[0]; - out[1] = p[3]; - return; - } - memcpy(out, p, im->pixelsize); - } + UINT8 *p = (UINT8 *)&im->image32[y][x]; + out[0] = p[0]; + out[1] = p[3]; } static void @@ -92,6 +81,12 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } +static void +get_pixel_16(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x + x]; + memcpy(color, in, sizeof(UINT16)); +} + static void get_pixel_32(Imaging im, int x, int y, void *color) { memcpy(color, &im->image32[y][x], sizeof(INT32)); @@ -121,15 +116,6 @@ get_pixel_32B(Imaging im, int x, int y, void *color) { /* store individual pixel */ -static void -put_pixel(Imaging im, int x, int y, const void *color) { - if (im->image8) { - im->image8[y][x] = *((UINT8 *)color); - } else { - memcpy(&im->image32[y][x], color, sizeof(INT32)); - } -} - static void put_pixel_8(Imaging im, int x, int y, const void *color) { im->image8[y][x] = *((UINT8 *)color); @@ -180,17 +166,18 @@ ImagingAccessInit() { /* populate access table */ ADD("1", get_pixel_8, put_pixel_8); ADD("L", get_pixel_8, put_pixel_8); - ADD("LA", get_pixel, put_pixel); - ADD("La", get_pixel, put_pixel); + ADD("LA", get_pixel_32_2bands, put_pixel_32); + ADD("La", get_pixel_32_2bands, put_pixel_32); ADD("I", get_pixel_32, put_pixel_32); ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); ADD("I;16B", get_pixel_16B, put_pixel_16B); + ADD("I;16N", get_pixel_16, put_pixel_16L); ADD("I;32L", get_pixel_32L, put_pixel_32L); ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); ADD("P", get_pixel_8, put_pixel_8); - ADD("PA", get_pixel, put_pixel); + ADD("PA", get_pixel_32_2bands, put_pixel_32); ADD("RGB", get_pixel_32, put_pixel_32); ADD("RGBA", get_pixel_32, put_pixel_32); ADD("RGBa", get_pixel_32, put_pixel_32); diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 5afe7cf50..adf425d0d 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -230,14 +230,14 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { } Imaging -ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { +ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) { int i; Imaging imTransposed; if (n < 1) { return ImagingError_ValueError("number of passes must be greater than zero"); } - if (radius < 0) { + if (xradius < 0 || yradius < 0) { return ImagingError_ValueError("radius must be >= 0"); } @@ -258,35 +258,45 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { return ImagingError_ModeError(); } - imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); - if (!imTransposed) { - return NULL; - } - /* Apply blur in one dimension. Use imOut as a destination at first pass, then use imOut as a source too. */ - ImagingHorizontalBoxBlur(imOut, imIn, radius); - for (i = 1; i < n; i++) { - ImagingHorizontalBoxBlur(imOut, imOut, radius); - } - /* Transpose result for blur in another direction. */ - ImagingTranspose(imTransposed, imOut); - /* Reuse imTransposed as a source and destination there. */ - for (i = 0; i < n; i++) { - ImagingHorizontalBoxBlur(imTransposed, imTransposed, radius); + if (xradius != 0) { + ImagingHorizontalBoxBlur(imOut, imIn, xradius); + for (i = 1; i < n; i++) { + ImagingHorizontalBoxBlur(imOut, imOut, xradius); + } } - /* Restore original orientation. */ - ImagingTranspose(imOut, imTransposed); + if (yradius != 0) { + imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); + if (!imTransposed) { + return NULL; + } - ImagingDelete(imTransposed); + /* Transpose result for blur in another direction. */ + ImagingTranspose(imTransposed, xradius == 0 ? imIn : imOut); + + /* Reuse imTransposed as a source and destination there. */ + for (i = 0; i < n; i++) { + ImagingHorizontalBoxBlur(imTransposed, imTransposed, yradius); + } + /* Restore original orientation. */ + ImagingTranspose(imOut, imTransposed); + + ImagingDelete(imTransposed); + } + if (xradius == 0 && yradius == 0) { + if (!ImagingCopy2(imOut, imIn)) { + return NULL; + } + } return imOut; } -Imaging -ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { +static float +_gaussian_blur_radius(float radius, int passes) { float sigma2, L, l, a; sigma2 = radius * radius / passes; @@ -299,5 +309,16 @@ ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { a = (2 * l + 1) * (l * (l + 1) - 3 * sigma2); a /= 6 * (sigma2 - (l + 1) * (l + 1)); - return ImagingBoxBlur(imOut, imIn, l + a, passes); + return l + a; +} + +Imaging +ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) { + return ImagingBoxBlur( + imOut, + imIn, + _gaussian_blur_radius(xradius, passes), + _gaussian_blur_radius(yradius, passes), + passes + ); } diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index b03bd02af..7fe24a639 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -990,6 +990,13 @@ static struct { {"I;16L", "L", I16L_L}, {"L", "I;16B", L_I16B}, {"I;16B", "L", I16B_L}, +#ifdef WORDS_BIGENDIAN + {"L", "I;16N", L_I16B}, + {"I;16N", "L", I16B_L}, +#else + {"L", "I;16N", L_I16L}, + {"I;16N", "L", I16L_L}, +#endif {"I;16", "F", I16L_F}, {"I;16L", "F", I16L_F}, diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 77343e583..82f290bd0 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -85,25 +85,22 @@ point32(Imaging im, int x, int y, int ink) { static inline void point32rgba(Imaging im, int x, int y, int ink) { - unsigned int tmp1; + unsigned int tmp; if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { UINT8 *out = (UINT8 *)im->image[y] + x * 4; UINT8 *in = (UINT8 *)&ink; - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); } } static inline void hline8(Imaging im, int x0, int y0, int x1, int ink) { - int tmp, pixelwidth; + int pixelwidth; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -126,13 +123,9 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; INT32 *p; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -152,13 +145,9 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; - unsigned int tmp1; + unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -173,9 +162,9 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); x0++; out += 4; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index fab3b4948..4dcd368ca 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -37,8 +37,19 @@ clip8(float in) { return (UINT8)in; } +static inline INT32 +clip32(float in) { + if (in <= 0.0) { + return 0; + } + if (in >= pow(2, 31) - 1) { + return pow(2, 31) - 1; + } + return (INT32)in; +} + Imaging -ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { +ImagingExpand(Imaging imIn, int xmargin, int ymargin) { Imaging imOut; int x, y; ImagingSectionCookie cookie; @@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[2]) + (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ + _i2f(in0[x + d]) * (kernel)[2]) int x = 0, y = 0; @@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 1; y < im->ysize - 1; y++) { - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 1; y < im->ysize - 1; y++) { + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - for (x = 1; x < im->xsize - 1; x++) { - float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip32(ss); + } + out[x] = in0[x]; + } + } else { + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip8(ss); + } + out[x] = in0[x]; } - out[x] = in0[x]; } } else { // Add one time for rounding @@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { void ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x5(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \ - _i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[3] + \ - _i2f((UINT8)in0[x + d + d]) * (kernel)[4]) + (_i2f(in0[x - d - d]) * (kernel)[0] + \ + _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \ + _i2f(in0[x + d]) * (kernel)[3] + \ + _i2f(in0[x + d + d]) * (kernel)[4]) int x = 0, y = 0; @@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 2; y < im->ysize - 2; y++) { - UINT8 *in_2 = (UINT8 *)im->image[y - 2]; - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *in2 = (UINT8 *)im->image[y + 2]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 2; y < im->ysize - 2; y++) { + INT32 *in_2 = (INT32 *)im->image[y - 2]; + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *in2 = (INT32 *)im->image[y + 2]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - out[1] = in0[1]; - for (x = 2; x < im->xsize - 2; x++) { - float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip32(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; + } + } else { + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; } } else { // Add one time for rounding @@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; ImagingSectionCookie cookie; - if (!im || im->type != IMAGING_TYPE_UINT8) { + if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { return (Imaging)ImagingError_ModeError(); } diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index e73153600..86c687ca0 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -19,7 +19,7 @@ #include "Imaging.h" int -ImagingGetBBox(Imaging im, int bbox[4]) { +ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { /* Get the bounding box for any non-zero data in the image.*/ int x, y; @@ -58,10 +58,11 @@ ImagingGetBBox(Imaging im, int bbox[4]) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if ( + } else if (alpha_only && ( strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || - strcmp(im->mode, "PA") == 0) { + strcmp(im->mode, "PA") == 0 + )) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index af9996ca9..f6e7fb6b9 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -25,7 +25,7 @@ #endif #endif -#if defined(_WIN32) || defined(__CYGWIN__) +#if defined(_WIN32) || defined(__CYGWIN__) /* WIN */ #define WIN32_LEAN_AND_MEAN #include @@ -37,15 +37,33 @@ #undef WIN32 #endif -#else +#else /* not WIN */ /* For System that are not Windows, we'll need to define these. */ +/* We have to define them instead of using typedef because the JPEG lib also + defines their own types with the same names, so we need to be able to undef + ours before including the JPEG code. */ + +#if __STDC_VERSION__ >= 199901L /* C99+ */ + +#include + +#define INT8 int8_t +#define UINT8 uint8_t +#define INT16 int16_t +#define UINT16 uint16_t +#define INT32 int32_t +#define UINT32 uint32_t + +#else /* < C99 */ + +#define INT8 signed char #if SIZEOF_SHORT == 2 #define INT16 short #elif SIZEOF_INT == 2 #define INT16 int #else -#define INT16 short /* most things works just fine anyway... */ +#error Cannot find required 16-bit integer type #endif #if SIZEOF_SHORT == 4 @@ -58,19 +76,13 @@ #error Cannot find required 32-bit integer type #endif -#if SIZEOF_LONG == 8 -#define INT64 long -#elif SIZEOF_LONG_LONG == 8 -#define INT64 long -#endif - -#define INT8 signed char #define UINT8 unsigned char - #define UINT16 unsigned INT16 #define UINT32 unsigned INT32 -#endif +#endif /* < C99 */ + +#endif /* not WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ #define FLOAT16 UINT16 diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index d9ded1852..afcd2229b 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -290,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); extern Imaging -ImagingExpand(Imaging im, int x, int y, int mode); +ImagingExpand(Imaging im, int x, int y); extern Imaging ImagingFill(Imaging im, const void *ink); extern int @@ -309,7 +309,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn); extern Imaging ImagingFlipTopBottom(Imaging imOut, Imaging imIn); extern Imaging -ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes); +ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes); extern Imaging ImagingGetBand(Imaging im, int band); extern Imaging @@ -317,7 +317,7 @@ ImagingMerge(const char *mode, Imaging bands[4]); extern int ImagingSplit(Imaging im, Imaging bands[4]); extern int -ImagingGetBBox(Imaging im, int bbox[4]); +ImagingGetBBox(Imaging im, int bbox[4], int alpha_only); typedef struct { int x, y; INT32 count; @@ -376,7 +376,7 @@ ImagingTransform( extern Imaging ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold); extern Imaging -ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n); +ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n); extern Imaging ImagingColorLUT3D_linear( Imaging imOut, diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index b28a0440a..e8d92f7b6 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -97,6 +97,12 @@ typedef struct { /* PRIVATE CONTEXT (set by decoder) */ const char *error_msg; + /* Custom comment */ + char *comment; + + /* Include PLT marker segment */ + int plt; + } JPEG2KENCODESTATE; /* diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index db1c5c0c9..3295373fd 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -281,7 +281,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { int ret = -1; unsigned prec = 8; - unsigned bpp = 8; unsigned _overflow_scale_factor; stream = opj_stream_create(BUFFER_SIZE, OPJ_FALSE); @@ -313,7 +312,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_i16; prec = 16; - bpp = 12; } else if (strcmp(im->mode, "LA") == 0) { components = 2; color_space = OPJ_CLRSPC_GRAY; @@ -342,7 +340,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { image_params[n].h = im->ysize; image_params[n].x0 = image_params[n].y0 = 0; image_params[n].prec = prec; - image_params[n].bpp = bpp; image_params[n].sgnd = context->sgnd == 0 ? 0 : 1; } @@ -439,6 +436,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { params.tcp_mct = context->mct; } + if (context->comment) { + params.cp_comment = context->comment; + } + params.prog_order = context->progression; params.cp_cinema = context->cinema_mode; @@ -463,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { } if (!context->num_resolutions) { - while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) { + while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) { params.numresolution -= 1; } } @@ -487,11 +488,25 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } + if (strcmp(im->mode, "RGBA") == 0) { + image->comps[3].alpha = 1; + } else if (strcmp(im->mode, "LA") == 0) { + image->comps[1].alpha = 1; + } + opj_set_error_handler(codec, j2k_error, context); opj_set_info_handler(codec, j2k_warn, context); opj_set_warning_handler(codec, j2k_warn, context); opj_setup_encoder(codec, ¶ms, image); + /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ +#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) + if (context->plt) { + const char *plt_option[2] = {"PLT=YES", NULL}; + opj_encoder_set_extra_options(codec, plt_option); + } +#endif + /* Start encoding */ if (!opj_start_compress(codec, image, stream)) { state->errcode = IMAGING_CODEC_BROKEN; @@ -624,7 +639,12 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { free((void *)context->error_msg); } + if (context->comment) { + free((void *)context->comment); + } + context->error_msg = NULL; + context->comment = NULL; return -1; } diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 01760e742..14c8f1461 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -664,6 +664,7 @@ static struct { #endif {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, packI16N_I16}, {"I;16B", "I;16N", 16, packI16N_I16B}, diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 783852c24..02a4a5c76 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -341,7 +341,10 @@ splitlists( PixelList *l, *r, *c, *n; int i; - int nRight, nLeft; + int nRight; +#ifndef NO_OUTPUT + int nLeft; +#endif int splitColourVal; #ifdef TEST_SPLIT @@ -396,12 +399,17 @@ splitlists( } #endif nCount[0] = nCount[1] = 0; - nLeft = nRight = 0; + nRight = 0; +#ifndef NO_OUTPUT + nLeft = 0; +#endif for (left = 0, c = h[axis]; c;) { left = left + c->count; nCount[0] += c->count; c->flag = 0; +#ifndef NO_OUTPUT nLeft++; +#endif c = c->next[axis]; if (left * 2 > pixelCount) { break; @@ -414,7 +422,9 @@ splitlists( break; } c->flag = 0; +#ifndef NO_OUTPUT nLeft++; +#endif nCount[0] += c->count; } } @@ -430,7 +440,9 @@ splitlists( } c->flag = 1; nRight++; +#ifndef NO_OUTPUT nLeft--; +#endif nCount[0] -= c->count; nCount[1] += c->count; } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 76750aaf7..128595f65 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -37,8 +37,6 @@ #include "Imaging.h" #include -int ImagingNewCount = 0; - /* -------------------------------------------------------------------- * Standard image object. */ @@ -131,7 +129,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ /* 15-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; @@ -139,7 +137,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;16") == 0) { /* EXPERIMENTAL */ /* 16-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; @@ -147,19 +145,11 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;24") == 0) { /* EXPERIMENTAL */ /* 24-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 3; im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; - } else if (strcmp(mode, "BGR;32") == 0) { - /* EXPERIMENTAL */ - /* 32-bit reversed true colour */ - im->bands = 1; - im->pixelsize = 4; - im->linesize = (xsize * 4 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 428cd93d2..35122f182 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -720,7 +720,16 @@ ImagingLibTiffDecode( } decode_err: - TIFFClose(tiff); + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup + if (clientstate->fp) { + // Pillow will manage the closing of the file rather than libtiff + // So only call TIFFCleanup + TIFFCleanup(tiff); + } else { + // When tif_closeproc refers to our custom _tiffCloseProc though, + // that is fine, as it does not close the file + TIFFClose(tiff); + } TRACE(("Done Decoding, Returning \n")); // Returning -1 here to force ImageFile.load to break, rather than // even think about looping back around. diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index e426ed74f..206403ba6 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1149,6 +1149,16 @@ unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { } } static void +unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + out[0] = in[1]; + out[1] = in[0]; + in += 2; + out += 2; + } +} +static void unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) { int i; for (i = 0; i < pixels; i++) { @@ -1542,10 +1552,12 @@ static struct { {"P", "P;4L", 4, unpackP4L}, {"P", "P", 8, copy1}, {"P", "P;R", 8, unpackLR}, + {"P", "L", 8, copy1}, /* palette w. alpha */ {"PA", "PA", 16, unpackLA}, {"PA", "PA;L", 16, unpackLAL}, + {"PA", "LA", 16, unpackLA}, /* true colour */ {"RGB", "RGB", 24, ImagingUnpackRGB}, @@ -1762,7 +1774,9 @@ static struct { {"I;16", "I;16", 16, copy2}, {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, + {"I;16", "I;16B", 16, unpackI16B_I16}, {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16B", "I;16N", 16, unpackI16N_I16B}, diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index 643ced49f..2853ce903 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -36,7 +36,7 @@ ImagingUnsharpMask( /* First, do a gaussian blur on the image, putting results in imOut temporarily. All format checks are in gaussian blur. */ - result = ImagingGaussianBlur(imOut, imIn, radius, 3); + result = ImagingGaussianBlur(imOut, imIn, radius, radius, 3); if (!result) { return NULL; } diff --git a/src/outline.c b/src/outline.c index 0a9a3646e..27cc255cf 100644 --- a/src/outline.c +++ b/src/outline.c @@ -155,18 +155,18 @@ static struct PyMethodDef _outline_methods[] = { static PyTypeObject OutlineType = { PyVarObject_HEAD_INIT(NULL, 0) "Outline", /*tp_name*/ - sizeof(OutlineObject), /*tp_size*/ + sizeof(OutlineObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_outline_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/path.c b/src/path.c index 3e3431575..cc0698c4d 100644 --- a/src/path.c +++ b/src/path.c @@ -439,6 +439,9 @@ path_tolist(PyPathObject *self, PyObject *args) { if (flat) { list = PyList_New(self->count * 2); + if (list == NULL) { + return NULL; + } for (i = 0; i < self->count * 2; i++) { PyObject *item; item = PyFloat_FromDouble(self->xy[i]); @@ -449,6 +452,9 @@ path_tolist(PyPathObject *self, PyObject *args) { } } else { list = PyList_New(self->count); + if (list == NULL) { + return NULL; + } for (i = 0; i < self->count; i++) { PyObject *item; item = Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); @@ -577,18 +583,18 @@ static PyMappingMethods path_as_mapping = { static PyTypeObject PyPathType = { PyVarObject_HEAD_INIT(NULL, 0) "Path", /*tp_name*/ - sizeof(PyPathObject), /*tp_size*/ + sizeof(PyPathObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)path_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - &path_as_sequence, /*tp_as_sequence */ - &path_as_mapping, /*tp_as_mapping */ + 0, /*tp_as_number*/ + &path_as_sequence, /*tp_as_sequence*/ + &path_as_mapping, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING index c605a5dc6..97e2489b7 100644 --- a/src/thirdparty/raqm/COPYING +++ b/src/thirdparty/raqm/COPYING @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright © 2015 Information Technology Authority (ITA) -Copyright © 2016-2022 Khaled Hosny +Copyright © 2016-2023 Khaled Hosny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS index ae1128485..e8bf32e0b 100644 --- a/src/thirdparty/raqm/NEWS +++ b/src/thirdparty/raqm/NEWS @@ -1,3 +1,38 @@ +Overview of changes leading to 0.10.1 +Wednesday, April 12, 2023 +==================================== + +Make combining marks always inherit the script of their base. + +Overview of changes leading to 0.10.0 +Wednesday, January 11, 2023 +==================================== + +Fix font feature ranges. + +Fix resolved direction for all-neutral text. + +Implement letter and word spacing support. + +New API: + * raqm_set_text_utf16 + +Overview of changes leading to 0.9.0 +Sunday, January 30, 2022 +==================================== + +Raise the minimum versions of Raqm dependencies: no longer conditionally +enabling any features based on specific dependency version. + +raqm_t objects can now be reused by calling raqm_clear_contents() before +re-use, to potentially reduce the number memory allocations. + +Don't hardcode python3 in tests. + +New API: + * raqm_set_freetype_load_flags_range + * raqm_clear_contents + Overview of changes leading to 0.8.0 Monday, December 13, 2021 ==================================== diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md index 315e0c8d8..ab729cdc0 100644 --- a/src/thirdparty/raqm/README.md +++ b/src/thirdparty/raqm/README.md @@ -81,5 +81,5 @@ The following projects have patches to support complex text layout using Raqm: [1]: https://github.com/fribidi/fribidi [2]: https://github.com/Tehreer/SheenBidi [3]: https://github.com/harfbuzz/harfbuzz -[4]: https://www.freetype.org +[4]: https://freetype.org/ [5]: https://www.gtk.org/gtk-doc diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index bdb6fb662..62d2d2064 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -33,9 +33,9 @@ #define RAQM_VERSION_MAJOR 0 #define RAQM_VERSION_MINOR 10 -#define RAQM_VERSION_MICRO 0 +#define RAQM_VERSION_MICRO 1 -#define RAQM_VERSION_STRING "0.10.0" +#define RAQM_VERSION_STRING "0.10.1" #define RAQM_VERSION_ATLEAST(major,minor,micro) \ ((major)*10000+(minor)*100+(micro) <= \ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 770ea3018..2b331e1af 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -1,6 +1,6 @@ /* * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2022 Khaled Hosny + * Copyright © 2016-2023 Khaled Hosny * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -1432,7 +1432,7 @@ raqm_get_glyphs (raqm_t *rq, * * Since: 0.8 */ -RAQM_API raqm_direction_t +raqm_direction_t raqm_get_par_resolved_direction (raqm_t *rq) { if (!rq) @@ -1455,7 +1455,7 @@ raqm_get_par_resolved_direction (raqm_t *rq) * * Since: 0.8 */ -RAQM_API raqm_direction_t +raqm_direction_t raqm_get_direction_at_index (raqm_t *rq, size_t index) { @@ -2021,6 +2021,22 @@ _get_pair_index (const uint32_t ch) #define STACK_IS_EMPTY(script) ((script)->size <= 0) #define IS_OPEN(pair_index) (((pair_index) & 1) == 0) +static hb_script_t +_raqm_unicode_script (hb_codepoint_t u) +{ + static hb_unicode_funcs_t* unicode_funcs; + + unicode_funcs = hb_unicode_funcs_get_default (); + + /* Make combining marks inherit the script of their bases, regardless of + * their own script. + */ + if (hb_unicode_general_category (unicode_funcs, u) == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) + return HB_SCRIPT_INHERITED; + + return hb_unicode_script (unicode_funcs, u); +} + /* Resolve the script for each character in the input string, if the character * script is common or inherited it takes the script of the character before it * except paired characters which we try to make them use the same script. We @@ -2033,10 +2049,9 @@ _raqm_resolve_scripts (raqm_t *rq) int last_set_index = -1; hb_script_t last_script = HB_SCRIPT_INVALID; _raqm_stack_t *stack = NULL; - hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default (); for (size_t i = 0; i < rq->text_len; ++i) - rq->text_info[i].script = hb_unicode_script (unicode_funcs, rq->text[i]); + rq->text_info[i].script = _raqm_unicode_script (rq->text[i]); #ifdef RAQM_TESTING RAQM_TEST ("Before script detection:\n"); diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h index 2fd836c86..6fd6089c7 100644 --- a/src/thirdparty/raqm/raqm.h +++ b/src/thirdparty/raqm/raqm.h @@ -1,6 +1,6 @@ /* * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2022 Khaled Hosny + * Copyright © 2016-2023 Khaled Hosny * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to diff --git a/tox.ini b/tox.ini index 9a41ca96b..5388ed243 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,9 @@ [tox] -envlist = +requires = + tox>=4.2 +env_list = lint - py{py3, 311, 310, 39, 38, 37} -minversion = 1.9 + py{py3, 312, 311, 310, 39, 38} [testenv] deps = @@ -12,18 +13,19 @@ extras = tests commands = make clean - {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . + {envpython} -m pip install . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -allowlist_externals = make +allowlist_externals = + make [testenv:lint] -passenv = - PRE_COMMIT_COLOR skip_install = true deps = check-manifest pre-commit +pass_env = + PRE_COMMIT_COLOR commands = pre-commit run --all-files --show-diff-on-failure check-manifest diff --git a/winbuild/README.md b/winbuild/README.md index d8538fbf3..7e81abcb0 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -10,7 +10,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. -* Requires CMake 3.12 or newer (available as Visual Studio component). +* Requires CMake 3.15 or newer (available as Visual Studio component). * Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). @@ -18,12 +18,12 @@ The following is a simplified version of the script used on AppVeyor: ``` set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild -C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends +%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends build\build_dep_all.cmd -build\build_pillow.cmd install cd .. +%PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests -build\build_pillow.cmd bdist_wheel +%PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . ``` diff --git a/winbuild/build.rst b/winbuild/build.rst index 716669771..a8e4ebaa6 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -21,10 +21,13 @@ Download and install: `_ (MSVC C++ build tools, and any Windows SDK version required) -* `CMake 3.12 or newer `_ +* `CMake 3.15 or newer `_ (also available as Visual Studio component C++ CMake tools for Windows) -* x86/x64: `NASM `_ +* `Ninja `_ + (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) + +* x86/x64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -35,53 +38,59 @@ Visual Studio is found automatically with ``vswhere.exe``. Build configuration ------------------- -The following environment variables, if set, will override the default -behaviour of ``build_prepare.py``: +Run ``build_prepare.py`` to configure the build:: -* ``PYTHON`` + ``EXECUTABLE`` point to the target version of Python. - If ``PYTHON`` is unset, the version of Python used to run - ``build_prepare.py`` will be used. If only ``PYTHON`` is set, - ``EXECUTABLE`` defaults to ``python.exe``. -* ``ARCHITECTURE`` is used to select a ``x86``, ``x64`` or ``ARM64`` build. - By default, uses same architecture as the version of Python used to run ``build_prepare.py``. -* ``PILLOW_BUILD`` can be used to override the ``winbuild\build`` directory - path, used to store generated build scripts and compiled libraries. - **Warning:** This directory is wiped when ``build_prepare.py`` is run. -* ``PILLOW_DEPS`` points to the directory used to store downloaded - dependencies. By default ``winbuild\depends`` is used. + usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] + [--depends PILLOW_DEPS] + [--architecture {x86,x64,ARM64}] [--nmake] + [--no-imagequant] [--no-fribidi] -``build_prepare.py`` also supports the following command line parameters: + Download and generate build scripts for Pillow dependencies. -* ``-v`` will print generated scripts. -* ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency -* ``--no-fribidi`` or ``--no-raqm`` will skip optional LGPL-licensed dependency FriBiDi - (required for Raqm text shaping). -* ``--python=`` and ``--executable=`` override ``PYTHON`` and ``EXECUTABLE``. -* ``--architecture=`` overrides ``ARCHITECTURE``. -* ``--dir=`` and ``--depends=`` override ``PILLOW_BUILD`` - and ``PILLOW_DEPS``. + options: + -h, --help show this help message and exit + -v, --verbose print generated scripts + -d PILLOW_BUILD, --dir PILLOW_BUILD, --build-dir PILLOW_BUILD + build directory (default: 'winbuild\build') + --depends PILLOW_DEPS + directory used to store cached dependencies (default: + 'winbuild\depends') + --architecture {x86,x64,ARM64} + build architecture (default: same as host Python) + --nmake build dependencies using NMake instead of Ninja + --no-imagequant skip GPL-licensed optional dependency libimagequant + --no-fribidi, --no-raqm + skip LGPL-licensed optional dependency FriBiDi + + Arguments can also be supplied using the environment variables PILLOW_BUILD, + PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. + +**Warning:** The build directory is wiped when ``build_prepare.py`` is run. Dependencies ------------ Dependencies will be automatically downloaded by ``build_prepare.py``. By default, downloaded dependencies are stored in ``winbuild\depends``; -set the ``PILLOW_DEPS`` environment variable to override this location. +use the ``--depends`` argument or ``PILLOW_DEPS`` environment variable +to override this location. To build all dependencies, run ``winbuild\build\build_dep_all.cmd``, -or run the individual scripts to build each dependency separately. +or run the individual scripts in order to build each dependency separately. Building Pillow --------------- -Once the dependencies are built, run -``winbuild\build\build_pillow.cmd install`` to build and install -Pillow for the selected version of Python. -``winbuild\build\build_pillow.cmd bdist_wheel`` will build wheels -instead of installing Pillow. +Once the dependencies are built, make sure the required environment variables +are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip:: -You can also use ``winbuild\build\build_pillow.cmd --inplace develop`` to build -and install Pillow in develop mode (instead of ``python3 -m pip install --editable``). + winbuild\build\build_env.cmd + python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . + +To build a wheel instead, run:: + + winbuild\build\build_env.cmd + python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . Testing Pillow -------------- @@ -96,17 +105,16 @@ directory. Example ------- -The following is a simplified version of the script used on AppVeyor: - -.. code-block:: +The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild - C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends + %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd - build\build_pillow.cmd install + build\build_env.cmd cd .. + %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - build\build_pillow.cmd bdist_wheel + %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 35980f19c..812799ac2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,10 +1,10 @@ +import argparse import os import platform import re import shutil import struct import subprocess -import sys def cmd_cd(path): @@ -55,24 +55,28 @@ def cmd_nmake(makefile=None, target="", params=None): ) -def cmd_cmake(params=None, file="."): - if params is None: - params = "" - elif isinstance(params, (list, tuple)): - params = " ".join(params) - else: - params = str(params) - return " ".join( - [ - "{cmake}", - "-DCMAKE_VERBOSE_MAKEFILE=ON", - "-DCMAKE_RULE_MESSAGES:BOOL=OFF", - "-DCMAKE_BUILD_TYPE=Release", - f"{params}", - '-G "NMake Makefiles"', - f'"{file}"', - ] - ) +def cmds_cmake(target, *params): + if not isinstance(target, str): + target = " ".join(target) + + return [ + " ".join( + [ + "{cmake}", + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake + "-DCMAKE_C_COMPILER=cl.exe", # for Ninja + "-DCMAKE_CXX_COMPILER=cl.exe", # for Ninja + "-DCMAKE_C_FLAGS=-nologo", + "-DCMAKE_CXX_FLAGS=-nologo", + *params, + '-G "{cmake_generator}"', + ".", + ] + ), + f"{{cmake}} --build . --clean-first --parallel --target {target}", + ] def cmd_msbuild( @@ -98,39 +102,27 @@ architectures = { "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } -header = [ - cmd_set("INCLUDE", "{inc_dir}"), - cmd_set("INCLIB", "{lib_dir}"), - cmd_set("LIB", "{lib_dir}"), - cmd_append("PATH", "{bin_dir}"), -] - # dependencies, listed in order of compilation deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.5.1/libjpeg-turbo-2.1.5.1.tar.gz/download", - "filename": "libjpeg-turbo-2.1.5.1.tar.gz", - "dir": "libjpeg-turbo-2.1.5.1", + + "/libjpeg-turbo/files/3.0.0/libjpeg-turbo-3.0.0.tar.gz/download", + "filename": "libjpeg-turbo-3.0.0.tar.gz", + "dir": "libjpeg-turbo-3.0.0", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" ".+(libjpeg-turbo Licenses\n======================\n\n.+)$" ), "build": [ - cmd_cmake( - [ - "-DENABLE_SHARED:BOOL=FALSE", - "-DWITH_JPEG8:BOOL=TRUE", - "-DWITH_CRT_DLL:BOOL=TRUE", - ] + *cmds_cmake( + ("jpeg-static", "cjpeg-static", "djpeg-static"), + "-DENABLE_SHARED:BOOL=FALSE", + "-DWITH_JPEG8:BOOL=TRUE", + "-DWITH_CRT_DLL:BOOL=TRUE", ), - cmd_nmake(target="clean"), - cmd_nmake(target="jpeg-static"), cmd_copy("jpeg-static.lib", "libjpeg.lib"), - cmd_nmake(target="cjpeg-static"), cmd_copy("cjpeg-static.exe", "cjpeg.exe"), - cmd_nmake(target="djpeg-static"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], "headers": ["j*.h"], @@ -138,9 +130,9 @@ deps = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": "https://zlib.net/zlib1213.zip", - "filename": "zlib1213.zip", - "dir": "zlib-1.2.13", + "url": "https://zlib.net/zlib13.zip", + "filename": "zlib13.zip", + "dir": "zlib-1.3", "license": "README", "license_pattern": "Copyright notice:\n\n(.+)$", "build": [ @@ -152,34 +144,22 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.1.tar.gz/download", - "filename": "xz-5.4.1.tar.gz", - "dir": "xz-5.4.1", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.4.tar.gz/download", + "filename": "xz-5.4.4.tar.gz", + "dir": "xz-5.4.4", "license": "COPYING", - "patch": { - r"src\liblzma\api\lzma.h": { - "#ifndef LZMA_API_IMPORT": "#ifndef LZMA_API_IMPORT\n#define LZMA_API_STATIC", # noqa: E501 - }, - r"windows\vs2019\liblzma.vcxproj": { - # retarget to default toolset (selected by vcvarsall.bat) - "v142": "$(DefaultPlatformToolset)", # noqa: E501 - # retarget to latest (selected by vcvarsall.bat) - "10.0": "$(WindowsSDKVersion)", # noqa: E501 - }, - }, "build": [ - cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Clean"), - cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Build"), + *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], "headers": [r"src\liblzma\api\lzma.h"], - "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"], + "libs": [r"liblzma.lib"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.0.tar.gz", - "filename": "libwebp-1.3.0.tar.gz", - "dir": "libwebp-1.3.0", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.1.tar.gz", + "filename": "libwebp-1.3.1.tar.gz", + "dir": "libwebp-1.3.1", "license": "COPYING", "build": [ cmd_rmdir(r"output\release-static"), # clean @@ -200,9 +180,9 @@ deps = { "libs": [r"output\release-static\{architecture}\lib\*.lib"], }, "libtiff": { - "url": "https://download.osgeo.org/libtiff/tiff-4.5.0.tar.gz", - "filename": "tiff-4.5.0.tar.gz", - "dir": "tiff-4.5.0", + "url": "https://download.osgeo.org/libtiff/tiff-4.5.1.tar.gz", + "filename": "tiff-4.5.1.tar.gz", + "dir": "tiff-4.5.1", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -213,11 +193,19 @@ deps = { # link against webp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 }, + r"test\CMakeLists.txt": { + "add_executable(test_write_read_tags ../placeholder.h)": "", + "target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501 + "target_link_libraries(test_write_read_tags PRIVATE tiff)": "", + "list(APPEND simple_tests test_write_read_tags)": "", + }, }, "build": [ - cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), - cmd_nmake(target="clean"), - cmd_nmake(target="tiff"), + *cmds_cmake( + "tiff", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', + ) ], "headers": [r"libtiff\tiff*.h"], "libs": [r"libtiff\*.lib"], @@ -229,10 +217,7 @@ deps = { "dir": "lpng1639", "license": "LICENSE", "build": [ - # lint: do not inline - cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), - cmd_nmake(target="clean"), - cmd_nmake(), + *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), cmd_copy("libpng16_static.lib", "libpng16.lib"), ], "headers": [r"png*.h"], @@ -244,18 +229,15 @@ deps = { "dir": "brotli-1.0.9", "license": "LICENSE", "build": [ - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="brotlicommon-static"), - cmd_nmake(target="brotlidec-static"), + *cmds_cmake(("brotlicommon-static", "brotlidec-static")), cmd_xcopy(r"c\include", "{inc_dir}"), ], "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.0.tar.gz", # noqa: E501 - "filename": "freetype-2.13.0.tar.gz", - "dir": "freetype-2.13.0", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.gz", # noqa: E501 + "filename": "freetype-2.13.2.tar.gz", + "dir": "freetype-2.13.2", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -289,9 +271,9 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download", - "filename": "lcms2-2.14.tar.gz", - "dir": "lcms2-2.14", + "url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download", + "filename": "lcms2-2.15.tar.gz", + "dir": "lcms2-2.15", "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -325,9 +307,9 @@ deps = { } }, "build": [ - cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), - cmd_nmake(target="clean"), - cmd_nmake(target="openjp2"), + *cmds_cmake( + "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" + ), cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"), ], @@ -346,40 +328,36 @@ deps = { } }, "build": [ - # lint: do not inline - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="imagequant_a"), + *cmds_cmake("imagequant_a"), cmd_copy("imagequant_a.lib", "imagequant.lib"), ], "headers": [r"*.h"], "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.1.zip", - "filename": "harfbuzz-7.0.1.zip", - "dir": "harfbuzz-7.0.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/8.1.1.zip", + "filename": "harfbuzz-8.1.1.zip", + "dir": "harfbuzz-8.1.1", "license": "COPYING", "build": [ - cmd_set("CXXFLAGS", "-d2FH4-"), - cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), - cmd_nmake(target="clean"), - cmd_nmake(target="harfbuzz"), + *cmds_cmake( + "harfbuzz", + "-DHB_HAVE_FREETYPE:BOOL=TRUE", + '-DCMAKE_CXX_FLAGS="-nologo -d2FH4-"', + ), ], "headers": [r"src\*.h"], "libs": [r"*.lib"], }, "fribidi": { - "url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip", - "filename": "fribidi-1.0.12.zip", - "dir": "fribidi-1.0.12", + "url": "https://github.com/fribidi/fribidi/archive/v1.0.13.zip", + "filename": "fribidi-1.0.13.zip", + "dir": "fribidi-1.0.13", "license": "COPYING", "build": [ - cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), + cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="fribidi"), + *cmds_cmake("fribidi"), ], "bins": [r"*.dll"], }, @@ -421,23 +399,12 @@ def find_msvs(): print("Visual Studio seems to be missing C compiler") return None - vs = { - "header": [], - # nmake selected by vcvarsall - "nmake": "nmake.exe", - "vs_dir": vspath, - } - # vs2017 msbuild = os.path.join(vspath, "MSBuild", "15.0", "Bin", "MSBuild.exe") - if os.path.isfile(msbuild): - vs["msbuild"] = f'"{msbuild}"' - else: + if not os.path.isfile(msbuild): # vs2019 msbuild = os.path.join(vspath, "MSBuild", "Current", "Bin", "MSBuild.exe") - if os.path.isfile(msbuild): - vs["msbuild"] = f'"{msbuild}"' - else: + if not os.path.isfile(msbuild): print("Visual Studio MSBuild not found") return None @@ -445,9 +412,13 @@ def find_msvs(): if not os.path.isfile(vcvarsall): print("Visual Studio vcvarsall not found") return None - vs["header"].append(f'call "{vcvarsall}" {{vcvars_arch}}') - return vs + return { + "vs_dir": vspath, + "msbuild": f'"{msbuild}"', + "vcvarsall": f'"{vcvarsall}"', + "nmake": "nmake.exe", # nmake selected by vcvarsall + } def extract_dep(url, filename): @@ -455,7 +426,7 @@ def extract_dep(url, filename): import urllib.request import zipfile - file = os.path.join(depends_dir, filename) + file = os.path.join(args.depends_dir, filename) if not os.path.exists(file): ex = None for i in range(3): @@ -496,12 +467,12 @@ def extract_dep(url, filename): def write_script(name, lines): - name = os.path.join(build_dir, name) + name = os.path.join(args.build_dir, name) lines = [line.format(**prefs) for line in lines] print("Writing " + name) with open(name, "w", newline="") as f: f.write(os.linesep.join(lines)) - if verbose: + if args.verbose: for line in lines: print(" " + line) @@ -517,6 +488,22 @@ def get_footer(dep): return lines +def build_env(): + lines = [ + "if defined DISTUTILS_USE_SDK goto end", + cmd_set("INCLUDE", "{inc_dir}"), + cmd_set("INCLIB", "{lib_dir}"), + cmd_set("LIB", "{lib_dir}"), + cmd_append("PATH", "{bin_dir}"), + "call {vcvarsall} {vcvars_arch}", + cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow + cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT + ":end", + "@echo on", + ] + write_script("build_env.cmd", lines) + + def build_dep(name): dep = deps[name] dir = dep["dir"] @@ -554,11 +541,11 @@ def build_dep(name): banner = f"Building {name} ({dir})" lines = [ + r'call "{build_dir}\build_env.cmd"', "@echo " + ("=" * 70), f"@echo ==== {banner:<60} ====", "@echo " + ("=" * 70), - "cd /D %s" % os.path.join(sources_dir, dir), - *prefs["header"], + cmd_cd(os.path.join(sources_dir, dir)), *dep.get("build", []), *get_footer(dep), ] @@ -568,81 +555,87 @@ def build_dep(name): def build_dep_all(): - lines = ["@echo on"] + lines = [r'call "{build_dir}\build_env.cmd"'] for dep_name in deps: + print() if dep_name in disabled: + print(f"Skipping disabled dependency {dep_name}") continue script = build_dep(dep_name) lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + print() lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines) -def build_pillow(): - lines = [ - "@echo ---- Building Pillow (build_ext %*) ----", - cmd_cd("{pillow_dir}"), - *prefs["header"], - cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow - cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT - r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501 - ] - - write_script("build_pillow.cmd", lines) - - if __name__ == "__main__": - # winbuild directory winbuild_dir = os.path.dirname(os.path.realpath(__file__)) + pillow_dir = os.path.realpath(os.path.join(winbuild_dir, "..")) - verbose = False - disabled = [] - depends_dir = os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")) - python_dir = os.environ.get("PYTHON") - python_exe = os.environ.get("EXECUTABLE", "python.exe") - architecture = os.environ.get( - "ARCHITECTURE", - "ARM64" - if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64"), + parser = argparse.ArgumentParser( + prog="winbuild\\build_prepare.py", + description="Download and generate build scripts for Pillow dependencies.", + epilog="""Arguments can also be supplied using the environment variables + PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\\build.rst + for more information.""", ) - build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")) - sources_dir = "" - for arg in sys.argv[1:]: - if arg == "-v": - verbose = True - elif arg == "--no-imagequant": - disabled += ["libimagequant"] - elif arg == "--no-raqm" or arg == "--no-fribidi": - disabled += ["fribidi"] - elif arg.startswith("--depends="): - depends_dir = arg[10:] - elif arg.startswith("--python="): - python_dir = arg[9:] - elif arg.startswith("--executable="): - python_exe = arg[13:] - elif arg.startswith("--architecture="): - architecture = arg[15:] - elif arg.startswith("--dir="): - build_dir = arg[6:] - elif arg == "--srcdir": - sources_dir = os.path.sep + "src" - else: - msg = "Unknown parameter: " + arg - raise ValueError(msg) + parser.add_argument( + "-v", "--verbose", action="store_true", help="print generated scripts" + ) + parser.add_argument( + "-d", + "--dir", + "--build-dir", + dest="build_dir", + metavar="PILLOW_BUILD", + default=os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")), + help="build directory (default: 'winbuild\\build')", + ) + parser.add_argument( + "--depends", + dest="depends_dir", + metavar="PILLOW_DEPS", + default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), + help="directory used to store cached dependencies " + "(default: 'winbuild\\depends')", + ) + parser.add_argument( + "--architecture", + choices=architectures, + default=os.environ.get( + "ARCHITECTURE", + ( + "ARM64" + if platform.machine() == "ARM64" + else ("x86" if struct.calcsize("P") == 4 else "x64") + ), + ), + help="build architecture (default: same as host Python)", + ) + parser.add_argument( + "--nmake", + dest="cmake_generator", + action="store_const", + const="NMake Makefiles", + default="Ninja", + help="build dependencies using NMake instead of Ninja", + ) + parser.add_argument( + "--no-imagequant", + action="store_true", + help="skip GPL-licensed optional dependency libimagequant", + ) + parser.add_argument( + "--no-fribidi", + "--no-raqm", + action="store_true", + help="skip LGPL-licensed optional dependency FriBiDi", + ) + args = parser.parse_args() - # dependency cache directory - os.makedirs(depends_dir, exist_ok=True) - print("Caching dependencies in:", depends_dir) - - if python_dir is None: - python_dir = os.path.dirname(os.path.realpath(sys.executable)) - python_exe = os.path.basename(sys.executable) - print("Target Python:", os.path.join(python_dir, python_exe)) - - arch_prefs = architectures[architecture] - print("Target Architecture:", architecture) + arch_prefs = architectures[args.architecture] + print("Target architecture:", args.architecture) msvs = find_msvs() if msvs is None: @@ -650,35 +643,44 @@ if __name__ == "__main__": raise RuntimeError(msg) print("Found Visual Studio at:", msvs["vs_dir"]) - print("Using output directory:", build_dir) + # dependency cache directory + args.depends_dir = os.path.abspath(args.depends_dir) + os.makedirs(args.depends_dir, exist_ok=True) + print("Caching dependencies in:", args.depends_dir) + + args.build_dir = os.path.abspath(args.build_dir) + print("Using output directory:", args.build_dir) # build directory for *.h files - inc_dir = os.path.join(build_dir, "inc") + inc_dir = os.path.join(args.build_dir, "inc") # build directory for *.lib files - lib_dir = os.path.join(build_dir, "lib") + lib_dir = os.path.join(args.build_dir, "lib") # build directory for *.bin files - bin_dir = os.path.join(build_dir, "bin") + bin_dir = os.path.join(args.build_dir, "bin") # directory for storing project files - sources_dir = build_dir + sources_dir + sources_dir = os.path.join(args.build_dir, "src") # copy dependency licenses to this directory - license_dir = os.path.join(build_dir, "license") + license_dir = os.path.join(args.build_dir, "license") - shutil.rmtree(build_dir, ignore_errors=True) - os.makedirs(build_dir, exist_ok=False) + shutil.rmtree(args.build_dir, ignore_errors=True) + os.makedirs(args.build_dir, exist_ok=False) for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]: os.makedirs(path, exist_ok=True) + disabled = [] + if args.no_imagequant: + disabled += ["libimagequant"] + if args.no_fribidi: + disabled += ["fribidi"] + prefs = { - # Python paths / preferences - "python_dir": python_dir, - "python_exe": python_exe, - "architecture": architecture, + "architecture": args.architecture, **arch_prefs, # Pillow paths - "pillow_dir": os.path.realpath(os.path.join(winbuild_dir, "..")), + "pillow_dir": pillow_dir, "winbuild_dir": winbuild_dir, # Build paths - "build_dir": build_dir, + "build_dir": args.build_dir, "inc_dir": inc_dir, "lib_dir": lib_dir, "bin_dir": bin_dir, @@ -687,9 +689,8 @@ if __name__ == "__main__": # Compilers / Tools **msvs, "cmake": "cmake.exe", # TODO find CMAKE automatically + "cmake_generator": args.cmake_generator, # TODO find NASM automatically - # script header - "header": sum([header, msvs["header"], ["@echo on"]], []), } for k, v in deps.items(): @@ -698,5 +699,5 @@ if __name__ == "__main__": print() write_script(".gitignore", ["*"]) + build_env() build_dep_all() - build_pillow()