diff --git a/.appveyor.yml b/.appveyor.yml index 4e2ca1071..d525e4cfc 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,29 +10,29 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python39 + - PYTHON: C:/Python310 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - - PYTHON: C:/Python36-x64 + - PYTHON: C:/Python37-x64 ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 install: -- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip +- '%PYTHON%\%EXECUTABLE% --version' +- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip - 7z x pillow-depends.zip -oc:\ -- mv c:\pillow-depends-master c:\pillow-depends +- mv c:\pillow-depends-main c:\pillow-depends - xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images -- 7z x ..\pillow-depends\nasm-2.14.02-win64.zip -oc:\ -- ..\pillow-depends\gs9533w32.exe /S -- path c:\nasm-2.14.02;C:\Program Files (x86)\gs\gs9.53.3\bin;%PATH% +- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ +- ..\pillow-depends\gs9550w32.exe /S +- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python37\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% -- '%PYTHON%\%EXECUTABLE% -m pip install -U setuptools' build_script: - ps: | @@ -84,7 +84,7 @@ deploy: artifact: /.*egg|wheel/ on: APPVEYOR_REPO_NAME: python-pillow/Pillow - branch: master + branch: main deploy: YES diff --git a/.ci/install.sh b/.ci/install.sh index 4917b3a7c..c48acf9ee 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -22,24 +22,20 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ cmake imagemagick libharfbuzz-dev libfribidi-dev python3 -m pip install --upgrade pip +python3 -m pip install --upgrade wheel PYTHONOPTIMIZE=0 python3 -m pip install cffi python3 -m pip install coverage +python3 -m pip install defusedxml python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install test-image-results -# TODO Remove condition when numpy supports 3.10 -if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi - -# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: -if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi -if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi +python3 -m pip install numpy # PyQt5 doesn't support PyPy3 -# Wheel doesn't yet support 3.10 -if [[ $GHA_PYTHON_VERSION == 3.* && $GHA_PYTHON_VERSION != "3.10-dev" ]]; then +if [[ $GHA_PYTHON_VERSION == 3.* ]]; then # arm64, ppc64le, s390x CPUs: # "ERROR: Could not find a version that satisfies the requirement pyqt5" sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools diff --git a/.ci/test.sh b/.ci/test.sh index 9d2c123da..8ff7c5f64 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -4,4 +4,4 @@ set -e python3 -c "from PIL import Image" -python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests +python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 35bd47be8..bc9587744 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,13 +4,13 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v ## Bug fixes, feature additions, etc. -Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil +Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil - Fork the Pillow repository. -- Create a branch from master. +- Create a branch from `main`. - Develop bug fixes, features, tests, etc. - Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. -- Create a pull request to pull the changes from your branch to the Pillow master. +- Create a pull request to pull the changes from your branch to the Pillow `main`. ### Guidelines @@ -18,7 +18,7 @@ Please send a pull request to the master branch. Please include [documentation]( - Provide tests for any newly added code. - 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/master/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. +- 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. ## Reporting Issues @@ -35,4 +35,4 @@ The best reproductions are self-contained scripts with minimal dependencies. If ## Security vulnerabilities -Please see our [security policy](https://github.com/python-pillow/Pillow/blob/master/.github/SECURITY.md). +Please see our [security policy](https://github.com/python-pillow/Pillow/blob/main/.github/SECURITY.md). diff --git a/.github/mergify.yml b/.github/mergify.yml index 4b8b113d3..8b289bda6 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -7,6 +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=continuous-integration/appveyor/pr actions: merge: diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 9fe8f774f..7e2fbf28f 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -1,4 +1,5 @@ name: CIFuzz + on: push: paths: @@ -8,6 +9,7 @@ on: paths: - "**.c" - "**.h" + workflow_dispatch: jobs: Fuzzing: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bddeb6150..533ce8cbd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,6 @@ name: Lint -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: @@ -12,14 +12,6 @@ jobs: steps: - uses: actions/checkout@v2 - - name: pip cache - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: lint-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - lint-pip- - - name: pre-commit cache uses: actions/cache@v2 with: @@ -31,7 +23,9 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: "3.10" + cache: pip + cache-dependency-path: "setup.py" - name: Build system information run: python3 .github/workflows/system-info.py @@ -45,4 +39,3 @@ jobs: run: tox -e lint env: PRE_COMMIT_COLOR: always - diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index f45824445..8260cf8d8 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -6,6 +6,7 @@ brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype op PYTHONOPTIMIZE=0 python3 -m pip install cffi python3 -m pip install coverage +python3 -m pip install defusedxml python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov @@ -14,12 +15,7 @@ python3 -m pip install pyroma python3 -m pip install test-image-results echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg -# TODO Remove condition when numpy supports 3.10 -if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi - -# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: -if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi -if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi +python3 -m pip install numpy # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 52456597b..ad66117b1 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -4,14 +4,15 @@ on: push: # branches to consider in the event; optional, defaults to all branches: - - master + - main + workflow_dispatch: jobs: update_release_draft: if: github.repository == 'python-pillow/Pillow' runs-on: ubuntu-latest steps: - # Drafts your next release notes as pull requests are merged into "master" + # Drafts your next release notes as pull requests are merged into "main" - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 2ecc27460..57396fddc 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -1,6 +1,6 @@ name: Test Docker -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: @@ -20,13 +20,14 @@ jobs: arch, centos-7-amd64, centos-8-amd64, + centos-stream-8-amd64, debian-10-buster-x86, - fedora-32-amd64, - fedora-33-amd64, + fedora-34-amd64, + fedora-35-amd64, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ] - dockerTag: [master] + dockerTag: [main] include: - docker: "ubuntu-20.04-focal-arm64v8" qemu-arch: "aarch64" diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml new file mode 100644 index 000000000..d94c7d537 --- /dev/null +++ b/.github/workflows/test-mingw.yml @@ -0,0 +1,84 @@ +name: Test MinGW + +on: [push, pull_request, workflow_dispatch] + +jobs: + build: + runs-on: windows-2019 + 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 }} + CHERE_INVOKING: 1 + + timeout-minutes: 30 + name: ${{ matrix.name }} + + steps: + - name: Checkout Pillow + uses: actions/checkout@v2 + + - name: Set up shell + run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH + shell: pwsh + + - name: Install dependencies + run: | + pacman -S --noconfirm \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python-pyqt6 \ + ${{ matrix.package }}-python3-setuptools \ + ${{ matrix.package }}-freetype \ + ${{ 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 \ + subversion + + 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" . + + - name: Test Pillow + run: | + python3 selftest.py --installed + python3 -c "from PIL import Image" + python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + + - name: Upload coverage + run: | + python3 -m pip install codecov + bash <(curl -s https://codecov.io/bash) -F GHA_Windows + env: + CODECOV_NAME: ${{ matrix.name }} + + success: + needs: build + runs-on: ubuntu-latest + name: MinGW Test Successful + steps: + - name: Success + run: echo MinGW Test Successful diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 7b8474d0f..4a8966ca8 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -11,6 +11,7 @@ on: paths: - "**.c" - "**.h" + workflow_dispatch: jobs: build: @@ -22,7 +23,7 @@ jobs: docker: [ ubuntu-20.04-focal-amd64-valgrind, ] - dockerTag: [master] + dockerTag: [main] name: ${{ matrix.docker }} @@ -42,11 +43,3 @@ jobs: sudo chown -R 1000 $GITHUB_WORKSPACE docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} sudo chown -R runner $GITHUB_WORKSPACE - - success: - needs: build - runs-on: ubuntu-latest - name: Valgrind Test Successful - steps: - - name: Success - run: echo Valgrind Test Successful diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e3b2201a7..c768838eb 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -1,6 +1,6 @@ name: Test Windows -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: @@ -8,21 +8,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.6", "pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10-dev"] + python-version: ["3.7", "3.8", "3.9", "3.10"] architecture: ["x86", "x64"] include: - - architecture: "x86" - platform-vcvars: "x86" - platform-msbuild: "Win32" - - architecture: "x64" - platform-vcvars: "x86_amd64" - platform-msbuild: "x64" - exclude: - # PyPy does not support 64-bit on Windows - - python-version: "pypy-3.6" - architecture: "x64" + # PyPy 7.3.4+ only ships 64-bit binaries for Windows - python-version: "pypy-3.7" architecture: "x64" + - python-version: "pypy-3.8" + architecture: "x64" + timeout-minutes: 30 name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} @@ -37,42 +31,29 @@ jobs: repository: python-pillow/pillow-depends path: winbuild\depends - - name: Cache pip - uses: actions/cache@v2 - with: - path: ~\AppData\Local\pip\Cache - key: - ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}-${{ hashFiles('**/.github/workflows/test-windows.yml') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}- - ${{ runner.os }}-${{ matrix.python-version }}- - # sets env: pythonLocation - name: Set up Python uses: actions/setup-python@v2 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: python .github/workflows/system-info.py - - name: python -m pip install wheel pytest pytest-cov pytest-timeout - run: python -m pip install wheel pytest pytest-cov pytest-timeout - - # TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: - - name: Upgrade setuptools - if: "contains(matrix.python-version, '3.8') || contains(matrix.python-version, '3.9')" - run: python -m pip install -U "setuptools>=49.3.2" + - name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install run: | - 7z x winbuild\depends\nasm-2.14.02-win64.zip "-o$env:RUNNER_WORKSPACE\" - echo "$env:RUNNER_WORKSPACE\nasm-2.14.02" >> $env:GITHUB_PATH + 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 - winbuild\depends\gs9533w32.exe /S - echo "C:\Program Files (x86)\gs\gs9.53.3\bin" >> $env:GITHUB_PATH + winbuild\depends\gs9550w32.exe /S + echo "C:\Program Files (x86)\gs\gs9.55.0\bin" >> $env:GITHUB_PATH xcopy /S /Y winbuild\depends\test_images\* Tests\images\ @@ -151,7 +132,7 @@ jobs: - name: Build Pillow run: | $FLAGS="" - if ('${{ github.event_name }}' -eq 'push') { $FLAGS="--disable-imagequant" } + if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" } & winbuild\build\build_pillow.cmd $FLAGS install & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh @@ -194,92 +175,20 @@ jobs: - name: Build wheel id: wheel - if: "github.event_name == 'push'" + if: "github.event_name != 'pull_request'" run: | for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel shell: cmd - uses: actions/upload-artifact@v2 - if: "github.event_name == 'push'" + if: "github.event_name != 'pull_request'" with: name: ${{ steps.wheel.outputs.dist }} path: dist\*.whl - msys: - runs-on: windows-2019 - - 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 }} - CHERE_INVOKING: 1 - - timeout-minutes: 30 - name: ${{ matrix.name }} - - steps: - - uses: actions/checkout@v2 - - - name: Set up shell - run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH - shell: pwsh - - - name: Install Dependencies - run: | - pacman -S --noconfirm \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-pyqt5 \ - ${{ matrix.package }}-python3-setuptools \ - ${{ matrix.package }}-freetype \ - ${{ 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 \ - subversion - - python3 -m pip install pyroma pytest pytest-cov - - pushd depends && ./install_extra_test_images.sh && popd - - - name: Build Pillow - run: CFLAGS="-coverage" python3 setup.py build_ext install - - - name: Test Pillow - run: | - python3 selftest.py --installed - python3 -c "from PIL import Image" - python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - - - name: Upload coverage - run: | - python3 -m pip install codecov - bash <(curl -s https://codecov.io/bash) -F GHA_Windows - env: - CODECOV_NAME: ${{ matrix.name }} - success: - needs: [build, msys] + needs: build runs-on: ubuntu-latest name: Windows Test Successful steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e52fefc69..414c7e94e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: Test -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: @@ -9,27 +9,27 @@ jobs: fail-fast: false matrix: os: [ + "macos-latest", "ubuntu-latest", - "macOS-latest", ] python-version: [ + "pypy-3.8", "pypy-3.7", - "pypy-3.6", - "3.10-dev", + "3.10", "3.9", "3.8", "3.7", - "3.6", ] include: - - python-version: "3.6" - PYTHONOPTIMIZE: 1 - python-version: "3.7" + PYTHONOPTIMIZE: 1 + REVERSE: "--reverse" + - python-version: "3.8" PYTHONOPTIMIZE: 2 # Include new variables for Codecov - os: ubuntu-latest codecov-flag: GHA_Ubuntu - - os: macOS-latest + - os: macos-latest codecov-flag: GHA_macOS runs-on: ${{ matrix.os }} @@ -42,20 +42,8 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(python3 -m pip cache dir)" - - - name: pip cache - uses: actions/cache@v2 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: - ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.ci/*.sh') }} - restore-keys: | - ${{ matrix.os }}-${{ matrix.python-version }}- + cache: pip + cache-dependency-path: ".ci/*.sh" - name: Build system information run: python3 .github/workflows/system-info.py @@ -71,8 +59,6 @@ jobs: if: startsWith(matrix.os, 'macOS') run: | .github/workflows/macos-install.sh - env: - GHA_PYTHON_VERSION: ${{ matrix.python-version }} - name: Build run: | @@ -80,6 +66,9 @@ jobs: - name: Test run: | + if [ $REVERSE ]; then + python3 -m pip install pytest-reverse + fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh else @@ -87,6 +76,7 @@ jobs: fi env: PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }} + REVERSE: ${{ matrix.REVERSE }} - name: Prepare to upload errors if: failure() @@ -103,7 +93,7 @@ jobs: - name: Docs if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9 run: | - python3 -m pip install sphinx-issues sphinx-removed-in sphinx-rtd-theme + python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph make doccheck - name: After success diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml new file mode 100644 index 000000000..c2b8b3bda --- /dev/null +++ b/.github/workflows/tidelift.yml @@ -0,0 +1,26 @@ +name: Tidelift Align +on: + schedule: + - cron: "30 2 * * *" # daily at 02:30 UTC + push: + paths: + - ".github/workflows/tidelift.yml" + pull_request: + paths: + - ".github/workflows/tidelift.yml" + workflow_dispatch: + +jobs: + build: + if: github.repository_owner == 'python-pillow' + name: Run Tidelift to ensure approved open source packages are in use + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Scan + uses: tidelift/alignment-action@main + env: + TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }} + TIDELIFT_ORGANIZATION: team/aclark4life + TIDELIFT_PROJECT: pillow diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d38375f0..822fa43ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,43 +1,46 @@ repos: - repo: https://github.com/psf/black - rev: e66be67b9b6811913470f70c28b4d50f94d05b22 # frozen: 20.8b1 + rev: f1d4e742c91dd5179d742b0db9293c4472b765f8 # frozen: 21.12b0 hooks: - id: black - args: ["--target-version", "py36"] + args: ["--target-version", "py37"] # Only .py files, until https://github.com/psf/black/issues/402 resolved files: \.py$ types: [] - repo: https://github.com/PyCQA/isort - rev: 377d260ffa6f746693f97b46d95025afc4bd8275 # frozen: 5.4.2 + rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1 # frozen: 5.10.1 hooks: - id: isort - repo: https://github.com/asottile/yesqa - rev: 7a009f3ee493c796827ee334f9058b110a0e0db8 # frozen: v1.2.1 + rev: 35cf7dc24fa922927caded7a21b2a8cb04bf8e10 # frozen: v1.3.0 hooks: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: f30f4974a08a6b2f6a1eeaf30a4d501cf909163a # frozen: v1.1.9 + rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) - - repo: https://gitlab.com/pycqa/flake8 - rev: 05f6544aef321e2fee03a1277ce2eef8880fb927 # frozen: 3.8.3 + - repo: https://github.com/PyCQA/flake8 + rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks - rev: eae6397e4c259ed3d057511f6dd5330b92867e62 # frozen: v1.6.0 + rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0 hooks: - id: python-check-blanket-noqa - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: e1668fe86af3810fbca72b8653fe478e66a0afdc # frozen: v3.2.0 + rev: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26 # frozen: v4.1.0 hooks: - id: check-merge-conflict - id: check-yaml + +ci: + autoupdate_schedule: quarterly diff --git a/CHANGES.rst b/CHANGES.rst index a2d40305f..c2d4892cb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,414 @@ Changelog (Pillow) ================== -8.2.0 (unreleased) +9.0.0 (2022-01-02) ------------------ +- Restrict builtins for ImageMath.eval(). CVE TBD #5923 + [radarhere] + +- Ensure JpegImagePlugin stops at the end of a truncated file #5921 + [radarhere] + +- Fixed ImagePath.Path array handling. CVEs TBD #5920 + [radarhere] + +- Remove consecutive duplicate tiles that only differ by their offset #5919 + [radarhere] + +- Improved I;16 operations on big endian #5901 + [radarhere] + +- Limit quantized palette to number of colors #5879 + [radarhere] + +- Fixed palette index for zeroed color in FASTOCTREE quantize #5869 + [radarhere] + +- When saving RGBA to GIF, make use of first transparent palette entry #5859 + [radarhere] + +- Pass SAMPLEFORMAT to libtiff #5848 + [radarhere] + +- Added rounding when converting P and PA #5824 + [radarhere] + +- Improved putdata() documentation and data handling #5910 + [radarhere] + +- Exclude carriage return in PDF regex to help prevent ReDoS #5912 + [hugovk] + +- Fixed freeing pointer in ImageDraw.Outline.transform #5909 + [radarhere] + +- Added ImageShow support for xdg-open #5897 + [m-shinder, radarhere] + +- Support 16-bit grayscale ImageQt conversion #5856 + [cmbruns, radarhere] + +- Convert subsequent GIF frames to RGB or RGBA #5857 + [radarhere] + +- Do not prematurely return in ImageFile when saving to stdout #5665 + [infmagic2047, radarhere] + +- Added support for top right and bottom right TGA orientations #5829 + [radarhere] + +- Corrected ICNS file length in header #5845 + [radarhere] + +- Block tile TIFF tags when saving #5839 + [radarhere] + +- Added line width argument to polygon #5694 + [radarhere] + +- Do not redeclare class each time when converting to NumPy #5844 + [radarhere] + +- Only prevent repeated polygon pixels when drawing with transparency #5835 + [radarhere] + +- Add support for pickling TrueType fonts #5826 + [hugovk, radarhere] + +- Only prefer command line tools SDK on macOS over default MacOSX SDK #5828 + [radarhere] + +- Drop support for soon-EOL Python 3.6 #5768 + [hugovk, nulano, radarhere] + +- Fix compilation on 64-bit Termux #5793 + [landfillbaby] + +- Use title for display in ImageShow #5788 + [radarhere] + +- Remove support for FreeType 2.7 and older #5777 + [hugovk, radarhere] + +- Fix for PyQt6 #5775 + [hugovk, radarhere] + +- Removed deprecated PILLOW_VERSION, Image.show command parameter, Image._showxv and ImageFile.raise_ioerror #5776 + [radarhere] + +8.4.0 (2021-10-15) +------------------ + +- Prefer global transparency in GIF when replacing with background color #5756 + [radarhere] + +- Added "exif" keyword argument to TIFF saving #5575 + [radarhere] + +- Copy Python palette to new image in quantize() #5696 + [radarhere] + +- Read ICO AND mask from end #5667 + [radarhere] + +- Actually check the framesize in FliDecode.c #5659 + [wiredfool] + +- Determine JPEG2000 mode purely from ihdr header box #5654 + [radarhere] + +- Fixed using info dictionary when writing multiple APNG frames #5611 + [radarhere] + +- Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655 + [radarhere] + +- For GIF save_all with palette, do not include palette with each frame #5603 + [radarhere] + +- Keep transparency when converting from P to LA or PA #5606 + [radarhere] + +- Copy palette to new image in transform() #5647 + [radarhere] + +- Added "transparency" argument to EpsImagePlugin load() #5620 + [radarhere] + +- Corrected pathlib.Path detection when saving #5633 + [radarhere] + +- Added WalImageFile class #5618 + [radarhere] + +- Consider I;16 pixel size when drawing text #5598 + [radarhere] + +- If default conversion from P is RGB with transparency, convert to RGBA #5594 + [radarhere] + +- Speed up rotating square images by 90 or 270 degrees #5646 + [radarhere] + +- Add support for reading DPI information from JPEG2000 images + [rogermb, radarhere] + +- Catch TypeError from corrupted DPI value in EXIF #5639 + [homm, radarhere] + +- Do not close file pointer when saving SGI images #5645 + [farizrahman4u, radarhere] + +- Deprecate ImagePalette size parameter #5641 + [radarhere, hugovk] + +- Prefer command line tools SDK on macOS #5624 + [radarhere] + +- Added tags when saving YCbCr TIFF #5597 + [radarhere] + +- PSD layer count may be negative #5613 + [radarhere] + +- Fixed ImageOps expand with tuple border on P image #5615 + [radarhere] + +- Fixed error saving APNG with duplicate frames and different duration times #5609 + [thak1411, radarhere] + +8.3.2 (2021-09-02) +------------------ + +- CVE-2021-23437 Raise ValueError if color specifier is too long + [hugovk, radarhere] + +- Fix 6-byte OOB read in FliDecode + [wiredfool] + +- Add support for Python 3.10 #5569, #5570 + [hugovk, radarhere] + +- Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression #5588 + [kmilos, radarhere] + +- Updates for ``ImagePalette`` channel order #5599 + [radarhere] + +- Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library #5651 + [nulano] + +8.3.1 (2021-07-06) +------------------ + +- Catch OSError when checking if fp is sys.stdout #5585 + [radarhere] + +- Handle removing orientation from alternate types of EXIF data #5584 + [radarhere] + +- Make Image.__array__ take optional dtype argument #5572 + [t-vi, radarhere] + +8.3.0 (2021-07-01) +------------------ + +- Use snprintf instead of sprintf. CVE-2021-34552 #5567 + [radarhere] + +- Limit TIFF strip size when saving with LibTIFF #5514 + [kmilos] + +- Allow ICNS save on all operating systems #4526 + [baletu, radarhere, newpanjing, hugovk] + +- De-zigzag JPEG's DQT when loading; deprecate convert_dict_qtables #4989 + [gofr, radarhere] + +- Replaced xml.etree.ElementTree #5565 + [radarhere] + +- Moved CVE image to pillow-depends #5561 + [radarhere] + +- Added tag data for IFD groups #5554 + [radarhere] + +- Improved ImagePalette #5552 + [radarhere] + +- Add DDS saving #5402 + [radarhere] + +- Improved getxmp() #5455 + [radarhere] + +- Convert to float for comparison with float in IFDRational __eq__ #5412 + [radarhere] + +- Allow getexif() to access TIFF tag_v2 data #5416 + [radarhere] + +- Read FITS image mode and size #5405 + [radarhere] + +- Merge parallel horizontal edges in ImagingDrawPolygon #5347 + [radarhere, hrdrq] + +- Use transparency behind first GIF frame and when disposing to background #5557 + [radarhere, zewt] + +- Avoid unstable nature of qsort in Quant.c #5367 + [radarhere] + +- Copy palette to new images in ImageOps expand #5551 + [radarhere] + +- Ensure palette string matches RGB mode #5549 + [radarhere] + +- Do not modify EXIF of original image instance in exif_transpose() #5547 + [radarhere] + +- Fixed default numresolution for small JPEG2000 images #5540 + [radarhere] + +- Added DDS BC5 reading #5501 + [radarhere] + +- Raise an error if ImageDraw.textbbox is used without a TrueType font #5510 + [radarhere] + +- Added ICO saving in BMP format #5513 + [radarhere] + +- Ensure PNG seeks to end of previous chunk at start of load_end #5493 + [radarhere] + +- Do not allow TIFF to seek to a past frame #5473 + [radarhere] + +- Avoid race condition when displaying images with eog #5507 + [mconst] + +- Added specific error messages when ink has incorrect number of bands #5504 + [radarhere] + +- Allow converting an image to a numpy array to raise errors #5379 + [radarhere] + +- Removed DPI rounding from BMP, JPEG, PNG and WMF loading #5476, #5470 + [radarhere] + +- Remove spikes when drawing thin pieslices #5460 + [xtsm] + +- Updated default value for SAMPLESPERPIXEL TIFF tag #5452 + [radarhere] + +- Removed TIFF DPI rounding #5446 + [radarhere, hugovk] + +- Include code in WebP error #5471 + [radarhere] + +- Do not alter pixels outside mask when drawing text on an image with transparency #5434 + [radarhere] + +- Reset handle when seeking backwards in TIFF #5443 + [radarhere] + +- Replace sys.stdout with sys.stdout.buffer when saving #5437 + [radarhere] + +- Fixed UNDEFINED TIFF tag of length 0 being changed in roundtrip #5426 + [radarhere] + +- Fixed bug when checking FreeType2 version if it is not installed #5445 + [radarhere] + +- Do not round dimensions when saving PDF #5459 + [radarhere] + +- Added ImageOps contain() #5417 + [radarhere, hugovk] + +- Changed WebP default "method" value to 4 #5450 + [radarhere] + +- Switched to saving 1-bit PDFs with DCTDecode #5430 + [radarhere] + +- Use bpp from ICO header #5429 + [radarhere] + +- Corrected JPEG APP14 transform value #5408 + [radarhere] + +- Changed TIFF tag 33723 length to 1 #5425 + [radarhere] + +- Changed ImageMorph incorrect mode errors to ValueError #5414 + [radarhere] + +- Add EXIF tags specified in EXIF 2.32 #5419 + [gladiusglad] + +- Treat previous contents of first GIF frame as transparent #5391 + [radarhere] + +- For special image modes, revert default resize resampling to NEAREST #5411 + [radarhere] + +- JPEG2000: Support decoding subsampled RGB and YCbCr images #4996 + [nulano, radarhere] + +- Stop decoding BC1 punchthrough alpha in BC2&3 #4144 + [jansol] + +- Use zero if GIF background color index is missing #5390 + [radarhere] + +- Fixed ensuring that GIF previous frame was loaded #5386 + [radarhere] + +- Valgrind fixes #5397 + [wiredfool] + +- Round down the radius in rounded_rectangle #5382 + [radarhere] + +- Fixed reading uncompressed RGB data from DDS #5383 + [radarhere] + +8.2.0 (2021-04-01) +------------------ + +- Added getxmp() method #5144 + [UrielMaD, radarhere] + +- Add ImageShow support for GraphicsMagick #5349 + [latosha-maltba, radarhere] + +- Do not load transparent pixels from subsequent GIF frames #5333 + [zewt, radarhere] + +- Use LZW encoding when saving GIF images #5291 + [raygard] + +- Set all transparent colors to be equal in quantize() #5282 + [radarhere] + +- Allow PixelAccess to use Python __int__ when parsing x and y #5206 + [radarhere] + +- Removed Image._MODEINFO #5316 + [radarhere] + +- Add preserve_tone option to autocontrast #5350 + [elejke, radarhere] + - Fixed linear_gradient and radial_gradient I and F modes #5274 [radarhere] @@ -68,7 +473,7 @@ Changelog (Pillow) - Changed Image.open formats parameter to be case-insensitive #5250 [Piolie, radarhere] -- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-01-02) #5216 +- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-07-01) #5216 [radarhere] - Added tk version to pilinfo #5226 diff --git a/LICENSE b/LICENSE index 1197291bc..40aabc323 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2021 by Alex Clark and contributors + Copyright © 2010-2022 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source HPND License: diff --git a/MANIFEST.in b/MANIFEST.in index e9aaa8318..26f9401f2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include *.c include *.h include *.in +include *.lock include *.md include *.py include *.rst @@ -9,6 +10,7 @@ include *.txt include *.yaml include LICENSE include Makefile +include Pipfile include tox.ini graft Tests graft src diff --git a/Makefile b/Makefile index 53eaa0566..0dac63d39 100644 --- a/Makefile +++ b/Makefile @@ -50,16 +50,16 @@ help: .PHONY: inplace inplace: clean - python3 setup.py develop build_ext --inplace + python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" . .PHONY: install install: - python3 setup.py install + python3 -m pip install . python3 selftest.py .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 setup.py build_ext install + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" . python3 selftest.py .PHONY: debug @@ -68,7 +68,7 @@ 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 setup.py build_ext install > /dev/null + CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null .PHONY: install-req install-req: @@ -83,10 +83,10 @@ install-venv: .PHONY: release-test release-test: $(MAKE) install-req - python3 setup.py develop + python3 -m pip install -e . python3 selftest.py python3 -m pytest Tests - python3 setup.py install + python3 -m pip install . -rm dist/*.egg -rmdir dist python3 -m pytest -qq @@ -96,12 +96,20 @@ release-test: .PHONY: sdist sdist: - python3 setup.py sdist --format=gztar + python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build + python3 -m build --sdist .PHONY: test test: pytest -qq +.PHONY: valgrind +valgrind: + python3 -c "import pytest_valgrind" || pip3 install pytest-valgrind + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output + .PHONY: readme readme: python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html @@ -114,5 +122,5 @@ lint: .PHONY: lint-fix lint-fix: - black --target-version py36 . + black --target-version py37 . isort . diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..1e611a63c --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +black = "*" +check-manifest = "*" +coverage = "*" +defusedxml = "*" +packaging = "*" +markdown2 = "*" +olefile = "*" +pyroma = "*" +pytest = "*" +pytest-cov = "*" +pytest-timeout = "*" + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..600b19050 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,324 @@ +{ + "_meta": { + "hash": { + "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "black": { + "hashes": [ + "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3", + "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f" + ], + "index": "pypi", + "version": "==21.12b0" + }, + "build": { + "hashes": [ + "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f", + "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "certifi": { + "hashes": [ + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" + ], + "markers": "python_version >= '3'", + "version": "==2.0.9" + }, + "check-manifest": { + "hashes": [ + "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95", + "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce" + ], + "index": "pypi", + "version": "==0.47" + }, + "click": { + "hashes": [ + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.3" + }, + "coverage": { + "hashes": [ + "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", + "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", + "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", + "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", + "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", + "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", + "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", + "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", + "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", + "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", + "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", + "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", + "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", + "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", + "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", + "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", + "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", + "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", + "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", + "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", + "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", + "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", + "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", + "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", + "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", + "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", + "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", + "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", + "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", + "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", + "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", + "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", + "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", + "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", + "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", + "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", + "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", + "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", + "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", + "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", + "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", + "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", + "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", + "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", + "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", + "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", + "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" + ], + "index": "pypi", + "version": "==6.2" + }, + "defusedxml": { + "hashes": [ + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" + ], + "index": "pypi", + "version": "==0.7.1" + }, + "docutils": { + "hashes": [ + "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", + "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.18.1" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3'", + "version": "==3.3" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "markdown2": { + "hashes": [ + "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0", + "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e" + ], + "index": "pypi", + "version": "==2.4.2" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "olefile": { + "hashes": [ + "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" + ], + "index": "pypi", + "version": "==0.46" + }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "index": "pypi", + "version": "==21.3" + }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, + "pep517": { + "hashes": [ + "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0", + "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161" + ], + "version": "==0.12.0" + }, + "platformdirs": { + "hashes": [ + "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", + "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" + ], + "markers": "python_version >= '3.6'", + "version": "==2.4.0" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "py": { + "hashes": [ + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" + }, + "pygments": { + "hashes": [ + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" + ], + "markers": "python_version >= '3.5'", + "version": "==2.10.0" + }, + "pyparsing": { + "hashes": [ + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.6" + }, + "pyroma": { + "hashes": [ + "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65", + "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a" + ], + "index": "pypi", + "version": "==3.2" + }, + "pytest": { + "hashes": [ + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + ], + "index": "pypi", + "version": "==6.2.5" + }, + "pytest-cov": { + "hashes": [ + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" + ], + "index": "pypi", + "version": "==3.0.0" + }, + "pytest-timeout": { + "hashes": [ + "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112", + "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717" + ], + "index": "pypi", + "version": "==2.0.2" + }, + "requests": { + "hashes": [ + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.26.0" + }, + "setuptools": { + "hashes": [ + "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c", + "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b" + ], + "markers": "python_version >= '3.7'", + "version": "==60.0.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "tomli": { + "hashes": [ + "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", + "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "urllib3": { + "hashes": [ + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.7" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 0408f4c28..782b81f33 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Pillow logo + Pillow logo

# Pillow @@ -38,13 +38,19 @@ As of 2019, Pillow development is src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"> AppVeyor CI build status (Windows) + src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"> + GitHub Actions wheels build status (Wheels) Travis CI build status (macOS) + alt="Travis CI wheels build status (aarch64)" + src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels"> Code coverage + src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"> + Tidelift Align @@ -90,12 +96,12 @@ The core image library is designed for fast access to data stored in a few basic - [Documentation](https://pillow.readthedocs.io/) - [Installation](https://pillow.readthedocs.io/en/latest/installation.html) - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) -- [Contribute](https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md) +- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md) - [Issues](https://github.com/python-pillow/Pillow/issues) - [Pull requests](https://github.com/python-pillow/Pillow/pulls) - [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) -- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst) - - [Pre-fork](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork) +- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) + - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) ## Report a Vulnerability diff --git a/RELEASING.md b/RELEASING.md index 6045f84ac..cbedd449c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -8,8 +8,8 @@ information about how the version numbers line up with releases. Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 -* [ ] Develop and prepare release in `master` 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 `master` branch. +* [ ] 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` * [ ] Update `CHANGES.rst`. @@ -26,7 +26,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. make sdist twine check dist/* ``` -* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions) +* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Check and upload all binaries and source distributions e.g.: ```bash twine check dist/* @@ -39,13 +39,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. Released as needed for security, installation or critical bug fixes. -* [ ] Make necessary changes in `master` branch. +* [ ] Make necessary changes in `main` branch. * [ ] Update `CHANGES.rst`. * [ ] Check out release branch e.g.: ```bash git checkout -t remotes/origin/5.2.x ``` -* [ ] Cherry pick individual commits from `master` branch to release branch e.g. `5.2.x`, then `git push`. +* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. @@ -63,7 +63,7 @@ Released as needed for security, installation or critical bug fixes. make sdist twine check dist/* ``` -* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions) +* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Check and upload all binaries and source distributions e.g.: ```bash twine check dist/* @@ -76,7 +76,7 @@ Released as needed for security, installation or critical bug fixes. Released as needed privately to individual vendors for critical security-related bug fixes. * [ ] Prepare patch for all versions that will get a fix. Test against local installations. -* [ ] Commit against master, cherry pick to affected release branches. +* [ ] Commit against `main`, cherry pick to affected release branches. * [ ] Run local test matrix on each release & Python version. * [ ] Privately send to distros. * [ ] Run pre-release check via `make release-test` @@ -93,7 +93,7 @@ Released as needed privately to individual vendors for critical security-related make sdist twine check dist/* ``` -* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions) +* [ ] 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) ## Binary Distributions diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index 26a91d5cd..e19cdf7a9 100755 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import sys diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py index 739ad224e..7b3d4d7ee 100644 --- a/Tests/check_fli_oob.py +++ b/Tests/check_fli_oob.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from PIL import Image @@ -61,8 +61,8 @@ repro_copy = ( for path in repro_ss2 + repro_lc + repro_advance + repro_brun + repro_copy: - im = Image.open(path) - try: - im.load() - except Exception as msg: - print(msg) + with Image.open(path) as im: + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 407f3ea80..d07082aba 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import pytest from PIL import Image diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py index a7a343c98..0210505f5 100755 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Reproductions/tests for OOB read errors in FliDecode.c @@ -19,8 +19,8 @@ from PIL import Image repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") for path in repro: - im = Image.open(path) - try: - im.load() - except Exception as msg: - print(msg) + with Image.open(path) as im: + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 723a1a21e..c191ffc1e 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -33,7 +33,7 @@ def _write_png(tmp_path, xdim, ydim): def test_large(tmp_path): - """ succeeded prepatch""" + """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 79d1cfd5b..70ae6d230 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -31,7 +31,7 @@ def _write_png(tmp_path, xdim, ydim): def test_large(tmp_path): - """ succeeded prepatch""" + """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) diff --git a/Tests/conftest.py b/Tests/conftest.py index 6f9945204..66da7593c 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -1,7 +1,4 @@ import io -import warnings - -import pytest def pytest_report_header(config): @@ -16,16 +13,19 @@ def pytest_report_header(config): def pytest_configure(config): + config.addinivalue_line( + "markers", + "pil_noop_mark: A conditional mark where nothing special happens", + ) + # We're marking some tests to ignore valgrind errors and XFAIL them. # Ensure that the mark is defined # even in cases where pytest-valgrind isn't installed - - with warnings.catch_warnings(): - warnings.simplefilter("error") - try: - getattr(pytest.mark, "valgrind_known_error") - except Exception: - config.addinivalue_line( - "markers", - "valgrind_known_error: Tests that have known issues with valgrind", - ) + try: + config.addinivalue_line( + "markers", + "valgrind_known_error: Tests that have known issues with valgrind", + ) + except Exception: + # valgrind is already installed + pass diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 011bb0bed..e318eb732 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import base64 import os diff --git a/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf new file mode 100644 index 000000000..790132515 Binary files /dev/null and b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf differ diff --git a/Tests/helper.py b/Tests/helper.py index be3bdb76f..feccce6bc 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -30,7 +30,6 @@ if os.environ.get("SHOW_ERRORS", None): a.show() b.show() - elif "GITHUB_ACTIONS" in os.environ: HAS_UPLOADER = True @@ -44,7 +43,6 @@ elif "GITHUB_ACTIONS" in os.environ: b.save(os.path.join(tmpdir, "b.png")) return tmpdir - else: try: import test_image_results @@ -173,6 +171,21 @@ def skip_unless_feature_version(feature, version_required, reason=None): return pytest.mark.skipif(version_available < version_required, reason=reason) +def mark_if_feature_version(mark, feature, version_blacklist, reason=None): + if not features.check(feature): + return pytest.mark.pil_noop_mark() + if reason is None: + reason = f"{feature} is {version_blacklist}" + version_required = parse_version(version_blacklist) + version_available = parse_version(features.version(feature)) + if ( + version_available.major == version_required.major + and version_available.minor == version_required.minor + ): + return mark(reason=reason) + return pytest.mark.pil_noop_mark() + + @pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") class PillowLeakTestCase: # requires unix/macOS @@ -257,8 +270,23 @@ def netpbm_available(): return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) -def imagemagick_available(): - return bool(IMCONVERT and shutil.which(IMCONVERT)) +def magick_command(): + if sys.platform == "win32": + magickhome = os.environ.get("MAGICK_HOME", "") + if magickhome: + imagemagick = [os.path.join(magickhome, "convert.exe")] + graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] + else: + imagemagick = None + graphicsmagick = None + else: + imagemagick = ["convert"] + graphicsmagick = ["gm", "convert"] + + if imagemagick and shutil.which(imagemagick[0]): + return imagemagick + elif graphicsmagick and shutil.which(graphicsmagick[0]): + return graphicsmagick def on_appveyor(): @@ -296,14 +324,6 @@ def is_mingw(): return sysconfig.get_platform() == "mingw" -if sys.platform == "win32": - IMCONVERT = os.environ.get("MAGICK_HOME", "") - if IMCONVERT: - IMCONVERT = os.path.join(IMCONVERT, "convert.exe") -else: - IMCONVERT = "convert" - - class cached_property: def __init__(self, func): self.func = func diff --git a/Tests/images/200x32_p_bl_raw_origin.tga b/Tests/images/200x32_p_bl_raw_origin.tga new file mode 100644 index 000000000..329f0ca4d Binary files /dev/null and b/Tests/images/200x32_p_bl_raw_origin.tga differ diff --git a/Tests/images/balloon_eciRGBv2_aware.jp2 b/Tests/images/balloon_eciRGBv2_aware.jp2 new file mode 100644 index 000000000..18fd1e172 Binary files /dev/null and b/Tests/images/balloon_eciRGBv2_aware.jp2 differ diff --git a/Tests/images/bc5_snorm.dds b/Tests/images/bc5_snorm.dds new file mode 100644 index 000000000..7458c67c6 Binary files /dev/null and b/Tests/images/bc5_snorm.dds differ diff --git a/Tests/images/bc5_typeless.dds b/Tests/images/bc5_typeless.dds new file mode 100644 index 000000000..b5bae52bb Binary files /dev/null and b/Tests/images/bc5_typeless.dds differ diff --git a/Tests/images/bc5_unorm.dds b/Tests/images/bc5_unorm.dds new file mode 100644 index 000000000..a04a026eb Binary files /dev/null and b/Tests/images/bc5_unorm.dds differ diff --git a/Tests/images/bc5_unorm.png b/Tests/images/bc5_unorm.png new file mode 100644 index 000000000..05279ddfb Binary files /dev/null and b/Tests/images/bc5_unorm.png differ diff --git a/Tests/images/bc5s.dds b/Tests/images/bc5s.dds new file mode 100644 index 000000000..0b999eed3 Binary files /dev/null and b/Tests/images/bc5s.dds differ diff --git a/Tests/images/bc5s.png b/Tests/images/bc5s.png new file mode 100644 index 000000000..39d7811bf Binary files /dev/null and b/Tests/images/bc5s.png differ diff --git a/Tests/images/bitmap_font_stroke_basic.png b/Tests/images/bitmap_font_stroke_basic.png new file mode 100644 index 000000000..86b2d09f6 Binary files /dev/null and b/Tests/images/bitmap_font_stroke_basic.png differ diff --git a/Tests/images/bitmap_font_stroke_raqm.png b/Tests/images/bitmap_font_stroke_raqm.png new file mode 100644 index 000000000..08029ce34 Binary files /dev/null and b/Tests/images/bitmap_font_stroke_raqm.png differ diff --git a/Tests/images/black_and_white.ico b/Tests/images/black_and_white.ico new file mode 100644 index 000000000..f98d7ac8e Binary files /dev/null and b/Tests/images/black_and_white.ico differ diff --git a/Tests/images/broken_exif_dpi.jpg b/Tests/images/broken_exif_dpi.jpg new file mode 100644 index 000000000..2c88b9463 Binary files /dev/null and b/Tests/images/broken_exif_dpi.jpg differ diff --git a/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif b/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif new file mode 100644 index 000000000..6e4e9b9ca Binary files /dev/null and b/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif differ diff --git a/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k new file mode 100644 index 000000000..c9bd7fc0a Binary files /dev/null and b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k differ diff --git a/Tests/images/crash-5762152299364352.fli b/Tests/images/crash-5762152299364352.fli new file mode 100644 index 000000000..944fe0b56 Binary files /dev/null and b/Tests/images/crash-5762152299364352.fli differ diff --git a/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif b/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif new file mode 100644 index 000000000..053e4e4e9 Binary files /dev/null and b/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif differ diff --git a/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k new file mode 100644 index 000000000..fd2f4dd36 Binary files /dev/null and b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k differ diff --git a/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k new file mode 100644 index 000000000..c3ad0d633 Binary files /dev/null and b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k differ diff --git a/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k new file mode 100644 index 000000000..3aadfc377 Binary files /dev/null and b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k differ diff --git a/Tests/images/different_transparency.gif b/Tests/images/different_transparency.gif new file mode 100644 index 000000000..2d36bef9e Binary files /dev/null and b/Tests/images/different_transparency.gif differ diff --git a/Tests/images/different_transparency_merged.png b/Tests/images/different_transparency_merged.png new file mode 100644 index 000000000..3438f62a6 Binary files /dev/null and b/Tests/images/different_transparency_merged.png differ diff --git a/Tests/images/dispose_bgnd_rgba.gif b/Tests/images/dispose_bgnd_rgba.gif new file mode 100644 index 000000000..c18a0ba71 Binary files /dev/null and b/Tests/images/dispose_bgnd_rgba.gif differ diff --git a/Tests/images/dispose_bgnd_transparency.gif b/Tests/images/dispose_bgnd_transparency.gif new file mode 100644 index 000000000..7c626fe72 Binary files /dev/null and b/Tests/images/dispose_bgnd_transparency.gif differ diff --git a/Tests/images/dispose_none_load_end_second.gif b/Tests/images/dispose_none_load_end_second.gif deleted file mode 100644 index 5d8462ceb..000000000 Binary files a/Tests/images/dispose_none_load_end_second.gif and /dev/null differ diff --git a/Tests/images/dispose_none_load_end_second.png b/Tests/images/dispose_none_load_end_second.png new file mode 100644 index 000000000..dc01ccbdd Binary files /dev/null and b/Tests/images/dispose_none_load_end_second.png differ diff --git a/Tests/images/dispose_prev_first_frame.gif b/Tests/images/dispose_prev_first_frame.gif new file mode 100644 index 000000000..4c19dd1ed Binary files /dev/null and b/Tests/images/dispose_prev_first_frame.gif differ diff --git a/Tests/images/dispose_prev_first_frame_seeked.png b/Tests/images/dispose_prev_first_frame_seeked.png new file mode 100644 index 000000000..85a3753e1 Binary files /dev/null and b/Tests/images/dispose_prev_first_frame_seeked.png differ diff --git a/Tests/images/drawing_roundDown.emf b/Tests/images/drawing_roundDown.emf deleted file mode 100644 index 6c3e20248..000000000 Binary files a/Tests/images/drawing_roundDown.emf and /dev/null differ diff --git a/Tests/images/dxt5-colorblock-alpha-issue-4142.dds b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds new file mode 100644 index 000000000..905527ead Binary files /dev/null and b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds differ diff --git a/Tests/images/exif_imagemagick_orientation.png b/Tests/images/exif_imagemagick_orientation.png new file mode 100644 index 000000000..819a0703f Binary files /dev/null and b/Tests/images/exif_imagemagick_orientation.png differ diff --git a/Tests/images/expected_to_read.jp2 b/Tests/images/expected_to_read.jp2 new file mode 100644 index 000000000..d8029a0d3 Binary files /dev/null and b/Tests/images/expected_to_read.jp2 differ diff --git a/Tests/images/first_frame_transparency.gif b/Tests/images/first_frame_transparency.gif new file mode 100644 index 000000000..86dc0de64 Binary files /dev/null and b/Tests/images/first_frame_transparency.gif differ diff --git a/Tests/images/hopper.dds b/Tests/images/hopper.dds new file mode 100644 index 000000000..8b9af9ed9 Binary files /dev/null and b/Tests/images/hopper.dds differ diff --git a/Tests/images/hopper_roundUp_2.tif b/Tests/images/hopper_float_dpi_2.tif similarity index 100% rename from Tests/images/hopper_roundUp_2.tif rename to Tests/images/hopper_float_dpi_2.tif diff --git a/Tests/images/hopper_roundUp_3.tif b/Tests/images/hopper_float_dpi_3.tif similarity index 100% rename from Tests/images/hopper_roundUp_3.tif rename to Tests/images/hopper_float_dpi_3.tif diff --git a/Tests/images/hopper_roundUp_None.tif b/Tests/images/hopper_float_dpi_None.tif similarity index 100% rename from Tests/images/hopper_roundUp_None.tif rename to Tests/images/hopper_float_dpi_None.tif diff --git a/Tests/images/hopper_mask.ico b/Tests/images/hopper_mask.ico new file mode 100644 index 000000000..e8d66c689 Binary files /dev/null and b/Tests/images/hopper_mask.ico differ diff --git a/Tests/images/hopper_mask.png b/Tests/images/hopper_mask.png new file mode 100644 index 000000000..c7bd2f708 Binary files /dev/null and b/Tests/images/hopper_mask.png differ diff --git a/Tests/images/hopper_naxis_zero.fits b/Tests/images/hopper_naxis_zero.fits new file mode 100644 index 000000000..580cf3a2c Binary files /dev/null and b/Tests/images/hopper_naxis_zero.fits differ diff --git a/Tests/images/hopper_resized.gif b/Tests/images/hopper_resized.gif new file mode 100644 index 000000000..f7be6c262 Binary files /dev/null and b/Tests/images/hopper_resized.gif differ diff --git a/Tests/images/hopper_roundDown.bmp b/Tests/images/hopper_roundDown.bmp deleted file mode 100644 index 62aada050..000000000 Binary files a/Tests/images/hopper_roundDown.bmp and /dev/null differ diff --git a/Tests/images/hopper_roundDown_2.tif b/Tests/images/hopper_roundDown_2.tif deleted file mode 100644 index ac8cd057d..000000000 Binary files a/Tests/images/hopper_roundDown_2.tif and /dev/null differ diff --git a/Tests/images/hopper_roundDown_3.tif b/Tests/images/hopper_roundDown_3.tif deleted file mode 100644 index 0542fab9a..000000000 Binary files a/Tests/images/hopper_roundDown_3.tif and /dev/null differ diff --git a/Tests/images/hopper_roundDown_None.tif b/Tests/images/hopper_roundDown_None.tif deleted file mode 100644 index 21c40e8fe..000000000 Binary files a/Tests/images/hopper_roundDown_None.tif and /dev/null differ diff --git a/Tests/images/hopper_wal.png b/Tests/images/hopper_wal.png new file mode 100644 index 000000000..b6067c219 Binary files /dev/null and b/Tests/images/hopper_wal.png differ diff --git a/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png b/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png new file mode 100644 index 000000000..beffed5b9 Binary files /dev/null and b/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png differ diff --git a/Tests/images/imagedraw/triangle_right_width.png b/Tests/images/imagedraw/triangle_right_width.png new file mode 100644 index 000000000..57b73553a Binary files /dev/null and b/Tests/images/imagedraw/triangle_right_width.png differ diff --git a/Tests/images/imagedraw/triangle_right_width_no_fill.png b/Tests/images/imagedraw/triangle_right_width_no_fill.png new file mode 100644 index 000000000..dd65be6be Binary files /dev/null and b/Tests/images/imagedraw/triangle_right_width_no_fill.png differ diff --git a/Tests/images/imagedraw_polygon_translucent.png b/Tests/images/imagedraw_polygon_translucent.png new file mode 100644 index 000000000..da8d790a3 Binary files /dev/null and b/Tests/images/imagedraw_polygon_translucent.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png new file mode 100644 index 000000000..59e55b2a1 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png new file mode 100644 index 000000000..c4e54896b Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png new file mode 100644 index 000000000..6b0f11fa6 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png differ diff --git a/Tests/images/invalid_header_length.jp2 b/Tests/images/invalid_header_length.jp2 new file mode 100644 index 000000000..c0c14f421 Binary files /dev/null and b/Tests/images/invalid_header_length.jp2 differ diff --git a/Tests/images/iptc_roundDown.jpg b/Tests/images/iptc_roundDown.jpg deleted file mode 100644 index f98206f18..000000000 Binary files a/Tests/images/iptc_roundDown.jpg and /dev/null differ diff --git a/Tests/images/missing_background.gif b/Tests/images/missing_background.gif new file mode 100644 index 000000000..550d68d81 Binary files /dev/null and b/Tests/images/missing_background.gif differ diff --git a/Tests/images/missing_background_first_frame.png b/Tests/images/missing_background_first_frame.png new file mode 100644 index 000000000..25237ba5d Binary files /dev/null and b/Tests/images/missing_background_first_frame.png differ diff --git a/Tests/images/multiline_text.png b/Tests/images/multiline_text.png index ff1308c5e..e39c6586c 100644 Binary files a/Tests/images/multiline_text.png and b/Tests/images/multiline_text.png differ diff --git a/Tests/images/multiline_text_center.png b/Tests/images/multiline_text_center.png index f44d0783a..837c6382a 100644 Binary files a/Tests/images/multiline_text_center.png and b/Tests/images/multiline_text_center.png differ diff --git a/Tests/images/multiline_text_right.png b/Tests/images/multiline_text_right.png index 1b32d9167..58b3bdddd 100644 Binary files a/Tests/images/multiline_text_right.png and b/Tests/images/multiline_text_right.png differ diff --git a/Tests/images/multiline_text_spacing.png b/Tests/images/multiline_text_spacing.png index 3c3bc0f26..3b367c7dd 100644 Binary files a/Tests/images/multiline_text_spacing.png and b/Tests/images/multiline_text_spacing.png differ diff --git a/Tests/images/multipage_multiple_frame_loop.tiff b/Tests/images/multipage_multiple_frame_loop.tiff new file mode 100644 index 000000000..b6759b080 Binary files /dev/null and b/Tests/images/multipage_multiple_frame_loop.tiff differ diff --git a/Tests/images/multipage_out_of_order.tiff b/Tests/images/multipage_out_of_order.tiff new file mode 100644 index 000000000..1576a549b Binary files /dev/null and b/Tests/images/multipage_out_of_order.tiff differ diff --git a/Tests/images/multipage_single_frame_loop.tiff b/Tests/images/multipage_single_frame_loop.tiff new file mode 100644 index 000000000..26f27c421 Binary files /dev/null and b/Tests/images/multipage_single_frame_loop.tiff differ diff --git a/Tests/images/negative_layer_count.psd b/Tests/images/negative_layer_count.psd new file mode 100644 index 000000000..b111c2d56 Binary files /dev/null and b/Tests/images/negative_layer_count.psd differ diff --git a/Tests/images/not_enough_data.jp2 b/Tests/images/not_enough_data.jp2 new file mode 100644 index 000000000..2d28bb5e9 Binary files /dev/null and b/Tests/images/not_enough_data.jp2 differ diff --git a/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif b/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif new file mode 100644 index 000000000..d43ba9192 Binary files /dev/null and b/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif differ diff --git a/Tests/images/p_16.png b/Tests/images/p_16.png new file mode 100644 index 000000000..e35886412 Binary files /dev/null and b/Tests/images/p_16.png differ diff --git a/Tests/images/p_16.tga b/Tests/images/p_16.tga new file mode 100644 index 000000000..2b2ca4c70 Binary files /dev/null and b/Tests/images/p_16.tga differ diff --git a/Tests/images/padded_idat.png b/Tests/images/padded_idat.png new file mode 100644 index 000000000..18c5a4990 Binary files /dev/null and b/Tests/images/padded_idat.png differ diff --git a/Tests/images/pal8_offset.bmp b/Tests/images/pal8_offset.bmp new file mode 100644 index 000000000..24be65f22 Binary files /dev/null and b/Tests/images/pal8_offset.bmp differ diff --git a/Tests/images/palette_negative.png b/Tests/images/palette_negative.png new file mode 100644 index 000000000..938a7285f Binary files /dev/null and b/Tests/images/palette_negative.png differ diff --git a/Tests/images/palette_sepia.png b/Tests/images/palette_sepia.png new file mode 100644 index 000000000..f3fc93253 Binary files /dev/null and b/Tests/images/palette_sepia.png differ diff --git a/Tests/images/palette_wedge.png b/Tests/images/palette_wedge.png new file mode 100644 index 000000000..23fb7940d Binary files /dev/null and b/Tests/images/palette_wedge.png differ diff --git a/Tests/images/reqd_showpage_transparency.png b/Tests/images/reqd_showpage_transparency.png new file mode 100644 index 000000000..3ce159d0f Binary files /dev/null and b/Tests/images/reqd_showpage_transparency.png differ diff --git a/Tests/images/rgb32rle_bottom_right.tga b/Tests/images/rgb32rle_bottom_right.tga new file mode 100644 index 000000000..bd4609e9c Binary files /dev/null and b/Tests/images/rgb32rle_bottom_right.tga differ diff --git a/Tests/images/rgb32rle_top_right.tga b/Tests/images/rgb32rle_top_right.tga new file mode 100644 index 000000000..78f9dc5df Binary files /dev/null and b/Tests/images/rgb32rle_top_right.tga differ diff --git a/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp new file mode 100644 index 000000000..97def320f Binary files /dev/null and b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp differ diff --git a/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd new file mode 100644 index 000000000..63319e545 Binary files /dev/null and b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd differ diff --git a/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp new file mode 100644 index 000000000..73022abfc Binary files /dev/null and b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp differ diff --git a/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd new file mode 100644 index 000000000..c259a15e7 Binary files /dev/null and b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd differ diff --git a/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp new file mode 100644 index 000000000..79e97dce3 Binary files /dev/null and b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp differ diff --git a/Tests/images/timeout-6646305047838720 b/Tests/images/timeout-6646305047838720 new file mode 100644 index 000000000..eae1f333a Binary files /dev/null and b/Tests/images/timeout-6646305047838720 differ diff --git a/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp new file mode 100644 index 000000000..9b9ecbcb0 Binary files /dev/null and b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp differ diff --git a/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli new file mode 100644 index 000000000..ce4607d2d Binary files /dev/null and b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli differ diff --git a/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp new file mode 100644 index 000000000..cb9a4e8b3 Binary files /dev/null and b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp differ diff --git a/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli new file mode 100644 index 000000000..77a94b87a Binary files /dev/null and b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli differ diff --git a/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd new file mode 100644 index 000000000..955fc3325 Binary files /dev/null and b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd differ diff --git a/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps new file mode 100644 index 000000000..5000ca9aa Binary files /dev/null and b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps differ diff --git a/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp new file mode 100644 index 000000000..5044fbde1 Binary files /dev/null and b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp differ diff --git a/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd new file mode 100644 index 000000000..c658ea45c Binary files /dev/null and b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd differ diff --git a/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp new file mode 100644 index 000000000..7ef78eeec Binary files /dev/null and b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp differ diff --git a/Tests/images/transparent_background_text.png b/Tests/images/transparent_background_text.png index 40acd92b6..8ddd65cc6 100644 Binary files a/Tests/images/transparent_background_text.png and b/Tests/images/transparent_background_text.png differ diff --git a/Tests/images/transparent_background_text_L.png b/Tests/images/transparent_background_text_L.png new file mode 100644 index 000000000..d37de20a7 Binary files /dev/null and b/Tests/images/transparent_background_text_L.png differ diff --git a/Tests/images/transparent_dispose.gif b/Tests/images/transparent_dispose.gif new file mode 100644 index 000000000..92b615543 Binary files /dev/null and b/Tests/images/transparent_dispose.gif differ diff --git a/Tests/images/truncated_app14.jpg b/Tests/images/truncated_app14.jpg new file mode 100644 index 000000000..232a4c35f Binary files /dev/null and b/Tests/images/truncated_app14.jpg differ diff --git a/Tests/images/uncompressed_rgb.png b/Tests/images/uncompressed_rgb.png index 50bca09ee..f02b50f6f 100644 Binary files a/Tests/images/uncompressed_rgb.png and b/Tests/images/uncompressed_rgb.png differ diff --git a/Tests/images/xmp_test.jpg b/Tests/images/xmp_test.jpg new file mode 100644 index 000000000..4b9354f3a Binary files /dev/null and b/Tests/images/xmp_test.jpg differ diff --git a/Tests/images/zero_dpi.jp2 b/Tests/images/zero_dpi.jp2 new file mode 100644 index 000000000..079271fc6 Binary files /dev/null and b/Tests/images/zero_dpi.jp2 differ diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index 513136fff..09cc7bc16 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -22,7 +22,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do fuzzer_basename=$(basename -s .py $fuzzer) fuzzer_package=${fuzzer_basename}.pkg pyinstaller \ - --add-binary /usr/local/lib/libjpeg.so.9:. \ + --add-binary /usr/local/lib/libjpeg.so.62.3.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/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index bdfda7a13..bc2ba9a7e 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -14,10 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys -import atheris_no_libfuzzer as atheris -import fuzzers +import atheris + +with atheris.instrument_imports(): + import sys + + import fuzzers def TestOneInput(data): @@ -26,14 +29,14 @@ def TestOneInput(data): except Exception: # We're catching all exceptions because Pillow's exceptions are # directly inheriting from Exception. - return - return + pass def main(): fuzzers.enable_decompressionbomb_error() - atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) + atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() + fuzzers.disable_decompressionbomb_error() if __name__ == "__main__": diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index d816d535f..545daccb6 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -14,10 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys -import atheris_no_libfuzzer as atheris -import fuzzers +import atheris + +with atheris.instrument_imports(): + import sys + + import fuzzers def TestOneInput(data): @@ -26,14 +29,14 @@ def TestOneInput(data): except Exception: # We're catching all exceptions because Pillow's exceptions are # directly inheriting from Exception. - return - return + pass def main(): fuzzers.enable_decompressionbomb_error() - atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) + atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() + fuzzers.disable_decompressionbomb_error() if __name__ == "__main__": diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 1e7a4e27d..5786764a6 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -10,6 +10,11 @@ def enable_decompressionbomb_error(): warnings.simplefilter("error", Image.DecompressionBombWarning) +def disable_decompressionbomb_error(): + ImageFile.LOAD_TRUNCATED_IMAGES = False + warnings.resetwarnings() + + def fuzz_image(data): # This will fail on some images in the corpus, as we have many # invalid images in the test suite. diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp new file mode 100644 index 000000000..94cc87db9 --- /dev/null +++ b/Tests/oss-fuzz/python.supp @@ -0,0 +1,16 @@ +{ + + Memcheck:Cond + ... + fun:encode_current_locale +} + + +{ + + Memcheck:Cond + fun:inflate + fun:ZIPDecode + fun:_TIFFReadEncodedTileAndAllocBuffer + ... +} diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index a243c0260..629e9ac00 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -2,12 +2,19 @@ import subprocess import sys import fuzzers +import packaging import pytest -from PIL import Image +from PIL import Image, features if sys.platform.startswith("win32"): pytest.skip("Fuzzer is linux only", allow_module_level=True) +if features.check("libjpeg_turbo"): + version = packaging.version.parse(features.version("libjpeg_turbo")) + if version.major == 2 and version.minor == 0: + pytestmark = pytest.mark.valgrind_known_error( + reason="Known failing with libjpeg_turbo 2.0" + ) @pytest.mark.parametrize( @@ -37,6 +44,8 @@ def test_fuzz_images(path): ): # Known Image.* exceptions assert True + finally: + fuzzers.disable_decompressionbomb_error() @pytest.mark.parametrize( diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 80ab92666..d918ef941 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -10,8 +10,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS class TestDecompressionBomb: - @classmethod - def teardown_class(cls): + def teardown_method(self, method): Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT def test_no_warning_small_file(self): @@ -52,6 +51,7 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass + @pytest.mark.xfail(reason="different exception") def test_exception_ico(self): with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.ico"): diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 97e2a150e..d48e5ce07 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -249,8 +249,8 @@ def test_apng_mode(): assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGBA") - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) + assert im.getpixel((0, 0)) == (255, 0, 0, 0) + assert im.getpixel((64, 32)) == (255, 0, 0, 0) with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: assert im.mode == "P" @@ -312,7 +312,7 @@ def test_apng_syntax_errors(): exception = e assert exception is None - with pytest.raises(SyntaxError): + with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: im.seek(im.n_frames - 1) im.load() @@ -433,12 +433,20 @@ def test_apng_save_duration_loop(tmp_path): # test removal of duplicated frames frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) - frame.save(test_file, save_all=True, append_images=[frame], duration=[500, 250]) + frame.save( + test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] + ) with Image.open(test_file) as im: im.load() assert im.n_frames == 1 assert im.info.get("duration") == 750 + # test info duration + frame.info["duration"] = 750 + frame.save(test_file, save_all=True) + with Image.open(test_file) as im: + assert im.info.get("duration") == 750 + def test_apng_save_disposal(tmp_path): test_file = str(tmp_path / "temp.png") @@ -529,6 +537,17 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) + # test info disposal + red.info["disposal"] = PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND + red.save( + test_file, + save_all=True, + append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))], + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + def test_apng_save_disposal_previous(tmp_path): test_file = str(tmp_path / "temp.png") @@ -609,3 +628,10 @@ def test_apng_save_blend(tmp_path): im.seek(2) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test info blend + red.info["blend"] = PngImagePlugin.APNG_BLEND_OP_OVER + red.save(test_file, save_all=True, append_images=[green, transparent]) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 864607301..15bd7e4f8 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import assert_image_equal_tofile @@ -16,3 +18,22 @@ def test_load_blp2_dxt1(): def test_load_blp2_dxt1a(): with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp", + "Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp", + "Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp", + "Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp", + "Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp", + "Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp", + "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", + ], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index d5fe2a4dd..47fc97df0 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -63,7 +63,7 @@ def test_dpi(): output.seek(0) with Image.open(output) as reloaded: - assert reloaded.info["dpi"] == dpi + assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) def test_save_bmp_with_dpi(tmp_path): @@ -71,6 +71,7 @@ def test_save_bmp_with_dpi(tmp_path): # Arrange outfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.bmp") as im: + assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -78,31 +79,17 @@ def test_save_bmp_with_dpi(tmp_path): # Assert with Image.open(outfile) as reloaded: reloaded.load() - assert im.info["dpi"] == reloaded.info["dpi"] - assert im.size == reloaded.size + assert reloaded.info["dpi"] == (96, 96) + assert reloaded.size == im.size assert reloaded.format == "JPEG" -def test_load_dpi_rounding(): - # Round up - with Image.open("Tests/images/hopper.bmp") as im: - assert im.info["dpi"] == (96, 96) - - # Round down - with Image.open("Tests/images/hopper_roundDown.bmp") as im: - assert im.info["dpi"] == (72, 72) - - -def test_save_dpi_rounding(tmp_path): +def test_save_float_dpi(tmp_path): outfile = str(tmp_path / "temp.bmp") with Image.open("Tests/images/hopper.bmp") as im: - im.save(outfile, dpi=(72.2, 72.2)) + im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) with Image.open(outfile) as reloaded: - assert reloaded.info["dpi"] == (72, 72) - - im.save(outfile, dpi=(72.8, 72.8)) - with Image.open(outfile) as reloaded: - assert reloaded.info["dpi"] == (73, 73) + assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) def test_load_dib(): @@ -136,3 +123,10 @@ def test_rgba_bitfields(): im = Image.merge("RGB", (r, g, b)) assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") + + +def test_offset(): + # This image has been hexedited + # to exclude the palette size from the pixel data offset + with Image.open("Tests/images/pal8_offset.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 682cd048b..2f46ed77e 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -5,16 +5,21 @@ import pytest from PIL import DdsImagePlugin, Image -from .helper import assert_image_equal, assert_image_equal_tofile +from .helper import assert_image_equal, assert_image_equal_tofile, hopper TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" +TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" +TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" +TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" +TEST_FILE_BC5S = "Tests/images/bc5s.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" -TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds" +TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" +TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" def test_sanity_dxt1(): @@ -31,6 +36,19 @@ def test_sanity_dxt1(): assert_image_equal(im, target) +def test_sanity_dxt3(): + """Check DXT3 images can be opened""" + + with Image.open(TEST_FILE_DXT3) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) + + def test_sanity_dxt5(): """Check DXT5 images can be opened""" @@ -44,17 +62,28 @@ def test_sanity_dxt5(): assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) -def test_sanity_dxt3(): - """Check DXT3 images can be opened""" +@pytest.mark.parametrize( + ("image_path", "expected_path"), + ( + # hexeditted to be typeless + (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), + (TEST_FILE_DX10_BC5_UNORM, TEST_FILE_DX10_BC5_UNORM), + # hexeditted to use DX10 FourCC + (TEST_FILE_DX10_BC5_SNORM, TEST_FILE_BC5S), + (TEST_FILE_BC5S, TEST_FILE_BC5S), + ), +) +def test_dx10_bc5(image_path, expected_path): + """Check DX10 BC5 images can be opened""" - with Image.open(TEST_FILE_DXT3) as im: + with Image.open(image_path) as im: im.load() assert im.format == "DDS" - assert im.mode == "RGBA" + assert im.mode == "RGB" assert im.size == (256, 256) - assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) + assert_image_equal_tofile(im, expected_path.replace(".dds", ".png")) def test_dx10_bc7(): @@ -124,44 +153,51 @@ def test_unimplemented_dxgi_format(): def test_uncompressed_rgb(): """Check uncompressed RGB images can be opened""" + # convert -format dds -define dds:compression=none hopper.jpg hopper.dds with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: - im.load() + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (128, 128) + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + # Test image with alpha + with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im: assert im.format == "DDS" assert im.mode == "RGBA" assert im.size == (800, 600) assert_image_equal_tofile( - im, TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png") + im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png") ) -def test__validate_true(): +def test__accept_true(): """Check valid prefix""" # Arrange prefix = b"DDS etc" # Act - output = DdsImagePlugin._validate(prefix) + output = DdsImagePlugin._accept(prefix) # Assert assert output -def test__validate_false(): +def test__accept_false(): """Check invalid prefix""" # Arrange prefix = b"something invalid" # Act - output = DdsImagePlugin._validate(prefix) + output = DdsImagePlugin._accept(prefix) # Assert assert not output def test_short_header(): - """ Check a short header""" + """Check a short header""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() @@ -174,7 +210,7 @@ def test_short_header(): def test_short_file(): - """ Check that the appropriate error is thrown for a short file""" + """Check that the appropriate error is thrown for a short file""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() @@ -187,7 +223,46 @@ def test_short_file(): short_file() +def test_dxt5_colorblock_alpha_issue_4142(): + """Check that colorblocks are decoded correctly in DXT5""" + + with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: + px = im.getpixel((0, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + px = im.getpixel((1, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + def test_unimplemented_pixel_format(): with pytest.raises(NotImplementedError): with Image.open("Tests/images/unimplemented_pixel_format.dds"): pass + + +def test_save_unsupported_mode(tmp_path): + out = str(tmp_path / "temp.dds") + im = hopper("HSV") + with pytest.raises(OSError): + im.save(out) + + +@pytest.mark.parametrize( + ("mode", "test_file"), + [ + ("RGB", "Tests/images/hopper.png"), + ("RGBA", "Tests/images/pil123rgba.png"), + ], +) +def test_save(mode, test_file, tmp_path): + out = str(tmp_path / "temp.dds") + with Image.open(test_file) as im: + assert im.mode == mode + im.save(out) + + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index ea91375da..4c0b96f73 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -8,6 +8,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, hopper, + mark_if_feature_version, skip_unless_feature, ) @@ -64,7 +65,9 @@ def test_invalid_file(): EpsImagePlugin.EpsImageFile(invalid_file) -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: @@ -93,6 +96,17 @@ def test_showpage(): assert_image_similar(plot_image, target, 6) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_transparency(): + with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + plot_image.load(transparency=True) + assert plot_image.mode == "RGBA" + + with Image.open("Tests/images/reqd_showpage_transparency.png") as target: + # fonts could be slightly different + assert_image_similar(plot_image, target, 6) + + @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_file_object(tmp_path): # issue 479 @@ -264,3 +278,15 @@ def test_emptyline(): assert image.mode == "RGB" assert image.size == (460, 352) assert image.format == "EPS" + + +@pytest.mark.timeout(timeout=5) +@pytest.mark.parametrize( + "test_file", + ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], +) +def test_timeout(test_file): + with open(test_file, "rb") as f: + with pytest.raises(Image.UnidentifiedImageError): + with Image.open(f): + pass diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py index 6dc7c4602..c77457947 100644 --- a/Tests/test_file_fitsstub.py +++ b/Tests/test_file_fitsstub.py @@ -1,3 +1,5 @@ +from io import BytesIO + import pytest from PIL import FitsStubImagePlugin, Image @@ -11,10 +13,8 @@ def test_open(): # Assert assert im.format == "FITS" - - # Dummy data from the stub - assert im.mode == "F" - assert im.size == (1, 1) + assert im.size == (128, 128) + assert im.mode == "L" def test_invalid_file(): @@ -35,6 +35,21 @@ def test_load(): im.load() +def test_truncated_fits(): + # No END to headers + image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" + with pytest.raises(OSError): + FitsStubImagePlugin.FITSStubImageFile(BytesIO(image_data)) + + +def test_naxis_zero(): + # This test image has been manually hexedited + # to set the number of data axes to zero + with pytest.raises(ValueError): + with Image.open("Tests/images/hopper_naxis_zero.fits"): + pass + + def test_save(): # Arrange with Image.open(TEST_FILE) as im: diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 0d9748a95..675e06bf8 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -123,3 +123,31 @@ def test_seek(): im.seek(50) assert_image_equal_tofile(im, "Tests/images/a_fli.png") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli", + "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", + ], +) +@pytest.mark.timeout(timeout=3) +def test_timeouts(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash-5762152299364352.fli", + ], +) +def test_crash(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 1b2314d51..00bf582fa 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -163,6 +163,32 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 +@pytest.mark.parametrize( + "path, mode", + ( + ("Tests/images/dispose_bgnd.gif", "RGB"), + # Hexeditted copy of dispose_bgnd to add transparency + ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), + ), +) +def test_loading_multiple_palettes(path, mode): + with Image.open(path) as im: + assert im.mode == "P" + first_frame_colors = im.palette.colors.keys() + original_color = im.convert("RGB").load()[0, 0] + + im.seek(1) + assert im.mode == mode + if mode == "RGBA": + im = im.convert("RGB") + + # Check a color only from the old palette + assert im.load()[0, 0] == original_color + + # Check a color from the new palette + assert im.load()[24, 24] not in first_frame_colors + + def test_headers_saving_for_animated_gifs(tmp_path): important_headers = ["background", "version", "duration", "loop"] # Multiframe image @@ -298,6 +324,12 @@ def test_eoferror(): im.seek(n_frames - 1) +def test_first_frame_transparency(): + with Image.open("Tests/images/first_frame_transparency.gif") as im: + px = im.load() + assert px[0, 0] == im.info["transparency"] + + def test_dispose_none(): with Image.open("Tests/images/dispose_none.gif") as img: try: @@ -318,7 +350,7 @@ def test_dispose_none_load_end(): with Image.open("Tests/images/dispose_none_load_end.gif") as img: img.seek(1) - assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.gif") + assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png") def test_dispose_background(): @@ -331,6 +363,27 @@ def test_dispose_background(): pass +def test_dispose_background_transparency(): + with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: + img.seek(2) + px = img.load() + assert px[35, 30][3] == 0 + + +def test_transparent_dispose(): + expected_colors = [ + (2, 1, 2), + ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)), + ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)), + ] + with Image.open("Tests/images/transparent_dispose.gif") as img: + for frame in range(3): + img.seek(frame) + for x in range(3): + color = img.getpixel((x, 0)) + assert color == expected_colors[frame][x] + + def test_dispose_previous(): with Image.open("Tests/images/dispose_prev.gif") as img: try: @@ -341,6 +394,25 @@ def test_dispose_previous(): pass +def test_dispose_previous_first_frame(): + with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: + im.seek(1) + assert_image_equal_tofile( + im, "Tests/images/dispose_prev_first_frame_seeked.png" + ) + + +def test_previous_frame_loaded(): + with Image.open("Tests/images/dispose_none.gif") as img: + img.load() + img.seek(1) + img.load() + img.seek(2) + with Image.open("Tests/images/dispose_none.gif") as img_skipped: + img_skipped.seek(2) + assert_image_equal(img_skipped, img) + + def test_save_dispose(tmp_path): out = str(tmp_path / "temp.gif") im_list = [ @@ -373,14 +445,15 @@ def test_save_dispose(tmp_path): def test_dispose2_palette(tmp_path): out = str(tmp_path / "temp.gif") - # 4 backgrounds: White, Grey, Black, Red + # Four colors: white, grey, black, red circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] im_list = [] for circle in circles: + # Red background img = Image.new("RGB", (100, 100), (255, 0, 0)) - # Red circle in center of each frame + # Circle in center of each frame d = ImageDraw.Draw(img) d.ellipse([(40, 40), (60, 60)], fill=circle) @@ -465,15 +538,28 @@ def test_dispose2_background(tmp_path): with Image.open(out) as im: im.seek(1) - assert im.getpixel((0, 0)) == 0 + assert im.getpixel((0, 0)) == (255, 0, 0) -def test_iss634(): +def test_transparency_in_second_frame(): + with Image.open("Tests/images/different_transparency.gif") as im: + assert im.info["transparency"] == 0 + + # Seek to the second frame + im.seek(im.tell() + 1) + assert "transparency" not in im.info + + assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png") + + +def test_no_transparency_in_second_frame(): with Image.open("Tests/images/iss634.gif") as img: # Seek to the second frame img.seek(img.tell() + 1) + assert "transparency" not in img.info + # All transparent pixels should be replaced with the color from the first frame - assert img.histogram()[img.info["transparency"]] == 0 + assert img.histogram()[255] == 0 def test_duration(tmp_path): @@ -717,10 +803,10 @@ def test_rgb_transparency(tmp_path): # Single frame im = Image.new("RGB", (1, 1)) im.info["transparency"] = (255, 0, 0) - pytest.warns(UserWarning, im.save, out) + im.save(out) with Image.open(out) as reloaded: - assert "transparency" not in reloaded.info + assert "transparency" in reloaded.info # Multiple frames im = Image.new("RGB", (1, 1)) @@ -762,7 +848,7 @@ def test_palette_save_P(tmp_path): # Forcing a non-straight grayscale palette. im = hopper("P") - palette = bytes([255 - i // 3 for i in range(768)]) + palette = bytes(255 - i // 3 for i in range(768)) out = str(tmp_path / "temp.gif") im.save(out, palette=palette) @@ -772,6 +858,29 @@ def test_palette_save_P(tmp_path): assert_image_equal(reloaded, im) +def test_palette_save_all_P(tmp_path): + frames = [] + colors = ((255, 0, 0), (0, 255, 0)) + for color in colors: + frame = Image.new("P", (100, 100)) + frame.putpalette(color) + frames.append(frame) + + out = str(tmp_path / "temp.gif") + frames[0].save( + out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] + ) + + with Image.open(out) as im: + # Assert that the frames are correct, and each frame has the same palette + assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) + assert im.palette.palette == im.global_palette.palette + + im.seek(1) + assert_image_equal(im.convert("RGB"), frames[1].convert("RGB")) + assert im.palette.palette == im.global_palette.palette + + def test_palette_save_ImagePalette(tmp_path): # Pass in a different palette, as an ImagePalette.ImagePalette # effectively the same as test_palette_save_P @@ -784,7 +893,7 @@ def test_palette_save_ImagePalette(tmp_path): with Image.open(out) as reloaded: im.putpalette(palette) - assert_image_equal(reloaded, im) + assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) def test_save_I(tmp_path): @@ -806,7 +915,7 @@ def test_getdata(): im.putpalette(ImagePalette.ImagePalette("RGB")) im.info = {"background": 0} - passed_palette = bytes([255 - i // 3 for i in range(768)]) + passed_palette = bytes(255 - i // 3 for i in range(768)) GifImagePlugin._FORCE_OPTIMIZE = True try: @@ -840,3 +949,21 @@ def test_extents(): assert im.size == (100, 100) im.seek(1) assert im.size == (150, 150) + + +def test_missing_background(): + # The Global Color Table Flag isn't set, so there is no background color index, + # but the disposal method is "Restore to background color" + with Image.open("Tests/images/missing_background.gif") as im: + im.seek(1) + assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png") + + +def test_saving_rgba(tmp_path): + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/transparent.png") as im: + im.save(out) + + with Image.open(out) as reloaded: + reloaded_rgba = reloaded.convert("RGBA") + assert reloaded_rgba.load()[0, 0][3] == 0 diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 30ec3dc72..3afbbeaac 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,9 +1,9 @@ import io -import sys +import os import pytest -from PIL import IcnsImagePlugin, Image, features +from PIL import IcnsImagePlugin, Image, _binary, features from .helper import assert_image_equal, assert_image_similar_tofile @@ -28,7 +28,6 @@ def test_sanity(): assert im.format == "ICNS" -@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") def test_save(tmp_path): temp_file = str(tmp_path / "temp.icns") @@ -40,8 +39,12 @@ def test_save(tmp_path): assert reread.size == (1024, 1024) assert reread.format == "ICNS" + file_length = os.path.getsize(temp_file) + with open(temp_file, "rb") as fp: + fp.seek(4) + assert _binary.i32be(fp.read(4)) == file_length + -@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") def test_save_append_images(tmp_path): temp_file = str(tmp_path / "temp.icns") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) @@ -57,7 +60,6 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") def test_save_fp(): fp = io.BytesIO() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 5ace0c55e..317264db6 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -18,6 +18,17 @@ def test_sanity(): assert im.get_format_mimetype() == "image/x-icon" +def test_mask(): + with Image.open("Tests/images/hopper_mask.ico") as im: + assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") + + +def test_black_and_white(): + with Image.open("Tests/images/black_and_white.ico") as im: + assert im.mode == "RGBA" + assert im.size == (16, 16) + + def test_invalid_file(): with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): @@ -50,6 +61,35 @@ def test_save_to_bytes(): assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) +def test_save_to_bytes_bmp(mode): + output = io.BytesIO() + im = hopper(mode) + im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) + + # The default image + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["sizes"] == {(32, 32), (64, 64)} + + assert "RGBA" == reloaded.mode + assert (64, 64) == reloaded.size + assert reloaded.format == "ICO" + im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA") + assert_image_equal(reloaded, im) + + # The other one + output.seek(0) + with Image.open(output) as reloaded: + reloaded.size = (32, 32) + + assert "RGBA" == reloaded.mode + assert (32, 32) == reloaded.size + assert reloaded.format == "ICO" + im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA") + assert_image_equal(reloaded, im) + + def test_incorrect_size(): with Image.open(TEST_ICO_FILE) as im: with pytest.raises(ValueError): @@ -119,5 +159,4 @@ def test_draw_reloaded(tmp_path): im.save(outfile) with Image.open(outfile) as im: - im.save("Tests/images/hopper_draw.ico") assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3ee33d65f..4b2ffe70d 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -24,9 +24,15 @@ from .helper import ( djpeg_available, hopper, is_win32, + mark_if_feature_version, skip_unless_feature, ) +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + TEST_FILE = "Tests/images/hopper.jpg" @@ -79,26 +85,26 @@ class TestFileJpeg: f = "Tests/images/pil_sample_cmyk.jpg" with Image.open(f) as im: # the source image has red pixels in the upper left corner. - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] + c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 # the opposite corner is black - c, m, y, k = [ + c, m, y, k = ( x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ] + ) assert k > 0.9 # roundtrip, and check again im = self.roundtrip(im) - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] + c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 - c, m, y, k = [ + c, m, y, k = ( x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ] + ) assert k > 0.9 @pytest.mark.parametrize( @@ -116,7 +122,9 @@ class TestFileJpeg: assert test(100, 200) == (100, 200) assert test(0) is None # square pixels - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_icc(self, tmp_path): # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: @@ -156,7 +164,9 @@ class TestFileJpeg: test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte test(ImageFile.MAXBLOCK * 4 + 3) # large block - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_large_icc_meta(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than @@ -423,7 +433,9 @@ class TestFileJpeg: with Image.open(filename): pass - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_truncated_jpeg_should_read_all_the_data(self): filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True @@ -442,7 +454,9 @@ class TestFileJpeg: with pytest.raises(OSError): im.load() - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_qtables(self, tmp_path): def _n_qtables_helper(n, test_file): with Image.open(test_file) as im: @@ -452,7 +466,7 @@ class TestFileJpeg: assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization - assert reloaded.quantization[0].typecode == "B" + assert max(reloaded.quantization[0]) <= 255 with Image.open("Tests/images/hopper.jpg") as im: qtables = im.quantization @@ -464,7 +478,8 @@ class TestFileJpeg: # valid bounds for baseline qtable bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] - self.roundtrip(im, qtables=[bounds_qtable]) + im2 = self.roundtrip(im, qtables=[bounds_qtable]) + assert im2.quantization == {0: bounds_qtable} # values from wizard.txt in jpeg9-a src package. standard_l_qtable = [ @@ -575,6 +590,12 @@ 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: @@ -609,7 +630,7 @@ class TestFileJpeg: reloaded.save(f, quality="keep", optimize=True) def test_bad_mpo_header(self): - """ Treat unknown MPO as JPEG """ + """Treat unknown MPO as JPEG""" # Arrange # Act @@ -647,15 +668,6 @@ class TestFileJpeg: reloaded.load() assert im.info["dpi"] == reloaded.info["dpi"] - def test_load_dpi_rounding(self): - # Round up - with Image.open("Tests/images/iptc_roundUp.jpg") as im: - assert im.info["dpi"] == (44, 44) - - # Round down - with Image.open("Tests/images/iptc_roundDown.jpg") as im: - assert im.info["dpi"] == (2, 2) - def test_save_dpi_rounding(self, tmp_path): outfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: @@ -706,6 +718,15 @@ class TestFileJpeg: # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) + def test_dpi_exif_string(self): + # Arrange + # 0x011A tag in this exif contains string '300300\x02' + with Image.open("Tests/images/broken_exif_dpi.jpg") as im: + + # Act / Assert + # This should return the default + assert im.info.get("dpi") == (72, 72) + def test_no_dpi_in_exif(self): # Arrange # This is photoshop-200dpi.jpg with resolution removed from EXIF: @@ -726,7 +747,9 @@ class TestFileJpeg: # OSError for unidentified image. assert im.info.get("dpi") == (72, 72) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_x_resolution(self, tmp_path): with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() @@ -757,7 +780,9 @@ class TestFileJpeg: # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" - @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_photoshop(self): with Image.open("Tests/images/photoshop-200dpi.jpg") as im: assert im.info["photoshop"][0x03ED] == { @@ -782,6 +807,20 @@ class TestFileJpeg: apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] assert [65504, 24] == apps_13_lengths + def test_adobe_transform(self): + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert im.info["adobe_transform"] == 1 + + with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: + assert im.info["adobe_transform"] == 2 + + # This image has been manually hexedited + # so that the APP14 reports its length to be 11, + # leaving no room for "adobe_transform" + with Image.open("Tests/images/truncated_app14.jpg") as im: + assert "adobe" in im.info + assert "adobe_transform" not in im.info + def test_icc_after_SOF(self): with Image.open("Tests/images/icc-after-SOF.jpg") as im: assert im.info["icc_profile"] == b"profile" @@ -805,6 +844,56 @@ class TestFileJpeg: # Assert the entire file has not been read assert 0 < buffer.max_pos < size + def test_getxmp(self): + with Image.open("Tests/images/xmp_test.jpg") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["DerivedFrom"] == { + "documentID": "8367D410E636EA95B7DE7EBA1C43A412", + "originalDocumentID": "8367D410E636EA95B7DE7EBA1C43A412", + } + assert description["Look"]["Description"]["Group"]["Alt"]["li"] == { + "lang": "x-default", + "text": "Profiles", + } + assert description["ToneCurve"]["Seq"]["li"] == ["0, 0", "255, 255"] + + # Attribute + assert description["Version"] == "10.4" + + if ElementTree is not None: + with Image.open("Tests/images/hopper.jpg") as im: + assert im.getxmp() == {} + + @pytest.mark.timeout(timeout=1) + def test_eof(self): + # Even though this decoder never says that it is finished + # the image should still end when there is no new data + class InfiniteMockPyDecoder(ImageFile.PyDecoder): + def decode(self, buffer): + return 0, 0 + + decoder = InfiniteMockPyDecoder(None) + + def closure(mode, *args): + decoder.__init__(mode, *args) + return decoder + + Image.register_decoder("INFINITE", closure) + + with Image.open(TEST_FILE) as im: + im.tile = [ + ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), + ] + ImageFile.LOAD_TRUNCATED_IMAGES = True + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + @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 13ae09af5..ca410162a 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,18 +1,20 @@ +import os import re from io import BytesIO import pytest -from PIL import Image, ImageFile, Jpeg2KImagePlugin, features +from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features from .helper import ( assert_image_equal, assert_image_similar, assert_image_similar_tofile, - is_big_endian, skip_unless_feature, ) +EXTRA_DIR = "Tests/images/jpeg2000" + pytestmark = skip_unless_feature("jpg_2000") test_card = Image.open("Tests/images/test-card.png") @@ -28,9 +30,9 @@ def roundtrip(im, **options): im.save(out, "JPEG2000", **options) test_bytes = out.tell() out.seek(0) - im = Image.open(out) - im.bytes = test_bytes # for testing only - im.load() + with Image.open(out) as im: + im.bytes = test_bytes # for testing only + im.load() return im @@ -124,6 +126,16 @@ def test_prog_res_rt(): assert_image_equal(im, test_card) +def test_default_num_resolutions(): + for num_resolutions in range(2, 6): + d = 1 << (num_resolutions - 1) + im = test_card.resize((d - 1, d - 1)) + with pytest.raises(OSError): + roundtrip(im, num_resolutions=num_resolutions) + reloaded = roundtrip(im) + assert_image_equal(im, reloaded) + + def test_reduce(): with Image.open("Tests/images/test-card-lossless.jp2") as im: assert callable(im.reduce) @@ -138,6 +150,38 @@ def test_reduce(): assert im.size == (40, 30) +def test_load_dpi(): + with Image.open("Tests/images/test-card-lossless.jp2") as im: + assert im.info["dpi"] == (71.9836, 71.9836) + + with Image.open("Tests/images/zero_dpi.jp2") as im: + assert "dpi" not in im.info + + +def test_restricted_icc_profile(): + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + # JPEG2000 image with a restricted ICC profile and a known colorspace + with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im: + assert im.mode == "RGB" + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + + +def test_header_errors(): + for path in ( + "Tests/images/invalid_header_length.jp2", + "Tests/images/not_enough_data.jp2", + ): + with pytest.raises(UnidentifiedImageError): + with Image.open(path): + pass + + with pytest.raises(OSError): + with Image.open("Tests/images/expected_to_read.jp2"): + pass + + def test_layers_type(tmp_path): outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: @@ -189,13 +233,11 @@ def test_16bit_monochrome_has_correct_mode(): assert jp2.mode == "I;16" -@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") def test_16bit_monochrome_jp2_like_tiff(): with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) -@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") def test_16bit_monochrome_j2k_like_tiff(): with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) @@ -231,3 +273,42 @@ def test_parser_feed(): # Assert assert p.image.size == (640, 480) + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) +def test_subsampling_decode(name): + test = f"{EXTRA_DIR}/{name}.jp2" + reference = f"{EXTRA_DIR}/{name}.ppm" + + with Image.open(test) as im: + epsilon = 3 # for YCbCr images + with Image.open(reference) as im2: + width, height = im2.size + if name[-1] == "2": + # RGB reference images are downscaled + epsilon = 3e-3 + width, height = width * 2, height * 2 + expected = im2.resize((width, height), Image.NEAREST) + assert_image_similar(im, expected, epsilon) + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k", + "Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k", + "Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k", + "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", + ], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + # Valgrind should not complain here + try: + im.load() + except OSError: + pass diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 22b641b5f..e40a19394 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -9,7 +9,7 @@ from ctypes import c_float import pytest from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features -from PIL.TiffImagePlugin import SUBIFD +from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD from .helper import ( assert_image_equal, @@ -17,6 +17,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, hopper, + mark_if_feature_version, skip_unless_feature, ) @@ -96,13 +97,13 @@ class TestFileLibTiff(LibTiffTestCase): self._assert_noerr(tmp_path, im) def test_g4_eq_png(self): - """ Checking that we're actually getting the data that we expect""" + """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/hopper_bw_500.png") as png: assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif") # see https://github.com/python-pillow/Pillow/issues/279 def test_g4_fillorder_eq_png(self): - """ Checking that we're actually getting the data that we expect""" + """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/g4-fillorder-test.tif") as g4: assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png") @@ -136,7 +137,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_write_metadata(self, tmp_path): - """ Test metadata writing through libtiff """ + """Test metadata writing through libtiff""" for legacy_api in [False, True]: f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: @@ -577,6 +578,17 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False + def test_multipage_seek_backwards(self): + TiffImagePlugin.READ_LIBTIFF = True + with Image.open("Tests/images/multipage.tiff") as im: + im.seek(1) + im.load() + + im.seek(0) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + + TiffImagePlugin.READ_LIBTIFF = False + def test__next(self): TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.tif") as im: @@ -658,6 +670,15 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False + def test_save_ycbcr(self, tmp_path): + im = hopper("YCbCr") + outfile = str(tmp_path / "temp.tif") + im.save(outfile, compression="jpeg") + + with Image.open(outfile) as reloaded: + assert reloaded.tag_v2[530] == (1, 1) + assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) + def test_crashing_metadata(self, tmp_path): # issue 1597 with Image.open("Tests/images/rdf.tif") as im: @@ -804,6 +825,17 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") + def test_sampleformat_write(self, tmp_path): + im = Image.new("F", (1, 1)) + out = str(tmp_path / "temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True + im.save(out) + TiffImagePlugin.WRITE_LIBTIFF = False + + with Image.open(out) as reloaded: + assert reloaded.mode == "F" + assert reloaded.getexif()[SAMPLEFORMAT] == 3 + def test_lzw(self): with Image.open("Tests/images/hopper_lzw.tif") as im: assert im.mode == "RGB" @@ -822,13 +854,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_strip_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_strip_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: @@ -839,13 +875,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiled_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/flower2.jpg") - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiled_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: @@ -891,9 +931,31 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + @pytest.mark.parametrize("compression", (None, "jpeg")) + def test_block_tile_tags(self, compression, tmp_path): + im = hopper() + out = str(tmp_path / "temp.tif") + + tags = { + TiffImagePlugin.TILEWIDTH: 256, + TiffImagePlugin.TILELENGTH: 256, + TiffImagePlugin.TILEOFFSETS: 256, + TiffImagePlugin.TILEBYTECOUNTS: 256, + } + im.save(out, exif=tags, compression=compression) + + with Image.open(out) as reloaded: + for tag in tags.keys(): + assert tag not in reloaded.getexif() + def test_old_style_jpeg(self): - infile = "Tests/images/old-style-jpeg-compression.tif" - with Image.open(infile) as im: + with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + + def test_open_missing_samplesperpixel(self): + with Image.open( + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" + ) as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") def test_no_rows_per_strip(self): @@ -942,3 +1004,34 @@ class TestFileLibTiff(LibTiffTestCase): # Assert that the error code is IMAGING_CODEC_MEMORY assert str(e.value) == "-9" TiffImagePlugin.READ_LIBTIFF = False + + @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) + def test_save_multistrip(self, compression, tmp_path): + im = hopper("RGB").resize((256, 256)) + out = str(tmp_path / "temp.tif") + im.save(out, compression=compression) + + with Image.open(out) as im: + # Assert that there are multiple strips + assert len(im.tag_v2[STRIPOFFSETS]) > 1 + + def test_save_single_strip(self, tmp_path): + im = hopper("RGB").resize((256, 256)) + out = str(tmp_path / "temp.tif") + + TiffImagePlugin.STRIP_SIZE = 2 ** 18 + try: + + im.save(out, compression="tiff_adobe_deflate") + + with Image.open(out) as im: + assert len(im.tag_v2[STRIPOFFSETS]) == 1 + finally: + TiffImagePlugin.STRIP_SIZE = 65536 + + @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) + def test_save_zero(self, compression, tmp_path): + im = Image.new("RGB", (0, 0)) + out = str(tmp_path / "temp.tif") + with pytest.raises(SystemError): + im.save(out, compression=compression) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 25d194b62..e1c1c361b 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -5,9 +5,7 @@ import pytest from PIL import Image -from .helper import IMCONVERT, assert_image_equal, hopper, imagemagick_available - -_roundtrip = imagemagick_available() +from .helper import assert_image_equal, hopper, magick_command def helper_save_as_palm(tmp_path, mode): @@ -23,13 +21,10 @@ def helper_save_as_palm(tmp_path, mode): assert os.path.getsize(outfile) > 0 -def open_with_imagemagick(tmp_path, f): - if not imagemagick_available(): - raise OSError() - +def open_with_magick(magick, tmp_path, f): outfile = str(tmp_path / "temp.png") rc = subprocess.call( - [IMCONVERT, f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT ) if rc: raise OSError @@ -37,14 +32,15 @@ def open_with_imagemagick(tmp_path, f): def roundtrip(tmp_path, mode): - if not _roundtrip: + magick = magick_command() + if not magick: return im = hopper(mode) outfile = str(tmp_path / "temp.palm") im.save(outfile) - converted = open_with_imagemagick(tmp_path, outfile) + converted = open_with_magick(magick, tmp_path, outfile) assert_image_equal(converted, im) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index e5bba483a..10daa414b 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -8,7 +8,7 @@ import pytest from PIL import Image, PdfParser -from .helper import hopper +from .helper import hopper, mark_if_feature_version def helper_save_as_pdf(tmp_path, mode, **kwargs): @@ -30,7 +30,7 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): with open(outfile, "rb") as fp: contents = fp.read() size = tuple( - int(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() ) assert im.size == size @@ -42,7 +42,8 @@ def test_monochrome(tmp_path): mode = "1" # Act / Assert - helper_save_as_pdf(tmp_path, mode) + outfile = helper_save_as_pdf(tmp_path, mode) + assert os.path.getsize(outfile) < 15000 def test_greyscale(tmp_path): @@ -85,7 +86,30 @@ def test_unsupported_mode(tmp_path): im.save(outfile) -@pytest.mark.valgrind_known_error(reason="Known Failing") +def test_resolution(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, resolution=150) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (61.44, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (61.44, 61.44) + + +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_save_all(tmp_path): # Single frame image helper_save_as_pdf(tmp_path, "RGB", save_all=True) @@ -286,3 +310,14 @@ def test_pdf_append_to_bytesio(): f = io.BytesIO(f.getvalue()) im.save(f, format="PDF", append=True) assert len(f.getvalue()) > initial_size + + +@pytest.mark.timeout(1) +@pytest.mark.parametrize("newline", (b"\r", b"\n")) +def test_redos(newline): + malicious = b" trailer<<>>" + newline * 3456 + + # This particular exception isn't relevant here. + # The important thing is it doesn't timeout, cause a ReDoS (CVE-2021-25292). + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=malicious) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index bbf5f5772..0869cc58b 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,4 +1,5 @@ import re +import sys import zlib from io import BytesIO @@ -10,12 +11,18 @@ from .helper import ( PillowLeakTestCase, assert_image, assert_image_equal, + assert_image_equal_tofile, hopper, - is_big_endian, is_win32, + mark_if_feature_version, skip_unless_feature, ) +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + # sample png stream TEST_PNG_FILE = "Tests/images/hopper.png" @@ -69,7 +76,6 @@ class TestFilePng: png.crc(cid, s) return chunks - @pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") def test_sanity(self, tmp_path): # internal version number @@ -383,25 +389,12 @@ class TestFilePng: # Check dpi roundtripping with Image.open(TEST_PNG_FILE) as im: - im = roundtrip(im, dpi=(100, 100)) - assert im.info["dpi"] == (100, 100) + im = roundtrip(im, dpi=(100.33, 100.33)) + assert im.info["dpi"] == (100.33, 100.33) - def test_load_dpi_rounding(self): - # Round up + def test_load_float_dpi(self): with Image.open(TEST_PNG_FILE) as im: - assert im.info["dpi"] == (96, 96) - - # Round down - with Image.open("Tests/images/icc_profile_none.png") as im: - assert im.info["dpi"] == (72, 72) - - def test_save_dpi_rounding(self): - with Image.open(TEST_PNG_FILE) as im: - im = roundtrip(im, dpi=(72.2, 72.2)) - assert im.info["dpi"] == (72, 72) - - im = roundtrip(im, dpi=(72.8, 72.8)) - assert im.info["dpi"] == (73, 73) + assert im.info["dpi"] == (95.9866, 95.9866) def test_roundtrip_text(self): # Check text roundtripping @@ -625,6 +618,23 @@ class TestFilePng: with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} + def test_padded_idat(self): + # This image has been manually hexedited + # so that the IDAT chunk has padding at the end + # Set MAXBLOCK to the length of the actual data + # so that the decoder finishes reading before the chunk ends + MAXBLOCK = ImageFile.MAXBLOCK + ImageFile.MAXBLOCK = 45 + ImageFile.LOAD_TRUNCATED_IMAGES = True + + with Image.open("Tests/images/padded_idat.png") as im: + im.load() + + ImageFile.MAXBLOCK = MAXBLOCK + ImageFile.LOAD_TRUNCATED_IMAGES = False + + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + def test_specify_bits(self, tmp_path): im = hopper("P") @@ -644,6 +654,18 @@ class TestFilePng: with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 + def test_getxmp(self): + with Image.open("Tests/images/color_snakes.png") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["PixelXDimension"] == "10" + assert description["subject"]["Seq"] is None + def test_exif(self): # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: @@ -679,7 +701,9 @@ class TestFilePng: exif = reloaded._getexif() assert exif[274] == 1 - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_from_jpg(self, tmp_path): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") @@ -708,6 +732,32 @@ class TestFilePng: with pytest.raises(EOFError): im.seek(1) + @pytest.mark.parametrize("buffer", (True, False)) + def test_save_stdout(self, buffer): + old_stdout = sys.stdout + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout + + with Image.open(TEST_PNG_FILE) as im: + im.save(sys.stdout, "PNG") + + # Reset stdout + sys.stdout = old_stdout + + if buffer: + mystdout = mystdout.buffer + with Image.open(mystdout) as reloaded: + assert_image_equal_tofile(reloaded, TEST_PNG_FILE) + @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 0ccfb5e88..ad36319db 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,3 +1,6 @@ +import sys +from io import BytesIO + import pytest from PIL import Image @@ -80,3 +83,30 @@ def test_mimetypes(tmp_path): f.write("PyCMYK\n128 128\n255") with Image.open(path) as im: assert im.get_format_mimetype() == "image/x-portable-anymap" + + +@pytest.mark.parametrize("buffer", (True, False)) +def test_save_stdout(buffer): + old_stdout = sys.stdout + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout + + with Image.open(TEST_FILE) as im: + im.save(sys.stdout, "PPM") + + # Reset stdout + sys.stdout = old_stdout + + if buffer: + mystdout = mystdout.buffer + with Image.open(mystdout) as reloaded: + assert_image_equal_tofile(reloaded, TEST_FILE) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 87373d2c4..f50fe133f 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -57,9 +57,10 @@ def test_n_frames(): assert im.n_frames == 1 assert not im.is_animated - with Image.open(test_file) as im: - assert im.n_frames == 2 - assert im.is_animated + for path in [test_file, "Tests/images/negative_layer_count.psd"]: + with Image.open(path) as im: + assert im.n_frames == 2 + assert im.is_animated def test_eoferror(): @@ -130,3 +131,25 @@ def test_combined_larger_than_size(): with pytest.raises(OSError): with Image.open("Tests/images/combined_larger_than_size.psd"): pass + + +@pytest.mark.parametrize( + "test_file,raises", + [ + ( + "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd", + Image.UnidentifiedImageError, + ), + ( + "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd", + Image.UnidentifiedImageError, + ), + ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), + ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), + ], +) +def test_crashes(test_file, raises): + with open(test_file, "rb") as f: + with pytest.raises(raises): + with Image.open(f): + pass diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 0210dd4f1..6a5d8887d 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -73,6 +73,13 @@ def test_write(tmp_path): img.save(out, format="sgi") assert_image_equal_tofile(img, out) + out = str(tmp_path / "fp.sgi") + with open(out, "wb") as fp: + img.save(fp) + assert_image_equal_tofile(img, out) + + assert not fp.closed + for mode in ("L", "RGB", "RGBA"): roundtrip(hopper(mode)) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 465e13316..e2351d723 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -6,7 +6,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper +from .helper import assert_image_equal, assert_image_equal_tofile, hopper _TGA_DIR = os.path.join("Tests", "images", "tga") _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") @@ -65,6 +65,16 @@ def test_sanity(tmp_path): roundtrip(original_im) +def test_palette_depth_16(tmp_path): + with Image.open("Tests/images/p_16.tga") as im: + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") + + out = str(tmp_path / "temp.png") + im.save(out) + with Image.open(out) as reloaded: + assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") + + def test_id_field(): # tga file with id field test_file = "Tests/images/tga_id_field.tga" @@ -112,6 +122,14 @@ def test_save_wrong_mode(tmp_path): im.save(out) +def test_save_mapdepth(): + # This image has been manually hexedited from 200x32_p_bl_raw.tga + # to include an origin + test_file = "Tests/images/200x32_p_bl_raw_origin.tga" + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png") + + def test_save_id_section(tmp_path): test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: @@ -153,6 +171,15 @@ def test_save_orientation(tmp_path): assert test_im.info["orientation"] == 1 +def test_horizontal_orientations(): + # These images have been manually hexedited to have the relevant orientations + with Image.open("Tests/images/rgb32rle_top_right.tga") as im: + assert im.load()[90, 90][:3] == (0, 0, 0) + + with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: + assert im.load()[90, 90][:3] == (0, 255, 0) + + def test_save_rle(tmp_path): test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index ba7f9a084..5801e1766 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -3,7 +3,7 @@ from io import BytesIO import pytest -from PIL import Image, TiffImagePlugin +from PIL import Image, ImageFile, TiffImagePlugin from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( @@ -16,6 +16,11 @@ from .helper import ( is_win32, ) +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + class TestFileTiff: def test_sanity(self, tmp_path): @@ -137,37 +142,33 @@ class TestFileTiff: im._setup() assert im.info["dpi"] == (71.0, 71.0) - def test_load_dpi_rounding(self): - for resolutionUnit, dpi in ((None, (72, 73)), (2, (72, 73)), (3, (183, 185))): - with Image.open( - "Tests/images/hopper_roundDown_" + str(resolutionUnit) + ".tif" - ) as im: - assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit - assert im.info["dpi"] == (dpi[0], dpi[0]) + @pytest.mark.parametrize( + "resolutionUnit, dpi", + [(None, 72.8), (2, 72.8), (3, 184.912)], + ) + def test_load_float_dpi(self, resolutionUnit, dpi): + with Image.open( + "Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif" + ) as im: + assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit + assert im.info["dpi"] == (dpi, dpi) - with Image.open( - "Tests/images/hopper_roundUp_" + str(resolutionUnit) + ".tif" - ) as im: - assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit - assert im.info["dpi"] == (dpi[1], dpi[1]) - - def test_save_dpi_rounding(self, tmp_path): + def test_save_float_dpi(self, tmp_path): outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: - for dpi in (72.2, 72.8): - im.save(outfile, dpi=(dpi, dpi)) + dpi = (72.2, 72.2) + im.save(outfile, dpi=dpi) - with Image.open(outfile) as reloaded: - reloaded.load() - assert (round(dpi), round(dpi)) == reloaded.info["dpi"] + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == dpi def test_save_setting_missing_resolution(self): b = BytesIO() with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) with Image.open(b) as im: - assert float(im.tag_v2[X_RESOLUTION]) == 123.45 - assert float(im.tag_v2[Y_RESOLUTION]) == 123.45 + assert im.tag_v2[X_RESOLUTION] == 123.45 + assert im.tag_v2[Y_RESOLUTION] == 123.45 def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -301,6 +302,19 @@ class TestFileTiff: assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) + def test_frame_order(self): + # A frame can't progress to itself after reading + with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: + assert im.n_frames == 1 + + # A frame can't progress to a frame that has already been read + with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: + assert im.n_frames == 2 + + # Frames don't have to be in sequence + with Image.open("Tests/images/multipage_out_of_order.tiff") as im: + assert im.n_frames == 3 + def test___str__(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: @@ -389,6 +403,75 @@ class TestFileTiff: with Image.open("Tests/images/ifd_tag_type.tiff") as im: assert 0x8825 in im.tag_v2 + def test_exif(self, tmp_path): + def check_exif(exif): + assert sorted(exif.keys()) == [ + 256, + 257, + 258, + 259, + 262, + 271, + 272, + 273, + 277, + 278, + 279, + 282, + 283, + 284, + 296, + 297, + 305, + 339, + 700, + 34665, + 34853, + 50735, + ] + assert exif[256] == 640 + assert exif[271] == "FLIR" + + gps = exif.get_ifd(0x8825) + assert list(gps.keys()) == [0, 1, 2, 3, 4, 5, 6, 18] + assert gps[0] == b"\x03\x02\x00\x00" + assert gps[18] == "WGS-84" + + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + exif = im.getexif() + check_exif(exif) + + im.save(outfile, exif=exif) + + outfile2 = str(tmp_path / "temp2.tif") + with Image.open(outfile) as im: + exif = im.getexif() + check_exif(exif) + + im.save(outfile2, exif=exif.tobytes()) + + with Image.open(outfile2) as im: + exif = im.getexif() + check_exif(exif) + + def test_exif_frames(self): + # Test that EXIF data can change across frames + with Image.open("Tests/images/g4-multi.tiff") as im: + assert im.getexif()[273] == (328, 815) + + im.seek(1) + assert im.getexif()[273] == (1408, 1907) + + @pytest.mark.parametrize("mode", ("1", "L")) + def test_photometric(self, mode, tmp_path): + filename = str(tmp_path / "temp.tif") + im = hopper(mode) + im.save(filename, tiffinfo={262: 0}) + with Image.open(filename) as reloaded: + assert reloaded.tag_v2[262] == 0 + assert_image_equal(im, reloaded) + def test_seek(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: @@ -590,6 +673,18 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert "icc_profile" not in reloaded.info + def test_getxmp(self): + with Image.open("Tests/images/lab.tif") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") @@ -619,15 +714,25 @@ class TestFileTiff: # Ignore this UserWarning which triggers for four tags: # "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..." @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") + # Ignore this UserWarning: + @pytest.mark.filterwarnings("ignore:Truncated File Read") @pytest.mark.skipif( not os.path.exists("Tests/images/string_dimension.tiff"), reason="Extra image files not installed", ) def test_string_dimension(self): # Assert that an error is raised if one of the dimensions is a string - with pytest.raises(ValueError): - with Image.open("Tests/images/string_dimension.tiff"): - pass + with Image.open("Tests/images/string_dimension.tiff") as im: + with pytest.raises(OSError): + im.load() + + @pytest.mark.timeout(6) + @pytest.mark.filterwarnings("ignore:Truncated File Read") + def test_timeout(self): + with Image.open("Tests/images/timeout-6646305047838720") as im: + ImageFile.LOAD_TRUNCATED_IMAGES = True + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False @pytest.mark.skipif(not is_win32(), reason="Windows only") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 0f7f8adf1..2213af5aa 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -122,7 +122,7 @@ def test_read_metadata(): def test_write_metadata(tmp_path): - """ Test metadata writing through the python code """ + """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: f = str(tmp_path / "temp.tiff") img.save(f, tiffinfo=img.tag) @@ -179,6 +179,27 @@ def test_no_duplicate_50741_tag(): assert TAG_IDS["BestQualityScale"] == 50780 +def test_iptc(tmp_path): + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.Lab.tif") as im: + im.save(out) + + +def test_undefined_zero(tmp_path): + # Check that the tag has not been changed since this test was created + tag = TiffTags.TAGS_V2[45059] + assert tag.type == TiffTags.UNDEFINED + assert tag.length == 0 + + info = TiffImagePlugin.ImageFileDirectory(b"II*\x00\x08\x00\x00\x00") + info[45059] = b"test" + + # Assert that the tag value does not change by setting it to itself + original = info[45059] + info[45059] = info[45059] + assert info[45059] == original + + def test_empty_metadata(): f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") head = f.read(8) @@ -355,3 +376,30 @@ def test_too_many_entries(): # Should not raise ValueError. pytest.warns(UserWarning, lambda: ifd[277]) + + +def test_tag_group_data(): + base_ifd = TiffImagePlugin.ImageFileDirectory_v2() + interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965) + for ifd in (base_ifd, interop_ifd): + ifd[2] = "test" + ifd[256] = 10 + + assert base_ifd.tagtype[256] == 4 + assert interop_ifd.tagtype[256] != base_ifd.tagtype[256] + + assert interop_ifd.tagtype[2] == 7 + assert base_ifd.tagtype[2] != interop_ifd.tagtype[256] + + +def test_empty_subifd(tmp_path): + out = str(tmp_path / "temp.jpg") + + im = hopper() + exif = im.getexif() + exif[TiffImagePlugin.EXIFIFD] = {} + im.save(out, exif=exif) + + with Image.open(out) as reloaded: + exif = reloaded.getexif() + assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {} diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 60be1d5bc..f25b42fe0 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,15 +1,21 @@ from PIL import WalImageFile +from .helper import assert_image_equal_tofile + def test_open(): # Arrange TEST_FILE = "Tests/images/hopper.wal" # Act - im = WalImageFile.open(TEST_FILE) + with WalImageFile.open(TEST_FILE) as im: - # Assert - assert im.format == "WAL" - assert im.format_description == "Quake2 Texture" - assert im.mode == "P" - assert im.size == (128, 128) + # Assert + assert im.format == "WAL" + assert im.format_description == "Quake2 Texture" + assert im.mode == "P" + assert im.size == (128, 128) + + assert isinstance(im, WalImageFile.WalImageFile) + + assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index cde7020ed..e72b4993c 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,5 +1,6 @@ import io import re +import sys import pytest @@ -103,6 +104,13 @@ class TestFileWebp: hopper().save(buffer_method, format="WEBP", method=6) assert buffer_no_args.getbuffer() != buffer_method.getbuffer() + def test_icc_profile(self, tmp_path): + self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) + if _webp.HAVE_WEBPANIM: + self._roundtrip( + tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} + ) + def test_write_unsupported_mode_L(self, tmp_path): """ Saving a black-and-white file to WebP format should work, and be @@ -119,6 +127,14 @@ class TestFileWebp: self._roundtrip(tmp_path, "P", 50.0) + @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") + def test_write_encoding_error_message(self, tmp_path): + temp_file = str(tmp_path / "temp.webp") + im = Image.new("RGB", (15000, 15000)) + with pytest.raises(ValueError) as e: + im.save(temp_file, method=0) + assert str(e.value) == "encoding error 6" + def test_WebPEncode_with_invalid_args(self): """ Calling encoder functions with no arguments should result in an error. @@ -172,9 +188,7 @@ class TestFileWebp: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) - difference = sum( - [abs(original_value[i] - reread_value[i]) for i in range(0, 3)] - ) + difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) assert difference < 5 @skip_unless_feature("webp") diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 26e903488..25ebffe02 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -45,12 +45,12 @@ def test_write_animation_L(tmp_path): # Compare first and last frames to the original animated GIF orig.load() im.load() - assert_image_similar(im, orig.convert("RGBA"), 25.0) + assert_image_similar(im, orig.convert("RGBA"), 32.9) orig.seek(orig.n_frames - 1) im.seek(im.n_frames - 1) orig.load() im.load() - assert_image_similar(im, orig.convert("RGBA"), 25.0) + assert_image_similar(im, orig.convert("RGBA"), 32.9) @pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index cb133e2c5..e6d6fc63f 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -4,7 +4,7 @@ import pytest from PIL import Image -from .helper import skip_unless_feature +from .helper import mark_if_feature_version, skip_unless_feature pytestmark = [ skip_unless_feature("webp"), @@ -41,7 +41,9 @@ def test_read_exif_metadata_without_prefix(): assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_write_exif_metadata(): file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() @@ -74,7 +76,9 @@ def test_read_icc_profile(): assert icc == expected_icc -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_write_icc_metadata(): file_path = "Tests/images/flower2.jpg" test_buffer = BytesIO() @@ -92,7 +96,9 @@ def test_write_icc_metadata(): assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match" -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_read_no_exif(): file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index bf9d105e5..3f8bc96cc 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -44,14 +44,9 @@ def test_register_handler(tmp_path): WmfImagePlugin.register_handler(original_handler) -def test_load_dpi_rounding(): - # Round up +def test_load_float_dpi(): with Image.open("Tests/images/drawing.emf") as im: - assert im.info["dpi"] == 1424 - - # Round down - with Image.open("Tests/images/drawing_roundDown.emf") as im: - assert im.info["dpi"] == 1426 + assert im.info["dpi"] == 1423.7668161434979 def test_load_set_dpi(): diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 015210b4d..38f7ddac5 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -4,7 +4,7 @@ from .helper import PillowLeakTestCase, skip_unless_feature class TestTTypeFontLeak(PillowLeakTestCase): - # fails at iteration 3 in master + # fails at iteration 3 in main iterations = 10 mem_limit = 4096 # k @@ -24,7 +24,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): class TestDefaultFontLeak(TestTTypeFontLeak): - # fails at iteration 37 in master + # fails at iteration 37 in main iterations = 100 mem_limit = 1024 # k diff --git a/Tests/test_image.py b/Tests/test_image.py index 30d093e15..4dde66f11 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -6,8 +6,7 @@ import tempfile import pytest -import PIL -from PIL import Image, ImageDraw, ImagePalette, ImageShow, UnidentifiedImageError +from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError from .helper import ( assert_image_equal, @@ -16,6 +15,7 @@ from .helper import ( assert_not_all_same, hopper, is_win32, + mark_if_feature_version, skip_unless_feature, ) @@ -148,10 +148,11 @@ class TestImage: assert im.mode == "RGB" assert im.size == (128, 128) - temp_file = str(tmp_path / "temp.jpg") - if os.path.exists(temp_file): - os.remove(temp_file) - im.save(Path(temp_file)) + for ext in (".jpg", ".jp2"): + temp_file = str(tmp_path / ("temp." + ext)) + if os.path.exists(temp_file): + os.remove(temp_file) + im.save(Path(temp_file)) def test_fp_name(self, tmp_path): temp_file = str(tmp_path / "temp.jpg") @@ -192,6 +193,10 @@ class TestImage: assert not im.readonly @pytest.mark.skipif(is_win32(), reason="Test requires opening tempfile twice") + @pytest.mark.skipif( + sys.platform == "cygwin", + reason="Test requires opening an mmaped file for writing", + ) def test_readonly_save(self, tmp_path): temp_file = str(tmp_path / "temp.bmp") shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) @@ -581,6 +586,10 @@ class TestImage: assert ext_individual == ext_multiple def test_remap_palette(self): + # Test identity transform + with Image.open("Tests/images/hopper.gif") as im: + assert_image_equal(im, im.remap_palette(list(range(256)))) + # Test illegal image mode with hopper() as im: with pytest.raises(ValueError): @@ -605,7 +614,7 @@ class TestImage: else: assert new_im.palette is None - _make_new(im, im_p, im_p.palette) + _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) _make_new(im_p, im, None) _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) @@ -620,22 +629,6 @@ class TestImage: expected = Image.new(mode, (100, 100), color) assert_image_equal(im.convert(mode), expected) - def test_showxv_deprecation(self): - class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options): - return True - - viewer = TestViewer() - ImageShow.register(viewer, -1) - - im = Image.new("RGB", (50, 50), "white") - - with pytest.warns(DeprecationWarning): - Image._showxv(im) - - # Restore original state - ImageShow._viewers.pop(0) - def test_no_resource_warning_on_save(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/835 # Arrange @@ -662,7 +655,9 @@ class TestImage: assert not fp.closed - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_jpeg(self, tmp_path): with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() @@ -770,9 +765,27 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) - @pytest.mark.skipif( - sys.version_info < (3, 7), reason="Python 3.7 or greater required" - ) + def test_exif_load_from_fp(self): + with Image.open("Tests/images/flower.jpg") as im: + data = im.info["exif"] + if data.startswith(b"Exif\x00\x00"): + data = data[6:] + fp = io.BytesIO(data) + + exif = Image.Exif() + exif.load_from_fp(fp) + assert exif == { + 271: "Canon", + 272: "Canon PowerShot S40", + 274: 1, + 282: 180.0, + 283: 180.0, + 296: 2, + 306: "2003:12:14 12:01:44", + 531: 1, + 34665: 196, + } + def test_categories_deprecation(self): with pytest.warns(DeprecationWarning): assert hopper().category == 0 @@ -784,35 +797,6 @@ class TestImage: with pytest.warns(DeprecationWarning): assert Image.CONTAINER == 2 - @pytest.mark.parametrize( - "test_module", - [PIL, Image], - ) - def test_pillow_version(self, test_module): - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION == PIL.__version__ - - with pytest.warns(DeprecationWarning): - str(test_module.PILLOW_VERSION) - - with pytest.warns(DeprecationWarning): - assert int(test_module.PILLOW_VERSION[0]) >= 7 - - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION < "9.9.0" - - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION <= "9.9.0" - - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION != "7.0.0" - - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION >= "7.0.0" - - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION > "7.0.0" - @pytest.mark.parametrize( "path", [ @@ -848,18 +832,6 @@ class TestImage: except OSError as e: assert str(e) == "buffer overrun when reading image file" - def test_show_deprecation(self, monkeypatch): - monkeypatch.setattr(Image, "_show", lambda *args, **kwargs: None) - - im = Image.new("RGB", (50, 50), "white") - - with pytest.warns(None) as raised: - im.show() - assert not raised - - with pytest.warns(DeprecationWarning): - im.show(command="mock") - class MockEncoder: pass diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index e86dc8530..7b3036979 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -23,6 +23,11 @@ else: except ImportError: cffi = None +try: + import numpy +except ImportError: + numpy = None + class AccessTest: # initial value @@ -66,6 +71,10 @@ class TestImagePutPixel(AccessTest): pix1 = im1.load() pix2 = im2.load() + for x, y in ((0, "0"), ("0", 0)): + with pytest.raises(TypeError): + pix1[x, y] + for y in range(im1.size[1]): for x in range(im1.size[0]): pix2[x, y] = pix1[x, y] @@ -109,6 +118,13 @@ class TestImagePutPixel(AccessTest): assert_image_equal(im1, im2) + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") + def test_numpy(self): + im = hopper() + pix = im.load() + + assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) + class TestImageGetPixel(AccessTest): @staticmethod @@ -339,6 +355,24 @@ class TestImagePutPixelError(AccessTest): with pytest.raises(TypeError, match="color must be int or tuple"): im.putpixel((0, 0), v) + @pytest.mark.parametrize( + ("mode", "band_numbers", "match"), + ( + ("L", (0, 2), "color must be int or single-element tuple"), + ("LA", (0, 3), "color must be int, or tuple of one or two elements"), + ( + "RGB", + (0, 2, 5), + "color must be int, or tuple of one, three or four elements", + ), + ), + ) + def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match): + im = hopper(mode) + for band_number in band_numbers: + with pytest.raises(TypeError, match=match): + im.putpixel((0, 0), (0,) * band_number) + @pytest.mark.parametrize("mode", IMAGE_MODES2) def test_putpixel_type_error2(self, mode): im = hopper(mode) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 980458407..5c9cdd7e0 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -4,31 +4,44 @@ from PIL import Image from .helper import hopper +numpy = pytest.importorskip("numpy", reason="NumPy not installed") + im = hopper().resize((128, 100)) def test_toarray(): def test(mode): - ai = im.convert(mode).__array_interface__ - return ai["version"], ai["shape"], ai["typestr"], len(ai["data"]) + ai = numpy.array(im.convert(mode)) + return ai.shape, ai.dtype.str, ai.nbytes - # assert test("1") == (3, (100, 128), '|b1', 1600)) - assert test("L") == (3, (100, 128), "|u1", 12800) + def test_with_dtype(dtype): + ai = numpy.array(im, dtype=dtype) + assert ai.dtype == dtype + + # assert test("1") == ((100, 128), '|b1', 1600)) + assert test("L") == ((100, 128), "|u1", 12800) # FIXME: wrong? - assert test("I") == (3, (100, 128), Image._ENDIAN + "i4", 51200) + assert test("I") == ((100, 128), Image._ENDIAN + "i4", 51200) # FIXME: wrong? - assert test("F") == (3, (100, 128), Image._ENDIAN + "f4", 51200) + assert test("F") == ((100, 128), Image._ENDIAN + "f4", 51200) - assert test("LA") == (3, (100, 128, 2), "|u1", 25600) - assert test("RGB") == (3, (100, 128, 3), "|u1", 38400) - assert test("RGBA") == (3, (100, 128, 4), "|u1", 51200) - assert test("RGBX") == (3, (100, 128, 4), "|u1", 51200) + assert test("LA") == ((100, 128, 2), "|u1", 25600) + assert test("RGB") == ((100, 128, 3), "|u1", 38400) + assert test("RGBA") == ((100, 128, 4), "|u1", 51200) + assert test("RGBX") == ((100, 128, 4), "|u1", 51200) + + test_with_dtype(numpy.float64) + test_with_dtype(numpy.uint8) + + with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: + with pytest.raises(OSError): + numpy.array(im_truncated) def test_fromarray(): class Wrapper: - """ Class with API matching Image.fromarray """ + """Class with API matching Image.fromarray""" def __init__(self, img, arr_params): self.img = img @@ -39,10 +52,18 @@ def test_fromarray(): def test(mode): i = im.convert(mode) - a = i.__array_interface__ - a["strides"] = 1 # pretend it's non-contiguous + a = numpy.array(i) # Make wrapper instance for image, new array interface - wrapped = Wrapper(i, a) + wrapped = Wrapper( + i, + { + "shape": a.shape, + "typestr": a.dtype.str, + "version": 3, + "data": a.data, + "strides": 1, # pretend it's non-contiguous + }, + ) out = Image.fromarray(wrapped) return out.mode, out.size, list(i.getdata()) == list(out.getdata()) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6fe1bd962..a5a95e962 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -41,11 +41,15 @@ def test_sanity(): def test_default(): im = hopper("P") - assert_image(im, "P", im.size) - im = im.convert() - assert_image(im, "RGB", im.size) - im = im.convert() - assert_image(im, "RGB", im.size) + assert im.mode == "P" + converted_im = im.convert() + assert_image(converted_im, "RGB", im.size) + converted_im = im.convert() + assert_image(converted_im, "RGB", im.size) + + im.info["transparency"] = 0 + converted_im = im.convert() + assert_image(converted_im, "RGBA", im.size) # ref https://github.com/python-pillow/Pillow/issues/274 @@ -89,29 +93,33 @@ def test_trns_p(tmp_path): f = str(tmp_path / "temp.png") im_l = im.convert("L") - assert im_l.info["transparency"] == 0 # undone + assert im_l.info["transparency"] == 1 # undone im_l.save(f) im_rgb = im.convert("RGB") - assert im_rgb.info["transparency"] == (0, 0, 0) # undone + assert im_rgb.info["transparency"] == (0, 1, 2) # undone im_rgb.save(f) # ref https://github.com/python-pillow/Pillow/issues/664 -def test_trns_p_rgba(): +@pytest.mark.parametrize("mode", ("LA", "PA", "RGBA")) +def test_trns_p_transparency(mode): # Arrange im = hopper("P") im.info["transparency"] = 128 # Act - im_rgba = im.convert("RGBA") + converted_im = im.convert(mode) # Assert - assert "transparency" not in im_rgba.info - # https://github.com/python-pillow/Pillow/issues/2702 - assert im_rgba.palette is None + assert "transparency" not in converted_im.info + if mode == "PA": + assert converted_im.palette is not None + else: + # https://github.com/python-pillow/Pillow/issues/2702 + assert converted_im.palette is None def test_trns_l(tmp_path): @@ -128,8 +136,8 @@ def test_trns_l(tmp_path): assert "transparency" in im_p.info im_p.save(f) - im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) - assert "transparency" not in im_p.info + im_p = im.convert("P", palette=Image.ADAPTIVE) + assert "transparency" in im_p.info im_p.save(f) @@ -155,13 +163,33 @@ def test_trns_RGB(tmp_path): assert "transparency" not in im_p.info im_p.save(f) + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = im.getpixel((0, 0)) + im_p = im.convert("P", palette=Image.ADAPTIVE) + assert im_p.info["transparency"] == im_p.getpixel((0, 0)) + im_p.save(f) + + +@pytest.mark.parametrize("convert_mode", ("L", "LA", "I")) +def test_l_macro_rounding(convert_mode): + for mode in ("P", "PA"): + im = Image.new(mode, (1, 1)) + im.palette.getcolor((0, 1, 2)) + + converted_im = im.convert(convert_mode) + px = converted_im.load() + converted_color = px[0, 0] + if convert_mode == "LA": + converted_color = converted_color[0] + assert converted_color == 1 + def test_gif_with_rgba_palette_to_p(): # See https://github.com/python-pillow/Pillow/issues/2433 with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 im.load() - assert im.palette.mode == "RGBA" + assert im.palette.mode == "RGB" im_p = im.convert("P") # Should not raise ValueError: unrecognized raw mode diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 3740fbcdc..1d3ca8135 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -236,7 +236,7 @@ class TestImagingPaste: [ (127, 191, 254, 191), (111, 207, 206, 110), - (255, 255, 255, 0) if mode == "RGBA" else (127, 254, 127, 0), + (127, 254, 127, 0), (207, 207, 239, 239), (191, 191, 190, 191), (207, 206, 111, 112), diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 51108ead2..366f45854 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -32,7 +32,7 @@ def test_16bit_lut(): def test_f_lut(): - """ Tests for floating point lut of 8bit gray image """ + """Tests for floating point lut of 8bit gray image""" im = hopper("L") lut = [0.5 * float(x) for x in range(256)] diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 54712fd6c..7e4bbaaec 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,6 +1,8 @@ import sys from array import array +import pytest + from PIL import Image from .helper import assert_image_equal, hopper @@ -47,6 +49,12 @@ def test_pypy_performance(): im.putdata(list(range(256)) * 256) +def test_mode_with_L_with_float(): + im = Image.new("L", (1, 1), 0) + im.putdata([2.0]) + assert im.getpixel((0, 0)) == 2 + + def test_mode_i(): src = hopper("L") data = list(src.getdata()) @@ -87,3 +95,18 @@ def test_array_F(): im.putdata(arr) assert len(im.getdata()) == len(arr) + + +def test_not_flattened(): + im = Image.new("L", (1, 1)) + with pytest.raises(TypeError): + im.putdata([[0]]) + with pytest.raises(TypeError): + im.putdata([[0]], 2) + + with pytest.raises(TypeError): + im = Image.new("I", (1, 1)) + im.putdata([[0]]) + with pytest.raises(TypeError): + im = Image.new("F", (1, 1)) + im.putdata([[0]]) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 5a9df11b1..012a57a09 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -2,7 +2,7 @@ import pytest from PIL import Image, ImagePalette -from .helper import assert_image_equal, hopper +from .helper import assert_image_equal, assert_image_equal_tofile, hopper def test_putpalette(): @@ -36,9 +36,15 @@ def test_putpalette(): def test_imagepalette(): im = hopper("P") im.putpalette(ImagePalette.negative()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png") + im.putpalette(ImagePalette.random()) + im.putpalette(ImagePalette.sepia()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png") + im.putpalette(ImagePalette.wedge()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png") def test_putpalette_with_alpha_values(): diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index af4172c88..53b6c9007 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -2,18 +2,18 @@ import pytest from PIL import Image -from .helper import assert_image, assert_image_similar, hopper, is_ppc64le +from .helper import assert_image_similar, hopper, is_ppc64le def test_sanity(): image = hopper() converted = image.quantize() - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 10) image = hopper() converted = image.quantize(palette=hopper("P")) - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 60) @@ -27,7 +27,7 @@ def test_libimagequant_quantize(): pytest.skip("libimagequant support not available") else: raise - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 15) assert len(converted.getcolors()) == 100 @@ -35,7 +35,7 @@ def test_libimagequant_quantize(): def test_octree_quantize(): image = hopper() converted = image.quantize(100, Image.FASTOCTREE) - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 20) assert len(converted.getcolors()) == 100 @@ -52,7 +52,7 @@ def test_quantize(): with Image.open("Tests/images/caption_6_33_22.png") as image: image = image.convert("RGB") converted = image.quantize() - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 1) @@ -62,7 +62,8 @@ def test_quantize_no_dither(): palette = palette.convert("P") converted = image.quantize(dither=0, palette=palette) - assert_image(converted, "P", converted.size) + assert converted.mode == "P" + assert converted.palette.palette == palette.palette.palette def test_quantize_dither_diff(): @@ -74,3 +75,37 @@ def test_quantize_dither_diff(): nodither = image.quantize(dither=0, palette=palette) assert dither.tobytes() != nodither.tobytes() + + +def test_colors(): + im = hopper() + colors = 2 + converted = im.quantize(colors) + assert len(converted.palette.palette) == colors * len("RGB") + + +def test_transparent_colors_equal(): + im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) + px = im.load() + px[0, 1] = (255, 255, 255, 0) + + converted = im.quantize() + converted_px = converted.load() + assert converted_px[0, 0] == converted_px[0, 1] + + +@pytest.mark.parametrize( + "method, color", + ( + (Image.MEDIANCUT, (0, 0, 0)), + (Image.MAXCOVERAGE, (0, 0, 0)), + (Image.FASTOCTREE, (0, 0, 0)), + (Image.FASTOCTREE, (0, 0, 0, 0)), + ), +) +def test_palette(method, color): + im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) + + converted = im.quantize(method=method) + converted_px = converted.load() + assert converted_px[0, 0] == converted.palette.colors[color] diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 69449198e..8bf2ce916 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -4,7 +4,12 @@ import pytest from PIL import Image, ImageDraw -from .helper import assert_image_equal, assert_image_similar, hopper +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + mark_if_feature_version, +) class TestImagingResampleVulnerability: @@ -455,7 +460,9 @@ class TestCoreResampleBox: tiled.paste(tile, (x0, y0)) return tiled - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiles(self): with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) @@ -466,7 +473,9 @@ class TestCoreResampleBox: tiled = self.resize_tiled(im, dst_size, *tiles) assert_image_similar(reference, tiled, 0.01) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_subsample(self): # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index a49abe1b9..1fe278052 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -7,7 +7,12 @@ import pytest from PIL import Image -from .helper import assert_image_equal, assert_image_similar, hopper +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) class TestImagingCoreResize: @@ -135,11 +140,22 @@ class TestImagingCoreResize: with pytest.raises(ValueError): self.resize(hopper(), (10, 10), 9) + def test_cross_platform(self, tmp_path): + # This test is intended for only check for consistent behaviour across + # platforms. So if a future Pillow change requires that the test file + # be updated, that is okay. + im = hopper().resize((64, 64)) + temp_file = str(tmp_path / "temp.gif") + im.save(temp_file) + + with Image.open(temp_file) as reloaded: + assert_image_equal_tofile(reloaded, "Tests/images/hopper_resized.gif") + @pytest.fixture def gradients_image(): - im = Image.open("Tests/images/radial_gradients.png") - im.load() + with Image.open("Tests/images/radial_gradients.png") as im: + im.load() try: yield im finally: @@ -250,3 +266,7 @@ class TestImageResize: for mode in "1", "P": im = hopper(mode) assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20)) + + for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16": + im = hopper(mode) + assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 79ed79042..2d72ffa68 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -33,6 +33,9 @@ def test_angle(): with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) + im = hopper() + assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) + def test_zero(): for angle in (0, 45, 90, 180, 270): diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6911ce460..dd140955d 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -88,6 +88,7 @@ def test_no_resize(): assert im.size == (64, 64) +# valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") def test_DCT_scaling_edges(): # Make an image with red borders and size (N * 8) + 1 to cross DCT grid diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 845900267..ea208362b 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -32,6 +32,11 @@ class TestImageTransform: new_im = im.transform((100, 100), transform) assert new_im.info["comment"] == comment + def test_palette(self): + with Image.open("Tests/images/hopper.gif") as im: + transformed = im.transform(im.size, Image.AFFINE, [1, 0, 0, 0, 1, 0]) + assert im.palette.palette == transformed.palette.palette + def test_extent(self): im = hopper("RGB") (w, h) = im.size diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index a19fbf239..b839a7b14 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -368,11 +368,11 @@ def test_subtract_modulo_no_clip(): def test_soft_light(): # Arrange - im1 = Image.open("Tests/images/hopper.png") - im2 = Image.open("Tests/images/hopper-XYZ.png") + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act - new = ImageChops.soft_light(im1, im2) + # Act + new = ImageChops.soft_light(im1, im2) # Assert assert new.getpixel((64, 64)) == (163, 54, 32) @@ -381,11 +381,11 @@ def test_soft_light(): def test_hard_light(): # Arrange - im1 = Image.open("Tests/images/hopper.png") - im2 = Image.open("Tests/images/hopper-XYZ.png") + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act - new = ImageChops.hard_light(im1, im2) + # Act + new = ImageChops.hard_light(im1, im2) # Assert assert new.getpixel((64, 64)) == (144, 50, 27) @@ -394,11 +394,11 @@ def test_hard_light(): def test_overlay(): # Arrange - im1 = Image.open("Tests/images/hopper.png") - im2 = Image.open("Tests/images/hopper-XYZ.png") + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act - new = ImageChops.overlay(im1, im2) + # Act + new = ImageChops.overlay(im1, im2) # Assert assert new.getpixel((64, 64)) == (159, 50, 27) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index b5d693796..dcc44e6e3 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -191,3 +191,12 @@ def test_rounding_errors(): assert (255, 255) == ImageColor.getcolor("white", "LA") assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA") Image.new("LA", (1, 1), "white") + + +def test_color_too_long(): + # Arrange + color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" + + # Act / Assert + with pytest.raises(ValueError): + ImageColor.getrgb(color_too_long) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 06c5b2503..b661494c7 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -467,6 +467,23 @@ def test_shape2(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png") +def test_transform(): + # Arrange + im = Image.new("RGB", (100, 100), "white") + expected = im.copy() + draw = ImageDraw.Draw(im) + + # Act + s = ImageDraw.Outline() + s.line(0, 0) + s.transform((0, 0, 0, 0, 0, 0)) + + draw.shape(s, fill=1) + + # Assert + assert_image_equal(im, expected) + + def helper_pieslice(bbox, start, end): # Arrange im = Image.new("RGB", (W, H)) @@ -538,6 +555,36 @@ def test_pieslice_wide(): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png") +def test_pieslice_no_spikes(): + im = Image.new("RGB", (161, 161), "white") + draw = ImageDraw.Draw(im) + cxs = ( + [140] * 3 + + list(range(140, 19, -20)) + + [20] * 5 + + list(range(20, 141, 20)) + + [140] * 2 + ) + cys = ( + list(range(80, 141, 20)) + + [140] * 5 + + list(range(140, 19, -20)) + + [20] * 5 + + list(range(20, 80, 20)) + ) + + for cx, cy, angle in zip(cxs, cys, range(0, 360, 15)): + draw.pieslice( + [cx - 100, cy - 100, cx + 100, cy + 100], angle, angle + 1, fill="black" + ) + draw.point([cx, cy], fill="red") + + im_pre_erase = im.copy() + draw.rectangle([21, 21, 139, 139], fill="white") + + assert_image_equal(im, im_pre_erase) + + def helper_point(points): # Arrange im = Image.new("RGB", (W, H)) @@ -608,6 +655,19 @@ def test_polygon_1px_high(): assert_image_equal_tofile(im, expected) +def test_polygon_translucent(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.polygon([(20, 80), (80, 80), (80, 20)], fill=(0, 255, 0, 127)) + + # Assert + expected = "Tests/images/imagedraw_polygon_translucent.png" + assert_image_equal_tofile(im, expected) + + def helper_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -722,6 +782,29 @@ def test_rounded_rectangle(xy): assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") +@pytest.mark.parametrize( + "xy, radius, type", + [ + ((10, 20, 190, 180), 30.5, "given"), + ((10, 10, 181, 190), 90, "width"), + ((10, 20, 190, 181), 85, "height"), + ], +) +def test_rounded_rectangle_non_integer_radius(xy, radius, type): + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(xy, radius, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile( + im, + "Tests/images/imagedraw_rounded_rectangle_non_integer_radius_" + type + ".png", + ) + + def test_rounded_rectangle_zero_radius(): # Arrange im = Image.new("RGB", (W, H)) @@ -879,6 +962,18 @@ def test_triangle_right(): ) +@pytest.mark.parametrize( + "fill, suffix", + ((BLACK, "width"), (None, "width_no_fill")), +) +def test_triangle_right_width(fill, suffix): + img, draw = create_base_image_draw((100, 100)) + draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) + assert_image_equal_tofile( + img, os.path.join(IMAGES_PATH, "triangle_right_" + suffix + ".png") + ) + + def test_line_horizontal(): img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 2) @@ -1326,3 +1421,22 @@ def test_compute_regular_polygon_vertices_input_error_handling( with pytest.raises(expected_error) as e: ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) assert str(e.value) == error_message + + +def test_continuous_horizontal_edges_polygon(): + xy = [ + (2, 6), + (6, 6), + (12, 6), + (12, 12), + (8, 12), + (8, 8), + (4, 8), + (2, 8), + ] + img, draw = create_base_image_draw((16, 16)) + draw.polygon(xy, BLACK) + expected = os.path.join(IMAGES_PATH, "continuous_horizontal_edges_polygon.png") + assert_image_equal_tofile( + img, expected, "continuous horizontal edges polygon failed" + ) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index b4107e8e3..a5c76700d 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -2,7 +2,7 @@ from io import BytesIO import pytest -from PIL import EpsImagePlugin, Image, ImageFile, features +from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features from .helper import ( assert_image, @@ -82,6 +82,19 @@ class TestImageFile: p.feed(data) assert (48, 48) == p.image.size + @skip_unless_feature("webp") + @skip_unless_feature("webp_anim") + def test_incremental_webp(self): + with ImageFile.Parser() as p: + with open("Tests/images/hopper.webp", "rb") as f: + p.feed(f.read(1024)) + + # Check that insufficient data was given in the first feed + assert not p.image + + p.feed(f.read()) + assert (128, 128) == p.image.size + @skip_unless_feature("zlib") def test_safeblock(self): im1 = hopper() @@ -94,12 +107,6 @@ class TestImageFile: assert_image_equal(im1, im2) - def test_raise_ioerror(self): - with pytest.raises(IOError): - with pytest.warns(DeprecationWarning) as record: - ImageFile.raise_ioerror(1) - assert len(record) == 1 - def test_raise_oserror(self): with pytest.raises(OSError): ImageFile.raise_oserror(1) @@ -117,6 +124,20 @@ class TestImageFile: with pytest.raises(OSError): p.close() + def test_truncated(self): + b = BytesIO( + b"BM000000000000" # head_data + + _binary.o32le( + ImageFile.SAFEBLOCK + 1 + 4 + ) # header_size, so BmpImagePlugin will try to read SAFEBLOCK + 1 bytes + + ( + b"0" * ImageFile.SAFEBLOCK + ) # only SAFEBLOCK bytes, so that the header is truncated + ) + with pytest.raises(OSError) as e: + BmpImagePlugin.BmpImageFile(b) + assert str(e.value) == "Truncated File Read" + @skip_unless_feature("zlib") def test_truncated_with_errors(self): with Image.open("Tests/images/truncated_image.png") as im: @@ -248,4 +269,4 @@ class TestPyDecoder: def test_oserror(self): im = Image.new("RGB", (1, 1)) with pytest.raises(OSError): - im.save(BytesIO(), "JPEG2000") + im.save(BytesIO(), "JPEG2000", num_resolutions=2) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index dc88cb31d..0d423aab7 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -131,6 +131,20 @@ class TestImageFont: target = "Tests/images/transparent_background_text.png" assert_image_similar_tofile(im, target, 4.09) + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) + + def test_I16(self): + im = Image.new(mode="I;16", size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = self.get_font() + + txt = "Hello World!" + draw.text((10, 10), txt, font=ttf) + + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) + def test_textsize_equal(self): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -141,7 +155,6 @@ class TestImageFont: draw.text((10, 10), txt, font=ttf) draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) - # Epsilon ~.5 fails with FreeType 2.7 assert_image_similar_tofile( im, "Tests/images/rectangle_surrounding_text.png", 2.5 ) @@ -202,8 +215,7 @@ class TestImageFont: draw = ImageDraw.Draw(im) draw.text((0, 0), TEST_TEXT, font=ttf) - # Epsilon ~.5 fails with FreeType 2.7 - assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01) # Test that text() can pass on additional arguments # to multiline_text() @@ -218,9 +230,8 @@ class TestImageFont: draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) - # Epsilon ~.5 fails with FreeType 2.7 assert_image_similar_tofile( - im, "Tests/images/multiline_text" + ext + ".png", 6.2 + im, "Tests/images/multiline_text" + ext + ".png", 0.01 ) def test_unknown_align(self): @@ -275,8 +286,7 @@ class TestImageFont: draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) - # Epsilon ~.5 fails with FreeType 2.7 - assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 6.2) + assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5) def test_rotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) @@ -716,31 +726,34 @@ class TestImageFont: font.set_variation_by_axes([100]) self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + def test_textbbox_non_freetypefont(self): + im = Image.new("RGB", (200, 200)) + d = ImageDraw.Draw(im) + default_font = ImageFont.load_default() + with pytest.raises(ValueError): + d.textbbox((0, 0), "test", font=default_font) + @pytest.mark.parametrize( - "anchor, left, left_old, top", + "anchor, left, top", ( # test horizontal anchors - ("ls", 0, 0, -36), - ("ms", -64, -65, -36), - ("rs", -128, -129, -36), + ("ls", 0, -36), + ("ms", -64, -36), + ("rs", -128, -36), # test vertical anchors - ("ma", -64, -65, 16), - ("mt", -64, -65, 0), - ("mm", -64, -65, -17), - ("mb", -64, -65, -44), - ("md", -64, -65, -51), + ("ma", -64, 16), + ("mt", -64, 0), + ("mm", -64, -17), + ("mb", -64, -44), + ("md", -64, -51), ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) - def test_anchor(self, anchor, left, left_old, top): + def test_anchor(self, anchor, left, top): name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" - freetype = parse_version(features.version_module("freetype2")) - if freetype < parse_version("2.4"): - width, height = (129, 44) - left = left_old - elif self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM: + if self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM: width, height = (129, 44) else: width, height = (128, 44) @@ -846,6 +859,22 @@ class TestImageFont: assert_image_equal_tofile(im, target) + def test_bitmap_font_stroke(self): + text = "Bitmap Font" + layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE] + target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" + font = ImageFont.truetype( + "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf", + 24, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red") + + assert_image_similar_tofile(im, target, 0.03) + def test_standard_embedded_color(self): txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE) @@ -857,7 +886,6 @@ class TestImageFont: assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2) - @skip_unless_feature_version("freetype2", "2.5.0") def test_cbdt(self): try: font = ImageFont.truetype( @@ -872,11 +900,10 @@ class TestImageFont: d.text((10, 10), "\U0001f469", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) - except IOError as e: # pragma: no cover + except OSError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or CBDT support") - @skip_unless_feature_version("freetype2", "2.5.0") def test_cbdt_mask(self): try: font = ImageFont.truetype( @@ -893,11 +920,10 @@ class TestImageFont: assert_image_similar_tofile( im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 ) - except IOError as e: # pragma: no cover + except OSError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or CBDT support") - @skip_unless_feature_version("freetype2", "2.5.1") def test_sbix(self): try: font = ImageFont.truetype( @@ -912,11 +938,10 @@ class TestImageFont: d.text((50, 50), "\uE901", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) - except IOError as e: # pragma: no cover + except OSError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or SBIX support") - @skip_unless_feature_version("freetype2", "2.5.1") def test_sbix_mask(self): try: font = ImageFont.truetype( @@ -931,7 +956,7 @@ class TestImageFont: d.text((50, 50), "\uE901", (100, 0, 0), font=font) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) - except IOError as e: # pragma: no cover + except OSError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or SBIX support") @@ -971,7 +996,6 @@ class TestImageFont_RaqmLayout(TestImageFont): LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM -@skip_unless_feature_version("freetype2", "2.4", "Different metrics") def test_render_mono_size(): # issue 4177 @@ -987,13 +1011,14 @@ def test_render_mono_size(): assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_freetype_deprecation(monkeypatch): - # Arrange: mock features.version_module to return fake FreeType version - def fake_version_module(module): - return "2.7" - - monkeypatch.setattr(features, "version_module", fake_version_module) - - # Act / Assert - with pytest.warns(DeprecationWarning): - ImageFont.truetype(FONT_PATH, FONT_SIZE) +@pytest.mark.parametrize( + "test_file", + [ + "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", + ], +) +def test_oom(test_file): + with open(test_file, "rb") as f: + font = ImageFont.truetype(BytesIO(f.read())) + with pytest.raises(Image.DecompressionBombError): + font.getmask("Test Text") diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index f2a914ff7..ffb70cf17 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,13 +1,8 @@ import pytest -from packaging.version import parse as parse_version -from PIL import Image, ImageDraw, ImageFont, features +from PIL import Image, ImageDraw, ImageFont -from .helper import ( - assert_image_similar_tofile, - skip_unless_feature, - skip_unless_feature_version, -) +from .helper import assert_image_similar_tofile, skip_unless_feature FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" @@ -252,11 +247,6 @@ def test_getlength_combine(mode, direction, text): pytest.skip("libraqm 0.7 or greater not available") -# FreeType 2.5.1 README: Miscellaneous Changes: -# Improved computation of emulated vertical metrics for TrueType fonts. -@skip_unless_feature_version( - "freetype2", "2.5.1", "FreeType <2.5.1 has incompatible ttb metrics" -) @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) def test_anchor_ttb(anchor): text = "f" @@ -315,14 +305,6 @@ combine_tests = ( "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] ) def test_combine(name, text, dir, anchor, epsilon): - if ( - parse_version(features.version_module("freetype2")) < parse_version("2.5.1") - and dir == "ttb" - ): - # FreeType 2.5.1 README: Miscellaneous Changes: - # Improved computation of emulated vertical metrics for TrueType fonts. - pytest.skip("FreeType <2.5.1 has incompatible ttb metrics") - path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index c36285451..fa2291582 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -6,7 +6,7 @@ import pytest from PIL import Image, ImageGrab -from .helper import assert_image, assert_image_equal_tofile, skip_unless_feature +from .helper import assert_image_equal_tofile, skip_unless_feature class TestImageGrab: @@ -14,25 +14,20 @@ class TestImageGrab: sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" ) def test_grab(self): - for im in [ - ImageGrab.grab(), - ImageGrab.grab(include_layered_windows=True), - ImageGrab.grab(all_screens=True), - ]: - assert_image(im, im.mode, im.size) + ImageGrab.grab() + ImageGrab.grab(include_layered_windows=True) + ImageGrab.grab(all_screens=True) im = ImageGrab.grab(bbox=(10, 20, 50, 80)) - assert_image(im, im.mode, (40, 60)) + assert im.size == (40, 60) @skip_unless_feature("xcb") def test_grab_x11(self): try: if sys.platform not in ("win32", "darwin"): - im = ImageGrab.grab() - assert_image(im, im.mode, im.size) + ImageGrab.grab() - im2 = ImageGrab.grab(xdisplay="") - assert_image(im2, im2.mode, im2.size) + ImageGrab.grab(xdisplay="") except OSError as e: pytest.skip(str(e)) @@ -71,8 +66,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only" return - im = ImageGrab.grabclipboard() - assert_image(im, im.mode, im.size) + ImageGrab.grabclipboard() @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_file(self): diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 239806796..25811aa89 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,9 +1,11 @@ +import pytest + from PIL import Image, ImageMath def pixel(im): if hasattr(im, "im"): - return "{} {}".format(im.mode, repr(im.getpixel((0, 0)))) + return f"{im.mode} {repr(im.getpixel((0, 0)))}" else: if isinstance(im, int): return int(im) # hack to deal with booleans @@ -50,6 +52,11 @@ def test_ops(): assert pixel(ImageMath.eval("float(B)**33", images)) == "F 8589934592.0" +def test_prevent_exec(): + with pytest.raises(ValueError): + ImageMath.eval("exec('pass')") + + def test_logical(): assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index eb41d2d08..368c2bba1 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -235,19 +235,19 @@ def test_negate(): ) -def test_non_binary_images(): +def test_incorrect_mode(): im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") - with pytest.raises(Exception) as e: + with pytest.raises(ValueError) as e: mop.apply(im) - assert str(e.value) == "Image must be binary, meaning it must use mode L" - with pytest.raises(Exception) as e: + assert str(e.value) == "Image mode must be L" + with pytest.raises(ValueError) as e: mop.match(im) - assert str(e.value) == "Image must be binary, meaning it must use mode L" - with pytest.raises(Exception) as e: + assert str(e.value) == "Image mode must be L" + with pytest.raises(ValueError) as e: mop.get_on_pixels(im) - assert str(e.value) == "Image must be binary, meaning it must use mode L" + assert str(e.value) == "Image mode must be L" def test_add_patterns(): diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 33489bd13..6aa1cf35e 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -29,6 +29,7 @@ def test_sanity(): ImageOps.autocontrast(hopper("L"), cutoff=(2, 10)) ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) ImageOps.autocontrast(hopper("L"), mask=hopper("L")) + ImageOps.autocontrast(hopper("L"), preserve_tone=True) ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) ImageOps.colorize(hopper("L"), "black", "white") @@ -36,6 +37,9 @@ def test_sanity(): ImageOps.pad(hopper("L"), (128, 128)) ImageOps.pad(hopper("RGB"), (128, 128)) + ImageOps.contain(hopper("L"), (128, 128)) + ImageOps.contain(hopper("RGB"), (128, 128)) + ImageOps.crop(hopper("L"), 1) ImageOps.crop(hopper("RGB"), 1) @@ -98,6 +102,13 @@ def test_fit_same_ratio(): assert new_im.size == (1000, 755) +@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) +def test_contain(new_size): + im = hopper() + new_im = ImageOps.contain(im, new_size) + assert new_im.size == (256, 256) + + def test_pad(): # Same ratio im = hopper() @@ -145,6 +156,33 @@ def test_scale(): assert newimg.size == (25, 25) +@pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) +def test_expand_palette(border): + with Image.open("Tests/images/p_16.tga") as im: + im_expanded = ImageOps.expand(im, border, (255, 0, 0)) + + if isinstance(border, int): + left = top = right = bottom = border + else: + left, top, right, bottom = border + px = im_expanded.convert("RGB").load() + for x in range(im_expanded.width): + for b in range(top): + assert px[x, b] == (255, 0, 0) + for b in range(bottom): + assert px[x, im_expanded.height - 1 - b] == (255, 0, 0) + for y in range(im_expanded.height): + for b in range(left): + assert px[b, y] == (255, 0, 0) + for b in range(right): + assert px[im_expanded.width - 1 - b, y] == (255, 0, 0) + + im_cropped = im_expanded.crop( + (left, top, im_expanded.width - right, im_expanded.height - bottom) + ) + assert_image_equal(im_cropped, im) + + def test_colorize_2color(): # Test the colorizing function with 2-color functionality @@ -291,6 +329,7 @@ def test_exif_transpose(): else: assert transposed_im.info["exif"] != original_exif + assert 0x0112 in im.getexif() assert 0x0112 not in transposed_im.getexif() # Repeat the operation to test that it does not keep transposing @@ -304,6 +343,28 @@ def test_exif_transpose(): ) as orientation_im: check(orientation_im) + # Orientation from "XML:com.adobe.xmp" info key + with Image.open("Tests/images/xmp_tags_orientation.png") as im: + assert im.getexif()[0x0112] == 3 + + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + + # Orientation from "Raw profile type exif" info key + # This test image has been manually hexedited from exif_imagemagick.png + # to have a different orientation + with Image.open("Tests/images/exif_imagemagick_orientation.png") as im: + assert im.getexif()[0x0112] == 3 + + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + + # Orientation set directly on Image.Exif + im = hopper() + im.getexif()[0x0112] = 3 + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + def test_autocontrast_cutoff(): # Test the cutoff argument of autocontrast @@ -336,7 +397,7 @@ def test_autocontrast_mask_toy_input(): assert ImageStat.Stat(result_nomask).median == [128] -def test_auto_contrast_mask_real_input(): +def test_autocontrast_mask_real_input(): # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: @@ -362,3 +423,52 @@ def test_auto_contrast_mask_real_input(): threshold=2, msg="autocontrast without mask pixel incorrect", ) + + +def test_autocontrast_preserve_tone(): + def autocontrast(mode, preserve_tone): + im = hopper(mode) + return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() + + assert autocontrast("RGB", True) != autocontrast("RGB", False) + assert autocontrast("L", True) == autocontrast("L", False) + + +def test_autocontrast_preserve_gradient(): + gradient = Image.linear_gradient("L") + + # test with a grayscale gradient that extends to 0,255. + # Should be a noop. + out = ImageOps.autocontrast(gradient, cutoff=0, preserve_tone=True) + + assert_image_equal(gradient, out) + + # cutoff the top and bottom + # autocontrast should make the first and last histogram entries equal + # and, with rounding, should be 10% of the image pixels + out = ImageOps.autocontrast(gradient, cutoff=10, preserve_tone=True) + hist = out.histogram() + assert hist[0] == hist[-1] + assert hist[-1] == 256 * round(256 * 0.10) + + # in rgb + img = gradient.convert("RGB") + out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) + assert_image_equal(img, out) + + +@pytest.mark.parametrize( + "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) +) +def test_autocontrast_preserve_one_color(color): + img = Image.new("RGB", (10, 10), color) + + # single color images shouldn't change + out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) + assert_image_equal(img, out) # single color, no cutoff + + # even if there is a cutoff + out = ImageOps.autocontrast( + img, cutoff=10, preserve_tone=True + ) # single color 10 cutoff + assert_image_equal(img, out) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 0ea2472a9..475d249ed 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -2,33 +2,77 @@ import pytest from PIL import Image, ImagePalette -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal, assert_image_equal_tofile def test_sanity(): - ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - with pytest.raises(ValueError): - ImagePalette.ImagePalette("RGB", list(range(256)) * 2) + 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: + original = im.copy() + im.palette.dirty = 1 + assert_image_equal(im.convert("RGB"), original.convert("RGB")) def test_getcolor(): palette = ImagePalette.ImagePalette() + assert len(palette.palette) == 0 + assert len(palette.colors) == 0 test_map = {} for i in range(256): test_map[palette.getcolor((i, i, i))] = i - assert len(test_map) == 256 + + # Colors can be converted between RGB and RGBA + rgba_palette = ImagePalette.ImagePalette("RGBA") + assert rgba_palette.getcolor((0, 0, 0)) == rgba_palette.getcolor((0, 0, 0, 255)) + + assert palette.getcolor((0, 0, 0)) == palette.getcolor((0, 0, 0, 255)) + + # An error is raised when the palette is full with pytest.raises(ValueError): palette.getcolor((1, 2, 3)) + # But not if the image is not using one of the palette entries + palette.getcolor((1, 2, 3), image=Image.new("P", (1, 1))) # Test unknown color specifier with pytest.raises(ValueError): palette.getcolor("unknown") +@pytest.mark.parametrize( + "index, palette", + [ + # Test when the palette is not full + (0, ImagePalette.ImagePalette()), + # Test when the palette is full + (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), + ], +) +def test_getcolor_not_special(index, palette): + im = Image.new("P", (1, 1)) + + # Do not use transparency index as a new color + im.info["transparency"] = index + index1 = palette.getcolor((0, 0, 0), im) + assert index1 != index + + # Do not use background index as a new color + im.info["background"] = index1 + index2 = palette.getcolor((0, 0, 1), im) + assert index2 not in (index, index1) + + def test_file(tmp_path): palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) @@ -116,7 +160,7 @@ def test_getdata(): mode, data_out = palette.getdata() # Assert - assert mode == "RGB;L" + assert mode == "RGB" def test_rawmode_getdata(): diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 0835fdb43..b18271cc5 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -90,6 +90,8 @@ def test_path_odd_number_of_coordinates(): [ ([0, 1, 2, 3], (0.0, 1.0, 2.0, 3.0)), ([3, 2, 1, 0], (1.0, 0.0, 3.0, 2.0)), + (0, (0.0, 0.0, 0.0, 0.0)), + (1, (0.0, 0.0, 0.0, 0.0)), ], ) def test_getbbox(coords, expected): diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 53b1fef7c..589cb5a21 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -2,7 +2,7 @@ import pytest from PIL import ImageQt -from .helper import hopper +from .helper import assert_image_similar, hopper pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" @@ -42,8 +42,17 @@ def test_rgb(): def test_image(): - for mode in ("1", "RGB", "RGBA", "L", "P"): - ImageQt.ImageQt(hopper(mode)) + modes = ["1", "RGB", "RGBA", "L", "P"] + qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage + if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + modes.append("I;16") + + for mode in modes: + im = hopper(mode) + roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im)) + if mode not in ("RGB", "RGBA"): + im = im.convert("RGB") + assert_image_similar(roundtripped_im, im, 1) def test_closed_file(): diff --git a/Tests/test_map.py b/Tests/test_map.py index 752c5f268..42f3447eb 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -24,11 +24,17 @@ def test_overflow(): def test_tobytes(): + # Note that this image triggers the decompression bomb warning: + max_pixels = Image.MAX_IMAGE_PIXELS + Image.MAX_IMAGE_PIXELS = None + # Previously raised an access violation on Windows with Image.open("Tests/images/l2rgb_read.bmp") as im: with pytest.raises((ValueError, MemoryError, OSError)): im.tobytes() + Image.MAX_IMAGE_PIXELS = max_pixels + @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") def test_ysize(): diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index a10dcec8c..5fd045855 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -2,9 +2,12 @@ import pickle import pytest -from PIL import Image +from PIL import Image, ImageDraw, ImageFont -from .helper import skip_unless_feature +from .helper import assert_image_equal, skip_unless_feature + +FONT_SIZE = 20 +FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): @@ -85,10 +88,55 @@ def test_pickle_la_mode_with_palette(tmp_path): @skip_unless_feature("webp") def test_pickle_tell(): # Arrange - image = Image.open("Tests/images/hopper.webp") + with Image.open("Tests/images/hopper.webp") as image: - # Act: roundtrip - unpickled_image = pickle.loads(pickle.dumps(image)) + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) # Assert assert unpickled_image.tell() == 0 + + +def helper_assert_pickled_font_images(font1, font2): + # Arrange + im1 = Image.new(mode="RGBA", size=(300, 100)) + im2 = Image.new(mode="RGBA", size=(300, 100)) + draw1 = ImageDraw.Draw(im1) + draw2 = ImageDraw.Draw(im2) + txt = "Hello World!" + + # Act + draw1.text((10, 10), txt, font=font1) + draw2.text((10, 10), txt, font=font2) + + # Assert + assert_image_equal(im1, im2) + + +@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +def test_pickle_font_string(protocol): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act: roundtrip + pickled_font = pickle.dumps(font, protocol) + unpickled_font = pickle.loads(pickled_font) + + # Assert + helper_assert_pickled_font_images(font, unpickled_font) + + +@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +def test_pickle_font_file(tmp_path, protocol): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + filename = str(tmp_path / "temp.pkl") + + # Act: roundtrip + with open(filename, "wb") as f: + pickle.dump(font, f, protocol) + with open(filename, "rb") as f: + unpickled_font = pickle.load(f) + + # Assert + helper_assert_pickled_font_images(font, unpickled_font) diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 31f0f493b..e74d79828 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,6 +1,8 @@ import os import sys -from io import StringIO +from io import BytesIO + +import pytest from PIL import Image, PSDraw @@ -44,10 +46,21 @@ def test_draw_postscript(tmp_path): assert os.path.getsize(tempfile) > 0 -def test_stdout(): +@pytest.mark.parametrize("buffer", (True, False)) +def test_stdout(buffer): # Temporarily redirect stdout old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout ps = PSDraw.PSDraw() _create_document(ps) @@ -55,4 +68,6 @@ def test_stdout(): # Reset stdout sys.stdout = old_stdout - assert mystdout.getvalue() != "" + if buffer: + mystdout = mystdout.buffer + assert mystdout.getvalue() != b"" diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index d4ddc12f9..b5f9d4424 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python import pytest from PIL import Image @@ -22,6 +21,6 @@ from PIL import Image ) def test_crashes(test_file): with open(test_file, "rb") as f: - im = Image.open(f) - with pytest.raises(OSError): - im.load() + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index ae4d0f100..143765b8e 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -33,10 +33,14 @@ from .helper import on_ci "Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif", "Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif", "Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif", + "Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif", + "Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif", + "Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif", ], ) @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") @pytest.mark.filterwarnings("ignore:Metadata warning") +@pytest.mark.filterwarnings("ignore:Truncated File Read") def test_tiff_crashes(test_file): try: with Image.open(test_file) as im: diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 1697a8d49..12f475df0 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -28,6 +28,8 @@ def test_sanity(): _test_equal(1, 2, Fraction(1, 2)) _test_equal(1, 2, IFDRational(1, 2)) + _test_equal(7, 5, 1.4) + def test_ranges(): for num in range(1, 10): diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 376d8ef9b..774f26767 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,9 +1,9 @@ #!/bin/bash # install libimagequant -archive=libimagequant-2.14.1 +archive=libimagequant-2.17.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 7321b80f0..914e71e53 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -3,7 +3,7 @@ archive=openjpeg-2.4.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index a7ce16792..3105465ec 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -4,7 +4,7 @@ archive=raqm-0.7.1 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/depends/install_raqm_cmake.sh b/depends/install_raqm_cmake.sh index c0dcd93b7..7d2c399df 100755 --- a/depends/install_raqm_cmake.sh +++ b/depends/install_raqm_cmake.sh @@ -4,7 +4,7 @@ archive=raqm-cmake-99300ff3 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 568cb2df9..8a9c96804 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,9 +1,9 @@ #!/bin/bash # install webp -archive=libwebp-1.2.0 +archive=libwebp-1.2.1 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/docs/COPYING b/docs/COPYING index f2466d659..25f03b343 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2021 by Alex Clark and contributors + Copyright © 2010-2022 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/Guardfile b/docs/Guardfile index 16f731611..b689b079a 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from livereload.compiler import shell from livereload.task import Task diff --git a/docs/about.rst b/docs/about.rst index 51b583ea0..96885d08d 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -19,7 +19,7 @@ The fork author's goal is to foster and support active development of PIL throug License ------- -Like PIL, Pillow is `licensed under the open source HPND License `_ +Like PIL, Pillow is `licensed under the open source HPND License `_ Why a fork? ----------- diff --git a/docs/conf.py b/docs/conf.py index 123e93c9b..7bbe8c4c9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,11 +29,13 @@ needs_sphinx = "2.4" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "sphinx_copybutton", + "sphinx_issues", + "sphinx_removed_in", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", - "sphinx_issues", - "sphinx_removed_in", + "sphinxext.opengraph", ] intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} @@ -51,7 +53,7 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2021 Alex Clark and Contributors" +copyright = "1995-2011 Fredrik Lundh, 2010-2022 Alex Clark and Contributors" author = "Fredrik Lundh, Alex Clark and Contributors" # The version info for the project you're documenting, acts as replacement for @@ -310,9 +312,17 @@ texinfo_documents = [ def setup(app): app.add_js_file("js/script.js") + app.add_css_file("css/styles.css") app.add_css_file("css/dark.css") app.add_css_file("css/light.css") # GitHub repo for sphinx-issues issues_github_path = "python-pillow/Pillow" + +# sphinxext.opengraph +ogp_image = ( + "https://raw.githubusercontent.com/python-pillow/pillow-logo/main/" + "pillow-logo-dark-text-1280x640.png" +) +ogp_image_alt = "Pillow" diff --git a/docs/deprecations.rst b/docs/deprecations.rst index ef88afa23..ce30fdf3b 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,25 +12,12 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -FreeType 2.7 -~~~~~~~~~~~~ - -.. deprecated:: 8.1.0 - -Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022-01-02), -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`). - -.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ - Tk/Tcl 8.4 ~~~~~~~~~~ .. deprecated:: 8.2.0 -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +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. Categories @@ -38,27 +25,66 @@ Categories .. deprecated:: 8.2.0 -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +``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. To determine if an image has multiple frames or not, ``getattr(im, "is_animated", False)`` can be used instead. +JpegImagePlugin.convert_dict_qtables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.3.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). + +ImagePalette size parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.4.0 + +The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). + +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. + +Removed features +---------------- + +Deprecated features are only removed in major releases after an appropriate +period of deprecation has passed. + +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2.0 +.. versionremoved:: 9.0.0 + +Use ``__version__`` instead. + +It was initially removed in Pillow 7.0.0, but temporarily brought back in 7.1.0 +to give projects more time to upgrade. + Image.show command parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 -The ``command`` parameter will be removed in Pillow 9.0.0 (2022-01-02). -Use a subclass of :py:class:`.ImageShow.Viewer` instead. +The ``command`` parameter has been removed. Use a subclass of +:py:class:`.ImageShow.Viewer` instead. Image._showxv ~~~~~~~~~~~~~ .. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 -``Image._showxv`` will be removed in Pillow 9.0.0 (2022-01-02). Use :py:meth:`.Image.Image.show` instead. If custom behaviour is required, use :py:func:`.ImageShow.register` to add a custom :py:class:`.ImageShow.Viewer` class. @@ -66,27 +92,24 @@ ImageFile.raise_ioerror ~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 ``IOError`` was merged into ``OSError`` in Python 3.3. -So, ``ImageFile.raise_ioerror`` will be removed in Pillow 9.0.0 (2022-01-02). +So, ``ImageFile.raise_ioerror`` has been removed. Use ``ImageFile.raise_oserror`` instead. -PILLOW_VERSION constant -~~~~~~~~~~~~~~~~~~~~~~~ +FreeType 2.7 +~~~~~~~~~~~~ -.. deprecated:: 5.2.0 +.. deprecated:: 8.1.0 +.. versionremoved:: 9.0.0 -``PILLOW_VERSION`` will be removed in Pillow 9.0.0 (2022-01-02). -Use ``__version__`` instead. +Support for FreeType 2.7 has been removed. -It was initially removed in Pillow 7.0.0, but brought back in 7.1.0 to give projects -more time to upgrade. +We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). -Removed features ----------------- - -Deprecated features are only removed in major releases after an appropriate -period of deprecation has passed. +.. _FreeType: https://www.freetype.org im.offset ~~~~~~~~~ @@ -125,7 +148,6 @@ Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6 they issued a ``DeprecationWarning``: ======================== =================================================== - Removed Use instead ======================== =================================================== ``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` @@ -251,7 +273,7 @@ PIL.OleFileIO .. deprecated:: 4.0.0 .. versionremoved:: 6.0.0 -PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of +PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0 (2018-01). The deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. ``python3 -m pip install olefile``). diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 78aa3ce72..272409416 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -269,9 +269,9 @@ Image.register_decoder("DXT1", DXT1Decoder) Image.register_decoder("DXT5", DXT5Decoder) -def _validate(prefix): +def _accept(prefix): return prefix[:4] == b"DDS " -Image.register_open(DdsImageFile.format, DdsImageFile, _validate) +Image.register_open(DdsImageFile.format, DdsImageFile, _accept) Image.register_extension(DdsImageFile.format, ".dds") diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index c4cdda78d..66eeaf6f8 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -44,7 +44,7 @@ supports the following standard modes: * ``I`` (32-bit signed integer pixels) * ``F`` (32-bit floating point pixels) -Pillow also provides limited support for a few special modes, including: +Pillow also provides limited support for a few additional modes, including: * ``LA`` (L with alpha) * ``PA`` (P with alpha) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index c67f8fb8f..bd44f63a3 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -13,6 +13,14 @@ contents, not their names, but the :py:meth:`~PIL.Image.Image.save` method looks at the name to determine which format to use, unless the format is given explicitly. +When an image is opened from a file, only that instance of the image is considered to +have the format. Copies of the image will contain data loaded from the file, but not +the file itself, meaning that it can no longer be considered to be in the original +format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method +internally creates a copy of the image, the ``fp`` (file pointer), along with any +methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format` +attribute will be ``None``. + Fully supported formats ----------------------- @@ -31,6 +39,13 @@ The :py:meth:`~PIL.Image.open` method sets the following **compression** Set to ``bmp_rle`` if the file is run-length encoded. +DDS +^^^ + +DDS is a popular container texture format used in video games and natively supported +by DirectX. Uncompressed RGB and RGBA can be read, and (since 8.3.0) written. DXT1, +DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode. + DIB ^^^ @@ -51,7 +66,7 @@ than leaving them in the original color space. The EPS driver can write images in ``L``, ``RGB`` and ``CMYK`` modes. If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load` -method with the following parameter to affect how Ghostscript renders the EPS +method with the following parameters to affect how Ghostscript renders the EPS **scale** Affects the scale of the resultant rasterized image. If the EPS suggests @@ -60,9 +75,14 @@ method with the following parameter to affect how Ghostscript renders the EPS relative position of the bounding box is maintained:: im = Image.open(...) - im.size #(100,100) + im.size # (100,100) im.load(scale=2) - im.size #(200,200) + im.size # (200,200) + +**transparency** + If true, generates an RGBA image with a transparent background, instead of + the default behaviour of an RGB image with a white background. + GIF ^^^ @@ -71,8 +91,9 @@ Pillow reads GIF87a and GIF89a versions of the GIF file format. The library writes run-length encoded files in GIF87a by default, unless GIF89a features are used or GIF89a is already in use. -Note that GIF files are always read as grayscale (``L``) -or palette mode (``P``) images. +GIF files are initially read as grayscale (``L``) or palette mode (``P``) +images, but seeking to later frames in an image will change the mode to either +``RGB`` or ``RGBA``, depending on whether the first frame had transparency. The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -200,12 +221,16 @@ attributes before loading the file:: ICNS ^^^^ -Pillow reads and (macOS only) writes macOS ``.icns`` files. By default, the +Pillow reads and writes macOS ``.icns`` files. By default, the largest available icon is read, though you can override this by setting the :py:attr:`~PIL.Image.Image.size` property before calling :py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` property: +.. note:: + + Prior to version 8.3.0, Pillow could only write ICNS files on macOS. + **sizes** A list of supported sizes found in this icon file; these are a 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina @@ -247,6 +272,12 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 8.1.0 +**bitmap_format** + By default, the image data will be saved in PNG format. With a bitmap format of + "bmp", image data will be saved in BMP format instead. + + .. versionadded:: 8.3.0 + IM ^^ @@ -730,7 +761,7 @@ The :py:meth:`~PIL.Image.open` method sets the following attributes: A convenience method, :py:meth:`~PIL.SpiderImagePlugin.SpiderImageFile.convert2byte`, is provided for converting floating point data to byte data (mode ``L``):: - im = Image.open('image001.spi').convert2byte() + im = Image.open("image001.spi").convert2byte() Writing files in SPIDER format ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -814,7 +845,7 @@ Reading Multi-frame TIFF Images The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers within the image file. You can combine these methods to seek to the next frame -(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.num_frames - 1``, +(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.n_frames - 1``, and can be accessed in any order. ``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the @@ -873,6 +904,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum require a matching type in :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` tagtype. +**exif** + Alternate keyword to "tiffinfo", for consistency with other formats. + + .. versionadded:: 8.4.0 + **compression** A string containing the desired compression method for the file. (valid only with libtiff installed) Valid compression @@ -940,7 +976,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: files compared to the slowest, but best, 100. **method** - Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 0. + Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. **icc_profile** The ICC Profile to include in the saved file. Only supported if @@ -1028,17 +1064,6 @@ is commonly used in fax applications. The DCX decoder can read files containing When the file is opened, only the first image is read. You can use :py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images. - -DDS -^^^ - -DDS is a popular container texture format used in video games and natively -supported by DirectX. -Currently, uncompressed RGB data and DXT1, DXT3, and DXT5 pixel formats are -supported, and only in ``RGBA`` mode. - -.. versionadded:: 3.4.0 DXT3 - FLI, FLC ^^^^^^^^ @@ -1177,6 +1202,7 @@ dpi. To load it at another resolution: .. code-block:: python from PIL import Image + with Image.open("drawing.wmf") as im: im.load(dpi=144) @@ -1188,15 +1214,19 @@ To add other read or write support, use from PIL import Image from PIL import WmfImagePlugin + class WmfHandler: def open(self, im): ... + def load(self, im): ... return image + def save(self, im, fp, filename): ... + wmf_handler = WmfHandler() WmfImagePlugin.register_handler(wmf_handler) @@ -1242,8 +1272,10 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 3.0.0 **append_images** - A list of images to append as additional pages. Each of the - images in the list can be single or multiframe images. + A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each + of the images in the list can be single or multiframe images. The ``save_all`` + parameter must be present and set to ``True`` in conjunction with + ``append_images``. .. versionadded:: 4.2.0 diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 6b68a0562..aa9efe192 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -176,12 +176,13 @@ Rolling an image xsize, ysize = image.size delta = delta % xsize - if delta == 0: return image + if delta == 0: + return image part1 = image.crop((0, 0, delta, ysize)) part2 = image.crop((delta, 0, xsize, ysize)) - image.paste(part1, (xsize-delta, 0, xsize, ysize)) - image.paste(part2, (0, 0, xsize-delta, ysize)) + image.paste(part1, (xsize - delta, 0, xsize, ysize)) + image.paste(part2, (0, 0, xsize - delta, ysize)) return image @@ -264,6 +265,7 @@ Converting between modes :: from PIL import Image + with Image.open("hopper.ppm") as im: im = im.convert("L") @@ -382,14 +384,14 @@ Reading sequences from PIL import Image with Image.open("animation.gif") as im: - im.seek(1) # skip to the second frame + im.seek(1) # skip to the second frame try: while 1: - im.seek(im.tell()+1) + im.seek(im.tell() + 1) # do something to im except EOFError: - pass # end of sequence + pass # end of sequence As seen in this example, you’ll get an :py:exc:`EOFError` exception when the sequence ends. @@ -422,9 +424,9 @@ Drawing PostScript with Image.open("hopper.ppm") as im: title = "hopper" - box = (1*72, 2*72, 7*72, 10*72) # in points + box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points - ps = PSDraw.PSDraw() # default is sys.stdout + ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer ps.begin_document(title) # draw the image (75 dpi) @@ -433,7 +435,7 @@ Drawing PostScript # draw title ps.setfont("HelveticaNarrow-Bold", 36) - ps.text((3*72, 4*72), title) + ps.text((3 * 72, 4 * 72), title) ps.end_document() @@ -462,6 +464,7 @@ Reading from an open file :: from PIL import Image + with open("hopper.ppm", "rb") as fp: im = Image.open(fp) @@ -475,6 +478,7 @@ Reading from binary data from PIL import Image import io + im = Image.open(io.BytesIO(buffer)) Note that the library rewinds the file (using ``seek(0)``) before reading the @@ -493,6 +497,43 @@ Reading from a tar archive fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") im = Image.open(fp) + +Batch processing +^^^^^^^^^^^^^^^^ + +Operations can be applied to multiple image files. For example, all PNG images +in the current directory can be saved as JPEGs at reduced quality. + +:: + + import glob + from PIL import Image + + + def compress_image(source_path, dest_path): + with Image.open(source_path) as img: + if img.mode != "RGB": + img = img.convert("RGB") + img.save(dest_path, "JPEG", optimize=True, quality=80) + + + paths = glob.glob("*.png") + for path in paths: + compress_image(path, path[:-4] + ".jpg") + +Since images can also be opened from a ``Path`` from the ``pathlib`` module, +the example could be modified to use ``pathlib`` instead of the ``glob`` +module. + +:: + + from pathlib import Path + + paths = Path(".").glob("*.png") + for path in paths: + compress_image(path, path.stem + ".jpg") + + Controlling the decoder ----------------------- diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index 9b670dba8..f69da9a94 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -87,10 +87,13 @@ true color. Image.register_open(SpamImageFile.format, SpamImageFile, _accept) - Image.register_extensions(SpamImageFile.format, [ - ".spam", - ".spa", # DOS version - ]) + Image.register_extensions( + SpamImageFile.format, + [ + ".spam", + ".spa", # DOS version + ], + ) The format handler must always set the @@ -111,6 +114,7 @@ Once the plugin has been imported, it can be used: from PIL import Image import SpamImagePlugin + with Image.open("hopper.spam") as im: pass @@ -163,16 +167,16 @@ TIFF, and many others. To use the raw decoder with the image = Image.frombytes( mode, size, data, "raw", - raw mode, stride, orientation + raw_mode, stride, orientation ) When used in a tile descriptor, the parameter field should look like:: - (raw mode, stride, orientation) + (raw_mode, stride, orientation) The fields are used as follows: -**raw mode** +**raw_mode** The pixel layout used in the file, and is used to properly convert data to PIL’s internal layout. For a summary of the available formats, see the table below. @@ -191,34 +195,34 @@ match PIL’s internal pixel layout. PIL supports a large set of raw modes; for complete list, see the table in the :file:`Unpack.c` module. The following table describes some commonly used **raw modes**: -+-----------+-----------------------------------------------------------------+ -| mode | description | -+===========+=================================================================+ -| ``1`` | 1-bit bilevel, stored with the leftmost pixel in the most | -| | significant bit. 0 means black, 1 means white. | -+-----------+-----------------------------------------------------------------+ -| ``1;I`` | 1-bit inverted bilevel, stored with the leftmost pixel in the | -| | most significant bit. 0 means white, 1 means black. | -+-----------+-----------------------------------------------------------------+ -| ``1;R`` | 1-bit reversed bilevel, stored with the leftmost pixel in the | -| | least significant bit. 0 means black, 1 means white. | -+-----------+-----------------------------------------------------------------+ -| ``L`` | 8-bit greyscale. 0 means black, 255 means white. | -+-----------+-----------------------------------------------------------------+ -| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. | -+-----------+-----------------------------------------------------------------+ -| ``P`` | 8-bit palette-mapped image. | -+-----------+-----------------------------------------------------------------+ -| ``RGB`` | 24-bit true colour, stored as (red, green, blue). | -+-----------+-----------------------------------------------------------------+ -| ``BGR`` | 24-bit true colour, stored as (blue, green, red). | -+-----------+-----------------------------------------------------------------+ -| ``RGBX`` | 24-bit true colour, stored as (red, green, blue, pad). The pad | -| | pixels may vary. | -+-----------+-----------------------------------------------------------------+ -| ``RGB;L`` | 24-bit true colour, line interleaved (first all red pixels, then| -| | all green pixels, finally all blue pixels). | -+-----------+-----------------------------------------------------------------+ ++-----------+-------------------------------------------------------------------+ +| mode | description | ++===========+===================================================================+ +| ``1`` | | 1-bit bilevel, stored with the leftmost pixel in the most | +| | | significant bit. 0 means black, 1 means white. | ++-----------+-------------------------------------------------------------------+ +| ``1;I`` | | 1-bit inverted bilevel, stored with the leftmost pixel in the | +| | | most significant bit. 0 means white, 1 means black. | ++-----------+-------------------------------------------------------------------+ +| ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the | +| | | least significant bit. 0 means black, 1 means white. | ++-----------+-------------------------------------------------------------------+ +| ``L`` | 8-bit greyscale. 0 means black, 255 means white. | ++-----------+-------------------------------------------------------------------+ +| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. | ++-----------+-------------------------------------------------------------------+ +| ``P`` | 8-bit palette-mapped image. | ++-----------+-------------------------------------------------------------------+ +| ``RGB`` | 24-bit true colour, stored as (red, green, blue). | ++-----------+-------------------------------------------------------------------+ +| ``BGR`` | 24-bit true colour, stored as (blue, green, red). | ++-----------+-------------------------------------------------------------------+ +| ``RGBX`` | | 24-bit true colour, stored as (red, green, blue, pad). The pad | +| | | pixels may vary. | ++-----------+-------------------------------------------------------------------+ +| ``RGB;L`` | | 24-bit true colour, line interleaved (first all red pixels, then| +| | | all green pixels, finally all blue pixels). | ++-----------+-------------------------------------------------------------------+ Note that for the most common cases, the raw mode is simply the same as the mode. diff --git a/docs/index.rst b/docs/index.rst index d2aca4bc4..0e16259f3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,15 +25,19 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 1.0 no longer supports "import Image". Please use "from PIL import Image" instead. +.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead. -.. warning:: Pillow >= 2.1.0 no longer supports "import _imaging". Please use "from PIL.Image import core as _imaging" instead. +.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead. Python Support -------------- Pillow supports these Python versions. -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| **Python** |**3.9**|**3.8**|**3.7**|**3.6**|**3.5**|**3.4**|**3.3**|**3.2**|**2.7**|**2.6**|**2.5**|**2.4**| -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow >= 8.0 | Yes | Yes | Yes | Yes | | | | | | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 7.0 - 7.2 | | Yes | Yes | Yes | Yes | | | | | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 6.2.1 - 6.2.2 | | Yes | Yes | Yes | Yes | | | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 6.0 - 6.2.0 | | | Yes | Yes | Yes | | | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 5.2 - 5.4 | | | Yes | Yes | Yes | Yes | | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 5.0 - 5.1 | | | | Yes | Yes | Yes | | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 4 | | | | Yes | Yes | Yes | Yes | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 2 - 3 | | | | | Yes | Yes | Yes | Yes | Yes | Yes | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow < 2 | | | | | | | | | Yes | Yes | Yes | Yes | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 | ++======================+=====+=====+=====+=====+=====+=====+=====+=====+ +| Pillow >= 9.0 | 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 | | | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 6.2.1 - 6.2.2 | | | Yes | Yes | Yes | Yes | | Yes | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 6.0 - 6.2.0 | | | | Yes | Yes | Yes | | Yes | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 5.2 - 5.4 | | | | Yes | Yes | Yes | Yes | Yes | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ + ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +| Python | 3.6 | 3.5 | 3.4 | 3.3 | 3.2 | 2.7 | 2.6 | 2.5 | 2.4 | ++==================+=====+=====+=====+=====+=====+=====+=====+=====+=====+ +| Pillow 5.0 - 5.1 | Yes | Yes | Yes | | | Yes | | | | ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 4 | Yes | Yes | Yes | Yes | | Yes | | | | ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 2 - 3 | | Yes | Yes | Yes | Yes | Yes | Yes | | | ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow < 2 | | | | | | Yes | Yes | Yes | Yes | ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ Basic Installation ------------------ @@ -57,8 +65,9 @@ Windows Installation 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 have all of the optional libraries included except -for raqm, libimagequant, and libxcb:: +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 @@ -71,8 +80,8 @@ macOS Installation We provide binaries for macOS for each of the supported Python versions in the wheel format. These include support for all optional -libraries except libimagequant and libxcb. Raqm support requires -libraqm, fribidi, and harfbuzz to be installed separately:: +libraries except libimagequant. Raqm support requires +FriBiDi to be installed separately:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow @@ -83,7 +92,7 @@ Linux Installation We provide binaries for Linux for each of the supported Python versions in the manylinux wheel format. These include support for all optional libraries except libimagequant. Raqm support requires -libraqm, fribidi, and harfbuzz to be installed separately:: +FriBiDi to be installed separately:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow @@ -104,7 +113,7 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems: **Packages**:: - pkg install py36-pillow + pkg install py38-pillow .. note:: @@ -153,7 +162,7 @@ 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.1** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.3** * **libfreetype** provides type related services @@ -178,7 +187,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-2.14.1** + * Pillow has been tested with libimagequant **2.6-2.17.0** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. @@ -191,11 +200,15 @@ Many of Pillow's features require external libraries: * libraqm depends on the following libraries: FreeType, HarfBuzz, FriBiDi, make sure that you install them before installing libraqm if not available as package in your system. - * setting text direction or font features is not supported without - libraqm. - * libraqm is dynamically loaded in Pillow 5.0.0 and above, so support - is available if all the libraries are installed. - * Windows support: Raqm is not included in prebuilt wheels + * Setting text direction or font features is not supported without libraqm. + * 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) + `_ + (``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. * **libxcb** provides X11 screengrab support. @@ -244,6 +257,12 @@ Build Options 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`` + 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 platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the @@ -256,10 +275,6 @@ Build Options Sample usage:: - MAX_CONCURRENCY=1 python3 setup.py build_ext --enable-[feature] install - -or using pip:: - python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" @@ -291,7 +306,7 @@ Now install Pillow with:: or from within the uncompressed source directory:: - python3 setup.py install + python3 -m pip install . Building on Windows ^^^^^^^^^^^^^^^^^^^ @@ -426,41 +441,42 @@ Continuous Integration Targets These platforms are built and tested for every change. -+----------------------------------+--------------------------+-----------------------+ -|**Operating system** |**Tested Python versions**|**Tested architecture**| -+----------------------------------+--------------------------+-----------------------+ -| Alpine | 3.8 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Arch | 3.8 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Amazon Linux 2 | 3.7 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| CentOS 7 | 3.6 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| CentOS 8 | 3.6 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Debian 10 Buster | 3.7 |x86 | -+----------------------------------+--------------------------+-----------------------+ -| Fedora 32 | 3.8 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Fedora 33 | 3.9 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Ubuntu Linux 16.04 LTS (Xenial) | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Ubuntu Linux 18.04 LTS (Bionic) | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.8 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Windows Server 2016 | 3.6 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Windows Server 2019 | 3.6, 3.7, 3.8, 3.9 |x86, x86-64 | -| +--------------------------+-----------------------+ -| | PyPy3 |x86 | -| +--------------------------+-----------------------+ -| | 3.8/MinGW |x86, x86-64 | -+----------------------------------+--------------------------+-----------------------+ ++----------------------------------+----------------------------+---------------------+ +| Operating system | Tested Python versions | Tested architecture | ++==================================+============================+=====================+ +| Alpine | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2 | 3.7 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Arch | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS 7 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS 8 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS Stream 8 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Debian 10 Buster | 3.7 | x86 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 34 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 35 | 3.10 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | +| +----------------------------+---------------------+ +| | 3.8 | arm64v8, ppc64le, | +| | | s390x | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2016 | 3.7 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2019 | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 | +| +----------------------------+---------------------+ +| | 3.9/MinGW | x86, x86-64 | ++----------------------------------+----------------------------+---------------------+ Other Platforms @@ -473,74 +489,79 @@ These platforms have been reported to work at the versions mentioned. Contributors please test Pillow on your platform then update this document and send a pull request. -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -|**Operating system** |**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 11.0 Big Sur | 3.8, 3.9 | 8.1.2 |arm | -| +------------------------------+--------------------------------+-----------------------+ -| | 3.6, 3.7, 3.8, 3.9 | 8.1.2 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.0.1 |x86-64 | -| +------------------------------+--------------------------------+ + -| | 3.5 | 7.2.0 | | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | -| +------------------------------+--------------------------------+ + -| | 2.7 | 6.0.0 | | -| +------------------------------+--------------------------------+ + -| | 3.4 | 5.4.1 | | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | -| +------------------------------+--------------------------------+ + -| | 3.3 | 4.1.0 | | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Redhat Linux 6 | 2.6 | |x86 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| CentOS 6.3 | 2.7, 3.3 | |x86 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 12.04 LTS (Precise) | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | -| | PyPy5.3.1, PyPy3 v2.4.0 | | | -| +------------------------------+--------------------------------+-----------------------+ -| | 2.7 | 4.3.0 |x86-64 | -| +------------------------------+--------------------------------+-----------------------+ -| | 2.7, 3.2 | 3.4.1 |ppc | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows 10 | 3.7 | 7.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ ++----------------------------------+---------------------------+------------------+--------------+ +| Operating system | | Tested Python | | Latest tested | | Tested | +| | | versions | | Pillow version | | processors | ++==================================+===========================+==================+==============+ +| macOS 11.0 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | +| +---------------------------+------------------+--------------+ +| | 3.6, 3.7, 3.8, 3.9, 3.10 | 8.4.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | +| +---------------------------+------------------+ | +| | 3.5 | 7.2.0 | | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | +| +---------------------------+------------------+ | +| | 2.7 | 6.0.0 | | +| +---------------------------+------------------+ | +| | 3.4 | 5.4.1 | | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | +| +---------------------------+------------------+ | +| | 3.3 | 4.1.0 | | ++----------------------------------+---------------------------+------------------+--------------+ +| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Redhat Linux 6 | 2.6 | |x86 | ++----------------------------------+---------------------------+------------------+--------------+ +| CentOS 6.3 | 2.7, 3.3 | |x86 | ++----------------------------------+---------------------------+------------------+--------------+ +| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | +| | | PyPy5.3.1, PyPy3 v2.4.0 | | | +| +---------------------------+------------------+--------------+ +| | 2.7 | 4.3.0 |x86-64 | +| +---------------------------+------------------+--------------+ +| | 2.7, 3.2 | 3.4.1 |ppc | ++----------------------------------+---------------------------+------------------+--------------+ +| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | ++----------------------------------+---------------------------+------------------+--------------+ +| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | ++----------------------------------+---------------------------+------------------+--------------+ +| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | +| +---------------------------+------------------+ | +| | 2.7 | 6.2.2 | | ++----------------------------------+---------------------------+------------------+--------------+ +| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows 10 | 3.7 | 7.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ Old Versions ------------ diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 37fb5f726..b95d8d591 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -18,6 +18,7 @@ Example: Draw a gray cross over an image .. code-block:: python + import sys from PIL import Image, ImageDraw with Image.open("hopper.jpg") as im: @@ -80,11 +81,12 @@ Example: Draw Partial Opacity Text .. code-block:: python from PIL import Image, ImageDraw, ImageFont + # get an image with Image.open("Pillow/Tests/images/hopper.png").convert("RGBA") as base: # make a blank image for the text, initialized to transparent text color - txt = Image.new("RGBA", base.size, (255,255,255,0)) + txt = Image.new("RGBA", base.size, (255, 255, 255, 0)) # get a font fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) @@ -92,9 +94,9 @@ Example: Draw Partial Opacity Text d = ImageDraw.Draw(txt) # draw text, half opacity - d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128)) + d.text((10, 10), "Hello", font=fnt, fill=(255, 255, 255, 128)) # draw text, full opacity - d.text((10,60), "World", font=fnt, fill=(255,255,255,255)) + d.text((10, 60), "World", font=fnt, fill=(255, 255, 255, 255)) out = Image.alpha_composite(base, txt) @@ -116,7 +118,7 @@ Example: Draw Multiline Text d = ImageDraw.Draw(out) # draw multiline text - d.multiline_text((10,10), "Hello\nWorld", font=fnt, fill=(0, 0, 0)) + d.multiline_text((10, 10), "Hello\nWorld", font=fnt, fill=(0, 0, 0)) out.show() @@ -241,7 +243,7 @@ Methods numeric values like ``[x, y, x, y, ...]``. :param fill: Color to use for the point. -.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None) +.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None, width=1) Draws a polygon. @@ -251,8 +253,9 @@ Methods :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. - :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. .. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) @@ -555,7 +558,9 @@ Methods .. code-block:: python - hello = draw.textlength("HelloW", font) - draw.textlength("W", font) # adjusted for kerning + hello = draw.textlength("HelloW", font) - draw.textlength( + "W", font + ) # adjusted for kerning world = draw.textlength("World", font) hello_world = hello + world # adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # True diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 813d325e0..5f718ce19 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -9,7 +9,7 @@ this class store bitmap fonts, and are used with the :py:meth:`PIL.ImageDraw.ImageDraw.text` method. PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use -`pilfont.py `_ +`pilfont.py `_ from `pillow-scripts `_ to convert BDF and PCF font descriptors (X window font formats) to this format. diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 9a16d6625..d1c43cf60 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -12,6 +12,7 @@ only work on L and RGB images. .. autofunction:: autocontrast .. autofunction:: colorize +.. autofunction:: contain .. autofunction:: pad .. autofunction:: crop .. autofunction:: scale diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst index f14c1c3a4..72ccfac7d 100644 --- a/docs/reference/ImagePalette.rst +++ b/docs/reference/ImagePalette.rst @@ -9,10 +9,6 @@ represent the color palette of palette mapped images. .. note:: - This module was never well-documented. It hasn't changed since 2001, - though, so it's probably safe for you to read the source code and puzzle - out the internals if you need to. - The :py:class:`~PIL.ImagePalette.ImagePalette` class has several methods, but they are all marked as "experimental." Read that as you will. The ``[source]`` link is there for a reason. diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index f1fbd90ce..45b50c846 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -17,7 +17,9 @@ All default viewers convert the image to be shown to PNG format. The following viewers may be registered on Unix-based systems, if the given command is found: + .. autoclass:: PIL.ImageShow.XDGViewer .. autoclass:: PIL.ImageShow.DisplayViewer + .. autoclass:: PIL.ImageShow.GmDisplayViewer .. autoclass:: PIL.ImageShow.EogViewer .. autoclass:: PIL.ImageShow.XVViewer diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index f28e58f86..173a0bcc0 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -17,11 +17,12 @@ changes it. .. code-block:: python from PIL import Image - with Image.open('hopper.jpg') as im: + + with Image.open("hopper.jpg") as im: px = im.load() - print (px[4,4]) - px[4,4] = (0,0,0) - print (px[4,4]) + print(px[4, 4]) + px[4, 4] = (0, 0, 0) + print(px[4, 4]) Results in the following:: @@ -32,8 +33,8 @@ Access using negative indexes is also possible. .. code-block:: python - px[-1,-1] = (0,0,0) - print (px[-1,-1]) + px[-1, -1] = (0, 0, 0) + print(px[-1, -1]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index 486c9fc21..e77944d20 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -18,11 +18,12 @@ The following script loads an image, accesses one pixel from it, then changes it .. code-block:: python from PIL import Image - with Image.open('hopper.jpg') as im: + + with Image.open("hopper.jpg") as im: px = im.load() - print (px[4,4]) - px[4,4] = (0,0,0) - print (px[4,4]) + print(px[4, 4]) + px[4, 4] = (0, 0, 0) + print(px[4, 4]) Results in the following:: @@ -33,8 +34,8 @@ Access using negative indexes is also possible. .. code-block:: python - px[-1,-1] = (0,0,0) - print (px[-1,-1]) + 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 527b9d7bc..2ba95b8a6 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -63,6 +63,7 @@ Take your test image, and make a really simple harness. :: from PIL import Image + with Image.open(path) as im: im.load() @@ -339,7 +340,7 @@ Take your test image, and make a really simple harness. (vpy38-dbg) ubuntu@primary:~/Home/tests$ gdb python GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2 Copyright (C) 2020 Free Software Foundation, Inc. - License GPLv3+: GNU GPL version 3 or later + License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. @@ -348,7 +349,7 @@ Take your test image, and make a really simple harness. For bug reporting instructions, please see: . Find the GDB manual and other documentation resources online at: - . + . For help, type "help". Type "apropos word" to search for commands related to "word"... diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index ed0ab1a0c..6bfd50588 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -14,17 +14,17 @@ The following are all equivalent:: import io import pathlib - with Image.open('test.jpg') as im: + with Image.open("test.jpg") as im: ... - with Image.open(pathlib.Path('test.jpg')) as im2: + with Image.open(pathlib.Path("test.jpg")) as im2: ... - with open('test.jpg', 'rb') as f: + with open("test.jpg", "rb") as f: im3 = Image.open(f) ... - with open('test.jpg', 'rb') as f: + with open("test.jpg", "rb") as f: im4 = Image.open(io.BytesIO(f.read())) ... @@ -47,6 +47,10 @@ Image Lifecycle memory. The image can now be used independently of the underlying image file. + Any Pillow method that creates a new image instance based on another will + internally call ``load()`` on the original image and then read the data. + The new image instance will not be associated with the original image file. + If a filename or a ``Path`` object was passed to ``Image.open()``, then the file object was opened by Pillow and is considered to be used exclusively by Pillow. So if the image is a single-frame image, the file will be closed in @@ -55,10 +59,16 @@ Image Lifecycle ``Image.Image.seek()`` can load the appropriate frame. * ``Image.Image.close()`` Closes the file and destroys the core image object. - This is used in the Pillow context manager support. e.g.:: - with Image.open('test.jpg') as img: - ... # image operations here. + The Pillow context manager will also close the file, but will not destroy + the core image object. e.g.: + +.. code-block:: python + + with Image.open("test.jpg") as img: + img.load() + assert img.fp is None + img.save("test.png") The lifecycle of a single-frame image is relatively simple. The file must @@ -80,13 +90,13 @@ Complications * After a file has been closed, operations that require file access will fail:: - with open('test.jpg', 'rb') as f: + with open("test.jpg", "rb") as f: im5 = Image.open(f) - im5.load() # FAILS, closed file + im5.load() # FAILS, closed file - with Image.open('test.jpg') as im6: + with Image.open("test.jpg") as im6: pass - im6.load() # FAILS, closed file + im6.load() # FAILS, closed file Proposed File Handling diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index 03000528f..660d33164 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -14,7 +14,7 @@ Png text chunk size limits To prevent potential denial of service attacks using compressed text chunks, there are now limits to the decompressed size of text chunks decoded from PNG images. If the limits are exceeded when opening a PNG -image a ``ValueError`` will be raised. +image a :py:exc:`ValueError` will be raised. Individual text chunks are limited to :py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 28dc8324d..2ff9b3799 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -78,7 +78,7 @@ Added a new ``formats`` parameter to :py:func:`.Image.open`: * A list or tuple of formats to attempt to load the file in. This can be used to restrict the set of formats checked. Pass ``None`` to try all supported formats. You can print the set of - available formats by running ``python -m PIL`` or using + available formats by running ``python3 -m PIL`` or using the :py:func:`PIL.features.pilinfo` function. ImageOps.autocontrast: add mask parameter diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 3ef05894d..c902ccf71 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -4,22 +4,22 @@ Deprecations ============ -Tk/Tcl 8.4 -^^^^^^^^^^ - -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), -when Tk/Tcl 8.5 will be the minimum supported. - Categories ^^^^^^^^^^ -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +``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. 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 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), +when Tk/Tcl 8.5 will be the minimum supported. + API Changes =========== @@ -45,9 +45,31 @@ This is now consistent with other IFDs, and must be accessed through These changes only affect :py:meth:`~PIL.Image.Image.getexif`, introduced in Pillow 6.0. The older ``_getexif()`` methods are unaffected. +Image._MODEINFO +^^^^^^^^^^^^^^^ + +This internal dictionary had been deprecated by a comment since PIL, and is now +removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``, +``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()`` +can be used. + API Additions ============= +getxmp() for JPEG images +^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method has been added to return +`XMP data `_ for JPEG +images. It reads the XML data into a dictionary of names and values. + +For example:: + + >>> from PIL import Image + >>> with Image.open("Tests/images/xmp_test.jpg") as im: + >>> print(im.getxmp()) + {'RDF': {}, 'Description': {'Version': '10.4', 'ProcessVersion': '10.0', ...}, ...} + ImageDraw.rounded_rectangle ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -63,6 +85,26 @@ create a circle, but not any other ellipse. draw = ImageDraw.Draw(im) draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red") +ImageOps.autocontrast: preserve_tone +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The default behaviour of :py:meth:`~PIL.ImageOps.autocontrast` is to normalize +separate histograms for each color channel, changing the tone of the image. The new +``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram +for all channels. + +ImageShow.GmDisplayViewer +^^^^^^^^^^^^^^^^^^^^^^^^^ + +If GraphicsMagick is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will +be registered. It uses GraphicsMagick_, an ImageMagick_ fork, to display images. + +The GraphicsMagick based viewer has a lower priority than its ImageMagick +counterpart. Thus, if both ImageMagick and GraphicsMagick are installed, +``im.show()`` and :py:func:`.ImageShow.show()` prefer the viewer based on +ImageMagick, i.e the behaviour stays the same for Pillow users having +ImageMagick installed. + ImageShow.IPythonViewer ^^^^^^^^^^^^^^^^^^^^^^^ @@ -83,16 +125,106 @@ be specified through a keyword argument:: im.save("out.tif", icc_profile=...) + Security ======== -TODO +These were all found with `OSS-Fuzz`_. + +:cve:`CVE-2021-25287`, :cve:`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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* :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 + denial-of-service on :py:meth:`~PIL.Image.open` prior to + :py:meth:`~PIL.Image.Image.load`. +* This dates to the PIL fork. + +:cve:`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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* 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 + for a line ending. +* A malicious EPS file could use this to perform a denial-of-service of Pillow in the + open phase, before an image was accepted for opening. +* This dates to the PIL fork. + +:cve:`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 + large number of times on empty data. +* This dates to Pillow 5.1.0. + +Fix memory DOS in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* A corrupt or specially crafted TTF font could have font metrics that lead to + unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not check + the image size before allocating memory for it. +* This dates to the PIL fork. Other Changes ============= +GIF writer uses LZW encoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +GIF files are now written using LZW encoding, which will generate smaller files, +typically about 70% of the size generated by the older encoder. + +The pixel data is encoded using the format specified in the `CompuServe GIF standard +`_. + +The older encoder used a variant of run-length encoding that was compatible but less +efficient. + +GraphicsMagick +^^^^^^^^^^^^^^ + +The test suite can now be run on systems which have GraphicsMagick_ but not +ImageMagick_ installed. If both are installed, the tests prefer ImageMagick. + +Libraqm and FriBiDi linking +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The way the libraqm dependency for complex text scripts is linked has been changed: + +Source builds will now link against the system version of libraqm at build time +rather than at runtime by default. + +Binary wheels now include a statically linked modified version of libraqm that +links against FriBiDi at runtime instead. This change is intended to address +issues with the previous implementation on some platforms. These are created +by building Pillow with the new build flags ``--vendor-raqm --vendor-fribidi``. + +Windows users will now need to install ``fribidi.dll`` (or ``fribidi-0.dll``) only, +``libraqm.dll`` is no longer used. + +See :doc:`installation documentation<../installation>` for more information. + PyQt6 ^^^^^ Support has been added for PyQt6. If it is installed, it will be used instead of PySide6, PyQt5 or PySide2. + +.. _GraphicsMagick: http://www.graphicsmagick.org/ +.. _ImageMagick: https://imagemagick.org/ +.. _OSS-Fuzz: https://github.com/google/oss-fuzz diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst new file mode 100644 index 000000000..0bfead144 --- /dev/null +++ b/docs/releasenotes/8.3.0.rst @@ -0,0 +1,113 @@ +8.3.0 +----- + +Deprecations +============ + +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 +performs any operations on the data given to it, has been deprecated and will be +removed in Pillow 10.0.0 (2023-07-01). + +API Changes +=========== + +Changed WebP default "method" value when saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, it was 0, for the best speed. The default has now been changed to 4, to +match WebP's default, for higher quality with still some speed optimisation. + +Default resampling filter for special image modes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 7.0 changed the default resampling filter to ``Image.BICUBIC``. However, as this +is not supported yet for images with a custom number of bits, the default filter for +those modes has been reverted to ``Image.NEAREST``. + +ImageMorph incorrect mode errors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For ``apply()``, ``match()`` and ``get_on_pixels()``, if the image mode is not L, an +:py:exc:`Exception` was thrown. This has now been changed to a :py:exc:`ValueError`. + +getxmp() +^^^^^^^^ + +`XMP data `_ can now be +returned for PNG and TIFF images, through ``getxmp()`` for each format. + +The returned dictionary will start from the base of the XML, meaning that the top level +should contain an "xmpmeta" key. JPEG's ``getxmp()`` method has also been updated to +this structure. + +TIFF getexif() +^^^^^^^^^^^^^^ + +TIFF :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` data can now be accessed +through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GPS and +EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and +``im.getexif().get_ifd(0x8769)`` respectively. + +API Additions +============= + +ImageOps.contain +^^^^^^^^^^^^^^^^ + +Returns a resized version of the image, set to the maximum width and height within +``size``, while maintaining the original aspect ratio. + +To compare it to other ImageOps methods: + +- :py:meth:`~PIL.ImageOps.fit` expands an image until is fills ``size``, cropping the + parts of the image that do not fit. +- :py:meth:`~PIL.ImageOps.pad` expands an image to fill ``size``, without cropping, but + instead filling the extra space with ``color``. +- :py:meth:`~PIL.ImageOps.contain` is similar to :py:meth:`~PIL.ImageOps.pad`, but it + does not fill the extra space. Instead, the original aspect ratio is maintained. So + unlike the other two methods, it is not guaranteed to return an image of ``size``. + +ICO saving: bitmap_format argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, Pillow saves ICO files in the PNG format. They can now also be saved in BMP +format, through the new ``bitmap_format`` argument:: + + im.save("out.ico", bitmap_format="bmp") + +Security +======== + +Buffer overflow +^^^^^^^^^^^^^^^ + +This release addresses :cve:`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. + +Parsing XML +^^^^^^^^^^^ + +Pillow previously parsed XMP data using Python's ``xml`` module. However, this module +is not secure. + +- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve + orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead. +- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It + will now use ``defusedxml`` instead. If the dependency is not present, an empty + dictionary will be returned and a warning raised. + +Other Changes +============= + +Added DDS BC5 reading and uncompressed saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read the BC5 format of DDS images, whether UNORM, SNORM or +TYPELESS. + +Support has also been added to write the uncompressed format of DDS images. diff --git a/docs/releasenotes/8.3.1.rst b/docs/releasenotes/8.3.1.rst new file mode 100644 index 000000000..e97070c11 --- /dev/null +++ b/docs/releasenotes/8.3.1.rst @@ -0,0 +1,40 @@ +8.3.1 +----- + +Fixed regression converting to NumPy arrays +=========================================== + +This fixes a regression introduced in 8.3.0 when converting an image to a NumPy array +with a ``dtype`` argument. + +.. code-block:: pycon + + >>> from PIL import Image + >>> import numpy + >>> im = Image.new("RGB", (100, 100)) + >>> numpy.array(im, dtype=numpy.float64) + Traceback (most recent call last): + File "", line 1, in + TypeError: __array__() takes 1 positional argument but 2 were given + >>> + +Catch OSError when checking if destination is sys.stdout +======================================================== + +In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was +updated. This lead to an OSError being raised if the environment restricted access. + +The OSError is now silently caught. + +Fixed removing orientation in ImageOps.exif_transpose +===================================================== + +In 8.3.0, :py:meth:`~PIL.ImageOps.exif_transpose` was changed to ensure that the +original image EXIF data was not modified, and the orientation was only removed from +the modified copy. + +However, for certain images the orientation was already missing from the modified +image, leading to a KeyError. + +This error has been resolved, and the copying of metadata to the modified image +improved. diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst new file mode 100644 index 000000000..6b5c759fc --- /dev/null +++ b/docs/releasenotes/8.3.2.rst @@ -0,0 +1,41 @@ +8.3.2 +----- + +Security +======== + +* :cve:`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. + +* Fix 6-byte out-of-bounds (OOB) read. The previous bounds check in ``FliDecode.c`` + incorrectly calculated the required read buffer size when copying a chunk, potentially + reading six extra bytes off the end of the allocated buffer from the heap. Present + since Pillow 7.1.0. This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs. + +Other Changes +============= + +Python 3.10 wheels +^^^^^^^^^^^^^^^^^^ + +Pillow now includes binary wheels for Python 3.10. + +The Python 3.10 release candidate was released on 2021-08-03 with the final release due +2021-10-04 (:pep:`619`). The CPython core team strongly encourages maintainers of +third-party Python projects to prepare for 3.10 compatibility. And as there are `no ABI +changes`_ planned we are releasing wheels to help others prepare for 3.10, and ensure +Pillow can be used immediately on release day of 3.10.0 final. + +Fixed regressions +^^^^^^^^^^^^^^^^^ + +* Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression (:pr:`5588`). + +* Updates for :py:class:`~PIL.ImagePalette` channel order (:pr:`5599`). + +* Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library (:pr:`5651`). + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz +.. _CIFuzz: https://google.github.io/oss-fuzz/getting-started/continuous-integration/ +.. _no ABI changes: https://www.python.org/downloads/release/python-3100rc1/ diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst new file mode 100644 index 000000000..9becf9146 --- /dev/null +++ b/docs/releasenotes/8.4.0.rst @@ -0,0 +1,53 @@ +8.4.0 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +ImagePalette size parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). + +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. + +API Additions +============= + +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 + + with Image.open("sample.eps") as im: + im.load(transparency=True) + +Added WalImageFile class +^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:func:`PIL.WalImageFile.open()` previously returned a generic +:py:class:`PIL.Image.Image` instance. It now returns a dedicated +:py:class:`PIL.WalImageFile.WalImageFile` class. + +Other Changes +============= + +Speed improvement when rotating square images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Starting with Pillow 3.3.0, the speed of rotating images by 90 or 270 degrees was +improved by quickly returning :py:meth:`~PIL.Image.Image.transpose` instead, if the +rotate operation allowed for expansion and did not specify a center or post-rotate +translation. + +Since the ``expand`` flag makes no difference for square images though, Pillow now +uses this faster method for square images without the ``expand`` flag as well. diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst new file mode 100644 index 000000000..f2be128bb --- /dev/null +++ b/docs/releasenotes/9.0.0.rst @@ -0,0 +1,170 @@ +9.0.0 +----- + +Fredrik Lundh +============= + +This release is dedicated to the memory of Fredrik Lundh, aka Effbot, who died in +November 2021. Fredrik created PIL in 1995 and he was instrumental in the early +success of Python. + +`Guido wrote `_: + + Fredrik was an early Python contributor (e.g. Elementtree and the 're' + module) and his enthusiasm for the language and community were inspiring + for all who encountered him or his work. He spent countless hours on + comp.lang.python answering questions from newbies and advanced users alike. + + He also co-founded an early Python startup, Secret Labs AB, which among + other software released an IDE named PythonWorks. Fredrik also created the + Python Imaging Library (PIL) which is still THE way to interact with images + in Python, now most often through its Pillow fork. His effbot.org site was + a valuable resource for generations of Python users, especially its Tkinter + documentation. + +Thank you, Fredrik. + +Backwards Incompatible Changes +============================== + +Python 3.6 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.6, which reached end-of-life on 2021-12-23. + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +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`). + +.. _FreeType: https://www.freetype.org + +Image.show command parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``command`` parameter has been removed. Use a subclass of +:py:class:`PIL.ImageShow.Viewer` instead. + +Image._showxv +^^^^^^^^^^^^^ + +``Image._showxv`` has been removed. Use :py:meth:`~PIL.Image.Image.show` +instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add +a custom :py:class:`~PIL.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +^^^^^^^^^^^^^^^^^^^^^^^ + +``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror`` +has been removed. Use ``ImageFile.raise_oserror`` instead. + + +API Changes +=========== + +Added line width parameter to ImageDraw polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``. + + +API Additions +============= + +ImageShow.XDGViewer +^^^^^^^^^^^^^^^^^^^ + +If ``xdg-open`` is present on Linux, this new :py:class:`PIL.ImageShow.Viewer` subclass +will be registered. It displays images using the application selected by the system. + +It is higher in priority than the other default :py:class:`PIL.ImageShow.Viewer` +instances, so it will be preferred by ``im.show()`` or :py:func:`.ImageShow.show()`. + +Added support for "title" argument to DisplayViewer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for the "title" argument in +:py:class:`~PIL.ImageShow.UnixViewer.DisplayViewer`, so that when ``im.show()`` or +:py:func:`.ImageShow.show()` use the ``display`` command line tool, the "title" +argument will also now be supported, e.g. ``im.show(title="My Image")`` and +``ImageShow.show(im, title="My Image")``. + +Security +======== + +Ensure JpegImagePlugin stops at the end of a truncated file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``JpegImagePlugin`` may append an EOF marker to the end of a truncated file, so that +the last segment of the data will still be processed by the decoder. + +If the EOF marker is not detected as such however, this could lead to an infinite +loop where ``JpegImagePlugin`` keeps trying to end the file. + +Remove consecutive duplicate tiles that only differ by their offset +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To prevent attempts to slow down loading times for images, if an image has consecutive +duplicate tiles that only differ by their offset, only load the last tile. Credit to +Google's `OSS-Fuzz`_ project for finding this issue. + +Restrict builtins available to ImageMath.eval +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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())")``. CVE TBD + +Fixed ImagePath.Path array handling +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CWE-126 and CWE-665 were found when initializing ``ImagePath.Path``. CVEs TBD + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz + +Other Changes +============= + +Convert subsequent GIF frames to RGB or RGBA +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since each frame of a GIF can have up to 256 colors, after the first frame it is +possible for there to be too many colors to fit in a P mode image. To allow for this, +seeking to any subsequent GIF frame will now convert the image to RGB or RGBA, +depending on whether or not the first frame had transparency. + +Switched to libjpeg-turbo in macOS and Linux wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Pillow wheels from PyPI for macOS and Linux have switched from libjpeg to +libjpeg-turbo. It is a fork of libjpeg, popular for its speed. + +Added support for pickling TrueType fonts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TrueType fonts may now be pickled and unpickled. For example: + +.. code-block:: python + + import pickle + from PIL import ImageFont + + font = ImageFont.truetype("arial.ttf", size=30) + pickled_font = pickle.dumps(font, protocol=pickle.HIGHEST_PROTOCOL) + + # Later... + unpickled_font = pickle.loads(pickled_font) + +Added support for additional TGA orientations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TGA images with top right or bottom right orientations are now supported. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 117738675..8d1ad7837 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,11 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.0.0 + 8.4.0 + 8.3.2 + 8.3.1 + 8.3.0 8.2.0 8.1.2 8.1.1 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index bf381114e..f7271ae2b 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -34,6 +34,9 @@ TODO Security ======== +TODO +^^^^ + TODO Other Changes diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst index a8c9fc998..87f2ba422 100644 --- a/docs/releasenotes/versioning.rst +++ b/docs/releasenotes/versioning.rst @@ -11,7 +11,7 @@ Pillow follows `Semantic Versioning `_: 2. MINOR version when you add functionality in a backwards compatible manner, and 3. PATCH version when you make backwards compatible bug fixes. -Quarterly releases ("`Main Release `_") +Quarterly releases ("`Main Release `_") bump at least the MINOR version, as new functionality has likely been added in the prior three months. @@ -21,10 +21,10 @@ these occur every 12-18 months, guided by `Python's EOL schedule `_, and any APIs that have been deprecated for at least a year are removed at the same time. -PATCH versions ("`Point Release `_" -or "`Embargoed Release `_") +PATCH versions ("`Point Release `_" +or "`Embargoed Release `_") are for security, installation or critical bug fixes. These are less common as it is preferred to stick to quarterly releases. -Between quarterly releases, ".dev0" is appended to the "master" branch, indicating that +Between quarterly releases, ``.dev0`` is appended to the ``main`` branch, indicating that this is not a formally released copy. diff --git a/docs/resources/css/dark.css b/docs/resources/css/dark.css index cc213d674..8866c07ea 100644 --- a/docs/resources/css/dark.css +++ b/docs/resources/css/dark.css @@ -1275,7 +1275,7 @@ .wy-body-for-nav { background-image: initial; - background-color: rgb(26, 28, 29); + background-color: rgb(24, 26, 27); } .wy-nav-side { @@ -1333,11 +1333,6 @@ color: rgb(152, 143, 129); } - .wy-body-for-nav { - background-image: initial; - background-color: rgb(26, 28, 29); - } - @media screen and (min-width: 1100px) { .wy-nav-content-wrap { background-image: initial; diff --git a/docs/resources/css/styles.css b/docs/resources/css/styles.css new file mode 100644 index 000000000..111f84085 --- /dev/null +++ b/docs/resources/css/styles.css @@ -0,0 +1,8 @@ +th p { + margin-bottom: 0; +} + +.rst-content tr .line-block { + font-size: 1rem; + margin-bottom: 0; +} diff --git a/requirements.txt b/requirements.txt index 4b534ae53..feaa2f718 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ black check-manifest coverage +defusedxml markdown2 olefile packaging @@ -10,6 +11,8 @@ pytest pytest-cov pytest-timeout sphinx>=2.4 +sphinx-copybutton sphinx-issues sphinx-removed-in -sphinx-rtd-theme +sphinx-rtd-theme>=1.0 +sphinxext-opengraph diff --git a/selftest.py b/selftest.py index 7e08d183b..4ebd7cc00 100755 --- a/selftest.py +++ b/selftest.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # minimal sanity check import sys @@ -14,17 +14,13 @@ except AttributeError: pass -def _info(im): - im.load() - return im.format, im.mode, im.size - - def testimage(): """ PIL lets you create in-memory images with various pixel types: >>> from PIL import Image, ImageDraw, ImageFilter, ImageMath >>> im = Image.new("1", (128, 128)) # monochrome + >>> def _info(im): return (im.format, im.mode, im.size) >>> _info(im) (None, '1', (128, 128)) >>> _info(Image.new("L", (128, 128))) # grayscale (luminance) diff --git a/setup.cfg b/setup.cfg index 129adeee7..c3b5a3197 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,40 @@ +[metadata] +name = Pillow +description = Python Imaging Library (Fork) +long_description = file: README.md +long_description_content_type = text/markdown +url = https://python-pillow.org +author = Alex Clark (PIL Fork Author) +author_email = aclark@python-pillow.org +license = HPND +classifiers = + Development Status :: 6 - Mature + 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 :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Topic :: Multimedia :: Graphics + Topic :: Multimedia :: Graphics :: Capture :: Digital Camera + Topic :: Multimedia :: Graphics :: Capture :: Screen Capture + Topic :: Multimedia :: Graphics :: Graphics Conversion + Topic :: Multimedia :: Graphics :: Viewers +keywords = Imaging +project_urls = + Documentation=https://pillow.readthedocs.io + Source=https://github.com/python-pillow/Pillow + Funding=https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi + Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html + Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst + Twitter=https://twitter.com/PythonPillow + +[options] +python_requires = >=3.7 + [flake8] extend-ignore = E203 max-line-length = 88 diff --git a/setup.py b/setup.py index 6c4840c75..23d91a5f2 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # > pyroma . # ------------------------------ # Checking . @@ -26,7 +26,6 @@ def get_version(): return locals()["__version__"] -NAME = "Pillow" PILLOW_VERSION = get_version() FREETYPE_ROOT = None HARFBUZZ_ROOT = None @@ -39,7 +38,7 @@ TIFF_ROOT = None ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 10): +if sys.platform == "win32" and sys.version_info >= (3, 11): import atexit atexit.register( @@ -186,7 +185,7 @@ def _find_library_dirs_ldconfig(): return [] [data, _] = p.communicate() if isinstance(data, bytes): - data = data.decode() + data = data.decode("latin1") dirs = [] for dll in re.findall(expr, data): @@ -405,6 +404,27 @@ class pil_build_ext(build_ext): self.extensions.remove(extension) break + def get_macos_sdk_path(self): + try: + sdk_path = ( + subprocess.check_output(["xcrun", "--show-sdk-path"]) + .strip() + .decode("latin1") + ) + except Exception: + sdk_path = None + if ( + not sdk_path + or sdk_path == "/Applications/Xcode.app/Contents/Developer" + "/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" + ): + commandlinetools_sdk_path = ( + "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" + ) + if os.path.exists(commandlinetools_sdk_path): + sdk_path = commandlinetools_sdk_path + return sdk_path + def build_extensions(self): library_dirs = [] @@ -532,15 +552,7 @@ class pil_build_ext(build_ext): _add_directory(library_dirs, "/usr/X11/lib") _add_directory(include_dirs, "/usr/X11/include") - # SDK install path - try: - sdk_path = ( - subprocess.check_output(["xcrun", "--show-sdk-path"]) - .strip() - .decode("latin1") - ) - except Exception: - sdk_path = None + sdk_path = self.get_macos_sdk_path() if sdk_path: _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include")) @@ -559,7 +571,11 @@ class pil_build_ext(build_ext): # headers are at $PREFIX/include # user libs are at $PREFIX/lib _add_directory( - library_dirs, os.path.join(os.environ["ANDROID_ROOT"], "lib") + library_dirs, + os.path.join( + os.environ["ANDROID_ROOT"], + "lib" if struct.calcsize("l") == 4 else "lib64", + ), ) elif sys.platform.startswith("netbsd"): @@ -810,9 +826,11 @@ class pil_build_ext(build_ext): if feature.tiff: libs.append(feature.tiff) defs.append(("HAVE_LIBTIFF", None)) - # FIXME the following define should be detected automatically - # based on system libtiff, see #4237 - if PLATFORM_MINGW: + 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) @@ -880,7 +898,7 @@ class pil_build_ext(build_ext): else: self._remove_extension("PIL._webp") - tk_libs = ["psapi"] if sys.platform == "win32" else [] + tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] self._update_extension("PIL._imagingtk", tk_libs) build_ext.build_extensions(self) @@ -967,54 +985,14 @@ ext_modules = [ Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] -with open("README.md") as f: - long_description = f.read() - try: setup( - name=NAME, version=PILLOW_VERSION, - description="Python Imaging Library (Fork)", - long_description=long_description, - long_description_content_type="text/markdown", - license="HPND", - author="Alex Clark (PIL Fork Author)", - author_email="aclark@python-pillow.org", - url="https://python-pillow.org", - project_urls={ - "Documentation": "https://pillow.readthedocs.io", - "Source": "https://github.com/python-pillow/Pillow", - "Funding": "https://tidelift.com/subscription/pkg/pypi-pillow?" - "utm_source=pypi-pillow&utm_medium=pypi", - "Release notes": "https://pillow.readthedocs.io/en/stable/releasenotes/" - "index.html", - "Changelog": "https://github.com/python-pillow/Pillow/blob/master/" - "CHANGES.rst", - }, - classifiers=[ - "Development Status :: 6 - Mature", - "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", # noqa: E501 - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Multimedia :: Graphics", - "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", - "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - "Topic :: Multimedia :: Graphics :: Viewers", - ], - python_requires=">=3.6", cmdclass={"build_ext": pil_build_ext}, ext_modules=ext_modules, include_package_data=True, packages=["PIL"], package_dir={"": "src"}, - keywords=["Imaging"], zip_safe=not (debug_build() or PLATFORM_MINGW), ) except RequiredDependencyException as err: diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 88aae80eb..7b78597b4 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -286,33 +286,36 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): raise OSError("Truncated Blp file") from e return 0, 0 + def _safe_read(self, length): + return ImageFile._safe_read(self.fd, length) + def _read_palette(self): ret = [] for i in range(256): try: - b, g, r, a = struct.unpack("<4B", self.fd.read(4)) + b, g, r, a = struct.unpack("<4B", self._safe_read(4)) except struct.error: break ret.append((b, g, r, a)) return ret def _read_blp_header(self): - (self._blp_compression,) = struct.unpack("= 52: for idx, mask in enumerate( @@ -160,6 +158,8 @@ class BmpImageFile(ImageFile.ImageFile): if file_info.get("colors", 0) else (1 << file_info["bits"]) ) + if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: + offset += 4 * file_info["colors"] # ---------------------- Check bit depth for unusual unsupported values self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) @@ -259,7 +259,7 @@ class BmpImageFile(ImageFile.ImageFile): ] def _open(self): - """ Open file, check magic number and read header """ + """Open file, check magic number and read header""" # read 14 bytes: magic number, filesize, reserved, header final offset head_data = self.fp.read(14) # choke if the file does not have the required magic bytes diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index df2d0060c..260924fca 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -14,6 +14,7 @@ import struct from io import BytesIO from . import Image, ImageFile +from ._binary import o32le as o32 # Magic ("DDS ") DDS_MAGIC = 0x20534444 @@ -97,6 +98,9 @@ DXT5_FOURCC = 0x35545844 DXGI_FORMAT_R8G8B8A8_TYPELESS = 27 DXGI_FORMAT_R8G8B8A8_UNORM = 28 DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 +DXGI_FORMAT_BC5_TYPELESS = 82 +DXGI_FORMAT_BC5_UNORM = 83 +DXGI_FORMAT_BC5_SNORM = 84 DXGI_FORMAT_BC7_TYPELESS = 97 DXGI_FORMAT_BC7_UNORM = 98 DXGI_FORMAT_BC7_UNORM_SRGB = 99 @@ -127,15 +131,17 @@ class DdsImageFile(ImageFile.ImageFile): fourcc = header.read(4) (bitcount,) = struct.unpack("i", file_length)) - if fp_only: - with open(filename, "rb") as f: - fp.write(f.read()) + # TOC + fp.write(b"TOC ") + fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) + for entry in entries: + fp.write(entry["type"]) + fp.write(struct.pack(">i", entry["size"])) + + # Data + for entry in entries: + fp.write(entry["type"]) + fp.write(struct.pack(">i", entry["size"])) + fp.write(entry["stream"]) + + if hasattr(fp, "flush"): + fp.flush() -Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns") +def _accept(prefix): + return prefix[:4] == MAGIC + + +Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) Image.register_extension(IcnsImageFile.format, ".icns") -if sys.platform == "darwin": - Image.register_save(IcnsImageFile.format, _save) - - Image.register_mime(IcnsImageFile.format, "image/icns") - +Image.register_save(IcnsImageFile.format, _save) +Image.register_mime(IcnsImageFile.format, "image/icns") if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python IcnsImagePlugin.py [file]") + print("Syntax: python3 IcnsImagePlugin.py [file]") sys.exit() with open(sys.argv[1], "rb") as fp: diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 5634bf8e9..d9ff9b5e7 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -30,6 +30,7 @@ from math import ceil, log from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from ._binary import i16le as i16 from ._binary import i32le as i32 +from ._binary import o32le as o32 # # -------------------------------------------------------------------- @@ -53,6 +54,7 @@ def _save(im, fp, filename): sizes = list(sizes) fp.write(struct.pack(". diff --git a/src/PIL/Image.py b/src/PIL/Image.py index be5efb953..0bafd3907 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -31,58 +31,38 @@ import logging import math import numbers import os +import re import struct import sys import tempfile import warnings -import xml.etree.ElementTree from collections.abc import Callable, MutableMapping from pathlib import Path +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + # VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION is deprecated and will be removed in a future release. +# PILLOW_VERSION was removed in Pillow 9.0.0. # Use __version__ instead. -from . import ( - ImageMode, - TiffTags, - UnidentifiedImageError, - __version__, - _plugins, - _raise_version_warning, -) +from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins from ._binary import i32le from ._util import deferred_error, isPath -if sys.version_info >= (3, 7): - def __getattr__(name): - if name == "PILLOW_VERSION": - _raise_version_warning() - return __version__ - else: - categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} - if name in categories: - warnings.warn( - "Image categories are deprecated and will be removed in Pillow 10 " - "(2023-01-02). Use is_animated instead.", - DeprecationWarning, - stacklevel=2, - ) - return categories[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -else: - - from . import PILLOW_VERSION - - # Silence warning - assert PILLOW_VERSION - - # categories - NORMAL = 0 - SEQUENCE = 1 - CONTAINER = 2 +def __getattr__(name): + categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} + if name in categories: + warnings.warn( + "Image categories are deprecated and will be removed in Pillow 10 " + "(2023-07-01). Use is_animated instead.", + DeprecationWarning, + stacklevel=2, + ) + return categories[name] + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") logger = logging.getLogger(__name__) @@ -158,8 +138,6 @@ def isImageType(t): # # Constants -NONE = 0 - # transpose FLIP_LEFT_RIGHT = 0 FLIP_TOP_BOTTOM = 1 @@ -223,28 +201,7 @@ DECODERS = {} ENCODERS = {} # -------------------------------------------------------------------- -# Modes supported by this version - -_MODEINFO = { - # NOTE: this table will be removed in future versions. use - # getmode* functions or ImageMode descriptors instead. - # official modes - "1": ("L", "L", ("1",)), - "L": ("L", "L", ("L",)), - "I": ("L", "I", ("I",)), - "F": ("L", "F", ("F",)), - "P": ("P", "L", ("P",)), - "RGB": ("RGB", "L", ("R", "G", "B")), - "RGBX": ("RGB", "L", ("R", "G", "B", "X")), - "RGBA": ("RGB", "L", ("R", "G", "B", "A")), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), - "LAB": ("RGB", "L", ("L", "A", "B")), - "HSV": ("RGB", "L", ("H", "S", "V")), - # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and - # BGR;24. Use these modes only if you know exactly what you're - # doing... -} +# Modes if sys.byteorder == "little": _ENDIAN = "<" @@ -290,7 +247,7 @@ def _conv_type_shape(im): return (im.size[1], im.size[0], extra), typ -MODES = sorted(_MODEINFO) +MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"] # raw modes that may be memory mapped. NOTE: if you change this, you # may have to modify the stride calculation in map.c too! @@ -554,7 +511,7 @@ class Image: if name == "category": warnings.warn( "Image categories are deprecated and will be removed in Pillow 10 " - "(2023-01-02). Use is_animated instead.", + "(2023-07-01). Use is_animated instead.", DeprecationWarning, stacklevel=2, ) @@ -670,7 +627,6 @@ class Image: and self.size == other.size and self.info == other.info and self._category == other._category - and self.readonly == other.readonly and self.getpalette() == other.getpalette() and self.tobytes() == other.tobytes() ) @@ -697,9 +653,14 @@ class Image: raise ValueError("Could not save to PNG for display") from e return b.getvalue() - @property - def __array_interface__(self): + class _ArrayData: + def __init__(self, new): + self.__array_interface__ = new + + def __array__(self, dtype=None): # numpy array interface support + import numpy as np + new = {} shape, typestr = _conv_type_shape(self) new["shape"] = shape @@ -711,7 +672,8 @@ class Image: new["data"] = self.tobytes("raw", "L") else: new["data"] = self.tobytes() - return new + + return np.array(self._ArrayData(new), dtype) def __getstate__(self): return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] @@ -846,10 +808,10 @@ class Image: arr = bytes( value for (index, value) in enumerate(arr) if index % 4 != 3 ) - self.im.putpalette(mode, arr) + palette_length = self.im.putpalette(mode, arr) self.palette.dirty = 0 self.palette.rawmode = None - if "transparency" in self.info: + if "transparency" in self.info and mode in ("LA", "PA"): if isinstance(self.info["transparency"], int): self.im.putpalettealpha(self.info["transparency"], 0) else: @@ -857,6 +819,7 @@ class Image: self.palette.mode = "RGBA" else: self.palette.mode = "RGB" + self.palette.palette = self.im.getpalette()[: palette_length * 3] if self.im: if cffi and USE_CFFI_ACCESS: @@ -924,16 +887,18 @@ class Image: self.load() + has_transparency = self.info.get("transparency") is not None if not mode and self.mode == "P": # determine default mode if self.palette: mode = self.palette.mode else: mode = "RGB" + if mode == "RGB" and has_transparency: + mode = "RGBA" if not mode or (mode == self.mode and not matrix): return self.copy() - has_transparency = self.info.get("transparency") is not None if matrix: # matrix conversion if mode not in ("L", "RGB"): @@ -951,12 +916,8 @@ class Image: transparency = convert_transparency(matrix, transparency) elif len(mode) == 3: transparency = tuple( - [ - convert_transparency( - matrix[i * 4 : i * 4 + 4], transparency - ) - for i in range(0, len(transparency)) - ] + convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) + for i in range(0, len(transparency)) ) new.info["transparency"] = transparency return new @@ -992,23 +953,30 @@ class Image: if self.mode == "P": trns_im.putpalette(self.palette) if isinstance(t, tuple): + err = "Couldn't allocate a palette color for transparency" try: - t = trns_im.palette.getcolor(t) - except Exception as e: - raise ValueError( - "Couldn't allocate a palette color for transparency" - ) from e - trns_im.putpixel((0, 0), t) - - if mode in ("L", "RGB"): - trns_im = trns_im.convert(mode) + t = trns_im.palette.getcolor(t, self) + except ValueError as e: + if str(e) == "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + t = None + else: + raise ValueError(err) from e + if t is None: + trns = None else: - # can't just retrieve the palette number, got to do it - # after quantization. - trns_im = trns_im.convert("RGB") - trns = trns_im.getpixel((0, 0)) + trns_im.putpixel((0, 0), t) - elif self.mode == "P" and mode == "RGBA": + if mode in ("L", "RGB"): + trns_im = trns_im.convert(mode) + else: + # can't just retrieve the palette number, got to do it + # after quantization. + trns_im = trns_im.convert("RGB") + trns = trns_im.getpixel((0, 0)) + + elif self.mode == "P" and mode in ("LA", "PA", "RGBA"): t = self.info["transparency"] delete_trns = True @@ -1024,14 +992,14 @@ class Image: new = self._new(im) from . import ImagePalette - new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB")) + new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB")) if delete_trns: # This could possibly happen if we requantize to fewer colors. # The transparency would be totally off in that case. del new.info["transparency"] if trns is not None: try: - new.info["transparency"] = new.palette.getcolor(trns) + new.info["transparency"] = new.palette.getcolor(trns, new) except Exception: # if we can't make a transparent color, don't leave the old # transparency hanging around to mess us up. @@ -1054,16 +1022,25 @@ class Image: raise ValueError("illegal conversion") from e new_im = self._new(im) + if mode == "P" and palette != ADAPTIVE: + from . import ImagePalette + + new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) if delete_trns: # crash fail if we leave a bytes transparency in an rgb/l mode. del new_im.info["transparency"] if trns is not None: if new_im.mode == "P": try: - new_im.info["transparency"] = new_im.palette.getcolor(trns) - except Exception: + new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) + except ValueError as e: del new_im.info["transparency"] - warnings.warn("Couldn't allocate palette entry for transparency") + if str(e) != "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + warnings.warn( + "Couldn't allocate palette entry for transparency" + ) else: new_im.info["transparency"] = trns return new_im @@ -1122,14 +1099,17 @@ class Image: "only RGB or L mode images can be quantized to a palette" ) im = self.im.convert("P", dither, palette.im) - return self._new(im) + new_im = self._new(im) + new_im.palette = palette.palette.copy() + return new_im im = self._new(self.im.quantize(colors, method, kmeans)) from . import ImagePalette mode = im.im.getpalettemode() - im.palette = ImagePalette.ImagePalette(mode, im.im.getpalette(mode, mode)) + palette = im.im.getpalette(mode, mode)[: colors * len(mode)] + im.palette = ImagePalette.ImagePalette(mode, palette) return im @@ -1333,30 +1313,60 @@ class Image: return tuple(extrema) return self.im.getextrema() + def _getxmp(self, xmp_tags): + def get_name(tag): + return tag.split("}")[1] + + def get_value(element): + value = {get_name(k): v for k, v in element.attrib.items()} + children = list(element) + if children: + for child in children: + name = get_name(child.tag) + child_value = get_value(child) + if name in value: + if not isinstance(value[name], list): + value[name] = [value[name]] + value[name].append(child_value) + else: + value[name] = child_value + elif value: + if element.text: + value["text"] = element.text + else: + return element.text + return value + + if ElementTree is None: + warnings.warn("XMP data cannot be read without defusedxml dependency") + return {} + else: + root = ElementTree.fromstring(xmp_tags) + return {get_name(root.tag): get_value(root)} + def getexif(self): if self._exif is None: self._exif = Exif() exif_info = self.info.get("exif") - if exif_info is None and "Raw profile type exif" in self.info: - exif_info = bytes.fromhex( - "".join(self.info["Raw profile type exif"].split("\n")[3:]) - ) - self._exif.load(exif_info) + if exif_info is None: + if "Raw profile type exif" in self.info: + exif_info = bytes.fromhex( + "".join(self.info["Raw profile type exif"].split("\n")[3:]) + ) + elif hasattr(self, "tag_v2"): + self._exif.endian = self.tag_v2._endian + self._exif.load_from_fp(self.fp, self.tag_v2._offset) + if exif_info is not None: + self._exif.load(exif_info) # XMP tags if 0x0112 not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") if xmp_tags: - root = xml.etree.ElementTree.fromstring(xmp_tags) - for elem in root.iter(): - if elem.tag.endswith("}Description"): - orientation = elem.attrib.get( - "{http://ns.adobe.com/tiff/1.0/}Orientation" - ) - if orientation: - self._exif[0x0112] = int(orientation) - break + match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags) + if match: + self._exif[0x0112] = int(match[1]) return self._exif @@ -1695,13 +1705,14 @@ class Image: def putdata(self, data, scale=1.0, offset=0.0): """ - Copies pixel data to this image. This method copies data from a - sequence object into the image, starting at the upper left - corner (0, 0), and continuing until either the image or the - sequence ends. The scale and offset values are used to adjust - the sequence values: **pixel = value*scale + offset**. + Copies pixel data from a flattened sequence object into the image. The + values should start at the upper left corner (0, 0), continue to the + end of the line, followed directly by the first value of the second + line, and so on. Data will be read until either the image or the + sequence ends. The scale and offset values are used to adjust the + sequence values: **pixel = value*scale + offset**. - :param data: A sequence object. + :param data: A flattened sequence object. :param scale: An optional scale value. The default is 1.0. :param offset: An optional offset value. The default is 0.0. """ @@ -1715,20 +1726,24 @@ class Image: Attaches a palette to this image. The image must be a "P", "PA", "L" or "LA" image. - The palette sequence must contain either 768 integer values, or 1024 - integer values if alpha is included. Each group of values represents - the red, green, blue (and alpha if included) values for the - corresponding pixel index. Instead of an integer sequence, you can use - an 8-bit string. + The palette sequence must contain at most 256 colors, made up of one + integer value for each channel in the raw mode. + For example, if the raw mode is "RGB", then it can contain at most 768 + values, made up of red, green and blue values for the corresponding pixel + index in the 256 colors. + If the raw mode is "RGBA", then it can contain at most 1024 values, + containing red, green, blue and alpha values. + + Alternatively, an 8-bit string may be used instead of an integer sequence. :param data: A palette sequence (either a list or a string). - :param rawmode: The raw mode of the palette. + :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a + mode that can be transformed to "RGB" (e.g. "R", "BGR;15", "RGBA;L"). """ from . import ImagePalette if self.mode not in ("L", "LA", "P", "PA"): raise ValueError("illegal image mode") - self.load() if isinstance(data, ImagePalette.ImagePalette): palette = ImagePalette.raw(data.rawmode, data.palette) else: @@ -1775,7 +1790,7 @@ class Image: and len(value) in [3, 4] ): # RGB or RGBA value for a P image - value = self.palette.getcolor(value) + value = self.palette.getcolor(value, self) return self.im.putpixel(xy, value) def remap_palette(self, dest_map, source_palette=None): @@ -1796,18 +1811,17 @@ class Image: if source_palette is None: if self.mode == "P": - real_source_palette = self.im.getpalette("RGB")[:768] + self.load() + source_palette = self.im.getpalette("RGB")[:768] else: # L-mode - real_source_palette = bytearray(i // 3 for i in range(768)) - else: - real_source_palette = source_palette + source_palette = bytearray(i // 3 for i in range(768)) palette_bytes = b"" new_positions = [0] * 256 # pick only the used colors from the palette for i, oldPosition in enumerate(dest_map): - palette_bytes += real_source_palette[oldPosition * 3 : oldPosition * 3 + 3] + palette_bytes += source_palette[oldPosition * 3 : oldPosition * 3 + 3] new_positions[oldPosition] = i # replace the palette color id of all pixel with the new id @@ -1833,23 +1847,19 @@ class Image: m_im = self.copy() m_im.mode = "P" - m_im.palette = ImagePalette.ImagePalette( - "RGB", palette=mapping_palette * 3, size=768 - ) + m_im.palette = ImagePalette.ImagePalette("RGB", palette=mapping_palette * 3) # possibly set palette dirty, then # m_im.putpalette(mapping_palette, 'L') # converts to 'P' # or just force it. # UNDONE -- this is part of the general issue with palettes - m_im.im.putpalette(*m_im.palette.getdata()) + m_im.im.putpalette("RGB;L", m_im.palette.tobytes()) m_im = m_im.convert("L") # Internally, we require 768 bytes for a palette. new_palette_bytes = palette_bytes + (768 - len(palette_bytes)) * b"\x00" m_im.putpalette(new_palette_bytes) - m_im.palette = ImagePalette.ImagePalette( - "RGB", palette=palette_bytes, size=len(palette_bytes) - ) + m_im.palette = ImagePalette.ImagePalette("RGB", palette=palette_bytes) return m_im @@ -1870,7 +1880,7 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=BICUBIC, box=None, reducing_gap=None): + def resize(self, size, resample=None, box=None, reducing_gap=None): """ Returns a resized copy of this image. @@ -1880,9 +1890,11 @@ class Image: one of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`, :py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`, :py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`. - Default filter is :py:data:`PIL.Image.BICUBIC`. - If the image has mode "1" or "P", it is - always set to :py:data:`PIL.Image.NEAREST`. + If the image has mode "1" or "P", it is always set to + :py:data:`PIL.Image.NEAREST`. + If the image mode specifies a number of bits, such as "I;16", then the + default filter is :py:data:`PIL.Image.NEAREST`. + Otherwise, the default filter is :py:data:`PIL.Image.BICUBIC`. See: :ref:`concept-filters`. :param box: An optional 4-tuple of floats providing the source image region to be scaled. @@ -1903,11 +1915,14 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ - if resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING): + if resample is None: + type_special = ";" in self.mode + resample = NEAREST if type_special else BICUBIC + elif resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING): message = f"Unknown resampling filter ({resample})." filters = [ - "{} ({})".format(filter[1], filter[0]) + f"{filter[1]} ({filter[0]})" for filter in ( (NEAREST, "Image.NEAREST"), (LANCZOS, "Image.LANCZOS"), @@ -1938,7 +1953,7 @@ class Image: resample = NEAREST if self.mode in ["LA", "RGBA"] and resample != NEAREST: - im = self.convert(self.mode[:-1] + "a") + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) im = im.resize(size, resample, box) return im.convert(self.mode) @@ -1988,7 +2003,7 @@ class Image: return self.copy() if self.mode in ["LA", "RGBA"]: - im = self.convert(self.mode[:-1] + "a") + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) im = im.reduce(factor, box) return im.convert(self.mode) @@ -2039,10 +2054,8 @@ class Image: return self.copy() if angle == 180: return self.transpose(ROTATE_180) - if angle == 90 and expand: - return self.transpose(ROTATE_90) - if angle == 270 and expand: - return self.transpose(ROTATE_270) + if angle in (90, 270) and (expand or self.width == self.height): + return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270) # Calculate the affine matrix. Note that this is the reverse # transformation (from destination image to source) because we @@ -2145,12 +2158,17 @@ class Image: filename = "" open_fp = False - if isPath(fp): - filename = fp - open_fp = True - elif isinstance(fp, Path): + if isinstance(fp, Path): filename = str(fp) open_fp = True + elif isPath(fp): + filename = fp + open_fp = True + elif fp == sys.stdout: + try: + fp = sys.stdout.buffer + except AttributeError: + pass if not filename and hasattr(fp, "name") and isPath(fp.name): # only set the name for metadata purposes filename = fp.name @@ -2217,7 +2235,7 @@ class Image: if frame != 0: raise EOFError - def show(self, title=None, command=None): + def show(self, title=None): """ Displays this image. This method is mainly intended for debugging purposes. @@ -2237,14 +2255,7 @@ class Image: :param title: Optional title to use for the image window, where possible. """ - if command is not None: - warnings.warn( - "The command parameter is deprecated and will be removed in Pillow 9 " - "(2022-01-02). Use a subclass of ImageShow.Viewer instead.", - DeprecationWarning, - ) - - _show(self, title=title, command=command) + _show(self, title=title) def split(self): """ @@ -2421,18 +2432,11 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ - if self.mode == "LA" and resample != NEAREST: + if self.mode in ("LA", "RGBA") and resample != NEAREST: return ( - self.convert("La") + self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) .transform(size, method, data, resample, fill, fillcolor) - .convert("LA") - ) - - if self.mode == "RGBA" and resample != NEAREST: - return ( - self.convert("RGBa") - .transform(size, method, data, resample, fill, fillcolor) - .convert("RGBA") + .convert(self.mode) ) if isinstance(method, ImageTransformHandler): @@ -2446,6 +2450,8 @@ class Image: raise ValueError("missing method data") im = new(self.mode, size, fillcolor) + if self.mode == "P" and self.palette: + im.palette = self.palette.copy() im.info = self.info.copy() if method == MESH: # list of quads @@ -2511,7 +2517,7 @@ class Image: message = f"Unknown resampling filter ({resample})." filters = [ - "{} ({})".format(filter[1], filter[0]) + f"{filter[1]} ({filter[0]})" for filter in ( (NEAREST, "Image.NEAREST"), (BILINEAR, "Image.BILINEAR"), @@ -2766,7 +2772,7 @@ def fromarray(obj, mode=None): from PIL import Image import numpy as np - im = Image.open('hopper.jpg') + im = Image.open("hopper.jpg") a = np.asarray(im) Then this can be used to convert it to a Pillow image:: @@ -2774,8 +2780,21 @@ def fromarray(obj, mode=None): im = Image.fromarray(a) :param obj: Object with array interface - :param mode: Mode to use (will be determined from type if None) - See: :ref:`concept-modes`. + :param mode: Optional mode to use when reading ``obj``. Will be determined from + type if ``None``. + + This will not be used to convert the data after reading, but will be used to + change how the data is read:: + + from PIL import Image + import numpy as np + a = np.full((1, 1), 300) + im = Image.fromarray(a, mode="L") + im.getpixel((0, 0)) # 44 + im = Image.fromarray(a, mode="RGB") + im.getpixel((0, 0)) # (44, 1, 0) + + See: :ref:`concept-modes` for general information about modes. :returns: An image object. .. versionadded:: 1.1.6 @@ -2898,7 +2917,7 @@ def open(fp, mode="r", formats=None): :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. Pass ``None`` to try all supported formats. You can print the set of - available formats by running ``python -m PIL`` or using + available formats by running ``python3 -m PIL`` or using the :py:func:`PIL.features.pilinfo` function. :returns: An :py:class:`~PIL.Image.Image` object. :exception FileNotFoundError: If the file cannot be found. @@ -3208,22 +3227,9 @@ def register_encoder(name, encoder): def _show(image, **options): - options["_internal_pillow"] = True - _showxv(image, **options) - - -def _showxv(image, title=None, **options): from . import ImageShow - if "_internal_pillow" in options: - del options["_internal_pillow"] - else: - warnings.warn( - "_showxv is deprecated and will be removed in Pillow 9 (2022-01-02). " - "Use Image.show instead.", - DeprecationWarning, - ) - ImageShow.show(image, title, **options) + ImageShow.show(image, **options) # -------------------------------------------------------------------- @@ -3313,7 +3319,7 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): - endian = "<" + endian = None def __init__(self): self._data = {} @@ -3348,6 +3354,12 @@ class Exif(MutableMapping): info.load(self.fp) return self._fixup_dict(info) + def _get_head(self): + if self.endian == "<": + return b"II\x2A\x00\x08\x00\x00\x00" + else: + return b"MM\x00\x2A\x00\x00\x00\x08" + def load(self, data): # Extract EXIF information. This is highly experimental, # and is likely to be replaced with something better in a future @@ -3360,8 +3372,8 @@ class Exif(MutableMapping): self._loaded_exif = data self._data.clear() self._ifds.clear() - self._info = None if not data: + self._info = None return if data.startswith(b"Exif\x00\x00"): @@ -3376,6 +3388,27 @@ class Exif(MutableMapping): self.fp.seek(self._info.next) self._info.load(self.fp) + def load_from_fp(self, fp, offset=None): + self._loaded_exif = None + self._data.clear() + self._ifds.clear() + + # process dictionary + from . import TiffImagePlugin + + self.fp = fp + if offset is not None: + self.head = self._get_head() + else: + self.head = self.fp.read(8) + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + if self.endian is None: + self.endian = self._info._endian + if offset is None: + offset = self._info.next + self.fp.seek(offset) + self._info.load(self.fp) + def _get_merged_dict(self): merged_dict = dict(self) @@ -3394,10 +3427,7 @@ class Exif(MutableMapping): def tobytes(self, offset=8): from . import TiffImagePlugin - if self.endian == "<": - head = b"II\x2A\x00\x08\x00\x00\x00" - else: - head = b"MM\x00\x2A\x00\x00\x00\x08" + head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) for tag, value in self.items(): if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict): diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 8c4740ddc..60e700f09 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -34,10 +34,10 @@ pyCMS a Python / PIL interface to the littleCMS ICC Color Management System Copyright (C) 2002-2003 Kevin Cazabon kevin@cazabon.com - http://www.cazabon.com + https://www.cazabon.com - pyCMS home page: http://www.cazabon.com/pyCMS - littleCMS home page: http://www.littlecms.com + pyCMS home page: https://www.cazabon.com/pyCMS + littleCMS home page: https://www.littlecms.com (littleCMS is Copyright (C) 1998-2001 Marti Maria) Originally released under LGPL. Graciously donated to PIL in diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 51df44040..25f92f2c7 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -32,6 +32,8 @@ def getrgb(color): :param color: A color string :return: ``(red, green, blue[, alpha])`` """ + if len(color) > 100: + raise ValueError("color specifier is too long") color = color.lower() rgb = colormap.get(color, None) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 8988e4233..610ccd4c7 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -33,7 +33,7 @@ import math import numbers -from . import Image, ImageColor +from . import Image, ImageColor, ImageFont """ A simple 2D drawing interface for PIL images. @@ -70,6 +70,7 @@ class ImageDraw: self.palette = im.palette else: self.palette = None + self._image = im self.im = im.im self.draw = Image.core.draw(self.im, blend) self.mode = mode @@ -108,13 +109,13 @@ class ImageDraw: if isinstance(ink, str): ink = ImageColor.getcolor(ink, self.mode) if self.palette and not isinstance(ink, numbers.Number): - ink = self.palette.getcolor(ink) + ink = self.palette.getcolor(ink, self._image) ink = self.draw.draw_ink(ink) if fill is not None: if isinstance(fill, str): fill = ImageColor.getcolor(fill, self.mode) if self.palette and not isinstance(fill, numbers.Number): - fill = self.palette.getcolor(fill) + fill = self.palette.getcolor(fill, self._image) fill = self.draw.draw_ink(fill) return ink, fill @@ -173,13 +174,11 @@ class ImageDraw: angle -= 90 distance = width / 2 - 1 return tuple( - [ - p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) - for p, p_d in ( - (x, distance * math.cos(math.radians(angle))), - (y, distance * math.sin(math.radians(angle))), - ) - ] + p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) + for p, p_d in ( + (x, distance * math.cos(math.radians(angle))), + (y, distance * math.sin(math.radians(angle))), + ) ) flipped = ( @@ -234,13 +233,35 @@ class ImageDraw: if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None): + def polygon(self, xy, fill=None, outline=None, width=1): """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_polygon(xy, fill, 1) - if ink is not None and ink != fill: - self.draw.draw_polygon(xy, ink, 0) + if ink is not None and ink != fill and width != 0: + if width == 1: + self.draw.draw_polygon(xy, ink, 0, width) + else: + # To avoid expanding the polygon outwards, + # use the fill as a mask + mask = Image.new("1", self.im.size) + mask_ink = self._getink(1)[0] + + fill_im = mask.copy() + draw = Draw(fill_im) + draw.draw.draw_polygon(xy, mask_ink, 1) + + ink_im = mask.copy() + draw = Draw(ink_im) + width = width * 2 - 1 + draw.draw.draw_polygon(xy, mask_ink, 0, width) + + mask.paste(ink_im, mask=fill_im) + + im = Image.new(self.mode, self.im.size) + draw = Draw(im) + draw.draw.draw_polygon(xy, ink, 0, width) + self.im.paste(im.im, (0, 0) + im.size, mask.im) def regular_polygon( self, bounding_circle, n_sides, rotation=0, fill=None, outline=None @@ -282,6 +303,7 @@ class ImageDraw: # If the corners have no curve, that is a rectangle return self.rectangle(xy, fill, outline, width) + r = d // 2 ink, fill = self._getink(outline, fill) def draw_corners(pieslice): @@ -315,36 +337,28 @@ class ImageDraw: draw_corners(True) if full_x: - self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 - ) + self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1) else: - self.draw.draw_rectangle( - (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y1), fill, 1 - ) + 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 + d / 2 + 1, x0 + d / 2, y1 - d / 2 - 1), fill, 1 - ) - self.draw.draw_rectangle( - (x1 - d / 2, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 - ) + 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) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: self.draw.draw_rectangle( - (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y0 + width - 1), ink, 1 + (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 ) self.draw.draw_rectangle( - (x0 + d / 2 + 1, y1 - width + 1, x1 - d / 2 - 1, y1), ink, 1 + (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 ) if not full_y: self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x0 + width - 1, y1 - d / 2 - 1), ink, 1 + (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 ) self.draw.draw_rectangle( - (x1 - width + 1, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), ink, 1 + (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 ) def _multiline_check(self, text): @@ -653,6 +667,8 @@ class ImageDraw: if font is None: font = self.getfont() + if not isinstance(font, ImageFont.FreeTypeFont): + raise ValueError("Only supported for TrueType fonts") mode = "RGBA" if embedded_color else self.fontmode bbox = font.getbbox( text, mode, direction, features, language, stroke_width, anchor @@ -983,6 +999,6 @@ def _color_diff(color1, color2): Uses 1-norm distance to calculate difference between two values. """ if isinstance(color2, tuple): - return sum([abs(color1[i] - color2[i]) for i in range(0, len(color2))]) + return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2))) else: return abs(color1 - color2) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f58de95bd..3374a5b1d 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -28,9 +28,9 @@ # import io +import itertools import struct import sys -import warnings from . import Image from ._util import isPath @@ -67,15 +67,6 @@ def raise_oserror(error): raise OSError(message + " when reading image file") -def raise_ioerror(error): - warnings.warn( - "raise_ioerror is deprecated and will be removed in Pillow 9 (2022-01-02). " - "Use raise_oserror instead.", - DeprecationWarning, - ) - return raise_oserror(error) - - def _tilesort(t): # sort on offset return t[2] @@ -220,6 +211,13 @@ class ImageFile(Image.Image): except AttributeError: prefix = b"" + # Remove consecutive duplicates that only differ by their offset + self.tile = [ + list(tiles)[-1] + for _, tiles in itertools.groupby( + self.tile, lambda tile: (tile[0], tile[1], tile[3]) + ) + ] for decoder_name, extents, offset, args in self.tile: decoder = Image._getdecoder( self.mode, decoder_name, args, self.decoderconfig @@ -493,9 +491,6 @@ def _save(im, fp, tile, bufsize=0): # But, it would need at least the image size in most cases. RawEncode is # a tricky case. bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c - if fp == sys.stdout: - fp.flush() - return try: fh = fp.fileno() fp.flush() @@ -545,19 +540,28 @@ def _safe_read(fp, size): :param fp: File handle. Must implement a read method. :param size: Number of bytes to read. - :returns: A string containing up to size bytes of data. + :returns: A string containing size bytes of data. + + Raises an OSError if the file is truncated and the read cannot be completed + """ if size <= 0: return b"" if size <= SAFEBLOCK: - return fp.read(size) + data = fp.read(size) + if len(data) < size: + raise OSError("Truncated File Read") + return data data = [] - while size > 0: - block = fp.read(min(size, SAFEBLOCK)) + remaining_size = size + while remaining_size > 0: + block = fp.read(min(remaining_size, SAFEBLOCK)) if not block: break data.append(block) - size -= len(block) + remaining_size -= len(block) + if sum(len(d) for d in data) < size: + raise OSError("Truncated File Read") return b"".join(data) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 6800bc3a0..d2ece3752 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -149,9 +149,11 @@ class ModeFilter(Filter): class GaussianBlur(MultibandFilter): - """Gaussian blur filter. + """Blurs the image with a sequence of extended box filters, which + approximates a Gaussian kernel. For details on accuracy see + - :param radius: Blur radius. + :param radius: Standard deviation of the Gaussian kernel. """ name = "GaussianBlur" diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c48d89835..805c8fff9 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -28,10 +28,9 @@ import base64 import os import sys -import warnings from io import BytesIO -from . import Image, features +from . import Image from ._util import isDirectory, isPath LAYOUT_BASIC = 0 @@ -165,21 +164,6 @@ class FreeTypeFont: self.index = index self.encoding = encoding - try: - from packaging.version import parse as parse_version - except ImportError: - pass - else: - freetype_version = parse_version(features.version_module("freetype2")) - if freetype_version < parse_version("2.8"): - warnings.warn( - "Support for FreeType 2.7 is deprecated and will be removed" - " in Pillow 9 (2022-01-02). Please upgrade to FreeType 2.8 " - "or newer, preferably FreeType 2.10.4 which fixes " - "CVE-2020-15999.", - DeprecationWarning, - ) - if layout_engine not in (LAYOUT_BASIC, LAYOUT_RAQM): layout_engine = LAYOUT_BASIC if core.HAVE_RAQM: @@ -212,6 +196,13 @@ class FreeTypeFont: else: load_from_bytes(font) + def __getstate__(self): + return [self.path, self.size, self.index, self.encoding, self.layout_engine] + + def __setstate__(self, state): + 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) @@ -669,6 +660,7 @@ class FreeTypeFont: ) size = size[0] + stroke_width * 2, size[1] + stroke_width * 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( text, im.id, mode, direction, features, language, stroke_width, ink diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 7f9c88e14..06bea800d 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -246,7 +246,12 @@ def eval(expression, _dict={}, **kw): if hasattr(v, "im"): args[k] = _Operand(v) - out = builtins.eval(expression, args) + code = compile(expression, "", "eval") + for name in code.co_names: + if name not in args and name != "abs": + raise ValueError(f"'{name}' not allowed") + + out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) try: return out.im except AttributeError: diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 988288329..0afcf9fe1 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -35,18 +35,28 @@ def getmode(mode): global _modes if not _modes: # initialize mode cache - - from . import Image - modes = {} - # core modes - for m, (basemode, basetype, bands) in Image._MODEINFO.items(): + for m, (basemode, basetype, bands) in { + # core modes + "1": ("L", "L", ("1",)), + "L": ("L", "L", ("L",)), + "I": ("L", "I", ("I",)), + "F": ("L", "F", ("F",)), + "P": ("P", "L", ("P",)), + "RGB": ("RGB", "L", ("R", "G", "B")), + "RGBX": ("RGB", "L", ("R", "G", "B", "X")), + "RGBA": ("RGB", "L", ("R", "G", "B", "A")), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), + "LAB": ("RGB", "L", ("L", "A", "B")), + "HSV": ("RGB", "L", ("H", "S", "V")), + # extra experimental modes + "RGBa": ("RGB", "L", ("R", "G", "B", "a")), + "LA": ("L", "L", ("L", "A")), + "La": ("L", "L", ("L", "a")), + "PA": ("RGB", "L", ("P", "A")), + }.items(): modes[m] = ModeDescriptor(m, bands, basemode, basetype) - # extra experimental modes - modes["RGBa"] = ModeDescriptor("RGBa", ("R", "G", "B", "a"), "RGB", "L") - modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L") - modes["La"] = ModeDescriptor("La", ("L", "a"), "L", "L") - modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L") # mapping modes for i16mode in ( "I;16", diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index b76dfa01f..fe0083754 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -196,7 +196,7 @@ class MorphOp: raise Exception("No operator loaded") if image.mode != "L": - raise Exception("Image must be binary, meaning it must use mode L") + raise ValueError("Image mode must be L") outimage = Image.new(image.mode, image.size, None) count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage @@ -211,7 +211,7 @@ class MorphOp: raise Exception("No operator loaded") if image.mode != "L": - raise Exception("Image must be binary, meaning it must use mode L") + raise ValueError("Image mode must be L") return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image): @@ -221,7 +221,7 @@ class MorphOp: of all matching pixels. See :ref:`coordinate-system`.""" if image.mode != "L": - raise Exception("Image must be binary, meaning it must use mode L") + raise ValueError("Image mode must be L") return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename): diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 14602a5c8..b170e9d8c 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -19,6 +19,7 @@ import functools import operator +import re from . import Image @@ -61,7 +62,7 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None, mask=None): +def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): """ Maximize (normalize) image contrast. This function calculates a histogram of the input image (or mask region), removes ``cutoff`` percent of the @@ -77,9 +78,17 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None): :param mask: Histogram used in contrast operation is computed using pixels within the mask. If no mask is given the entire image is used for histogram computation. + :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast. + + .. versionadded:: 8.2.0 + :return: An image. """ - histogram = image.histogram(mask) + if preserve_tone: + histogram = image.convert("L").histogram(mask) + else: + histogram = image.histogram(mask) + lut = [] for layer in range(0, len(histogram), 256): h = histogram[layer : layer + 256] @@ -228,15 +237,43 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi return _lut(image, red + green + blue) -def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)): +def contain(image, size, method=Image.BICUBIC): """ - Returns a sized and padded version of the image, expanded to fill the - requested aspect ratio and size. + Returns a resized version of the image, set to the maximum width and height + within the requested size, while maintaining the original aspect ratio. - :param image: The image to size and crop. + :param image: The image to resize and crop. :param size: The requested output size in pixels, given as a (width, height) tuple. - :param method: What resampling method to use. Default is + :param method: Resampling method to use. Default is + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :return: An image. + """ + + im_ratio = image.width / image.height + dest_ratio = size[0] / size[1] + + if im_ratio != dest_ratio: + if im_ratio > dest_ratio: + new_height = int(image.height / image.width * size[0]) + if new_height != size[1]: + size = (size[0], new_height) + else: + new_width = int(image.width / image.height * size[1]) + if new_width != size[0]: + size = (new_width, size[1]) + return image.resize(size, resample=method) + + +def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)): + """ + Returns a resized and padded version of the image, expanded to fill the + requested aspect ratio and size. + + :param image: The image to resize and crop. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :param color: The background color of the padded image. :param centering: Control the position of the original image within the @@ -249,27 +286,17 @@ def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)): :return: An image. """ - im_ratio = image.width / image.height - dest_ratio = size[0] / size[1] - - if im_ratio == dest_ratio: - out = image.resize(size, resample=method) + resized = contain(image, size, method) + if resized.size == size: + out = resized else: out = Image.new(image.mode, size, color) - if im_ratio > dest_ratio: - new_height = int(image.height / image.width * size[0]) - if new_height != size[1]: - image = image.resize((size[0], new_height), resample=method) - - y = int((size[1] - new_height) * max(0, min(centering[1], 1))) - out.paste(image, (0, y)) + if resized.width != size[0]: + x = int((size[0] - resized.width) * max(0, min(centering[0], 1))) + out.paste(resized, (x, 0)) else: - new_width = int(image.width / image.height * size[1]) - if new_width != size[0]: - image = image.resize((new_width, size[1]), resample=method) - - x = int((size[0] - new_width) * max(0, min(centering[0], 1))) - out.paste(image, (x, 0)) + y = int((size[1] - resized.height) * max(0, min(centering[1], 1))) + out.paste(resized, (0, y)) return out @@ -296,7 +323,7 @@ def scale(image, factor, resample=Image.BICUBIC): :param image: The image to rescale. :param factor: The expansion factor, as a float. - :param resample: What resampling method to use. Default is + :param resample: Resampling method to use. Default is :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ @@ -366,22 +393,32 @@ def expand(image, border=0, fill=0): left, top, right, bottom = _border(border) width = left + image.size[0] + right height = top + image.size[1] + bottom - out = Image.new(image.mode, (width, height), _color(fill, image.mode)) + color = _color(fill, image.mode) + if image.mode == "P" and image.palette: + image.load() + palette = image.palette.copy() + if isinstance(color, tuple): + color = palette.getcolor(color) + else: + palette = None + out = Image.new(image.mode, (width, height), color) + if palette: + out.putpalette(palette.palette) out.paste(image, (left, top)) return out def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): """ - Returns a sized and cropped version of the image, cropped to the + Returns a resized and cropped version of the image, cropped to the requested aspect ratio and size. This function was contributed by Kevin Cazabon. - :param image: The image to size and crop. + :param image: The image to resize and crop. :param size: The requested output size in pixels, given as a (width, height) tuple. - :param method: What resampling method to use. Default is + :param method: Resampling method to use. Default is :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :param bleed: Remove a border around the outside of the image from all four edges. The value is a decimal percentage (use 0.01 for @@ -402,7 +439,7 @@ def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): # by Kevin Cazabon, Feb 17/2000 # kevin@cazabon.com - # http://www.cazabon.com + # https://www.cazabon.com # ensure centering is mutable centering = list(centering) @@ -552,7 +589,20 @@ def exif_transpose(image): }.get(orientation) if method is not None: transposed_image = image.transpose(method) - del exif[0x0112] - transposed_image.info["exif"] = exif.tobytes() + 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: + transposed_image.info["XML:com.adobe.xmp"] = re.sub( + r'tiff:Orientation="([0-9])"', + "", + transposed_image.info["XML:com.adobe.xmp"], + ) return transposed_image return image.copy() diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index d0604112f..1e0d36b41 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -17,6 +17,7 @@ # import array +import warnings from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile @@ -25,27 +26,45 @@ class ImagePalette: """ Color palette for palette mapped images - :param mode: The mode to use for the Palette. See: + :param mode: The mode to use for the palette. See: :ref:`concept-modes`. Defaults to "RGB" :param palette: An optional palette. If given, it must be a bytearray, - an array or a list of ints between 0-255 and of length ``size`` - times the number of colors in ``mode``. The list must be aligned - by channel (All R values must be contiguous in the list before G - and B values.) Defaults to 0 through 255 per channel. - :param size: An optional palette size. If given, it cannot be equal to - or greater than 256. Defaults to 0. + an array or a list of ints between 0-255. The list must consist of + all channels for one color followed by the next color (e.g. RGBRGBRGB). + Defaults to an empty palette. + :param size: An optional palette size. If given, an error is raised + if ``palette`` is not of equal length. """ def __init__(self, mode="RGB", palette=None, size=0): self.mode = mode self.rawmode = None # if set, palette contains raw data - self.palette = palette or bytearray(range(256)) * len(self.mode) - self.colors = {} + self.palette = palette or bytearray() self.dirty = None - if (size == 0 and len(self.mode) * 256 != len(self.palette)) or ( - size != 0 and size != len(self.palette) - ): - raise ValueError("wrong palette size") + if size != 0: + warnings.warn( + "The size parameter is deprecated and will be removed in Pillow 10 " + "(2023-07-01).", + DeprecationWarning, + ) + if size != len(self.palette): + raise ValueError("wrong palette size") + + @property + def palette(self): + return self._palette + + @palette.setter + def palette(self, palette): + self._palette = palette + + mode_len = len(self.mode) + self.colors = {} + for i in range(0, len(self.palette), mode_len): + color = tuple(self.palette[i : i + mode_len]) + if color in self.colors: + continue + self.colors[color] = i // mode_len def copy(self): new = ImagePalette() @@ -54,7 +73,6 @@ class ImagePalette: new.rawmode = self.rawmode if self.palette is not None: new.palette = self.palette[:] - new.colors = self.colors.copy() new.dirty = self.dirty return new @@ -68,7 +86,7 @@ class ImagePalette: """ if self.rawmode: return self.rawmode, self.palette - return self.mode + ";L", self.tobytes() + return self.mode, self.tobytes() def tobytes(self): """Convert palette to bytes. @@ -80,14 +98,12 @@ class ImagePalette: if isinstance(self.palette, bytes): return self.palette arr = array.array("B", self.palette) - if hasattr(arr, "tobytes"): - return arr.tobytes() - return arr.tostring() + return arr.tobytes() # Declare tostring as an alias for tobytes tostring = tobytes - def getcolor(self, color): + def getcolor(self, color, image=None): """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. @@ -95,19 +111,45 @@ class ImagePalette: if self.rawmode: raise ValueError("palette contains raw palette data") if isinstance(color, tuple): + if self.mode == "RGB": + if len(color) == 4 and color[3] == 255: + color = color[:3] + elif self.mode == "RGBA": + if len(color) == 3: + color += (255,) try: return self.colors[color] except KeyError as e: # allocate new color slot - if isinstance(self.palette, bytes): - self.palette = bytearray(self.palette) - index = len(self.colors) + if not isinstance(self.palette, bytearray): + self._palette = bytearray(self.palette) + index = len(self.palette) // 3 + special_colors = () + if image: + special_colors = ( + image.info.get("background"), + image.info.get("transparency"), + ) + while index in special_colors: + index += 1 if index >= 256: - raise ValueError("cannot allocate more than 256 colors") from e + if image: + # Search for an unused index + for i, count in reversed(list(enumerate(image.histogram()))): + if count == 0 and i not in special_colors: + index = i + break + if index >= 256: + raise ValueError("cannot allocate more than 256 colors") from e self.colors[color] = index - self.palette[index] = color[0] - self.palette[index + 256] = color[1] - self.palette[index + 512] = color[2] + if index * 3 < len(self.palette): + self._palette = ( + self.palette[: index * 3] + + bytes(color) + + self.palette[index * 3 + 3 :] + ) + else: + self._palette += bytes(color) self.dirty = 1 return index else: @@ -169,9 +211,9 @@ def make_gamma_lut(exp): def negative(mode="RGB"): - palette = list(range(256)) + palette = list(range(256 * len(mode))) palette.reverse() - return ImagePalette(mode, palette * len(mode)) + return ImagePalette(mode, [i // len(mode) for i in palette]) def random(mode="RGB"): @@ -184,15 +226,13 @@ def random(mode="RGB"): def sepia(white="#fff0c0"): - r, g, b = ImageColor.getrgb(white) - r = make_linear_lut(0, r) - g = make_linear_lut(0, g) - b = make_linear_lut(0, b) - return ImagePalette("RGB", r + g + b) + bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] + return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) def wedge(mode="RGB"): - return ImagePalette(mode, list(range(256)) * len(mode)) + palette = list(range(256 * len(mode))) + return ImagePalette(mode, [i // len(mode) for i in palette]) def load(filename): diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 32630f2ca..db8fa0fa9 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -66,7 +66,13 @@ def fromqimage(im): :param im: QImage or PIL ImageQt object """ buffer = QBuffer() - qt_openmode = QIODevice.OpenMode if qt_version == "6" else QIODevice + if qt_version == "6": + try: + qt_openmode = QIODevice.OpenModeFlag + except AttributeError: + qt_openmode = QIODevice.OpenMode + else: + qt_openmode = QIODevice buffer.open(qt_openmode.ReadWrite) # preserve alpha channel with png # otherwise ppm is more friendly with Image.open @@ -102,7 +108,7 @@ def align8to32(bytes, width, mode): converts each scanline of data from 8 bit to 32 bit aligned """ - bits_per_pixel = {"1": 1, "L": 8, "P": 8}[mode] + bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode] # calculate bytes per line and the extra padding if needed bits_per_line = bits_per_pixel * width @@ -161,6 +167,10 @@ def _toqclass_helper(im): elif im.mode == "RGBA": data = im.tobytes("raw", "BGRA") format = qt_format.Format_ARGB32 + elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + im = im.point(lambda i: i * 256) + + format = qt_format.Format_Grayscale16 else: if exclusive_fp: im.close() diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 3368865a4..2135293e5 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -133,7 +133,7 @@ if sys.platform == "win32": class MacViewer(Viewer): - """The default viewer on MacOS using ``Preview.app``.""" + """The default viewer on macOS using ``Preview.app``.""" format = "PNG" options = {"compress_level": 1} @@ -186,11 +186,35 @@ class UnixViewer(Viewer): return 1 -class DisplayViewer(UnixViewer): - """The ImageMagick ``display`` command.""" +class XDGViewer(UnixViewer): + """ + The freedesktop.org ``xdg-open`` command. + """ def get_command_ex(self, file, **options): + command = executable = "xdg-open" + return command, executable + + +class DisplayViewer(UnixViewer): + """ + The ImageMagick ``display`` command. + This viewer supports the ``title`` parameter. + """ + + def get_command_ex(self, file, title=None, **options): command = executable = "display" + if title: + command += f" -name {quote(title)}" + return command, executable + + +class GmDisplayViewer(UnixViewer): + """The GraphicsMagick ``gm display`` command.""" + + def get_command_ex(self, file, **options): + executable = "gm" + command = "gm display" return command, executable @@ -198,7 +222,8 @@ class EogViewer(UnixViewer): """The GNOME Image Viewer ``eog`` command.""" def get_command_ex(self, file, **options): - command = executable = "eog" + executable = "eog" + command = "eog -n" return command, executable @@ -218,8 +243,12 @@ class XVViewer(UnixViewer): if sys.platform not in ("win32", "darwin"): # unixoids + if shutil.which("xdg-open"): + register(XDGViewer) if shutil.which("display"): register(DisplayViewer) + if shutil.which("gm"): + register(GmDisplayViewer) if shutil.which("eog"): register(EogViewer) if shutil.which("xv"): @@ -245,7 +274,7 @@ else: if __name__ == "__main__": if len(sys.argv) < 2: - print("Syntax: python ImageShow.py imagefile [title]") + print("Syntax: python3 ImageShow.py imagefile [title]") sys.exit() with Image.open(sys.argv[1]) as im: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 0b0d433db..cc7980278 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -6,6 +6,7 @@ # # History: # 2014-03-12 ajh Created +# 2021-06-30 rogermb Extract dpi information from the 'resc' header box # # Copyright (c) 2014 Coriolis Systems Limited # Copyright (c) 2014 Alastair Houghton @@ -19,6 +20,79 @@ import struct from . import Image, ImageFile +class BoxReader: + """ + A small helper class to read fields stored in JPEG2000 header boxes + and to easily step into and read sub-boxes. + """ + + def __init__(self, fp, length=-1): + self.fp = fp + self.has_length = length >= 0 + self.length = length + self.remaining_in_box = -1 + + def _can_read(self, num_bytes): + if self.has_length and self.fp.tell() + num_bytes > self.length: + # Outside box: ensure we don't read past the known file length + return False + if self.remaining_in_box >= 0: + # Inside box contents: ensure read does not go past box boundaries + return num_bytes <= self.remaining_in_box + else: + return True # No length known, just read + + def _read_bytes(self, num_bytes): + if not self._can_read(num_bytes): + raise SyntaxError("Not enough data in header") + + data = self.fp.read(num_bytes) + if len(data) < num_bytes: + raise OSError( + f"Expected to read {num_bytes} bytes but only got {len(data)}." + ) + + if self.remaining_in_box > 0: + self.remaining_in_box -= num_bytes + return data + + def read_fields(self, field_format): + size = struct.calcsize(field_format) + data = self._read_bytes(size) + return struct.unpack(field_format, data) + + def read_boxes(self): + size = self.remaining_in_box + data = self._read_bytes(size) + return BoxReader(io.BytesIO(data), size) + + def has_next_box(self): + if self.has_length: + return self.fp.tell() + self.remaining_in_box < self.length + else: + return True + + def next_box_type(self): + # Skip the rest of the box if it has not been read + if self.remaining_in_box > 0: + self.fp.seek(self.remaining_in_box, os.SEEK_CUR) + self.remaining_in_box = -1 + + # Read the length and type of the next box + lbox, tbox = self.read_fields(">I4s") + if lbox == 1: + lbox = self.read_fields(">Q")[0] + hlen = 16 + else: + hlen = 8 + + if lbox < hlen or not self._can_read(lbox - hlen): + raise SyntaxError("Invalid header length") + + self.remaining_in_box = lbox - hlen + return tbox + + def _parse_codestream(fp): """Parse the JPEG 2000 codestream to extract the size and component count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" @@ -53,101 +127,71 @@ def _parse_codestream(fp): return (size, mode) +def _res_to_dpi(num, denom, exp): + """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, + calculated as (num / denom) * 10^exp and stored in dots per meter, + to floating-point dots per inch.""" + if denom != 0: + return (254 * num * (10 ** exp)) / (10000 * denom) + + def _parse_jp2_header(fp): - """Parse the JP2 header box to extract size, component count and - color space information, returning a (size, mode, mimetype) tuple.""" + """Parse the JP2 header box to extract size, component count, + color space information, and optionally DPI information, + returning a (size, mode, mimetype, dpi) tuple.""" # Find the JP2 header box + reader = BoxReader(fp) header = None mimetype = None - while True: - lbox, tbox = struct.unpack(">I4s", fp.read(8)) - if lbox == 1: - lbox = struct.unpack(">Q", fp.read(8))[0] - hlen = 16 - else: - hlen = 8 - - if lbox < hlen: - raise SyntaxError("Invalid JP2 header length") + while reader.has_next_box(): + tbox = reader.next_box_type() if tbox == b"jp2h": - header = fp.read(lbox - hlen) + header = reader.read_boxes() break elif tbox == b"ftyp": - if fp.read(4) == b"jpx ": + if reader.read_fields(">4s")[0] == b"jpx ": mimetype = "image/jpx" - fp.seek(lbox - hlen - 4, os.SEEK_CUR) - else: - fp.seek(lbox - hlen, os.SEEK_CUR) - - if header is None: - raise SyntaxError("could not find JP2 header") size = None mode = None bpc = None nc = None + dpi = None # 2-tuple of DPI info, or None - hio = io.BytesIO(header) - while True: - lbox, tbox = struct.unpack(">I4s", hio.read(8)) - if lbox == 1: - lbox = struct.unpack(">Q", hio.read(8))[0] - hlen = 16 - else: - hlen = 8 - - content = hio.read(lbox - hlen) + while header.has_next_box(): + tbox = header.next_box_type() if tbox == b"ihdr": - height, width, nc, bpc, c, unkc, ipr = struct.unpack(">IIHBBBB", content) + height, width, nc, bpc = header.read_fields(">IIHB") size = (width, height) - if unkc: - if nc == 1 and (bpc & 0x7F) > 8: - mode = "I;16" - elif nc == 1: - mode = "L" - elif nc == 2: - mode = "LA" - elif nc == 3: - mode = "RGB" - elif nc == 4: - mode = "RGBA" - break - elif tbox == b"colr": - meth, prec, approx = struct.unpack_from(">BBB", content) - if meth == 1: - cs = struct.unpack_from(">I", content, 3)[0] - if cs == 16: # sRGB - if nc == 1 and (bpc & 0x7F) > 8: - mode = "I;16" - elif nc == 1: - mode = "L" - elif nc == 3: - mode = "RGB" - elif nc == 4: - mode = "RGBA" - break - elif cs == 17: # grayscale - if nc == 1 and (bpc & 0x7F) > 8: - mode = "I;16" - elif nc == 1: - mode = "L" - elif nc == 2: - mode = "LA" - break - elif cs == 18: # sYCC - if nc == 3: - mode = "RGB" - elif nc == 4: - mode = "RGBA" + if nc == 1 and (bpc & 0x7F) > 8: + mode = "I;16" + elif nc == 1: + mode = "L" + elif nc == 2: + mode = "LA" + elif nc == 3: + mode = "RGB" + elif nc == 4: + mode = "RGBA" + elif tbox == b"res ": + res = header.read_boxes() + while res.has_next_box(): + tres = res.next_box_type() + if tres == b"resc": + vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") + hres = _res_to_dpi(hrcn, hrcd, hrce) + vres = _res_to_dpi(vrcn, vrcd, vrce) + if hres is not None and vres is not None: + dpi = (hres, vres) break if size is None or mode is None: - raise SyntaxError("Malformed jp2 header") + raise SyntaxError("Malformed JP2 header") - return (size, mode, mimetype) + return (size, mode, mimetype, dpi) ## @@ -169,7 +213,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile): 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 = header + self._size, self.mode, self.custom_mimetype, dpi = header + if dpi is not None: + self.info["dpi"] = dpi else: raise SyntaxError("not a JPEG 2000 file") diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index ad260acbd..ccdcc20a8 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -33,6 +33,7 @@ # import array import io +import math import os import struct import subprocess @@ -139,8 +140,8 @@ def APP(self, marker): self.info["adobe"] = i16(s, 5) # extract Adobe custom properties try: - adobe_transform = s[1] - except Exception: + adobe_transform = s[11] + except IndexError: pass else: self.info["adobe_transform"] = adobe_transform @@ -161,15 +162,17 @@ def APP(self, marker): dpi = float(x_resolution[0]) / x_resolution[1] except TypeError: dpi = x_resolution + if math.isnan(dpi): + raise ValueError if resolution_unit == 3: # cm # 1 dpcm = 2.54 dpi dpi *= 2.54 - self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5) - except (KeyError, SyntaxError, ValueError, ZeroDivisionError): + self.info["dpi"] = dpi, dpi + except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError): # SyntaxError for invalid/unreadable EXIF # KeyError for dpi not included # ZeroDivisionError for invalid dpi rational value - # ValueError for x_resolution[0] being an invalid float + # ValueError or TypeError for dpi being an invalid float self.info["dpi"] = 72, 72 @@ -251,7 +254,7 @@ def DQT(self, marker): data = array.array("B" if precision == 1 else "H", s[1:qt_length]) if sys.byteorder == "little" and precision > 1: data.byteswap() # the values are always big-endian - self.quantization[v & 15] = data + self.quantization[v & 15] = [data[i] for i in zigzag_index] s = s[qt_length:] @@ -398,9 +401,10 @@ class JpegImageFile(ImageFile.ImageFile): """ s = self.fp.read(read_bytes) - if not s and ImageFile.LOAD_TRUNCATED_IMAGES: + if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): # Premature EOF. # Pretend file is finished adding EOI marker + self._ended = True return b"\xFF\xD9" return s @@ -474,6 +478,20 @@ class JpegImageFile(ImageFile.ImageFile): def _getmp(self): return _getmp(self) + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + :returns: XMP tags in a dictionary. + """ + + for segment, content in self.applist: + if segment == "APP1": + marker, xmp_tags = content.rsplit(b"\x00", 1) + if marker == b"http://ns.adobe.com/xap/1.0/": + return self._getxmp(xmp_tags) + return {} + def _getexif(self): if "exif" not in self.info: @@ -584,9 +602,11 @@ samplings = { def convert_dict_qtables(qtables): - qtables = [qtables[key] for key in range(len(qtables)) if key in qtables] - for idx, table in enumerate(qtables): - qtables[idx] = [table[i] for i in zigzag_index] + warnings.warn( + "convert_dict_qtables is deprecated and will be removed in Pillow 10" + "(2023-07-01). Conversion is no longer needed.", + DeprecationWarning, + ) return qtables @@ -667,7 +687,9 @@ def _save(im, fp, filename): qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] if isinstance(qtables, (tuple, list, dict)): if isinstance(qtables, dict): - qtables = convert_dict_qtables(qtables) + qtables = [ + qtables[key] for key in range(len(qtables)) if key in qtables + ] elif isinstance(qtables, tuple): qtables = list(qtables) if not (0 < len(qtables) < 5): diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 79d10ebb2..e5a5d178a 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -52,19 +52,11 @@ You can get the quantization tables of a JPEG with:: im.quantization -This will return a dict with a number of arrays. You can pass this dict +This will return a dict with a number of lists. You can pass this dict directly as the qtables argument when saving a JPEG. -The tables format between im.quantization and quantization in presets differ in -3 ways: - -1. The base container of the preset is a list with sublists instead of dict. - dict[0] -> list[0], dict[1] -> list[1], ... -2. Each table in a preset is a list instead of an array. -3. The zigzag order is remove in the preset (needed by libjpeg >= 6a). - -You can convert the dict format to the preset format with the -:func:`.JpegImagePlugin.convert_dict_qtables()` function. +The quantization table format in presets is a list with sublists. These formats +are interchangeable. Libjpeg ref.: https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 7244aa2a9..7ccf27c42 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -21,9 +21,8 @@ from . import Image, ImageFile, JpegImagePlugin from ._binary import i16be as i16 - -def _accept(prefix): - return JpegImagePlugin._accept(prefix) +# def _accept(prefix): +# return JpegImagePlugin._accept(prefix) def _save(im, fp, filename): diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index e1fdc1fdf..32b28d44d 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -21,7 +21,7 @@ # Figure 205. Windows Paint Version 1: "DanM" Format # Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03 # -# See also: http://www.fileformat.info/format/mspaint/egff.htm +# See also: https://www.fileformat.info/format/mspaint/egff.htm import io import struct @@ -73,7 +73,7 @@ class MspImageFile(ImageFile.ImageFile): class MspDecoder(ImageFile.PyDecoder): # The algo for the MSP decoder is from - # http://www.fileformat.info/format/mspaint/egff.htm + # https://www.fileformat.info/format/mspaint/egff.htm # cc-by-attribution -- That page references is taken from the # Encyclopedia of Graphics File Formats and is licensed by # O'Reilly under the Creative Common/Attribution license diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index c1bd933d3..743c35f01 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -26,39 +26,36 @@ from . import EpsImagePlugin class PSDraw: """ Sets up printing to the given file. If ``fp`` is omitted, - :py:data:`sys.stdout` is assumed. + ``sys.stdout.buffer`` or ``sys.stdout`` is assumed. """ def __init__(self, fp=None): if not fp: - fp = sys.stdout + try: + fp = sys.stdout.buffer + except AttributeError: + fp = sys.stdout self.fp = fp - def _fp_write(self, to_write): - if self.fp == sys.stdout: - self.fp.write(to_write) - else: - self.fp.write(bytes(to_write, "UTF-8")) - def begin_document(self, id=None): """Set up printing of a document. (Write PostScript DSC header.)""" # FIXME: incomplete - self._fp_write( - "%!PS-Adobe-3.0\n" - "save\n" - "/showpage { } def\n" - "%%EndComments\n" - "%%BeginDocument\n" + self.fp.write( + b"%!PS-Adobe-3.0\n" + b"save\n" + b"/showpage { } def\n" + b"%%EndComments\n" + b"%%BeginDocument\n" ) - # self._fp_write(ERROR_PS) # debugging! - self._fp_write(EDROFF_PS) - self._fp_write(VDI_PS) - self._fp_write("%%EndProlog\n") + # self.fp.write(ERROR_PS) # debugging! + self.fp.write(EDROFF_PS) + self.fp.write(VDI_PS) + self.fp.write(b"%%EndProlog\n") self.isofont = {} def end_document(self): """Ends printing. (Write PostScript DSC footer.)""" - self._fp_write("%%EndDocument\nrestore showpage\n%%End\n") + self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") if hasattr(self.fp, "flush"): self.fp.flush() @@ -69,12 +66,13 @@ class PSDraw: :param font: A PostScript font name :param size: Size in points. """ + font = bytes(font, "UTF-8") if font not in self.isofont: # reencode font - self._fp_write(f"/PSDraw-{font} ISOLatin1Encoding /{font} E\n") + self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font)) self.isofont[font] = 1 # rough - self._fp_write(f"/F0 {size} /PSDraw-{font} F\n") + self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font)) def line(self, xy0, xy1): """ @@ -82,7 +80,7 @@ class PSDraw: PostScript point coordinates (72 points per inch, (0, 0) is the lower left corner of the page). """ - self._fp_write("%d %d %d %d Vl\n" % (*xy0, *xy1)) + self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) def rectangle(self, box): """ @@ -97,16 +95,18 @@ class PSDraw: %d %d M %d %d 0 Vr\n """ - self._fp_write("%d %d M %d %d 0 Vr\n" % box) + self.fp.write(b"%d %d M %d %d 0 Vr\n" % box) def text(self, xy, text): """ Draws text at the given position. You must use :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. """ - text = "\\(".join(text.split("(")) - text = "\\)".join(text.split(")")) - self._fp_write(f"{xy[0]} {xy[1]} M ({text}) S\n") + text = bytes(text, "UTF-8") + text = b"\\(".join(text.split(b"(")) + text = b"\\)".join(text.split(b")")) + xy += (text,) + self.fp.write(b"%d %d M (%s) S\n" % xy) def image(self, box, im, dpi=None): """Draw a PIL image, centered in the given box.""" @@ -130,14 +130,14 @@ class PSDraw: y = ymax dx = (xmax - x) / 2 + box[0] dy = (ymax - y) / 2 + box[1] - self._fp_write(f"gsave\n{dx:f} {dy:f} translate\n") + self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy)) if (x, y) != im.size: # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) sx = x / im.size[0] sy = y / im.size[1] - self._fp_write(f"{sx:f} {sy:f} scale\n") + self.fp.write(b"%f %f scale\n" % (sx, sy)) EpsImagePlugin._save(im, self.fp, None, 0) - self._fp_write("\ngrestore\n") + self.fp.write(b"\ngrestore\n") # -------------------------------------------------------------------- @@ -153,7 +153,7 @@ class PSDraw: # -EDROFF_PS = """\ +EDROFF_PS = b"""\ /S { show } bind def /P { moveto show } bind def /M { moveto } bind def @@ -182,7 +182,7 @@ EDROFF_PS = """\ # Copyright (c) Fredrik Lundh 1994. # -VDI_PS = """\ +VDI_PS = b"""\ /Vm { moveto } bind def /Va { newpath arcn stroke } bind def /Vl { moveto lineto stroke } bind def @@ -207,7 +207,7 @@ VDI_PS = """\ # 89-11-21 fl: created (pslist 1.10) # -ERROR_PS = """\ +ERROR_PS = b"""\ /landscape false def /errorBUF 200 string def /errorNL { currentpoint 10 sub exch pop 72 exch moveto } def diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 36c8fb849..1131c6325 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -124,7 +124,7 @@ def _save(im, fp, filename, save_all=False): decode = None if im.mode == "1": - filter = "ASCIIHexDecode" + filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale bits = 1 @@ -135,7 +135,7 @@ def _save(im, fp, filename, save_all=False): procset = "ImageB" # grayscale elif im.mode == "P": filter = "ASCIIHexDecode" - palette = im.im.getpalette("RGB") + palette = im.getpalette() colorspace = [ PdfParser.PdfName("Indexed"), PdfParser.PdfName("DeviceRGB"), @@ -161,12 +161,6 @@ def _save(im, fp, filename, save_all=False): op = io.BytesIO() if filter == "ASCIIHexDecode": - if bits == 1: - # FIXME: the hex encoder doesn't support packed 1-bit - # images; do things the hard way... - data = im.tobytes("raw", "1") - im = Image.new("L", im.size) - im.putdata(data) ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) @@ -208,8 +202,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - int(width * 72.0 / resolution), - int(height * 72.0 / resolution), + width * 72.0 / resolution, + height * 72.0 / resolution, ], Contents=contents_refs[pageNumber], ) @@ -217,9 +211,9 @@ def _save(im, fp, filename, save_all=False): # # page contents - page_contents = b"q %d 0 0 %d 0 0 cm /image Do Q\n" % ( - int(width * 72.0 / resolution), - int(height * 72.0 / resolution), + page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( + width * 72.0 / resolution, + height * 72.0 / resolution, ) existing_pdf.write_obj(contents_refs[pageNumber], stream=page_contents) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 86d78a95c..6ac9c7a7c 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -330,6 +330,8 @@ def pdf_repr(x): return bytes(x) elif isinstance(x, int): return str(x).encode("us-ascii") + elif isinstance(x, float): + return str(x).encode("us-ascii") elif isinstance(x, time.struct_time): return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" elif isinstance(x, dict): @@ -423,7 +425,7 @@ class PdfParser: self.f.write(b"%PDF-1.4\n") def write_comment(self, s): - self.f.write(f"% {s}\n".encode("utf-8")) + self.f.write(f"% {s}\n".encode()) def write_catalog(self): self.del_root() @@ -580,7 +582,8 @@ class PdfParser: whitespace_or_hex = br"[\000\011\012\014\015\0400-9a-fA-F]" whitespace_optional = whitespace + b"*" whitespace_mandatory = whitespace + b"+" - whitespace_optional_no_nl = br"[\000\011\014\015\040]*" # no "\012" aka "\n" + # No "\012" aka "\n" or "\015" aka "\r": + whitespace_optional_no_nl = br"[\000\011\014\040]*" newline_only = br"[\r\n]+" newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl re_trailer_end = re.compile( @@ -860,7 +863,7 @@ class PdfParser: if m: # filter out whitespace hex_string = bytearray( - [b for b in m.group(1) if b in b"0123456789abcdefABCDEF"] + b for b in m.group(1) if b in b"0123456789abcdefABCDEF" ) if len(hex_string) % 2 == 1: # append a 0 if the length is not even - yes, at the end diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 07bbc5228..0f596f1fd 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -500,7 +500,7 @@ class PngStream(ChunkStream): px, py = i32(s, 0), i32(s, 4) unit = s[8] if unit == 1: # meter - dpi = int(px * 0.0254 + 0.5), int(py * 0.0254 + 0.5) + dpi = px * 0.0254, py * 0.0254 self.im_info["dpi"] = dpi elif unit == 0: self.im_info["aspect"] = px, py @@ -920,6 +920,8 @@ class PngImageFile(ImageFile.ImageFile): def load_end(self): """internal: finished reading image data""" + if self.__idat != 0: + self.fp.read(self.__idat) while True: self.fp.read(4) # CRC @@ -976,6 +978,18 @@ class PngImageFile(ImageFile.ImageFile): return super().getexif() + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + :returns: XMP tags in a dictionary. + """ + return ( + self._getxmp(self.info["XML:com.adobe.xmp"]) + if "XML:com.adobe.xmp" in self.info + else {} + ) + def _close__fp(self): try: if self.__fp != self.fp: @@ -1047,8 +1061,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode): default_image = im.encoderinfo.get("default_image", im.info.get("default_image")) duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) - disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) - blend = im.encoderinfo.get("blend", im.info.get("blend")) + disposal = im.encoderinfo.get( + "disposal", im.info.get("disposal", APNG_DISPOSE_OP_NONE) + ) + blend = im.encoderinfo.get("blend", im.info.get("blend", APNG_BLEND_OP_SOURCE)) if default_image: chain = itertools.chain(im.encoderinfo.get("append_images", [])) @@ -1103,12 +1119,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode): and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") ): - duration = encoderinfo.get("duration", 0) - if duration: - if "duration" in previous["encoderinfo"]: - previous["encoderinfo"]["duration"] += duration - else: - previous["encoderinfo"]["duration"] = duration + if isinstance(duration, (list, tuple)): + previous["encoderinfo"]["duration"] += encoderinfo["duration"] continue else: bbox = None @@ -1135,9 +1147,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode): bbox = frame_data["bbox"] im_frame = im_frame.crop(bbox) size = im_frame.size - duration = int(round(frame_data["encoderinfo"].get("duration", 0))) - disposal = frame_data["encoderinfo"].get("disposal", APNG_DISPOSE_OP_NONE) - blend = frame_data["encoderinfo"].get("blend", APNG_BLEND_OP_SOURCE) + encoderinfo = frame_data["encoderinfo"] + frame_duration = int(round(encoderinfo.get("duration", duration))) + frame_disposal = encoderinfo.get("disposal", disposal) + frame_blend = encoderinfo.get("blend", blend) # frame control chunk( fp, @@ -1147,10 +1160,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode): o32(size[1]), # height o32(bbox[0]), # x_offset o32(bbox[1]), # y_offset - o16(duration), # delay_numerator + o16(frame_duration), # delay_numerator o16(1000), # delay_denominator - o8(disposal), # dispose_op - o8(blend), # blend_op + o8(frame_disposal), # dispose_op + o8(frame_blend), # blend_op ) seq_num += 1 # frame data diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index d3799edc3..04b21e3de 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette from ._binary import i8 from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._binary import si16be as si16 MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -119,7 +120,8 @@ class PsdImageFile(ImageFile.ImageFile): end = self.fp.tell() + size size = i32(read(4)) if size: - self.layers = _layerinfo(self.fp) + _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size)) + self.layers = _layerinfo(_layer_data, size) self.fp.seek(end) self.n_frames = len(self.layers) self.is_animated = self.n_frames > 1 @@ -171,11 +173,20 @@ class PsdImageFile(ImageFile.ImageFile): self.__fp = None -def _layerinfo(file): +def _layerinfo(fp, ct_bytes): # read layerinfo block layers = [] - read = file.read - for i in range(abs(i16(read(2)))): + + def read(size): + return ImageFile._safe_read(fp, size) + + ct = si16(read(2)) + + # sanity check + if ct_bytes < (abs(ct) * 20): + raise SyntaxError("Layer block too short for number of layers requested") + + for i in range(abs(ct)): # bounding box y0 = i32(read(4)) @@ -186,7 +197,8 @@ def _layerinfo(file): # image info info = [] mode = [] - types = list(range(i16(read(2)))) + ct_types = i16(read(2)) + types = list(range(ct_types)) if len(types) > 4: continue @@ -219,16 +231,16 @@ def _layerinfo(file): size = i32(read(4)) # length of the extra data field combined = 0 if size: - data_end = file.tell() + size + data_end = fp.tell() + size length = i32(read(4)) if length: - file.seek(length - 16, io.SEEK_CUR) + fp.seek(length - 16, io.SEEK_CUR) combined += length + 4 length = i32(read(4)) if length: - file.seek(length, io.SEEK_CUR) + fp.seek(length, io.SEEK_CUR) combined += length + 4 length = i8(read(1)) @@ -238,7 +250,7 @@ def _layerinfo(file): name = read(length).decode("latin-1", "replace") combined += length + 1 - file.seek(data_end) + fp.seek(data_end) layers.append((name, mode, (x0, y0, x1, y1))) # get tiles @@ -246,7 +258,7 @@ def _layerinfo(file): for name, mode, bbox in layers: tile = [] for m in mode: - t = _maketile(file, m, bbox, 1) + t = _maketile(fp, m, bbox, 1) if t: tile.extend(t) layers[i] = name, mode, bbox, tile diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 494f5f9f4..eeaa0ccc4 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -54,6 +54,7 @@ class PyAccess: self.image32 = ffi.cast("int **", vals["image32"]) self.image = ffi.cast("unsigned char **", vals["image"]) self.xsize, self.ysize = img.im.size + self._img = img # Keep pointer to im object to prevent dereferencing. self._im = img.im @@ -93,7 +94,7 @@ class PyAccess: and len(color) in [3, 4] ): # RGB or RGBA value for a P image - color = self._palette.getcolor(color) + color = self._palette.getcolor(color, self._img) return self.set_pixel(x, y, color) @@ -127,7 +128,7 @@ class PyAccess: class _PyAccess32_2(PyAccess): - """ PA, LA, stored in first and last bytes of a 32 bit word """ + """PA, LA, stored in first and last bytes of a 32 bit word""" def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) @@ -144,7 +145,7 @@ class _PyAccess32_2(PyAccess): class _PyAccess32_3(PyAccess): - """ RGB and friends, stored in the first three bytes of a 32 bit word """ + """RGB and friends, stored in the first three bytes of a 32 bit word""" def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) @@ -163,7 +164,7 @@ class _PyAccess32_3(PyAccess): class _PyAccess32_4(PyAccess): - """ RGBA etc, all 4 bytes of a 32 bit word """ + """RGBA etc, all 4 bytes of a 32 bit word""" def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) @@ -182,7 +183,7 @@ class _PyAccess32_4(PyAccess): class _PyAccess8(PyAccess): - """ 1, L, P, 8 bit images stored as uint8 """ + """1, L, P, 8 bit images stored as uint8""" def _post_init(self, *args, **kwargs): self.pixels = self.image8 @@ -200,7 +201,7 @@ class _PyAccess8(PyAccess): class _PyAccessI16_N(PyAccess): - """ I;16 access, native bitendian without conversion """ + """I;16 access, native bitendian without conversion""" def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("unsigned short **", self.image) @@ -218,7 +219,7 @@ class _PyAccessI16_N(PyAccess): class _PyAccessI16_L(PyAccess): - """ I;16L access, with conversion """ + """I;16L access, with conversion""" def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_I16 **", self.image) @@ -239,7 +240,7 @@ class _PyAccessI16_L(PyAccess): class _PyAccessI16_B(PyAccess): - """ I;16B access, with conversion """ + """I;16B access, with conversion""" def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_I16 **", self.image) @@ -260,7 +261,7 @@ class _PyAccessI16_B(PyAccess): class _PyAccessI32_N(PyAccess): - """ Signed Int32 access, native endian """ + """Signed Int32 access, native endian""" def _post_init(self, *args, **kwargs): self.pixels = self.image32 @@ -273,7 +274,7 @@ class _PyAccessI32_N(PyAccess): class _PyAccessI32_Swap(PyAccess): - """ I;32L/B access, with byteswapping conversion """ + """I;32L/B access, with byteswapping conversion""" def _post_init(self, *args, **kwargs): self.pixels = self.image32 @@ -292,7 +293,7 @@ class _PyAccessI32_Swap(PyAccess): class _PyAccessF(PyAccess): - """ 32 bit float access """ + """32 bit float access""" def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("float **", self.image32) diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index d0f7c9993..5f1ef6edc 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -193,7 +193,8 @@ def _save(im, fp, filename): for channel in im.split(): fp.write(channel.tobytes("raw", rawmode, 0, orientation)) - fp.close() + if hasattr(fp, "flush"): + fp.flush() class SGI16Decoder(ImageFile.PyDecoder): diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 819f2ed0a..062af9f98 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -296,7 +296,7 @@ Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": if len(sys.argv) < 2: - print("Syntax: python SpiderImagePlugin.py [infile] [outfile]") + print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") sys.exit() filename = sys.argv[1] diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 2b936d687..ed63da95f 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -93,9 +93,10 @@ class TgaImageFile(ImageFile.ImageFile): # orientation orientation = flags & 0x30 - if orientation == 0x20: + self._flip_horizontally = orientation in [0x10, 0x30] + if orientation in [0x20, 0x30]: orientation = 1 - elif not orientation: + elif orientation in [0, 0x10]: orientation = -1 else: raise SyntaxError("unknown TGA orientation") @@ -110,10 +111,10 @@ class TgaImageFile(ImageFile.ImageFile): if colormaptype: # read palette - start, size, mapdepth = i16(s, 3), i16(s, 5), i16(s, 7) + start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] if mapdepth == 16: self.palette = ImagePalette.raw( - "BGR;16", b"\0" * 2 * start + self.fp.read(2 * size) + "BGR;15", b"\0" * 2 * start + self.fp.read(2 * size) ) elif mapdepth == 24: self.palette = ImagePalette.raw( @@ -149,6 +150,10 @@ class TgaImageFile(ImageFile.ImageFile): except KeyError: pass # cannot decode + def load_end(self): + if self._flip_horizontally: + self.im = self.im.transpose(Image.FLIP_LEFT_RIGHT) + # # -------------------------------------------------------------------- diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 24821d130..5df5c4f4c 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -48,7 +48,7 @@ from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational -from . import Image, ImageFile, ImagePalette, TiffTags +from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import o8 from .TiffTags import TYPES @@ -58,6 +58,7 @@ logger = logging.getLogger(__name__) READ_LIBTIFF = False WRITE_LIBTIFF = False IFD_LEGACY_API = True +STRIP_SIZE = 65536 II = b"II" # little-endian (Intel style) MM = b"MM" # big-endian (Motorola style) @@ -88,11 +89,15 @@ DATE_TIME = 306 ARTIST = 315 PREDICTOR = 317 COLORMAP = 320 +TILEWIDTH = 322 +TILELENGTH = 323 TILEOFFSETS = 324 +TILEBYTECOUNTS = 325 SUBIFD = 330 EXTRASAMPLES = 338 SAMPLEFORMAT = 339 JPEGTABLES = 347 +YCBCRSUBSAMPLING = 530 REFERENCEBLACKWHITE = 532 COPYRIGHT = 33432 IPTC_NAA_CHUNK = 33723 # newsphoto properties @@ -354,9 +359,22 @@ class IFDRational(Rational): return self._val.__hash__() def __eq__(self, other): + val = self._val if isinstance(other, IFDRational): other = other._val - return self._val == other + if isinstance(other, float): + val = float(val) + return val == other + + def __getstate__(self): + return [self._val, self._numerator, self._denominator] + + def __setstate__(self, state): + IFDRational.__init__(self, 0) + _val, _numerator, _denominator = state + self._val = _val + self._numerator = _numerator + self._denominator = _denominator def _delegate(op): def delegate(self, *args): @@ -423,39 +441,45 @@ class ImageFileDirectory_v2(MutableMapping): Data Structures: - * self.tagtype = {} + * ``self.tagtype = {}`` - * Key: numerical tiff tag number + * Key: numerical TIFF tag number * Value: integer corresponding to the data type from - ~PIL.TiffTags.TYPES` + :py:data:`.TiffTags.TYPES` - .. versionadded:: 3.0.0 - """ + .. versionadded:: 3.0.0 - """ - Documentation: + 'Internal' data structures: - 'internal' data structures: - * self._tags_v2 = {} Key: numerical tiff tag number - Value: decoded data, as tuple for multiple values - * self._tagdata = {} Key: numerical tiff tag number - Value: undecoded byte string from file - * self._tags_v1 = {} Key: numerical tiff tag number - Value: decoded data in the v1 format + * ``self._tags_v2 = {}`` - Tags will be found in the private attributes self._tagdata, and in - self._tags_v2 once decoded. + * Key: numerical TIFF tag number + * Value: decoded data, as tuple for multiple values - Self.legacy_api is a value for internal use, and shouldn't be - changed from outside code. In cooperation with the - ImageFileDirectory_v1 class, if legacy_api is true, then decoded - tags will be populated into both _tags_v1 and _tags_v2. _Tags_v2 - will be used if this IFD is used in the TIFF save routine. Tags - should be read from tags_v1 if legacy_api == true. + * ``self._tagdata = {}`` + + * Key: numerical TIFF tag number + * Value: undecoded byte string from file + + * ``self._tags_v1 = {}`` + + * Key: numerical TIFF tag number + * Value: decoded data in the v1 format + + Tags will be found in the private attributes ``self._tagdata``, and in + ``self._tags_v2`` once decoded. + + ``self.legacy_api`` is a value for internal use, and shouldn't be changed + from outside code. In cooperation with + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api`` + is true, then decoded tags will be populated into both ``_tags_v1`` and + ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF + save routine. Tags should be read from ``_tags_v1`` if + ``legacy_api == true``. """ - def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): """Initialize an ImageFileDirectory. To construct an ImageFileDirectory from a real file, pass the 8-byte @@ -475,6 +499,7 @@ class ImageFileDirectory_v2(MutableMapping): self._endian = "<" else: raise SyntaxError("not a TIFF IFD") + self.group = group self.tagtype = {} """ Dictionary of tag types """ self.reset() @@ -506,7 +531,10 @@ class ImageFileDirectory_v2(MutableMapping): Returns the complete tag dictionary, with named tags where possible. """ - return {TiffTags.lookup(code).name: value for code, value in self.items()} + return { + TiffTags.lookup(code, self.group).name: value + for code, value in self.items() + } def __len__(self): return len(set(self._tagdata) | set(self._tags_v2)) @@ -531,7 +559,7 @@ class ImageFileDirectory_v2(MutableMapping): def _setitem(self, tag, value, legacy_api): basetypes = (Number, bytes, str) - info = TiffTags.lookup(tag) + info = TiffTags.lookup(tag, self.group) values = [value] if isinstance(value, basetypes) else value if tag not in self.tagtype: @@ -565,7 +593,8 @@ class ImageFileDirectory_v2(MutableMapping): if self.tagtype[tag] == TiffTags.UNDEFINED: values = [ - value.encode("ascii", "replace") if isinstance(value, str) else value + v.encode("ascii", "replace") if isinstance(v, str) else v + for v in values ] elif self.tagtype[tag] == TiffTags.RATIONAL: values = [float(v) if isinstance(v, int) else v for v in values] @@ -648,7 +677,7 @@ class ImageFileDirectory_v2(MutableMapping): _load_dispatch[idx] = ( # noqa: F821 size, lambda self, data, legacy_api=True: ( - self._unpack("{}{}".format(len(data) // size, fmt), data) + self._unpack(f"{len(data) // size}{fmt}", data) ), ) _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 @@ -692,7 +721,7 @@ class ImageFileDirectory_v2(MutableMapping): @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): - vals = self._unpack("{}L".format(len(data) // 4), data) + vals = self._unpack(f"{len(data) // 4}L", data) def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) @@ -715,7 +744,7 @@ class ImageFileDirectory_v2(MutableMapping): @_register_loader(10, 8) def load_signed_rational(self, data, legacy_api=True): - vals = self._unpack("{}l".format(len(data) // 4), data) + vals = self._unpack(f"{len(data) // 4}l", data) def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) @@ -747,7 +776,7 @@ class ImageFileDirectory_v2(MutableMapping): for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12)) - tagname = TiffTags.lookup(tag).name + tagname = TiffTags.lookup(tag, self.group).name typname = TYPES.get(typ, "unknown") msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" @@ -814,15 +843,16 @@ class ImageFileDirectory_v2(MutableMapping): ifh = b"II\x2A\x00\x08\x00\x00\x00" else: ifh = b"MM\x00\x2A\x00\x00\x00\x08" - ifd = ImageFileDirectory_v2(ifh) - for ifd_tag, ifd_value in self._tags_v2[tag].items(): + ifd = ImageFileDirectory_v2(ifh, group=tag) + values = self._tags_v2[tag] + for ifd_tag, ifd_value in values.items(): ifd[ifd_tag] = ifd_value data = ifd.tobytes(offset) else: values = value if isinstance(value, tuple) else (value,) data = self._write_dispatch[typ](self, *values) - tagname = TiffTags.lookup(tag).name + tagname = TiffTags.lookup(tag, self.group).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" msg += " - value: " + ( @@ -1052,6 +1082,11 @@ class TiffImageFile(ImageFile.ImageFile): def _seek(self, frame): self.fp = self.__fp + + # reset buffered io handle in case fp + # was passed to libtiff, invalidating the buffer + self.fp.tell() + while len(self._frame_pos) <= frame: if not self.__next: raise EOFError("no more images in TIFF file") @@ -1059,14 +1094,16 @@ class TiffImageFile(ImageFile.ImageFile): f"Seeking to frame {frame}, on frame {self.__frame}, " f"__next {self.__next}, location: {self.fp.tell()}" ) - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() self.fp.seek(self.__next) self._frame_pos.append(self.__next) logger.debug("Loading tags, location: %s" % self.fp.tell()) self.tag_v2.load(self.fp) - self.__next = self.tag_v2.next + if self.tag_v2.next in self._frame_pos: + # This IFD has already been processed + # Declare this to be the end of the image + self.__next = 0 + else: + self.__next = self.tag_v2.next if self.__next == 0: self._n_frames = frame + 1 if len(self._frame_pos) == 1: @@ -1083,6 +1120,14 @@ class TiffImageFile(ImageFile.ImageFile): """Return the current frame number""" return self.__frame + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + :returns: XMP tags in a dictionary. + """ + return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {} + def load(self): if self.tile and self.use_load_libtiff: return self._load_libtiff() @@ -1108,6 +1153,17 @@ class TiffImageFile(ImageFile.ImageFile): if not self.is_animated: self._close_exclusive_fp_after_loading = True + # reset buffered io handle in case fp + # was passed to libtiff, invalidating the buffer + self.fp.tell() + + # load IFD data from fp before it is closed + exif = self.getexif() + for key in TiffTags.TAGS_V2_GROUPS.keys(): + if key not in exif: + continue + exif.get_ifd(key) + def _load_libtiff(self): """Overload method triggered when we detect a compressed tiff Calls out to libtiff""" @@ -1250,6 +1306,13 @@ class TiffImageFile(ImageFile.ImageFile): if bps_count > len(bps_tuple) and len(bps_tuple) == 1: bps_tuple = bps_tuple * bps_count + samplesPerPixel = self.tag_v2.get( + SAMPLESPERPIXEL, + 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, + ) + if len(bps_tuple) != samplesPerPixel: + raise SyntaxError("unknown data organization") + # mode: check photometric interpretation and bits per pixel key = ( self.tag_v2.prefix, @@ -1277,11 +1340,11 @@ class TiffImageFile(ImageFile.ImageFile): if xres and yres: resunit = self.tag_v2.get(RESOLUTION_UNIT) if resunit == 2: # dots per inch - self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) + self.info["dpi"] = (xres, yres) elif resunit == 3: # dots per centimeter. convert to dpi - self.info["dpi"] = int(xres * 2.54 + 0.5), int(yres * 2.54 + 0.5) + self.info["dpi"] = (xres * 2.54, yres * 2.54) elif resunit is None: # used to default to 1, but now 2) - self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) + self.info["dpi"] = (xres, yres) # For backward compatibility, # we also preserve the old behavior self.info["resolution"] = xres, yres @@ -1445,7 +1508,9 @@ def _save(im, fp, filename): ifd = ImageFileDirectory_v2(prefix=prefix) - compression = im.encoderinfo.get("compression", im.info.get("compression")) + encoderinfo = im.encoderinfo + encoderconfig = im.encoderconfig + compression = encoderinfo.get("compression", im.info.get("compression")) if compression is None: compression = "raw" elif compression == "tiff_jpeg": @@ -1463,12 +1528,24 @@ def _save(im, fp, filename): ifd[IMAGELENGTH] = im.size[1] # write any arbitrary tags passed in as an ImageFileDirectory - info = im.encoderinfo.get("tiffinfo", {}) + if "tiffinfo" in encoderinfo: + info = encoderinfo["tiffinfo"] + elif "exif" in encoderinfo: + info = encoderinfo["exif"] + if isinstance(info, bytes): + exif = Image.Exif() + exif.load(info) + info = exif + else: + info = {} logger.debug("Tiffinfo Keys: %s" % list(info)) if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() for key in info: - ifd[key] = info.get(key) + if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys(): + ifd[key] = info.get_ifd(key) + else: + ifd[key] = info.get(key) try: ifd.tagtype[key] = info.tagtype[key] except Exception: @@ -1492,7 +1569,7 @@ def _save(im, fp, filename): # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech - icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) + icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) if icc: ifd[ICCPROFILE] = icc @@ -1508,14 +1585,14 @@ def _save(im, fp, filename): (ARTIST, "artist"), (COPYRIGHT, "copyright"), ]: - if name in im.encoderinfo: - ifd[key] = im.encoderinfo[name] + if name in encoderinfo: + ifd[key] = encoderinfo[name] - dpi = im.encoderinfo.get("dpi") + dpi = encoderinfo.get("dpi") if dpi: ifd[RESOLUTION_UNIT] = 2 - ifd[X_RESOLUTION] = int(dpi[0] + 0.5) - ifd[Y_RESOLUTION] = int(dpi[1] + 0.5) + ifd[X_RESOLUTION] = dpi[0] + ifd[Y_RESOLUTION] = dpi[1] if bits != (1,): ifd[BITSPERSAMPLE] = bits @@ -1526,25 +1603,59 @@ def _save(im, fp, filename): if format != 1: ifd[SAMPLEFORMAT] = format - ifd[PHOTOMETRIC_INTERPRETATION] = photo + if PHOTOMETRIC_INTERPRETATION not in ifd: + ifd[PHOTOMETRIC_INTERPRETATION] = photo + elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: + if im.mode == "1": + inverted_im = im.copy() + px = inverted_im.load() + for y in range(inverted_im.height): + for x in range(inverted_im.width): + px[x, y] = 0 if px[x, y] == 255 else 255 + im = inverted_im + else: + im = ImageOps.invert(im) if im.mode in ["P", "PA"]: lut = im.im.getpalette("RGB", "RGB;L") ifd[COLORMAP] = tuple(v * 256 for v in lut) # data orientation stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) - ifd[ROWSPERSTRIP] = im.size[1] - strip_byte_counts = stride * im.size[1] + # aim for given strip size (64 KB by default) when using libtiff writer + if libtiff: + rows_per_strip = 1 if stride == 0 else min(STRIP_SIZE // stride, im.size[1]) + # JPEG encoder expects multiple of 8 rows + if compression == "jpeg": + rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) + else: + rows_per_strip = im.size[1] + if rows_per_strip == 0: + rows_per_strip = 1 + strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip + strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip + ifd[ROWSPERSTRIP] = rows_per_strip if strip_byte_counts >= 2 ** 16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG - ifd[STRIPBYTECOUNTS] = strip_byte_counts - ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer + ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( + stride * im.size[1] - strip_byte_counts * (strips_per_image - 1), + ) + ifd[STRIPOFFSETS] = tuple( + range(0, strip_byte_counts * strips_per_image, strip_byte_counts) + ) # this is adjusted by IFD writer # no compression by default: ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) + if im.mode == "YCbCr": + for tag, value in { + YCBCRSUBSAMPLING: (1, 1), + REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), + }.items(): + ifd.setdefault(tag, value) + + blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] if libtiff: - if "quality" in im.encoderinfo: - quality = im.encoderinfo["quality"] + if "quality" in encoderinfo: + quality = encoderinfo["quality"] if not isinstance(quality, int) or quality < 0 or quality > 100: raise ValueError("Invalid quality setting") if compression != "jpeg": @@ -1565,17 +1676,14 @@ def _save(im, fp, filename): # optional types for non core tags types = {} - # SAMPLEFORMAT is determined by the image format and should not be copied - # from legacy_ifd. # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library # based on the data in the strip. # The other tags expect arrays with a certain length (fixed or depending on # BITSPERSAMPLE, etc), passing arrays with a different length will result in # segfaults. Block these tags until we add extra validation. # SUBIFD may also cause a segfault. - blocklist = [ + blocklist += [ REFERENCEBLACKWHITE, - SAMPLEFORMAT, STRIPBYTECOUNTS, STRIPOFFSETS, TRANSFERFUNCTION, @@ -1591,9 +1699,14 @@ def _save(im, fp, filename): legacy_ifd = {} if hasattr(im, "tag"): legacy_ifd = im.tag.to_v2() - for tag, value in itertools.chain( - ifd.items(), getattr(im, "tag_v2", {}).items(), legacy_ifd.items() - ): + + # SAMPLEFORMAT is determined by the image format and should not be copied + # from legacy_ifd. + supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd} + if SAMPLEFORMAT in supplied_tags: + del supplied_tags[SAMPLEFORMAT] + + for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): # Libtiff can only process certain core items without adding # them to the custom dictionary. # Custom items are supported for int, float, unicode, string and byte @@ -1618,6 +1731,9 @@ def _save(im, fp, filename): else: atts[tag] = value + if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: + atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] + logger.debug("Converted items: %s" % sorted(atts.items())) # libtiff always expects the bytes in native order. @@ -1633,7 +1749,7 @@ def _save(im, fp, filename): tags = list(atts.items()) tags.sort() a = (rawmode, compression, _fp, filename, tags, types) - e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig) + e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: @@ -1646,6 +1762,8 @@ def _save(im, fp, filename): raise OSError(f"encoder error {s} when writing image file") else: + for tag in blocklist: + del ifd[tag] offset = ifd.save(fp) ImageFile._save( @@ -1653,7 +1771,7 @@ def _save(im, fp, filename): ) # -- helper for multi-page save -- - if "_debug_multipage" in im.encoderinfo: + if "_debug_multipage" in encoderinfo: # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 9e9e117a4..88856aa92 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -33,7 +33,7 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")): return self.enum.get(value, value) if self.enum else value -def lookup(tag): +def lookup(tag, group=None): """ :param tag: Integer tag number :returns: Taginfo namedtuple, From the TAGS_V2 info if possible, @@ -42,7 +42,11 @@ def lookup(tag): """ - return TAGS_V2.get(tag, TagInfo(tag, TAGS.get(tag, "unknown"))) + if group is not None: + info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None + else: + info = TAGS_V2.get(tag) + return info or TagInfo(tag, TAGS.get(tag, "unknown")) ## @@ -178,13 +182,15 @@ TAGS_V2 = { 532: ("ReferenceBlackWhite", RATIONAL, 6), 700: ("XMP", BYTE, 0), 33432: ("Copyright", ASCII, 1), - 33723: ("IptcNaaInfo", UNDEFINED, 0), + 33723: ("IptcNaaInfo", UNDEFINED, 1), 34377: ("PhotoshopInfo", BYTE, 0), # FIXME add more tags here 34665: ("ExifIFD", LONG, 1), 34675: ("ICCProfile", UNDEFINED, 1), 34853: ("GPSInfoIFD", LONG, 1), + 36864: ("ExifVersion", UNDEFINED, 1), 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), # MPInfo 45056: ("MPFVersion", UNDEFINED, 1), 45057: ("NumberOfImages", LONG, 1), @@ -205,11 +211,25 @@ TAGS_V2 = { 45579: ("YawAngle", SIGNED_RATIONAL, 1), 45580: ("PitchAngle", SIGNED_RATIONAL, 1), 45581: ("RollAngle", SIGNED_RATIONAL, 1), + 40960: ("FlashPixVersion", UNDEFINED, 1), 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), 50780: ("BestQualityScale", RATIONAL, 1), 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 } +TAGS_V2_GROUPS = { + # ExifIFD + 34665: { + 36864: ("ExifVersion", UNDEFINED, 1), + 40960: ("FlashPixVersion", UNDEFINED, 1), + 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), + }, + # GPSInfoIFD + 34853: {}, + # InteroperabilityIFD + 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)}, +} # Legacy Tags structure # these tags aren't included above, but were in the previous versions @@ -368,6 +388,10 @@ def _populate(): TAGS_V2[k] = TagInfo(k, *v) + for group, tags in TAGS_V2_GROUPS.items(): + for k, v in tags.items(): + tags[k] = TagInfo(k, *v) + _populate() ## @@ -485,9 +509,6 @@ LIBTIFF_CORE = { 65537, } -LIBTIFF_CORE.remove(301) # Array of short, crashes -LIBTIFF_CORE.remove(532) # Array of long, crashes - LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff LIBTIFF_CORE.remove(323) # Tiled images diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index b578d6981..1354ad32b 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -23,12 +23,44 @@ and has been tested with a few sample files found using google. To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. """ -import builtins - -from . import Image +from . import Image, ImageFile from ._binary import i32le as i32 +class WalImageFile(ImageFile.ImageFile): + + format = "WAL" + format_description = "Quake2 Texture" + + def _open(self): + self.mode = "P" + + # read header fields + header = self.fp.read(32 + 24 + 32 + 12) + self._size = i32(header, 32), i32(header, 36) + Image._decompression_bomb_check(self.size) + + # load pixel data + offset = i32(header, 40) + self.fp.seek(offset) + + # strings are null-terminated + self.info["name"] = header[:32].split(b"\0", 1)[0] + next_name = header[56 : 56 + 32].split(b"\0", 1)[0] + if next_name: + self.info["next_name"] = next_name + + def load(self): + if self.im: + # Already loaded + return + + self.im = Image.core.new(self.mode, self.size) + self.frombytes(self.fp.read(self.size[0] * self.size[1])) + self.putpalette(quake2palette) + Image.Image.load(self) + + def open(filename): """ Load texture from a Quake2 WAL texture file. @@ -39,38 +71,7 @@ def open(filename): :param filename: WAL file name, or an opened file handle. :returns: An image instance. """ - # FIXME: modify to return a WalImageFile instance instead of - # plain Image object ? - - def imopen(fp): - # read header fields - header = fp.read(32 + 24 + 32 + 12) - size = i32(header, 32), i32(header, 36) - offset = i32(header, 40) - - # load pixel data - fp.seek(offset) - - Image._decompression_bomb_check(size) - im = Image.frombytes("P", size, fp.read(size[0] * size[1])) - im.putpalette(quake2palette) - - im.format = "WAL" - im.format_description = "Quake2 Texture" - - # strings are null-terminated - im.info["name"] = header[:32].split(b"\0", 1)[0] - next_name = header[56 : 56 + 32].split(b"\0", 1)[0] - if next_name: - im.info["next_name"] = next_name - - return im - - if hasattr(filename, "read"): - return imopen(filename) - else: - with builtins.open(filename, "rb") as fp: - return imopen(fp) + return WalImageFile(filename) quake2palette = ( diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index dfc8351ef..590161f3e 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -202,7 +202,7 @@ def _save_all(im, fp, filename): lossless = im.encoderinfo.get("lossless", False) quality = im.encoderinfo.get("quality", 80) method = im.encoderinfo.get("method", 0) - icc_profile = im.encoderinfo.get("icc_profile", "") + icc_profile = im.encoderinfo.get("icc_profile") or "" exif = im.encoderinfo.get("exif", "") if isinstance(exif, Image.Exif): exif = exif.tobytes() @@ -309,18 +309,18 @@ def _save_all(im, fp, filename): def _save(im, fp, filename): lossless = im.encoderinfo.get("lossless", False) quality = im.encoderinfo.get("quality", 80) - icc_profile = im.encoderinfo.get("icc_profile", "") + icc_profile = im.encoderinfo.get("icc_profile") or "" exif = im.encoderinfo.get("exif", "") if isinstance(exif, Image.Exif): exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") - method = im.encoderinfo.get("method", 0) + method = im.encoderinfo.get("method", 4) if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( "A" in im.mode or "a" in im.mode - or (im.mode == "P" and "A" in im.im.getpalettemode()) + or (im.mode == "P" and "transparency" in im.info) ) im = im.convert("RGBA" if alpha else "RGB") diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 87847a107..27f5d2f87 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -127,8 +127,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame - xdpi = int(2540.0 * (x1 - y0) / (frame[2] - frame[0]) + 0.5) - ydpi = int(2540.0 * (y1 - y0) / (frame[3] - frame[1]) + 0.5) + xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) + ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) self.info["wmf_bbox"] = x0, y0, x1, y1 @@ -152,7 +152,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): def load(self, dpi=None): if dpi is not None and self._inch is not None: - self.info["dpi"] = int(dpi + 0.5) + self.info["dpi"] = dpi x0, y0, x1, y1 = self.info["wmf_bbox"] self._size = ( (x1 - x0) * self.info["dpi"] // self._inch, diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 890ae44f5..45fef241e 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -13,72 +13,12 @@ Use PIL.__version__ for this Pillow version. ;-) """ -import sys -import warnings - from . import _version # VERSION was removed in Pillow 6.0.0. -__version__ = _version.__version__ - - -# PILLOW_VERSION is deprecated and will be removed in a future release. +# PILLOW_VERSION was removed in Pillow 9.0.0. # Use __version__ instead. -def _raise_version_warning(): - warnings.warn( - "PILLOW_VERSION is deprecated and will be removed in Pillow 9 (2022-01-02). " - "Use __version__ instead.", - DeprecationWarning, - stacklevel=3, - ) - - -if sys.version_info >= (3, 7): - - def __getattr__(name): - if name == "PILLOW_VERSION": - _raise_version_warning() - return __version__ - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -else: - - class _Deprecated_Version(str): - def __str__(self): - _raise_version_warning() - return super().__str__() - - def __getitem__(self, key): - _raise_version_warning() - return super().__getitem__(key) - - def __eq__(self, other): - _raise_version_warning() - return super().__eq__(other) - - def __ne__(self, other): - _raise_version_warning() - return super().__ne__(other) - - def __gt__(self, other): - _raise_version_warning() - return super().__gt__(other) - - def __lt__(self, other): - _raise_version_warning() - return super().__lt__(other) - - def __ge__(self, other): - _raise_version_warning() - return super().__gt__(other) - - def __le__(self, other): - _raise_version_warning() - return super().__lt__(other) - - PILLOW_VERSION = _Deprecated_Version(__version__) - +__version__ = _version.__version__ del _version diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 5564f450d..a74ee9eb6 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -47,6 +47,16 @@ def si16le(c, o=0): return unpack_from("h", c, o)[0] + + def i32le(c, o=0): """ Converts a 4-bytes (32 bits) string to an unsigned integer. diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 58aeffbfb..ba4d045e6 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -14,7 +14,7 @@ tk_version = str(tkinter.TkVersion) if tk_version == "8.4": warnings.warn( "Support for Tk/Tcl 8.4 is deprecated and will be removed" - " in Pillow 10 (2023-01-02). Please upgrade to Tk/Tcl 8.5 " + " in Pillow 10 (2023-07-01). Please upgrade to Tk/Tcl 8.5 " "or newer.", DeprecationWarning, ) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 20e8754a4..62c7dba55 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "8.2.0.dev0" +__version__ = "9.1.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 66d0ba10a..3838568f3 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -218,7 +218,7 @@ def get_supported(): def pilinfo(out=None, supported_formats=True): """ Prints information about this installation of Pillow. - This function can be called with ``python -m PIL``. + This function can be called with ``python3 -m PIL``. :param out: The output stream to print to. Defaults to ``sys.stdout`` if ``None``. diff --git a/src/Tk/_tkmini.h b/src/Tk/_tkmini.h index b6945eb1a..9852fc9d6 100644 --- a/src/Tk/_tkmini.h +++ b/src/Tk/_tkmini.h @@ -1,7 +1,7 @@ /* Small excerpts from the Tcl / Tk 8.6 headers * * License terms copied from: - * http://www.tcl.tk/software/tcltk/license.html + * https://www.tcl.tk/software/tcltk/license.html * as of 20 May 2016. * * Copyright (c) 1987-1994 The Regents of the University of California. diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 1c6c5f34a..9ae7edff1 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -219,7 +219,7 @@ TkImaging_Init(Tcl_Interp *interp) { #define TKINTER_FINDER "PIL._tkinter_finder" -#if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) +#if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__CYGWIN__) /* * On Windows, we can't load the tkinter module to get the Tcl or Tk symbols, diff --git a/src/_imaging.c b/src/_imaging.c index a5b12d325..2a42c0461 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -498,7 +498,7 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - if (PyTuple_Check(color) && PyTuple_Size(color) == 1) { + if (PyTuple_Check(color) && PyTuple_GET_SIZE(color) == 1) { color = PyTuple_GetItem(color, 0); } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || @@ -527,7 +527,10 @@ getink(PyObject *color, Imaging im, char *ink) { if (im->bands == 1) { /* unsigned integer, single layer */ if (rIsInt != 1) { - if (!PyArg_ParseTuple(color, "L", &r)) { + if (PyTuple_GET_SIZE(color) != 1) { + PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); + return NULL; + } else if (!PyArg_ParseTuple(color, "L", &r)) { return NULL; } } @@ -542,13 +545,20 @@ getink(PyObject *color, Imaging im, char *ink) { g = (UINT8)(r >> 8); r = (UINT8)r; } else { + int tupleSize = PyTuple_GET_SIZE(color); if (im->bands == 2) { - if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { + if (tupleSize != 1 && tupleSize != 2) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { return NULL; } g = b = r; } else { - if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { + if (tupleSize != 3 && tupleSize != 4) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one, three or four elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { return NULL; } } @@ -1086,7 +1096,7 @@ _getpalette(ImagingObject *self, PyObject *args) { } static PyObject * -_getpalettemode(ImagingObject *self, PyObject *args) { +_getpalettemode(ImagingObject *self) { if (!self->image->palette) { PyErr_SetString(PyExc_ValueError, no_palette); return NULL; @@ -1109,7 +1119,12 @@ _getxy(PyObject *xy, int *x, int *y) { } else if (PyFloat_Check(value)) { *x = (int)PyFloat_AS_DOUBLE(value); } else { - goto badval; + PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); + if (int_value != NULL && PyLong_Check(int_value)) { + *x = PyLong_AS_LONG(int_value); + } else { + goto badval; + } } value = PyTuple_GET_ITEM(xy, 1); @@ -1118,7 +1133,12 @@ _getxy(PyObject *xy, int *x, int *y) { } else if (PyFloat_Check(value)) { *y = (int)PyFloat_AS_DOUBLE(value); } else { - goto badval; + PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); + if (int_value != NULL && PyLong_Check(int_value)) { + *y = PyLong_AS_LONG(int_value); + } else { + goto badval; + } } return 0; @@ -1474,6 +1494,14 @@ _putdata(ImagingObject *self, PyObject *args) { return NULL; } +#define set_value_to_item(seq, i) \ +op = PySequence_Fast_GET_ITEM(seq, i); \ +if (PySequence_Check(op)) { \ + PyErr_SetString(PyExc_TypeError, "sequence must be flattened"); \ + return NULL; \ +} else { \ + value = PyFloat_AsDouble(op); \ +} if (image->image8) { if (PyBytes_Check(data)) { unsigned char *p; @@ -1502,11 +1530,12 @@ _putdata(ImagingObject *self, PyObject *args) { PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; } + double value; if (scale == 1.0 && offset == 0.0) { /* Clipped data */ for (i = x = y = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(seq, i); - image->image8[y][x] = (UINT8)CLIP8(PyLong_AsLong(op)); + set_value_to_item(seq, i); + image->image8[y][x] = (UINT8)CLIP8(value); if (++x >= (int)image->xsize) { x = 0, y++; } @@ -1515,9 +1544,8 @@ _putdata(ImagingObject *self, PyObject *args) { } else { /* Scaled and clipped data */ for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_Fast_GET_ITEM(seq, i); - image->image8[y][x] = - CLIP8((int)(PyFloat_AsDouble(op) * scale + offset)); + set_value_to_item(seq, i); + image->image8[y][x] = CLIP8(value * scale + offset); if (++x >= (int)image->xsize) { x = 0, y++; } @@ -1535,9 +1563,10 @@ _putdata(ImagingObject *self, PyObject *args) { switch (image->type) { case IMAGING_TYPE_INT32: for (i = x = y = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(seq, i); + double value; + set_value_to_item(seq, i); IMAGING_PIXEL_INT32(image, x, y) = - (INT32)(PyFloat_AsDouble(op) * scale + offset); + (INT32)(value * scale + offset); if (++x >= (int)image->xsize) { x = 0, y++; } @@ -1546,9 +1575,10 @@ _putdata(ImagingObject *self, PyObject *args) { break; case IMAGING_TYPE_FLOAT32: for (i = x = y = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(seq, i); + double value; + set_value_to_item(seq, i); IMAGING_PIXEL_FLOAT32(image, x, y) = - (FLOAT32)(PyFloat_AsDouble(op) * scale + offset); + (FLOAT32)(value * scale + offset); if (++x >= (int)image->xsize) { x = 0, y++; } @@ -1643,8 +1673,7 @@ _putpalette(ImagingObject *self, PyObject *args) { unpack(self->image->palette->palette, palette, palettesize * 8 / bits); - Py_INCREF(Py_None); - return Py_None; + return PyLong_FromLong(palettesize * 8 / bits); } static PyObject * @@ -2085,12 +2114,12 @@ _box_blur(ImagingObject *self, PyObject *args) { /* -------------------------------------------------------------------- */ static PyObject * -_isblock(ImagingObject *self, PyObject *args) { +_isblock(ImagingObject *self) { return PyBool_FromLong(self->image->block != NULL); } static PyObject * -_getbbox(ImagingObject *self, PyObject *args) { +_getbbox(ImagingObject *self) { int bbox[4]; if (!ImagingGetBBox(self->image, bbox)) { Py_INCREF(Py_None); @@ -2135,7 +2164,7 @@ _getcolors(ImagingObject *self, PyObject *args) { } static PyObject * -_getextrema(ImagingObject *self, PyObject *args) { +_getextrema(ImagingObject *self) { union { UINT8 u[2]; INT32 i[2]; @@ -2169,7 +2198,7 @@ _getextrema(ImagingObject *self, PyObject *args) { } static PyObject * -_getprojection(ImagingObject *self, PyObject *args) { +_getprojection(ImagingObject *self) { unsigned char *xprofile; unsigned char *yprofile; PyObject *result; @@ -2287,7 +2316,7 @@ _merge(PyObject *self, PyObject *args) { } static PyObject * -_split(ImagingObject *self, PyObject *args) { +_split(ImagingObject *self) { int fails = 0; Py_ssize_t i; PyObject *list; @@ -2318,7 +2347,7 @@ _split(ImagingObject *self, PyObject *args) { #ifdef WITH_IMAGECHOPS static PyObject * -_chop_invert(ImagingObject *self, PyObject *args) { +_chop_invert(ImagingObject *self) { return PyImagingNew(ImagingNegative(self->image)); } @@ -2697,8 +2726,8 @@ _font_getsize(ImagingFontObject *self, PyObject *args) { } static struct PyMethodDef _font_methods[] = { - {"getmask", (PyCFunction)_font_getmask, 1}, - {"getsize", (PyCFunction)_font_getsize, 1}, + {"getmask", (PyCFunction)_font_getmask, METH_VARARGS}, + {"getsize", (PyCFunction)_font_getsize, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; @@ -3105,7 +3134,8 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { PyObject *data; int ink; int fill = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &fill)) { + int width = 0; + if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { return NULL; } @@ -3134,7 +3164,7 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { free(xy); - if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, self->blend) < 0) { + if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < 0) { free(ixy); return NULL; } @@ -3192,19 +3222,19 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { static struct PyMethodDef _draw_methods[] = { #ifdef WITH_IMAGEDRAW /* Graphics (ImageDraw) */ - {"draw_lines", (PyCFunction)_draw_lines, 1}, + {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS}, #ifdef WITH_ARROW - {"draw_outline", (PyCFunction)_draw_outline, 1}, + {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS}, #endif - {"draw_polygon", (PyCFunction)_draw_polygon, 1}, - {"draw_rectangle", (PyCFunction)_draw_rectangle, 1}, - {"draw_points", (PyCFunction)_draw_points, 1}, - {"draw_arc", (PyCFunction)_draw_arc, 1}, - {"draw_bitmap", (PyCFunction)_draw_bitmap, 1}, - {"draw_chord", (PyCFunction)_draw_chord, 1}, - {"draw_ellipse", (PyCFunction)_draw_ellipse, 1}, - {"draw_pieslice", (PyCFunction)_draw_pieslice, 1}, - {"draw_ink", (PyCFunction)_draw_ink, 1}, + {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS}, + {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS}, + {"draw_points", (PyCFunction)_draw_points, METH_VARARGS}, + {"draw_arc", (PyCFunction)_draw_arc, METH_VARARGS}, + {"draw_bitmap", (PyCFunction)_draw_bitmap, METH_VARARGS}, + {"draw_chord", (PyCFunction)_draw_chord, METH_VARARGS}, + {"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS}, + {"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS}, + {"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS}, #endif {NULL, NULL} /* sentinel */ }; @@ -3411,100 +3441,100 @@ _save_ppm(ImagingObject *self, PyObject *args) { static struct PyMethodDef methods[] = { /* Put commonly used methods first */ - {"getpixel", (PyCFunction)_getpixel, 1}, - {"putpixel", (PyCFunction)_putpixel, 1}, + {"getpixel", (PyCFunction)_getpixel, METH_VARARGS}, + {"putpixel", (PyCFunction)_putpixel, METH_VARARGS}, - {"pixel_access", (PyCFunction)pixel_access_new, 1}, + {"pixel_access", (PyCFunction)pixel_access_new, METH_VARARGS}, /* Standard processing methods (Image) */ - {"color_lut_3d", (PyCFunction)_color_lut_3d, 1}, - {"convert", (PyCFunction)_convert, 1}, - {"convert2", (PyCFunction)_convert2, 1}, - {"convert_matrix", (PyCFunction)_convert_matrix, 1}, - {"convert_transparent", (PyCFunction)_convert_transparent, 1}, - {"copy", (PyCFunction)_copy, 1}, - {"crop", (PyCFunction)_crop, 1}, - {"expand", (PyCFunction)_expand_image, 1}, - {"filter", (PyCFunction)_filter, 1}, - {"histogram", (PyCFunction)_histogram, 1}, - {"entropy", (PyCFunction)_entropy, 1}, + {"color_lut_3d", (PyCFunction)_color_lut_3d, METH_VARARGS}, + {"convert", (PyCFunction)_convert, METH_VARARGS}, + {"convert2", (PyCFunction)_convert2, METH_VARARGS}, + {"convert_matrix", (PyCFunction)_convert_matrix, METH_VARARGS}, + {"convert_transparent", (PyCFunction)_convert_transparent, METH_VARARGS}, + {"copy", (PyCFunction)_copy, METH_VARARGS}, + {"crop", (PyCFunction)_crop, METH_VARARGS}, + {"expand", (PyCFunction)_expand_image, METH_VARARGS}, + {"filter", (PyCFunction)_filter, METH_VARARGS}, + {"histogram", (PyCFunction)_histogram, METH_VARARGS}, + {"entropy", (PyCFunction)_entropy, METH_VARARGS}, #ifdef WITH_MODEFILTER - {"modefilter", (PyCFunction)_modefilter, 1}, + {"modefilter", (PyCFunction)_modefilter, METH_VARARGS}, #endif - {"offset", (PyCFunction)_offset, 1}, - {"paste", (PyCFunction)_paste, 1}, - {"point", (PyCFunction)_point, 1}, - {"point_transform", (PyCFunction)_point_transform, 1}, - {"putdata", (PyCFunction)_putdata, 1}, + {"offset", (PyCFunction)_offset, METH_VARARGS}, + {"paste", (PyCFunction)_paste, METH_VARARGS}, + {"point", (PyCFunction)_point, METH_VARARGS}, + {"point_transform", (PyCFunction)_point_transform, METH_VARARGS}, + {"putdata", (PyCFunction)_putdata, METH_VARARGS}, #ifdef WITH_QUANTIZE - {"quantize", (PyCFunction)_quantize, 1}, + {"quantize", (PyCFunction)_quantize, METH_VARARGS}, #endif #ifdef WITH_RANKFILTER - {"rankfilter", (PyCFunction)_rankfilter, 1}, + {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS}, #endif - {"resize", (PyCFunction)_resize, 1}, - {"reduce", (PyCFunction)_reduce, 1}, - {"transpose", (PyCFunction)_transpose, 1}, - {"transform2", (PyCFunction)_transform2, 1}, + {"resize", (PyCFunction)_resize, METH_VARARGS}, + {"reduce", (PyCFunction)_reduce, METH_VARARGS}, + {"transpose", (PyCFunction)_transpose, METH_VARARGS}, + {"transform2", (PyCFunction)_transform2, METH_VARARGS}, - {"isblock", (PyCFunction)_isblock, 1}, + {"isblock", (PyCFunction)_isblock, METH_NOARGS}, - {"getbbox", (PyCFunction)_getbbox, 1}, - {"getcolors", (PyCFunction)_getcolors, 1}, - {"getextrema", (PyCFunction)_getextrema, 1}, - {"getprojection", (PyCFunction)_getprojection, 1}, + {"getbbox", (PyCFunction)_getbbox, METH_NOARGS}, + {"getcolors", (PyCFunction)_getcolors, METH_VARARGS}, + {"getextrema", (PyCFunction)_getextrema, METH_NOARGS}, + {"getprojection", (PyCFunction)_getprojection, METH_NOARGS}, - {"getband", (PyCFunction)_getband, 1}, - {"putband", (PyCFunction)_putband, 1}, - {"split", (PyCFunction)_split, 1}, - {"fillband", (PyCFunction)_fillband, 1}, + {"getband", (PyCFunction)_getband, METH_VARARGS}, + {"putband", (PyCFunction)_putband, METH_VARARGS}, + {"split", (PyCFunction)_split, METH_NOARGS}, + {"fillband", (PyCFunction)_fillband, METH_VARARGS}, - {"setmode", (PyCFunction)im_setmode, 1}, + {"setmode", (PyCFunction)im_setmode, METH_VARARGS}, - {"getpalette", (PyCFunction)_getpalette, 1}, - {"getpalettemode", (PyCFunction)_getpalettemode, 1}, - {"putpalette", (PyCFunction)_putpalette, 1}, - {"putpalettealpha", (PyCFunction)_putpalettealpha, 1}, - {"putpalettealphas", (PyCFunction)_putpalettealphas, 1}, + {"getpalette", (PyCFunction)_getpalette, METH_VARARGS}, + {"getpalettemode", (PyCFunction)_getpalettemode, METH_NOARGS}, + {"putpalette", (PyCFunction)_putpalette, METH_VARARGS}, + {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS}, + {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS}, #ifdef WITH_IMAGECHOPS /* Channel operations (ImageChops) */ - {"chop_invert", (PyCFunction)_chop_invert, 1}, - {"chop_lighter", (PyCFunction)_chop_lighter, 1}, - {"chop_darker", (PyCFunction)_chop_darker, 1}, - {"chop_difference", (PyCFunction)_chop_difference, 1}, - {"chop_multiply", (PyCFunction)_chop_multiply, 1}, - {"chop_screen", (PyCFunction)_chop_screen, 1}, - {"chop_add", (PyCFunction)_chop_add, 1}, - {"chop_subtract", (PyCFunction)_chop_subtract, 1}, - {"chop_add_modulo", (PyCFunction)_chop_add_modulo, 1}, - {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, 1}, - {"chop_and", (PyCFunction)_chop_and, 1}, - {"chop_or", (PyCFunction)_chop_or, 1}, - {"chop_xor", (PyCFunction)_chop_xor, 1}, - {"chop_soft_light", (PyCFunction)_chop_soft_light, 1}, - {"chop_hard_light", (PyCFunction)_chop_hard_light, 1}, - {"chop_overlay", (PyCFunction)_chop_overlay, 1}, + {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS}, + {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS}, + {"chop_darker", (PyCFunction)_chop_darker, METH_VARARGS}, + {"chop_difference", (PyCFunction)_chop_difference, METH_VARARGS}, + {"chop_multiply", (PyCFunction)_chop_multiply, METH_VARARGS}, + {"chop_screen", (PyCFunction)_chop_screen, METH_VARARGS}, + {"chop_add", (PyCFunction)_chop_add, METH_VARARGS}, + {"chop_subtract", (PyCFunction)_chop_subtract, METH_VARARGS}, + {"chop_add_modulo", (PyCFunction)_chop_add_modulo, METH_VARARGS}, + {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, METH_VARARGS}, + {"chop_and", (PyCFunction)_chop_and, METH_VARARGS}, + {"chop_or", (PyCFunction)_chop_or, METH_VARARGS}, + {"chop_xor", (PyCFunction)_chop_xor, METH_VARARGS}, + {"chop_soft_light", (PyCFunction)_chop_soft_light, METH_VARARGS}, + {"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS}, + {"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS}, #endif #ifdef WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask extension */ - {"gaussian_blur", (PyCFunction)_gaussian_blur, 1}, - {"unsharp_mask", (PyCFunction)_unsharp_mask, 1}, + {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, + {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, #endif - {"box_blur", (PyCFunction)_box_blur, 1}, + {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, #ifdef WITH_EFFECTS /* Special effects */ - {"effect_spread", (PyCFunction)_effect_spread, 1}, + {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, #endif /* Misc. */ - {"new_block", (PyCFunction)_new_block, 1}, + {"new_block", (PyCFunction)_new_block, METH_VARARGS}, - {"save_ppm", (PyCFunction)_save_ppm, 1}, + {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; @@ -3979,111 +4009,111 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args); static PyMethodDef functions[] = { /* Object factories */ - {"alpha_composite", (PyCFunction)_alpha_composite, 1}, - {"blend", (PyCFunction)_blend, 1}, - {"fill", (PyCFunction)_fill, 1}, - {"new", (PyCFunction)_new, 1}, - {"merge", (PyCFunction)_merge, 1}, + {"alpha_composite", (PyCFunction)_alpha_composite, METH_VARARGS}, + {"blend", (PyCFunction)_blend, METH_VARARGS}, + {"fill", (PyCFunction)_fill, METH_VARARGS}, + {"new", (PyCFunction)_new, METH_VARARGS}, + {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ - {"convert", (PyCFunction)_convert2, 1}, + {"convert", (PyCFunction)_convert2, METH_VARARGS}, /* Codecs */ - {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, 1}, - {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, 1}, - {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1}, - {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, 1}, - {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, 1}, - {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, 1}, - {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, 1}, - {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1}, /* EPS=HEX! */ + {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, + {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, + {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, + {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, + {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, METH_VARARGS}, + {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, METH_VARARGS}, + {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, METH_VARARGS}, + {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, /* EPS=HEX! */ #ifdef HAVE_LIBJPEG - {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, 1}, - {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, 1}, + {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, METH_VARARGS}, + {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, METH_VARARGS}, #endif #ifdef HAVE_OPENJPEG - {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, 1}, - {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, 1}, + {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, METH_VARARGS}, + {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, METH_VARARGS}, #endif #ifdef HAVE_LIBTIFF - {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, 1}, - {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, 1}, + {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, METH_VARARGS}, + {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, METH_VARARGS}, #endif - {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, 1}, - {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, 1}, - {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, 1}, - {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, 1}, - {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, 1}, - {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, 1}, - {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, 1}, - {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, 1}, - {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, 1}, - {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, 1}, - {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, 1}, - {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, 1}, + {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, METH_VARARGS}, + {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, METH_VARARGS}, + {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, METH_VARARGS}, + {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, METH_VARARGS}, + {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, METH_VARARGS}, + {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, METH_VARARGS}, + {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, METH_VARARGS}, + {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, METH_VARARGS}, + {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, METH_VARARGS}, + {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, METH_VARARGS}, + {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, METH_VARARGS}, + {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, METH_VARARGS}, #ifdef HAVE_LIBZ - {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, 1}, - {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, 1}, + {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, METH_VARARGS}, + {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS}, #endif /* Memory mapping */ #ifdef WITH_MAPPING - {"map_buffer", (PyCFunction)PyImaging_MapBuffer, 1}, + {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, #endif /* Display support */ #ifdef _WIN32 - {"display", (PyCFunction)PyImaging_DisplayWin32, 1}, - {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, 1}, - {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, 1}, - {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, 1}, - {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, 1}, - {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, 1}, - {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, 1}, - {"drawwmf", (PyCFunction)PyImaging_DrawWmf, 1}, + {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS}, + {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS}, + {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS}, + {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, + {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, + {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, + {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS}, + {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, #endif #ifdef HAVE_XCB - {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, 1}, + {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, METH_VARARGS}, #endif /* Utilities */ - {"getcodecstatus", (PyCFunction)_getcodecstatus, 1}, + {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, /* Special effects (experimental) */ #ifdef WITH_EFFECTS - {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, 1}, - {"effect_noise", (PyCFunction)_effect_noise, 1}, - {"linear_gradient", (PyCFunction)_linear_gradient, 1}, - {"radial_gradient", (PyCFunction)_radial_gradient, 1}, - {"wedge", (PyCFunction)_linear_gradient, 1}, /* Compatibility */ + {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, + {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, + {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, + {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, + {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ #endif /* Drawing support stuff */ #ifdef WITH_IMAGEDRAW - {"font", (PyCFunction)_font_new, 1}, - {"draw", (PyCFunction)_draw_new, 1}, + {"font", (PyCFunction)_font_new, METH_VARARGS}, + {"draw", (PyCFunction)_draw_new, METH_VARARGS}, #endif /* Experimental path stuff */ #ifdef WITH_IMAGEPATH - {"path", (PyCFunction)PyPath_Create, 1}, + {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, #endif /* Experimental arrow graphics stuff */ #ifdef WITH_ARROW - {"outline", (PyCFunction)PyOutline_Create, 1}, + {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, #endif /* Resource management */ - {"get_stats", (PyCFunction)_get_stats, 1}, - {"reset_stats", (PyCFunction)_reset_stats, 1}, - {"get_alignment", (PyCFunction)_get_alignment, 1}, - {"get_block_size", (PyCFunction)_get_block_size, 1}, - {"get_blocks_max", (PyCFunction)_get_blocks_max, 1}, - {"set_alignment", (PyCFunction)_set_alignment, 1}, - {"set_block_size", (PyCFunction)_set_block_size, 1}, - {"set_blocks_max", (PyCFunction)_set_blocks_max, 1}, - {"clear_cache", (PyCFunction)_clear_cache, 1}, + {"get_stats", (PyCFunction)_get_stats, METH_VARARGS}, + {"reset_stats", (PyCFunction)_reset_stats, METH_VARARGS}, + {"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS}, + {"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS}, + {"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS}, + {"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS}, + {"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS}, + {"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS}, + {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 314150420..9b5a121d7 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -3,14 +3,14 @@ * a Python / PIL interface to the littleCMS ICC Color Management System * Copyright (C) 2002-2003 Kevin Cazabon * kevin@cazabon.com - * http://www.cazabon.com + * https://www.cazabon.com * Adapted/reworked for PIL by Fredrik Lundh * Copyright (c) 2009 Fredrik Lundh * Updated to LCMS2 * Copyright (c) 2013 Eric Soroos * - * pyCMS home page: http://www.cazabon.com/pyCMS - * littleCMS home page: http://www.littlecms.com + * pyCMS home page: https://www.cazabon.com/pyCMS + * littleCMS home page: https://www.littlecms.com * (littleCMS is Copyright (C) 1998-2001 Marti Maria) * * Originally released under LGPL. Graciously donated to PIL in @@ -23,7 +23,7 @@ pyCMS\n\ a Python / PIL interface to the littleCMS ICC Color Management System\n\ Copyright (C) 2002-2003 Kevin Cazabon\n\ kevin@cazabon.com\n\ -http://www.cazabon.com\n\ +https://www.cazabon.com\n\ " #define PY_SSIZE_T_CLEAN @@ -959,25 +959,25 @@ _is_intent_supported(CmsProfileObject *self, int clut) { static PyMethodDef pyCMSdll_methods[] = { - {"profile_open", cms_profile_open, 1}, - {"profile_frombytes", cms_profile_fromstring, 1}, - {"profile_fromstring", cms_profile_fromstring, 1}, - {"profile_tobytes", cms_profile_tobytes, 1}, + {"profile_open", cms_profile_open, METH_VARARGS}, + {"profile_frombytes", cms_profile_fromstring, METH_VARARGS}, + {"profile_fromstring", cms_profile_fromstring, METH_VARARGS}, + {"profile_tobytes", cms_profile_tobytes, METH_VARARGS}, /* profile and transform functions */ - {"buildTransform", buildTransform, 1}, - {"buildProofTransform", buildProofTransform, 1}, - {"createProfile", createProfile, 1}, + {"buildTransform", buildTransform, METH_VARARGS}, + {"buildProofTransform", buildProofTransform, METH_VARARGS}, + {"createProfile", createProfile, METH_VARARGS}, /* platform specific tools */ #ifdef _WIN32 - {"get_display_profile_win32", cms_get_display_profile_win32, 1}, + {"get_display_profile_win32", cms_get_display_profile_win32, METH_VARARGS}, #endif {NULL, NULL}}; static struct PyMethodDef cms_profile_methods[] = { - {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, 1}, + {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/_imagingft.c b/src/_imagingft.c index 73f0f6362..8f19b763c 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -277,7 +277,8 @@ text_layout_raqm( direction = RAQM_DIRECTION_LTR; } else if (strcmp(dir, "ttb") == 0) { direction = RAQM_DIRECTION_TTB; -#if !defined(RAQM_VERSION_ATLEAST) || !RAQM_VERSION_ATLEAST(0, 7, 0) +#if !defined(RAQM_VERSION_ATLEAST) + /* RAQM_VERSION_ATLEAST was added in Raqm 0.7.0 */ PyErr_SetString( PyExc_ValueError, "libraqm 0.7 or greater required for 'ttb' direction"); @@ -832,7 +833,7 @@ font_render(FontObject *self, PyObject *args) { } im = (Imaging)id; - load_flags = FT_LOAD_DEFAULT; + load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } @@ -932,11 +933,7 @@ font_render(FontObject *self, PyObject *args) { case FT_PIXEL_MODE_GRAY2: case FT_PIXEL_MODE_GRAY4: if (!bitmap_converted_ready) { -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 6) FT_Bitmap_Init(&bitmap_converted); -#else - FT_Bitmap_New(&bitmap_converted); -#endif bitmap_converted_ready = 1; } error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1); diff --git a/src/_webp.c b/src/_webp.c index 4d51d99df..fd99116cb 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -392,10 +392,11 @@ _anim_decoder_new(PyObject *self, PyObject *args) { return (PyObject *)decp; } } + WebPDataClear(&(decp->data)); } PyObject_Del(decp); } - PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); + PyErr_SetString(PyExc_OSError, "could not create decoder object"); return NULL; } @@ -485,7 +486,7 @@ static struct PyMethodDef _anim_encoder_methods[] = { {NULL, NULL} /* sentinel */ }; -// WebPAnimDecoder type definition +// WebPAnimEncoder type definition static PyTypeObject WebPAnimEncoder_Type = { PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimEncoder", /*tp_name */ sizeof(WebPAnimEncoderObject), /*tp_size */ @@ -663,7 +664,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { WebPPictureFree(&pic); if (!ok) { - PyErr_SetString(PyExc_ValueError, "encoding error"); + PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code); return NULL; } output = writer.mem; diff --git a/src/decode.c b/src/decode.c index 5d64bd0b9..e236264cd 100644 --- a/src/decode.c +++ b/src/decode.c @@ -34,9 +34,10 @@ #include "libImaging/Imaging.h" +#include "libImaging/Bit.h" +#include "libImaging/Bcn.h" #include "libImaging/Gif.h" #include "libImaging/Raw.h" -#include "libImaging/Bit.h" #include "libImaging/Sgi.h" /* -------------------------------------------------------------------- */ @@ -199,7 +200,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { state->bytes = (state->bits * state->xsize + 7) / 8; } /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes); + state->buffer = (UINT8 *)calloc(1, state->bytes); if (!state->buffer) { return ImagingError_MemoryError(); } @@ -234,15 +235,15 @@ _setfd(ImagingDecoderObject *decoder, PyObject *args) { } static PyObject * -_get_pulls_fd(ImagingDecoderObject *decoder) { +_get_pulls_fd(ImagingDecoderObject *decoder, void *closure) { return PyBool_FromLong(decoder->pulls_fd); } static struct PyMethodDef methods[] = { - {"decode", (PyCFunction)_decode, 1}, - {"cleanup", (PyCFunction)_decode_cleanup, 1}, - {"setimage", (PyCFunction)_setimage, 1}, - {"setfd", (PyCFunction)_setfd, 1}, + {"decode", (PyCFunction)_decode, METH_VARARGS}, + {"cleanup", (PyCFunction)_decode_cleanup, METH_VARARGS}, + {"setimage", (PyCFunction)_setimage, METH_VARARGS}, + {"setfd", (PyCFunction)_setfd, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; @@ -298,7 +299,7 @@ get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmod unpack = ImagingFindUnpacker(mode, rawmode, &bits); if (!unpack) { Py_DECREF(decoder); - PyErr_SetString(PyExc_ValueError, "unknown raw mode"); + PyErr_SetString(PyExc_ValueError, "unknown raw mode for given image mode"); return -1; } @@ -359,8 +360,8 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { char *mode; char *actual; int n = 0; - int ystep = 1; - if (!PyArg_ParseTuple(args, "s|ii", &mode, &n, &ystep)) { + char *pixel_format = ""; + if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) { return NULL; } @@ -368,13 +369,15 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { case 1: /* BC1: 565 color, 1-bit alpha */ case 2: /* BC2: 565 color, 4-bit alpha */ case 3: /* BC3: 565 color, 2-endpoint 8-bit interpolated alpha */ - case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ case 7: /* BC7: 4-channel 8-bit via everything */ actual = "RGBA"; break; case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */ actual = "L"; break; + case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ + actual = "RGB"; + break; case 6: /* BC6: 3-channel 16-bit float */ /* TODO: support 4-channel floating point images */ actual = "RGBAF"; @@ -389,14 +392,14 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { return NULL; } - decoder = PyImaging_DecoderNew(0); + decoder = PyImaging_DecoderNew(sizeof(char *)); if (decoder == NULL) { return NULL; } decoder->decode = ImagingBcnDecode; decoder->state.state = n; - decoder->state.ystep = ystep; + ((BCNSTATE *)decoder->state.context)->pixel_format = pixel_format; return (PyObject *)decoder; } @@ -497,7 +500,7 @@ PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) { char *rawmode; char *compname; int fp; - uint32 ifdoffset; + uint32_t ifdoffset; if (!PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) { return NULL; diff --git a/src/display.c b/src/display.c index 3541655cf..0ce10e249 100644 --- a/src/display.c +++ b/src/display.c @@ -224,14 +224,14 @@ _tobytes(ImagingDisplayObject *display, PyObject *args) { } static struct PyMethodDef methods[] = { - {"draw", (PyCFunction)_draw, 1}, - {"expose", (PyCFunction)_expose, 1}, - {"paste", (PyCFunction)_paste, 1}, - {"query_palette", (PyCFunction)_query_palette, 1}, - {"getdc", (PyCFunction)_getdc, 1}, - {"releasedc", (PyCFunction)_releasedc, 1}, - {"frombytes", (PyCFunction)_frombytes, 1}, - {"tobytes", (PyCFunction)_tobytes, 1}, + {"draw", (PyCFunction)_draw, METH_VARARGS}, + {"expose", (PyCFunction)_expose, METH_VARARGS}, + {"paste", (PyCFunction)_paste, METH_VARARGS}, + {"query_palette", (PyCFunction)_query_palette, METH_VARARGS}, + {"getdc", (PyCFunction)_getdc, METH_VARARGS}, + {"releasedc", (PyCFunction)_releasedc, METH_VARARGS}, + {"frombytes", (PyCFunction)_frombytes, METH_VARARGS}, + {"tobytes", (PyCFunction)_tobytes, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/encode.c b/src/encode.c index f92ba62c2..2ecf9723b 100644 --- a/src/encode.c +++ b/src/encode.c @@ -264,7 +264,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { } state->bytes = (state->bits * state->xsize + 7) / 8; /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes); + state->buffer = (UINT8 *)calloc(1, state->bytes); if (!state->buffer) { return ImagingError_MemoryError(); } @@ -299,17 +299,17 @@ _setfd(ImagingEncoderObject *encoder, PyObject *args) { } static PyObject * -_get_pushes_fd(ImagingEncoderObject *encoder) { +_get_pushes_fd(ImagingEncoderObject *encoder, void *closure) { return PyBool_FromLong(encoder->pushes_fd); } static struct PyMethodDef methods[] = { - {"encode", (PyCFunction)_encode, 1}, - {"cleanup", (PyCFunction)_encode_cleanup, 1}, - {"encode_to_file", (PyCFunction)_encode_to_file, 1}, - {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, 1}, - {"setimage", (PyCFunction)_setimage, 1}, - {"setfd", (PyCFunction)_setfd, 1}, + {"encode", (PyCFunction)_encode, METH_VARARGS}, + {"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS}, + {"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS}, + {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_VARARGS}, + {"setimage", (PyCFunction)_setimage, METH_VARARGS}, + {"setfd", (PyCFunction)_setfd, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; @@ -644,10 +644,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { int key_int, status, is_core_tag, is_var_length, num_core_tags, i; TIFFDataType type = TIFF_NOTYPE; // This list also exists in TiffTags.py - const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, - 277, 278, 280, 281, 340, 341, 282, 283, 284, - 286, 287, 296, 297, 320, 321, 338, 32995, 32998, - 32996, 339, 32997, 330, 531, 530, 65537}; + const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, + 277, 278, 280, 281, 340, 341, 282, 283, 284, + 286, 287, 296, 297, 320, 321, 338, 32995, 32998, + 32996, 339, 32997, 330, 531, 530, 65537, 301, 532}; Py_ssize_t tags_size; PyObject *item; @@ -790,7 +790,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { int stride = 256; if (len != 768) { PyErr_SetString( - PyExc_ValueError, "Requiring 768 items for for Colormap"); + PyExc_ValueError, "Requiring 768 items for Colormap"); return NULL; } UINT16 *av; @@ -808,6 +808,12 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av + stride * 2); free(av); } + } else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) { + status = ImagingLibTiffSetField( + &encoder->state, + (ttag_t)key_int, + (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)), + (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1))); } else if (type == TIFF_SHORT) { UINT16 *av; /* malloc check ok, calloc checks for overflow */ diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 6bb16fe3a..514fb2929 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -11,7 +11,7 @@ #include "Imaging.h" -/* use Tests/make_hash.py to calculate these values */ +/* use make_hash.py from the pillow-scripts repository to calculate these values */ #define ACCESS_TABLE_SIZE 27 #define ACCESS_TABLE_HASH 3078 diff --git a/src/libImaging/Bcn.h b/src/libImaging/Bcn.h new file mode 100644 index 000000000..1a6fbee45 --- /dev/null +++ b/src/libImaging/Bcn.h @@ -0,0 +1,3 @@ +typedef struct { + char *pixel_format; +} BCNSTATE; diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index b6a4cbadc..22b36eb7a 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -13,6 +13,8 @@ #include "Imaging.h" +#include "Bcn.h" + typedef struct { UINT8 r, g, b, a; } rgba; @@ -35,6 +37,11 @@ typedef struct { UINT8 lut[6]; } bc3_alpha; +typedef struct { + INT8 a0, a1; + UINT8 lut[6]; +} bc5s_alpha; + #define LOAD16(p) (p)[0] | ((p)[1] << 8) #define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) @@ -46,11 +53,6 @@ bc1_color_load(bc1_color *dst, const UINT8 *src) { dst->lut = LOAD32(src + 4); } -static void -bc3_alpha_load(bc3_alpha *dst, const UINT8 *src) { - memcpy(dst, src, sizeof(bc3_alpha)); -} - static rgba decode_565(UINT16 x) { rgba c; @@ -69,7 +71,7 @@ decode_565(UINT16 x) { } static void -decode_bc1_color(rgba *dst, const UINT8 *src) { +decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { bc1_color col; rgba p[4]; int n, cw; @@ -84,7 +86,10 @@ decode_bc1_color(rgba *dst, const UINT8 *src) { r1 = p[1].r; g1 = p[1].g; b1 = p[1].b; - if (col.c0 > col.c1) { + + + /* NOTE: BC2 and BC3 reuse BC1 color blocks but always act like c0 > c1 */ + if (col.c0 > col.c1 || separate_alpha) { p[2].r = (2 * r0 + 1 * r1) / 3; p[2].g = (2 * g0 + 1 * g1) / 3; p[2].b = (2 * b0 + 1 * b1) / 3; @@ -110,15 +115,26 @@ decode_bc1_color(rgba *dst, const UINT8 *src) { } static void -decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) { - bc3_alpha b; +decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o, int sign) { UINT16 a0, a1; UINT8 a[8]; - int n, lut, aw; - bc3_alpha_load(&b, src); + int n, lut1, lut2, aw; + if (sign == 1) { + bc5s_alpha b; + memcpy(&b, src, sizeof(bc5s_alpha)); + a0 = (b.a0 + 255) / 2; + a1 = (b.a1 + 255) / 2; + lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); + lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); + } else { + bc3_alpha b; + memcpy(&b, src, sizeof(bc3_alpha)); + a0 = b.a0; + a1 = b.a1; + lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); + lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); + } - a0 = b.a0; - a1 = b.a1; a[0] = (UINT8)a0; a[1] = (UINT8)a1; if (a0 > a1) { @@ -136,27 +152,25 @@ decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) { a[6] = 0; a[7] = 0xff; } - lut = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); for (n = 0; n < 8; n++) { - aw = 7 & (lut >> (3 * n)); + aw = 7 & (lut1 >> (3 * n)); dst[stride * n + o] = a[aw]; } - lut = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); for (n = 0; n < 8; n++) { - aw = 7 & (lut >> (3 * n)); + aw = 7 & (lut2 >> (3 * n)); dst[stride * (8 + n) + o] = a[aw]; } } static void decode_bc1_block(rgba *col, const UINT8 *src) { - decode_bc1_color(col, src); + decode_bc1_color(col, src, 0); } static void decode_bc2_block(rgba *col, const UINT8 *src) { int n, bitI, byI, av; - decode_bc1_color(col, src + 8); + decode_bc1_color(col, src + 8, 1); for (n = 0; n < 16; n++) { bitI = n * 4; byI = bitI >> 3; @@ -168,19 +182,19 @@ decode_bc2_block(rgba *col, const UINT8 *src) { static void decode_bc3_block(rgba *col, const UINT8 *src) { - decode_bc1_color(col, src + 8); - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3); + decode_bc1_color(col, src + 8, 1); + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3, 0); } static void decode_bc4_block(lum *col, const UINT8 *src) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, 0); } static void -decode_bc5_block(rgba *col, const UINT8 *src) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); - decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1); +decode_bc5_block(rgba *col, const UINT8 *src, int sign) { + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, sign); + decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1, sign); } /* BC6 and BC7 are described here: @@ -810,7 +824,7 @@ put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) { static int decode_bcn( - Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C) { + Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C, char *pixel_format) { int ymax = state->ysize + state->yoff; const UINT8 *ptr = src; switch (N) { @@ -833,7 +847,19 @@ decode_bcn( DECODE_LOOP(2, 16, rgba); DECODE_LOOP(3, 16, rgba); DECODE_LOOP(4, 8, lum); - DECODE_LOOP(5, 16, rgba); + case 5: + while (bytes >= 16) { + rgba col[16]; + memset(col, 0, 16 * sizeof(col[0])); + decode_bc5_block(col, ptr, strcmp(pixel_format, "BC5S") == 0 ? 1 : 0); + put_block(im, state, (const char *)col, sizeof(col[0]), C); + ptr += 16; + bytes -= 16; + if (state->y >= ymax) { + return -1; + } + } + break; case 6: while (bytes >= 16) { rgb32f col[16]; @@ -846,7 +872,7 @@ decode_bcn( } } break; - DECODE_LOOP(7, 16, rgba); + DECODE_LOOP(7, 16, rgba); #undef DECODE_LOOP } return (int)(ptr - src); @@ -857,9 +883,7 @@ ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt int N = state->state & 0xf; int width = state->xsize; int height = state->ysize; - if ((width & 3) | (height & 3)) { - return decode_bcn(im, state, buf, bytes, N, 1); - } else { - return decode_bcn(im, state, buf, bytes, N, 0); - } + int C = (width & 3) | (height & 3) ? 1 : 0; + char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; + return decode_bcn(im, state, buf, bytes, N, C, pixel_format); } diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 88862eb73..2e45a3358 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -287,7 +287,7 @@ ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { float sigma2, L, l, a; sigma2 = radius * radius / passes; - // from http://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf + // from https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf // [7] Box length. L = sqrt(12.0 * sigma2 + 1.0); // [11] Integer part of box radius. diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 8c7be36a2..517a4dbe3 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1013,7 +1013,7 @@ p2l(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { int x; /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++) { - *out++ = L(&palette[in[x] * 4]) / 1000; + *out++ = L24(&palette[in[x] * 4]) >> 16; } } @@ -1022,7 +1022,7 @@ pa2l(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { int x; /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++, in += 4) { - *out++ = L(&palette[in[0] * 4]) / 1000; + *out++ = L24(&palette[in[0] * 4]) >> 16; } } @@ -1044,7 +1044,7 @@ p2la(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++, out += 4) { const UINT8 *rgba = &palette[*in++ * 4]; - out[0] = out[1] = out[2] = L(rgba) / 1000; + out[0] = out[1] = out[2] = L24(rgba) >> 16; out[3] = rgba[3]; } } @@ -1054,7 +1054,7 @@ pa2la(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { int x; /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++, in += 4, out += 4) { - out[0] = out[1] = out[2] = L(&palette[in[0] * 4]) / 1000; + out[0] = out[1] = out[2] = L24(&palette[in[0] * 4]) >> 16; out[3] = in[3]; } } @@ -1063,7 +1063,7 @@ static void p2i(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { int x; for (x = 0; x < xsize; x++, out_ += 4) { - INT32 v = L(&palette[in[x] * 4]) / 1000; + INT32 v = L24(&palette[in[x] * 4]) >> 16; memcpy(out_, &v, sizeof(v)); } } @@ -1073,7 +1073,7 @@ pa2i(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { int x; INT32 *out = (INT32 *)out_; for (x = 0; x < xsize; x++, in += 4) { - *out++ = L(&palette[in[0] * 4]) / 1000; + *out++ = L24(&palette[in[0] * 4]) >> 16; } } @@ -1594,9 +1594,8 @@ convert( #ifdef notdef return (Imaging)ImagingError_ValueError("conversion not supported"); #else - static char buf[256]; - /* FIXME: may overflow if mode is too large */ - sprintf(buf, "conversion from %s to %s not supported", imIn->mode, mode); + static char buf[100]; + snprintf(buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode); return (Imaging)ImagingError_ValueError(buf); #endif } @@ -1645,11 +1644,11 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { } #else { - static char buf[256]; - /* FIXME: may overflow if mode is too large */ - sprintf( + static char buf[100]; + snprintf( buf, - "conversion from %s to %s not supported in convert_transparent", + 100, + "conversion from %.10s to %.10s not supported in convert_transparent", imIn->mode, mode); return (Imaging)ImagingError_ValueError(buf); diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index b6f63b7e8..0e4899b5b 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -444,7 +444,7 @@ draw_horizontal_lines( * Filled polygon draw function using scan line algorithm. */ static inline int -polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline) { +polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha) { Edge **edge_table; float *xx; int edge_count = 0; @@ -471,6 +471,9 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h ymax = e[i].ymax; } if (e[i].ymin == e[i].ymax) { + if (hasAlpha != 1) { + (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); + } continue; } edge_table[edge_count++] = (e + i); @@ -491,7 +494,6 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h } for (; ymin <= ymax; ymin++) { int j = 0; - int x_pos = 0; for (i = 0; i < edge_count; i++) { Edge *current = edge_table[i]; if (ymin >= current->ymin && ymin <= current->ymax) { @@ -504,31 +506,38 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h } } qsort(xx, j, sizeof(float), x_cmp); - for (i = 1; i < j; i += 2) { - int x_end = ROUND_DOWN(xx[i]); - if (x_end < x_pos) { - // Line would be before the current position - continue; - } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); - if (x_end < x_pos) { - // Line would be before the current position - continue; - } - - int x_start = ROUND_UP(xx[i - 1]); - if (x_pos > x_start) { - // Line would be partway through x_pos, so increase the starting point - x_start = x_pos; - if (x_end < x_start) { - // Line would now end before it started + if (hasAlpha == 1) { + int x_pos = 0; + for (i = 1; i < j; i += 2) { + int x_end = ROUND_DOWN(xx[i]); + if (x_end < x_pos) { + // Line would be before the current position continue; } + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + if (x_end < x_pos) { + // Line would be before the current position + continue; + } + + int x_start = ROUND_UP(xx[i - 1]); + if (x_pos > x_start) { + // Line would be partway through x_pos, so increase the starting point + x_start = x_pos; + if (x_end < x_start) { + // Line would now end before it started + continue; + } + } + (*hline)(im, x_start, ymin, x_end, ink); + x_pos = x_end + 1; + } + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + } else { + for (i = 1; i < j; i += 2) { + (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); } - (*hline)(im, x_start, ymin, x_end, ink); - x_pos = x_end + 1; } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); } free(xx); @@ -538,17 +547,17 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h static inline int polygon8(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline8); + return polygon_generic(im, n, e, ink, eofill, hline8, 0); } static inline int polygon32(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32); + return polygon_generic(im, n, e, ink, eofill, hline32, 0); } static inline int polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32rgba); + return polygon_generic(im, n, e, ink, eofill, hline32rgba, 1); } static inline void @@ -733,8 +742,8 @@ ImagingDrawRectangle( } int -ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, int op) { - int i, n; +ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op) { + int i, n, x0, y0, x1, y1; DRAW *draw; INT32 ink; @@ -753,20 +762,45 @@ ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, i return -1; } for (i = n = 0; i < count - 1; i++) { - add_edge(&e[n++], xy[i + i], xy[i + i + 1], xy[i + i + 2], xy[i + i + 3]); + x0 = xy[i * 2]; + y0 = xy[i * 2 + 1]; + x1 = xy[i * 2 + 2]; + y1 = xy[i * 2 + 3]; + if (y0 == y1 && i != 0 && y0 == xy[i * 2 - 1]) { + // This is a horizontal line, + // that immediately follows another horizontal line + Edge *last_e = &e[n-1]; + if (x1 > x0 && x0 > xy[i * 2 - 2]) { + // They are both increasing in x + last_e->xmax = x1; + continue; + } else if (x1 < x0 && x0 < xy[i * 2 - 2]) { + // They are both decreasing in x + last_e->xmin = x1; + continue; + } + } + add_edge(&e[n++], x0, y0, x1, y1); } - if (xy[i + i] != xy[0] || xy[i + i + 1] != xy[1]) { - add_edge(&e[n++], xy[i + i], xy[i + i + 1], xy[0], xy[1]); + if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { + add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); } draw->polygon(im, n, e, ink, 0); free(e); } else { /* Outline */ - for (i = 0; i < count - 1; i++) { - draw->line(im, xy[i + i], xy[i + i + 1], xy[i + i + 2], xy[i + i + 3], ink); + if (width == 1) { + for (i = 0; i < count - 1; i++) { + draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink); + } + draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink); + } else { + for (i = 0; i < count - 1; i++) { + ImagingDrawWideLine(im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink_, width, op); + } + ImagingDrawWideLine(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op); } - draw->line(im, xy[i + i], xy[i + i + 1], xy[0], xy[1], ink); } return 0; @@ -1347,6 +1381,22 @@ pie_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float s->root->l = lc; s->root->r = rc; s->root->type = ar - al < 180 ? CT_AND : CT_OR; + + // add one more semiplane to avoid spikes + if (ar - al < 90) { + clip_node *old_root = s->root; + clip_node *spike_clipper = s->nodes + s->node_count++; + s->root = s->nodes + s->node_count++; + s->root->l = old_root; + s->root->r = spike_clipper; + s->root->type = CT_AND; + + spike_clipper->l = spike_clipper->r = NULL; + spike_clipper->type = CT_CLIP; + spike_clipper->a = (xl + xr) / 2.0; + spike_clipper->b = (yl + yr) / 2.0; + spike_clipper->c = 0; + } } void @@ -1804,14 +1854,8 @@ ImagingOutlineTransform(ImagingOutline outline, double a[6]) { eIn = outline->edges; n = outline->count; - /* FIXME: ugly! */ - outline->edges = NULL; - outline->count = outline->size = 0; - eOut = allocate(outline, n); if (!eOut) { - outline->edges = eIn; - outline->count = outline->size = n; ImagingError_MemoryError(); return -1; } @@ -1847,7 +1891,11 @@ ImagingOutlineTransform(ImagingOutline outline, double a[6]) { eOut++; } - free(eIn); + free(outline->edges); + + /* FIXME: ugly! */ + outline->edges = NULL; + outline->count = outline->size = 0; return 0; } diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index e9000fc99..d6e4ea0ff 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -46,7 +46,8 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt ptr = buf; framesize = I32(ptr); - if (framesize < I32(ptr)) { + // there can be one pad byte in the framesize + if (bytes + (bytes % 2) < framesize) { return 0; } @@ -223,8 +224,15 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt break; case 16: /* COPY chunk */ - if (state->xsize > bytes / state->ysize) { + if (INT32_MAX / state->xsize < state->ysize) { + /* Integer overflow, bail */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + /* Note, have to check Data + size, not just ptr + size) */ + if (data + (state->xsize * state->ysize) > ptr + bytes) { /* not enough data for frame */ + /* UNDONE Unclear that we're actually going to leave the buffer at the right place. */ return ptr - buf; /* bytes consumed */ } for (y = 0; y < state->ysize; y++) { @@ -243,6 +251,11 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt return -1; } advance = I32(ptr); + if (advance == 0 ) { + // If there's no advance, we're in an infinite loop + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } if (advance < 0 || advance > bytes) { state->errcode = IMAGING_CODEC_OVERRUN; return -1; diff --git a/src/libImaging/Gif.h b/src/libImaging/Gif.h index a85ce2b6e..4029bbfe5 100644 --- a/src/libImaging/Gif.h +++ b/src/libImaging/Gif.h @@ -9,10 +9,10 @@ /* Max size for a LZW code word. */ -#define GIFBITS 12 +#define GIFBITS 12 -#define GIFTABLE (1 << GIFBITS) -#define GIFBUFFER (1 << GIFBITS) +#define GIFTABLE (1<next < GIFTABLE) { /* We'll only add this symbol if we have room - for it (take advise, Netscape!) */ + for it (take the advice, Netscape!) */ context->data[context->next] = c; context->link[context->next] = context->lastcode; @@ -248,8 +248,6 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t /* To squeeze some extra pixels out of this loop, we test for some common cases and handle them separately. */ - /* FIXME: should we handle the transparency index in here??? */ - if (i == 1) { if (state->x < state->xsize - 1) { /* Single pixel, not at the end of the line. */ diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index e9064b3f5..f23245405 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -10,6 +10,7 @@ * 98-07-09 fl added interlace write support * 99-02-07 fl rewritten, now uses a run-length encoding strategy * 99-02-08 fl improved run-length encoding for long runs + * 2020-12-12 rdg Reworked for LZW compression. * * Copyright (c) Secret Labs AB 1997-99. * Copyright (c) Fredrik Lundh 1997. @@ -21,136 +22,206 @@ #include "Gif.h" -/* codes from 0 to 255 are literals */ -#define CLEAR_CODE 256 -#define EOF_CODE 257 -#define FIRST_CODE 258 -#define LAST_CODE 511 +enum { INIT, ENCODE, FINISH }; -enum { INIT, ENCODE, ENCODE_EOF, FLUSH, EXIT }; +/* GIF LZW encoder by Raymond Gardner. */ +/* Released here under PIL license. */ -/* to make things a little less complicated, we use a simple output - queue to hold completed blocks. the following inlined function - adds a byte to the current block. it allocates a new block if - necessary. */ +/* This LZW encoder conforms to the GIF LZW format specified in the original + * Compuserve GIF 87a and GIF 89a specifications (see e.g. + * https://www.w3.org/Graphics/GIF/spec-gif87.txt Appendix C and + * https://www.w3.org/Graphics/GIF/spec-gif89a.txt Appendix F). + */ -static inline int -emit(GIFENCODERSTATE *context, int byte) { - /* write a byte to the output buffer */ +/* Return values */ +#define GLZW_OK 0 +#define GLZW_NO_INPUT_AVAIL 1 +#define GLZW_NO_OUTPUT_AVAIL 2 +#define GLZW_INTERNAL_ERROR 3 - if (!context->block || context->block->size == 255) { - GIFENCODERBLOCK *block; +#define CODE_LIMIT 4096 - /* no room in the current block (or no current block); - allocate a new one */ +/* Values of entry_state */ +enum { LZW_INITIAL, LZW_TRY_IN1, LZW_TRY_IN2, LZW_TRY_OUT1, LZW_TRY_OUT2, + LZW_FINISHED }; - /* add current block to end of flush queue */ - if (context->block) { - block = context->flush; - while (block && block->next) { - block = block->next; - } - if (block) { - block->next = context->block; - } else { - context->flush = context->block; - } - } +/* Values of control_state */ +enum { PUT_HEAD, PUT_INIT_CLEAR, PUT_CLEAR, PUT_LAST_HEAD, PUT_END }; - /* get a new block */ - if (context->free) { - block = context->free; - context->free = NULL; - } else { - /* malloc check ok, small constant allocation */ - block = malloc(sizeof(GIFENCODERBLOCK)); - if (!block) { - return 0; - } - } - - block->size = 0; - block->next = NULL; - - context->block = block; - } - - /* write new byte to block */ - context->block->data[context->block->size++] = byte; - - return 1; +static void glzwe_reset(GIFENCODERSTATE *st) { + st->next_code = st->end_code + 1; + st->max_code = 2 * st->clear_code - 1; + st->code_width = st->bits + 1; + memset(st->codes, 0, sizeof(st->codes)); } -/* write a code word to the current block. this is a macro to make - sure it's inlined on all platforms */ +static void glzwe_init(GIFENCODERSTATE *st) { + st->clear_code = 1 << st->bits; + st->end_code = st->clear_code + 1; + glzwe_reset(st); + st->entry_state = LZW_INITIAL; + st->buf_bits_left = 8; + st->code_buffer = 0; +} -#define EMIT(code) \ - { \ - context->bitbuffer |= ((INT32)(code)) << context->bitcount; \ - context->bitcount += 9; \ - while (context->bitcount >= 8) { \ - if (!emit(context, (UINT8)context->bitbuffer)) { \ - state->errcode = IMAGING_CODEC_MEMORY; \ - return 0; \ - } \ - context->bitbuffer >>= 8; \ - context->bitcount -= 8; \ - } \ - } - -/* write a run. we use a combination of literals and combinations of - literals. this can give quite decent compression for images with - long stretches of identical pixels. but remember: if you want - really good compression, use another file format. */ - -#define EMIT_RUN(label) \ - { \ - label: \ - while (context->count > 0) { \ - int run = 2; \ - EMIT(context->last); \ - context->count--; \ - if (state->count++ == LAST_CODE) { \ - EMIT(CLEAR_CODE); \ - state->count = FIRST_CODE; \ - goto label; \ - } \ - while (context->count >= run) { \ - EMIT(state->count - 1); \ - context->count -= run; \ - run++; \ - if (state->count++ == LAST_CODE) { \ - EMIT(CLEAR_CODE); \ - state->count = FIRST_CODE; \ - goto label; \ - } \ - } \ - if (context->count > 1) { \ - EMIT(state->count - 1 - (run - context->count)); \ - context->count = 0; \ - if (state->count++ == LAST_CODE) { \ - EMIT(CLEAR_CODE); \ - state->count = FIRST_CODE; \ - } \ - break; \ - } \ - } \ +static int glzwe(GIFENCODERSTATE *st, const UINT8 *in_ptr, UINT8 *out_ptr, + UINT32 *in_avail, UINT32 *out_avail, + UINT32 end_of_data) { + switch (st->entry_state) { + + case LZW_TRY_IN1: +get_first_byte: + if (!*in_avail) { + if (end_of_data) { + goto end_of_data; + } + st->entry_state = LZW_TRY_IN1; + return GLZW_NO_INPUT_AVAIL; + } + st->head = *in_ptr++; + (*in_avail)--; + + case LZW_TRY_IN2: +encode_loop: + if (!*in_avail) { + if (end_of_data) { + st->code = st->head; + st->put_state = PUT_LAST_HEAD; + goto put_code; + } + st->entry_state = LZW_TRY_IN2; + return GLZW_NO_INPUT_AVAIL; + } + st->tail = *in_ptr++; + (*in_avail)--; + + /* Knuth TAOCP vol 3 sec. 6.4 algorithm D. */ + /* Hash found experimentally to be pretty good. */ + /* This works ONLY with TABLE_SIZE a power of 2. */ + st->probe = ((st->head ^ (st->tail << 6)) * 31) & (TABLE_SIZE - 1); + while (st->codes[st->probe]) { + if ((st->codes[st->probe] & 0xFFFFF) == + ((st->head << 8) | st->tail)) { + st->head = st->codes[st->probe] >> 20; + goto encode_loop; + } else { + /* Reprobe decrement must be nonzero and relatively prime to table + * size. So, any odd positive number for power-of-2 size. */ + if ((st->probe -= ((st->tail << 2) | 1)) < 0) { + st->probe += TABLE_SIZE; + } + } + } + /* Key not found, probe is at empty slot. */ + st->code = st->head; + st->put_state = PUT_HEAD; + goto put_code; +insert_code_or_clear: /* jump here after put_code */ + if (st->next_code < CODE_LIMIT) { + st->codes[st->probe] = (st->next_code << 20) | + (st->head << 8) | st->tail; + if (st->next_code > st->max_code) { + st->max_code = st->max_code * 2 + 1; + st->code_width++; + } + st->next_code++; + } else { + st->code = st->clear_code; + st->put_state = PUT_CLEAR; + goto put_code; +reset_after_clear: /* jump here after put_code */ + glzwe_reset(st); + } + st->head = st->tail; + goto encode_loop; + + case LZW_INITIAL: + glzwe_reset(st); + st->code = st->clear_code; + st->put_state = PUT_INIT_CLEAR; +put_code: + st->code_bits_left = st->code_width; +check_buf_bits: + if (!st->buf_bits_left) { /* out buffer full */ + + case LZW_TRY_OUT1: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT1; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + st->code_buffer = 0; + st->buf_bits_left = 8; + } + /* code bits to pack */ + UINT32 n = st->buf_bits_left < st->code_bits_left + ? st->buf_bits_left : st->code_bits_left; + st->code_buffer |= + (st->code & ((1 << n) - 1)) << (8 - st->buf_bits_left); + st->code >>= n; + st->buf_bits_left -= n; + st->code_bits_left -= n; + if (st->code_bits_left) { + goto check_buf_bits; + } + switch (st->put_state) { + case PUT_INIT_CLEAR: + goto get_first_byte; + case PUT_HEAD: + goto insert_code_or_clear; + case PUT_CLEAR: + goto reset_after_clear; + case PUT_LAST_HEAD: + goto end_of_data; + case PUT_END: + goto flush_code_buffer; + default: + return GLZW_INTERNAL_ERROR; + } + +end_of_data: + st->code = st->end_code; + st->put_state = PUT_END; + goto put_code; +flush_code_buffer: /* jump here after put_code */ + if (st->buf_bits_left < 8) { + + case LZW_TRY_OUT2: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT2; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + } + st->entry_state = LZW_FINISHED; + return GLZW_OK; + + case LZW_FINISHED: + return GLZW_OK; + + default: + return GLZW_INTERNAL_ERROR; } +} +/* -END- GIF LZW encoder. */ int -ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - UINT8 *ptr; - int this; +ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { + UINT8* ptr; + UINT8* sub_block_ptr; + UINT8* sub_block_limit; + UINT8* buf_limit; + GIFENCODERSTATE *context = (GIFENCODERSTATE*) state->context; + int r; - GIFENCODERBLOCK *block; - GIFENCODERSTATE *context = (GIFENCODERSTATE *)state->context; + UINT32 in_avail, in_used; + UINT32 out_avail, out_used; - if (!state->state) { - /* place a clear code in the output buffer */ - context->bitbuffer = CLEAR_CODE; - context->bitcount = 9; - - state->count = FIRST_CODE; + if (state->state == INIT) { + state->state = ENCODE; + glzwe_init(context); if (context->interlace) { context->interlace = 1; @@ -159,166 +230,132 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { context->step = 1; } - context->last = -1; - + /* Need at least 2 bytes for data sub-block; 5 for empty image */ + if (bytes < 5) { + state->errcode = IMAGING_CODEC_CONFIG; + return 0; + } /* sanity check */ if (state->xsize <= 0 || state->ysize <= 0) { - state->state = ENCODE_EOF; + /* Is this better than an error return? */ + /* This will handle any legal "LZW Minimum Code Size" */ + memset(buf, 0, 5); + in_avail = 0; + out_avail = 5; + r = glzwe(context, (const UINT8 *)"", buf + 1, &in_avail, &out_avail, 1); + if (r == GLZW_OK) { + r = 5 - out_avail; + if (r < 1 || r > 3) { + state->errcode = IMAGING_CODEC_BROKEN; + return 0; + } + buf[0] = r; + state->errcode = IMAGING_CODEC_END; + return r + 2; + } else { + /* Should not be possible unless something external to this + * routine messes with our state data */ + state->errcode = IMAGING_CODEC_BROKEN; + return 0; + } } + /* Init state->x to make if() below true the first time through. */ + state->x = state->xsize; } - ptr = buf; + buf_limit = buf + bytes; + sub_block_limit = sub_block_ptr = ptr = buf; + /* On entry, buf is output buffer, bytes is space available in buf. + * Loop here getting input until buf is full or image is all encoded. */ for (;;) { - switch (state->state) { - case INIT: - case ENCODE: + /* Set up sub-block ptr and limit. sub_block_ptr stays at beginning + * of sub-block until it is full. ptr will advance when any data is + * placed in buf. + */ + if (ptr >= sub_block_limit) { + if (buf_limit - ptr < 2) { /* Need at least 2 for data sub-block */ + return ptr - buf; + } + sub_block_ptr = ptr; + sub_block_limit = sub_block_ptr + + (256 < buf_limit - sub_block_ptr ? + 256 : buf_limit - sub_block_ptr); + *ptr++ = 0; + } - /* identify and store a run of pixels */ + /* Get next row of pixels. */ + /* This if() originally tested state->x==0 for the first time through. + * This no longer works, as the loop will not advance state->x if + * glzwe() does not consume any input; this would advance the row + * spuriously. Now pre-init state->x above for first time, and avoid + * entering if() when state->state is FINISH, or it will loop + * infinitely. + */ + if (state->x >= state->xsize && state->state == ENCODE) { + if (!context->interlace && state->y >= state->ysize) { + state->state = FINISH; + continue; + } - if (state->x == 0 || state->x >= state->xsize) { - if (!context->interlace && state->y >= state->ysize) { - state->state = ENCODE_EOF; + /* get another line of data */ + state->shuffle( + state->buffer, + (UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->xsize + ); + state->x = 0; + + /* step forward, according to the interlace settings */ + state->y += context->step; + while (context->interlace && state->y >= state->ysize) { + switch (context->interlace) { + case 1: + state->y = 4; + context->interlace = 2; break; - } - - if (context->flush) { - state->state = FLUSH; + case 2: + context->step = 4; + state->y = 2; + context->interlace = 3; break; - } - - /* get another line of data */ - state->shuffle( - state->buffer, - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - - state->x = 0; - - if (state->state == INIT) { - /* preload the run-length buffer and get going */ - context->last = state->buffer[0]; - context->count = state->x = 1; - state->state = ENCODE; - } - - /* step forward, according to the interlace settings */ - state->y += context->step; - while (context->interlace && state->y >= state->ysize) - switch (context->interlace) { - case 1: - state->y = 4; - context->interlace = 2; - break; - case 2: - context->step = 4; - state->y = 2; - context->interlace = 3; - break; - case 3: - context->step = 2; - state->y = 1; - context->interlace = 0; - break; - default: - /* just make sure we don't loop forever */ - context->interlace = 0; - } - } - /* Potential special case for xsize==1 */ - if (state->x < state->xsize) { - this = state->buffer[state->x++]; - } else { - EMIT_RUN(label0); - break; + case 3: + context->step = 2; + state->y = 1; + context->interlace = 0; + break; + default: + /* just make sure we don't loop forever */ + context->interlace = 0; } + } + } - if (this == context->last) { - context->count++; - } else { - EMIT_RUN(label1); - context->last = this; - context->count = 1; - } - break; + in_avail = state->xsize - state->x; /* bytes left in line */ + out_avail = sub_block_limit - ptr; /* bytes left in sub-block */ + r = glzwe(context, &state->buffer[state->x], ptr, &in_avail, + &out_avail, state->state == FINISH); + out_used = sub_block_limit - ptr - out_avail; + *sub_block_ptr += out_used; + ptr += out_used; + in_used = state->xsize - state->x - in_avail; + state->x += in_used; - case ENCODE_EOF: - - /* write the final run */ - EMIT_RUN(label2); - - /* write an end of image marker */ - EMIT(EOF_CODE); - - /* empty the bit buffer */ - while (context->bitcount > 0) { - if (!emit(context, (UINT8)context->bitbuffer)) { - state->errcode = IMAGING_CODEC_MEMORY; - return 0; - } - context->bitbuffer >>= 8; - context->bitcount -= 8; - } - - /* flush the last block, and exit */ - if (context->block) { - GIFENCODERBLOCK *block; - block = context->flush; - while (block && block->next) { - block = block->next; - } - if (block) { - block->next = context->block; - } else { - context->flush = context->block; - } - context->block = NULL; - } - - state->state = EXIT; - - /* fall through... */ - - case EXIT: - case FLUSH: - - while (context->flush) { - /* get a block from the flush queue */ - block = context->flush; - - if (block->size > 0) { - /* make sure it fits into the output buffer */ - if (bytes < block->size + 1) { - return ptr - buf; - } - - ptr[0] = block->size; - memcpy(ptr + 1, block->data, block->size); - - ptr += block->size + 1; - bytes -= block->size + 1; - } - - context->flush = block->next; - - if (context->free) { - free(context->free); - } - context->free = block; - } - - if (state->state == EXIT) { - /* this was the last block! */ - if (context->free) { - free(context->free); - } - state->errcode = IMAGING_CODEC_END; - return ptr - buf; - } - - state->state = ENCODE; - break; + if (r == GLZW_OK) { + /* Should not be possible when end-of-data flag is false. */ + state->errcode = IMAGING_CODEC_END; + return ptr - buf; + } else if (r == GLZW_NO_INPUT_AVAIL) { + /* Used all the input line; get another line */ + continue; + } else if (r == GLZW_NO_OUTPUT_AVAIL) { + /* subblock is full */ + continue; + } else { + /* Should not be possible unless something external to this + * routine messes with our state data */ + state->errcode = IMAGING_CODEC_BROKEN; + return 0; } } } diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 9a2060edf..af9996ca9 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -9,12 +9,6 @@ #include "Python.h" -/* Workaround issue #2479 */ -#if PY_VERSION_HEX < 0x03070000 && defined(PySlice_GetIndicesEx) && \ - !defined(PYPY_VERSION) -#undef PySlice_GetIndicesEx -#endif - /* Check that we have an ANSI compliant compiler */ #ifndef HAVE_PROTOTYPES #error Sorry, this library requires support for ANSI prototypes. @@ -31,11 +25,18 @@ #endif #endif -#ifdef _WIN32 +#if defined(_WIN32) || defined(__CYGWIN__) #define WIN32_LEAN_AND_MEAN #include +#ifdef __CYGWIN__ +#undef _WIN64 +#undef _WIN32 +#undef __WIN32__ +#undef WIN32 +#endif + #else /* For System that are not Windows, we'll need to define these. */ diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index ae323f390..9b1c1024d 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -370,7 +370,7 @@ ImagingTransform( int y0, int x1, int y1, - double *a, + double a[8], int filter, int fill); extern Imaging @@ -487,7 +487,7 @@ ImagingDrawPieslice( extern int ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); extern int -ImagingDrawPolygon(Imaging im, int points, int *xy, const void *ink, int fill, int op); +ImagingDrawPolygon(Imaging im, int points, int *xy, const void *ink, int fill, int width, int op); extern int ImagingDrawRectangle( Imaging im, diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index ad6f280ac..0c0c1eda9 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -29,7 +29,7 @@ /* This is to work around a bug in GCC prior 4.9 in 64 bit mode. GCC generates code with partial dependency which is 3 times slower. - See: http://stackoverflow.com/a/26588074/253146 */ + See: https://stackoverflow.com/a/26588074/253146 */ #if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \ !defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) static float __attribute__((always_inline)) inline _i2f(int v) { diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index a2a7354db..8f27d87d8 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -73,6 +73,8 @@ struct j2k_decode_unpacker { const char *mode; OPJ_COLOR_SPACE color_space; unsigned components; + /* bool indicating if unpacker supports subsampling */ + int subsampling; j2k_unpacker_t unpacker; }; @@ -178,9 +180,11 @@ j2ku_gray_i( case 2: for (y = 0; y < h; ++y) { const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; - UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; for (x = 0; x < w; ++x) { - *row++ = j2ku_shift(offset + *data++, shift); + UINT16 pixel = j2ku_shift(offset + *data++, shift); + *row++ = pixel; + *row++ = pixel >> 8; } } break; @@ -350,6 +354,7 @@ j2ku_srgb_rgb( unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[3], offsets[3], csiz[3]; + unsigned dx[3], dy[3]; const UINT8 *cdata[3]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -359,6 +364,8 @@ j2ku_srgb_rgb( shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); if (csiz[n] == 3) { csiz[n] = 4; @@ -368,14 +375,14 @@ j2ku_srgb_rgb( offsets[n] += 1 << (-shifts[n] - 1); } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { const UINT8 *data[3]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; for (n = 0; n < 3; ++n) { - data[n] = &cdata[n][csiz[n] * y * w]; + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; } for (x = 0; x < w; ++x) { @@ -384,15 +391,13 @@ j2ku_srgb_rgb( switch (csiz[n]) { case 1: - word = *data[n]++; + word = data[n][x / dx[n]]; break; case 2: - word = *(const UINT16 *)data[n]; - data[n] += 2; + word = ((const UINT16 *)data[n])[x / dx[n]]; break; case 4: - word = *(const UINT32 *)data[n]; - data[n] += 4; + word = ((const UINT32 *)data[n])[x / dx[n]]; break; } @@ -415,6 +420,7 @@ j2ku_sycc_rgb( unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[3], offsets[3], csiz[3]; + unsigned dx[3], dy[3]; const UINT8 *cdata[3]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -424,6 +430,8 @@ j2ku_sycc_rgb( shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); if (csiz[n] == 3) { csiz[n] = 4; @@ -433,7 +441,7 @@ j2ku_sycc_rgb( offsets[n] += 1 << (-shifts[n] - 1); } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { @@ -441,7 +449,7 @@ j2ku_sycc_rgb( UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; UINT8 *row_start = row; for (n = 0; n < 3; ++n) { - data[n] = &cdata[n][csiz[n] * y * w]; + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; } for (x = 0; x < w; ++x) { @@ -450,15 +458,13 @@ j2ku_sycc_rgb( switch (csiz[n]) { case 1: - word = *data[n]++; + word = data[n][x / dx[n]]; break; case 2: - word = *(const UINT16 *)data[n]; - data[n] += 2; + word = ((const UINT16 *)data[n])[x / dx[n]]; break; case 4: - word = *(const UINT32 *)data[n]; - data[n] += 4; + word = ((const UINT32 *)data[n])[x / dx[n]]; break; } @@ -483,6 +489,7 @@ j2ku_srgba_rgba( unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[4], offsets[4], csiz[4]; + unsigned dx[4], dy[4]; const UINT8 *cdata[4]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -492,6 +499,8 @@ j2ku_srgba_rgba( shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); if (csiz[n] == 3) { csiz[n] = 4; @@ -501,14 +510,14 @@ j2ku_srgba_rgba( offsets[n] += 1 << (-shifts[n] - 1); } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { const UINT8 *data[4]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; for (n = 0; n < 4; ++n) { - data[n] = &cdata[n][csiz[n] * y * w]; + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; } for (x = 0; x < w; ++x) { @@ -517,15 +526,13 @@ j2ku_srgba_rgba( switch (csiz[n]) { case 1: - word = *data[n]++; + word = data[n][x / dx[n]]; break; case 2: - word = *(const UINT16 *)data[n]; - data[n] += 2; + word = ((const UINT16 *)data[n])[x / dx[n]]; break; case 4: - word = *(const UINT32 *)data[n]; - data[n] += 4; + word = ((const UINT32 *)data[n])[x / dx[n]]; break; } @@ -547,6 +554,7 @@ j2ku_sycca_rgba( unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[4], offsets[4], csiz[4]; + unsigned dx[4], dy[4]; const UINT8 *cdata[4]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -556,6 +564,8 @@ j2ku_sycca_rgba( shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); if (csiz[n] == 3) { csiz[n] = 4; @@ -565,7 +575,7 @@ j2ku_sycca_rgba( offsets[n] += 1 << (-shifts[n] - 1); } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { @@ -573,7 +583,7 @@ j2ku_sycca_rgba( UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; UINT8 *row_start = row; for (n = 0; n < 4; ++n) { - data[n] = &cdata[n][csiz[n] * y * w]; + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; } for (x = 0; x < w; ++x) { @@ -582,15 +592,13 @@ j2ku_sycca_rgba( switch (csiz[n]) { case 1: - word = *data[n]++; + word = data[n][x / dx[n]]; break; case 2: - word = *(const UINT16 *)data[n]; - data[n] += 2; + word = ((const UINT16 *)data[n])[x / dx[n]]; break; case 4: - word = *(const UINT32 *)data[n]; - data[n] += 4; + word = ((const UINT32 *)data[n])[x / dx[n]]; break; } @@ -604,22 +612,22 @@ j2ku_sycca_rgba( } static const struct j2k_decode_unpacker j2k_unpackers[] = { - {"L", OPJ_CLRSPC_GRAY, 1, j2ku_gray_l}, - {"I;16", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i}, - {"I;16B", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i}, - {"LA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la}, - {"RGB", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb}, - {"RGB", OPJ_CLRSPC_GRAY, 2, j2ku_gray_rgb}, - {"RGB", OPJ_CLRSPC_SRGB, 3, j2ku_srgb_rgb}, - {"RGB", OPJ_CLRSPC_SYCC, 3, j2ku_sycc_rgb}, - {"RGB", OPJ_CLRSPC_SRGB, 4, j2ku_srgb_rgb}, - {"RGB", OPJ_CLRSPC_SYCC, 4, j2ku_sycc_rgb}, - {"RGBA", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb}, - {"RGBA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la}, - {"RGBA", OPJ_CLRSPC_SRGB, 3, j2ku_srgb_rgb}, - {"RGBA", OPJ_CLRSPC_SYCC, 3, j2ku_sycc_rgb}, - {"RGBA", OPJ_CLRSPC_SRGB, 4, j2ku_srgba_rgba}, - {"RGBA", OPJ_CLRSPC_SYCC, 4, j2ku_sycca_rgba}, + {"L", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, + {"I;16", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {"I;16B", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {"LA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {"RGB", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {"RGB", OPJ_CLRSPC_GRAY, 2, 0, j2ku_gray_rgb}, + {"RGB", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {"RGB", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {"RGB", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgb_rgb}, + {"RGB", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycc_rgb}, + {"RGBA", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, + {"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, }; /* -------------------------------------------------------------------- */ @@ -644,7 +652,8 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { j2k_unpacker_t unpack = NULL; size_t buffer_size = 0, tile_bytes = 0; unsigned n, tile_height, tile_width; - int components; + int subsampling; + int total_component_width = 0; stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE); @@ -706,11 +715,16 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } - for (n = 1; n < image->numcomps; ++n) { + /* + * Find first component with subsampling. + * + * This is a heuristic to determine the colorspace if unspecified. + */ + subsampling = -1; + for (n = 0; n < image->numcomps; ++n) { if (image->comps[n].dx != 1 || image->comps[n].dy != 1) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; + subsampling = n; + break; } } @@ -726,12 +740,14 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { If colorspace is unspecified, we assume: - Number of components Colorspace - ----------------------------------------- - 1 gray - 2 gray (+ alpha) - 3 sRGB - 4 sRGB (+ alpha) + Number of components Subsampling Colorspace + ------------------------------------------------------- + 1 Any gray + 2 Any gray (+ alpha) + 3 -1, 0 sRGB + 3 1, 2 YCbCr + 4 -1, 0, 3 sRGB (+ alpha) + 4 1, 2 YCbCr (+ alpha) */ @@ -746,14 +762,25 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { break; case 3: case 4: - color_space = OPJ_CLRSPC_SRGB; - break; + switch (subsampling) { + case -1: + case 0: + case 3: + color_space = OPJ_CLRSPC_SRGB; + break; + case 1: + case 2: + color_space = OPJ_CLRSPC_SYCC; + break; + } + break; } } for (n = 0; n < sizeof(j2k_unpackers) / sizeof(j2k_unpackers[0]); ++n) { if (color_space == j2k_unpackers[n].color_space && image->numcomps == j2k_unpackers[n].components && + (j2k_unpackers[n].subsampling || (subsampling == -1)) && strcmp(im->mode, j2k_unpackers[n].mode) == 0) { unpack = j2k_unpackers[n].unpacker; break; @@ -814,23 +841,40 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } - /* Sometimes the tile_info.datasize we get back from openjpeg - is less than numcomps*w*h, and we overflow in the - shuffle stage */ - - tile_width = tile_info.x1 - tile_info.x0; - tile_height = tile_info.y1 - tile_info.y0; - components = tile_info.nb_comps == 3 ? 4 : tile_info.nb_comps; - if ((tile_width > UINT_MAX / components) || - (tile_height > UINT_MAX / components) || - (tile_width > UINT_MAX / (tile_height * components)) || - (tile_height > UINT_MAX / (tile_width * components))) { + if (tile_info.nb_comps != image->numcomps) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; } - tile_bytes = tile_width * tile_height * components; + /* Sometimes the tile_info.datasize we get back from openjpeg + is less than sum(comp_bytes)*w*h, and we overflow in the + shuffle stage */ + + tile_width = tile_info.x1 - tile_info.x0; + tile_height = tile_info.y1 - tile_info.y0; + + /* Total component width = sum (component_width) e.g, it's + legal for an la file to have a 1 byte width for l, and 4 for + a, and then a malicious file could have a smaller tile_bytes + */ + + for (n=0; n < tile_info.nb_comps; n++) { + // see csize /acsize calcs + int csize = (image->comps[n].prec + 7) >> 3; + csize = (csize == 3) ? 4 : csize; + total_component_width += csize; + } + if ((tile_width > UINT_MAX / total_component_width) || + (tile_height > UINT_MAX / total_component_width) || + (tile_width > UINT_MAX / (tile_height * total_component_width)) || + (tile_height > UINT_MAX / (tile_width * total_component_width))) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + tile_bytes = tile_width * tile_height * total_component_width; if (tile_bytes > tile_info.data_size) { tile_info.data_size = tile_bytes; @@ -844,6 +888,10 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { state->state = J2K_STATE_FAILED; goto quick_exit; } + /* Undefined behavior, sometimes decode_tile_data doesn't + fill the buffer and we do things with it later, leading + to valgrind errors. */ + memset(new, 0, tile_info.data_size); state->buffer = new; buffer_size = tile_info.data_size; } diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 2e6b5daf0..86cd7d5af 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -110,8 +110,15 @@ j2k_pack_i16(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsig for (y = 0; y < h; ++y) { UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); for (x = 0; x < w; ++x) { - *ptr++ = *data++; - *ptr++ = *data++; +#ifdef WORDS_BIGENDIAN + ptr[0] = data[1]; + ptr[1] = data[0]; +#else + ptr[0] = data[0]; + ptr[1] = data[1]; +#endif + ptr += 2; + data += 2; } } } @@ -301,13 +308,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_l; - } else if (strcmp(im->mode, "I;16") == 0) { - components = 1; - color_space = OPJ_CLRSPC_GRAY; - pack = j2k_pack_i16; - prec = 16; - bpp = 12; - } else if (strcmp(im->mode, "I;16B") == 0) { + } else if (strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16B") == 0) { components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_i16; @@ -458,6 +459,12 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { break; } + if (!context->num_resolutions) { + while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) { + params.numresolution -= 1; + } + } + if (context->cinema_mode != OPJ_OFF) { j2k_set_cinema_params(im, components, ¶ms); } diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 2fdee919f..0c7c0497e 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -656,7 +656,11 @@ static struct { /* storage modes */ {"I;16", "I;16", 16, copy2}, +#ifdef WORDS_BIGENDIAN + {"I;16", "I;16B", 16, packI16N_I16}, +#else {"I;16", "I;16B", 16, packI16N_I16B}, +#endif {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 03b17f571..be26cd260 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -417,9 +417,16 @@ fill_mask_L( if (imOut->image8) { for (y = 0; y < ysize; y++) { UINT8 *out = imOut->image8[y + dy] + dx; + if (strncmp(imOut->mode, "I;16", 4) == 0) { + out += dx; + } UINT8 *mask = imMask->image8[y + sy] + sx; for (x = 0; x < xsize; x++) { *out = BLEND(*mask, *out, ink[0], tmp1); + if (strncmp(imOut->mode, "I;16", 4) == 0) { + out++; + *out = BLEND(*mask, *out, ink[0], tmp1); + } out++, mask++; } } @@ -436,7 +443,7 @@ fill_mask_L( strcmp(imOut->mode, "La") == 0 || strcmp(imOut->mode, "LA") == 0 || strcmp(imOut->mode, "PA") == 0) && - i != 3) { + i != 3 && channel_mask != 0) { channel_mask = 255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255); } diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index f5a5d567c..1c6b9d6a2 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -753,11 +753,19 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { return 1; } +typedef struct { + uint32_t *distance; + uint32_t index; +} DistanceWithIndex; + static int -_sort_ulong_ptr_keys(const void *a, const void *b) { - uint32_t A = **(uint32_t **)a; - uint32_t B = **(uint32_t **)b; - return (A == B) ? 0 : ((A < B) ? -1 : +1); +_distance_index_cmp(const void *a, const void *b) { + DistanceWithIndex *A = (DistanceWithIndex *)a; + DistanceWithIndex *B = (DistanceWithIndex *)b; + if (*A->distance == *B->distance) { + return A->index < B->index ? -1 : +1; + } + return *A->distance < *B->distance ? -1 : +1; } static int @@ -789,10 +797,11 @@ resort_distance_tables( return 1; } -static void +static int build_distance_tables( uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { uint32_t i, j; + DistanceWithIndex *dwi; for (i = 0; i < nEntries; i++) { avgDist[i * nEntries + i] = 0; @@ -804,13 +813,29 @@ build_distance_tables( avgDistSortKey[i * nEntries + j] = &(avgDist[i * nEntries + j]); } } - for (i = 0; i < nEntries; i++) { - qsort( - avgDistSortKey + i * nEntries, - nEntries, - sizeof(uint32_t *), - _sort_ulong_ptr_keys); + + dwi = calloc(nEntries, sizeof(DistanceWithIndex)); + if (!dwi) { + return 0; } + for (i = 0; i < nEntries; i++) { + for (j = 0; j < nEntries; j++) { + dwi[j] = (DistanceWithIndex){ + &(avgDist[i * nEntries + j]), + j + }; + } + qsort( + dwi, + nEntries, + sizeof(DistanceWithIndex), + _distance_index_cmp); + for (j = 0; j < nEntries; j++) { + avgDistSortKey[i * nEntries + j] = dwi[j].distance; + } + } + free(dwi); + return 1; } static int @@ -1175,8 +1200,10 @@ k_means( if (!built) { compute_palette_from_quantized_pixels( pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp); - build_distance_tables( - avgDist, avgDistSortKey, paletteData, nPaletteEntries); + if (!build_distance_tables( + avgDist, avgDistSortKey, paletteData, nPaletteEntries)) { + goto error_3; + } built = 1; } else { recompute_palette_from_averages(paletteData, nPaletteEntries, avg, count); @@ -1243,7 +1270,7 @@ error_1: return 0; } -int +static int quantize( Pixel *pixelData, uint32_t nPixels, @@ -1372,7 +1399,9 @@ quantize( goto error_6; } - build_distance_tables(avgDist, avgDistSortKey, p, nPaletteEntries); + if (!build_distance_tables(avgDist, avgDistSortKey, p, nPaletteEntries)) { + goto error_7; + } if (!map_image_pixels_from_median_box( pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp)) { @@ -1511,7 +1540,7 @@ compute_distances(const HashTable *h, const Pixel pixel, uint32_t *dist, void *u } } -int +static int quantize2( Pixel *pixelData, uint32_t nPixels, @@ -1577,7 +1606,9 @@ quantize2( goto error_3; } - build_distance_tables(avgDist, avgDistSortKey, p, nQuantPixels); + if (!build_distance_tables(avgDist, avgDistSortKey, p, nQuantPixels)) { + goto error_4; + } if (!map_image_pixels( pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp)) { @@ -1683,9 +1714,26 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { } else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) { /* true colour */ + withAlpha = !strcmp(im->mode, "RGBA"); + int transparency = 0; + unsigned char r, g, b; for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { p[i].v = im->image32[y][x]; + if (withAlpha && p[i].c.a == 0) { + if (transparency == 0) { + transparency = 1; + r = p[i].c.r; + g = p[i].c.g; + b = p[i].c.b; + } else { + /* Set all subsequent transparent pixels + to the same colour as the first */ + p[i].c.r = r; + p[i].c.g = g; + p[i].c.b = b; + } + } } } @@ -1720,9 +1768,6 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { kmeans); break; case 2: - if (!strcmp(im->mode, "RGBA")) { - withAlpha = 1; - } result = quantize_octree( p, im->xsize * im->ysize, @@ -1734,9 +1779,6 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { break; case 3: #ifdef HAVE_LIBIMAGEQUANT - if (!strcmp(im->mode, "RGBA")) { - withAlpha = 1; - } result = quantize_pngquant( p, im->xsize, diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index b8d4d1d7c..5e79bce35 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -317,7 +317,7 @@ void add_lookup_buckets(ColorCube cube, ColorBucket palette, long nColors, long offset) { long i; Pixel p; - for (i = offset; i < offset + nColors; i++) { + for (i = offset + nColors - 1; i >= offset; i--) { avg_color_from_color_bucket(&palette[i], &p); set_lookup_value(cube, &p, i); } diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 021c2898c..38deb5360 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -56,7 +56,7 @@ _tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { dump_state(state); if (state->loc > state->eof) { - TIFFError("_tiffReadProc", "Invalid Read at loc %llu, eof: %llu", state->loc, state->eof); + TIFFError("_tiffReadProc", "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, state->loc, state->eof); return 0; } to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); @@ -181,7 +181,7 @@ _tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) { } int -ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) { +ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; TRACE(("initing libtiff\n")); @@ -213,10 +213,10 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) { } int -_pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16 planarconfig, ImagingShuffler *unpackers) { +_pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16_t planarconfig, ImagingShuffler *unpackers) { // if number of bands is 1, there is no difference with contig case if (planarconfig == PLANARCONFIG_SEPARATE && im->bands > 1) { - uint16 bits_per_sample = 8; + uint16_t bits_per_sample = 8; TIFFGetFieldDefaulted(tiff, TIFFTAG_BITSPERSAMPLE, &bits_per_sample); if (bits_per_sample != 8 && bits_per_sample != 16) { @@ -265,7 +265,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_block); } - if (ret != 1) { + if (ret != 1 || rows_per_block==(UINT32)(-1)) { rows_per_block = state->ysize; } @@ -281,17 +281,6 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { img.req_orientation = ORIENTATION_TOPLEFT; img.col_offset = 0; - if (state->xsize != img.width || state->ysize != img.height) { - TRACE( - ("Inconsistent Image Error: %d =? %d, %d =? %d", - state->xsize, - img.width, - state->ysize, - img.height)); - state->errcode = IMAGING_CODEC_BROKEN; - goto decodergba_err; - } - /* overflow check for row byte size */ if (INT_MAX / 4 < img.width) { state->errcode = IMAGING_CODEC_MEMORY; @@ -427,15 +416,6 @@ _decodeTile(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imaging for (plane = 0; plane < planes; plane++) { ImagingShuffler shuffler = unpackers[plane]; for (x = state->xoff; x < state->xsize; x += tile_width) { - /* Sanity Check. Apparently in some cases, the TiffReadRGBA* functions - have a different view of the size of the tiff than we're getting from - other functions. So, we need to check here. - */ - if (!TIFFCheckTile(tiff, x, y, 0, plane)) { - TRACE(("Check Tile Error, Tile at %dx%d\n", x, y)); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, plane) == -1) { TRACE(("Decode Error, Tile at %dx%d\n", x, y)); state->errcode = IMAGING_CODEC_BROKEN; @@ -471,7 +451,7 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin UINT8 *new_data; UINT32 rows_per_strip; int ret; - tsize_t strip_size, row_byte_size; + tsize_t strip_size, row_byte_size, unpacker_row_byte_size; ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); if (ret != 1 || rows_per_strip==(UINT32)(-1)) { @@ -491,7 +471,8 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin return -1; } - if (strip_size > ((state->xsize * state->bits / planes + 7) / 8) * rows_per_strip) { + unpacker_row_byte_size = (state->xsize * state->bits / planes + 7) / 8; + if (strip_size > (unpacker_row_byte_size * rows_per_strip)) { // If the strip size as expected by LibTiff isn't what we're expecting, abort. // man: TIFFStripSize returns the equivalent size for a strip of data as it would be returned in a // call to TIFFReadEncodedStrip ... @@ -505,7 +486,9 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin row_byte_size = TIFFScanlineSize(tiff); - if (row_byte_size == 0 || row_byte_size > strip_size) { + // if the unpacker calculated row size is > row byte size, (at least) the last + // row of the strip will have a read buffer overflow. + if (row_byte_size == 0 || unpacker_row_byte_size > row_byte_size) { state->errcode = IMAGING_CODEC_BROKEN; return -1; } @@ -560,14 +543,15 @@ ImagingLibTiffDecode( Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; char *filename = "tempfile.tif"; - char *mode = "r"; + char *mode = "rC"; TIFF *tiff; - uint16 photometric = 0; // init to not PHOTOMETRIC_YCBCR - uint16 compression; + uint16_t photometric = 0; // init to not PHOTOMETRIC_YCBCR + uint16_t compression; int readAsRGBA = 0; - uint16 planarconfig = 0; + uint16_t planarconfig = 0; int planes = 1; ImagingShuffler unpackers[4]; + UINT32 img_width, img_height; memset(unpackers, 0, sizeof(ImagingShuffler) * 4); @@ -655,7 +639,7 @@ ImagingLibTiffDecode( if (clientstate->ifd) { int rv; - uint32 ifdoffset = clientstate->ifd; + uint32_t ifdoffset = clientstate->ifd; TRACE(("reading tiff ifd %u\n", ifdoffset)); rv = TIFFSetSubDirectory(tiff, ifdoffset); if (!rv) { @@ -664,6 +648,20 @@ ImagingLibTiffDecode( } } + TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &img_width); + TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &img_height); + + if (state->xsize != img_width || state->ysize != img_height) { + TRACE( + ("Inconsistent Image Error: %d =? %d, %d =? %d", + state->xsize, + img_width, + state->ysize, + img_height)); + state->errcode = IMAGING_CODEC_BROKEN; + goto decode_err; + } + TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); TIFFGetField(tiff, TIFFTAG_COMPRESSION, &compression); @@ -674,7 +672,7 @@ ImagingLibTiffDecode( readAsRGBA = photometric == PHOTOMETRIC_YCBCR; if (readAsRGBA && compression == COMPRESSION_JPEG && planarconfig == PLANARCONFIG_CONTIG) { - // If using new JPEG compression, let libjpeg do RGB convertion for performance reasons + // If using new JPEG compression, let libjpeg do RGB conversion for performance reasons TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); readAsRGBA = 0; } @@ -699,8 +697,8 @@ ImagingLibTiffDecode( // Check if raw mode was RGBa and it was stored on separate planes // so we have to convert it to RGBA if (planes > 3 && strcmp(im->mode, "RGBA") == 0) { - uint16 extrasamples; - uint16* sampleinfo; + uint16_t extrasamples; + uint16_t* sampleinfo; ImagingShuffler shuffle; INT32 y; @@ -812,7 +810,7 @@ ImagingLibTiffMergeFieldInfo( ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length) { // Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html) TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - uint32 n; + uint32_t n; int status = 0; // custom fields added with ImagingLibTiffMergeFieldInfo are only used for @@ -935,7 +933,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt state->xsize); if (TIFFWriteScanline( - tiff, (tdata_t)(state->buffer), (uint32)state->y, 0) == -1) { + tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0) == -1) { TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; TIFFClose(tiff); diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 2c3d88caa..c7c7d48ed 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -32,17 +32,17 @@ typedef struct { toff_t loc; /* toff_t == uint32 */ tsize_t size; /* tsize_t == int32 */ int fp; - uint32 ifd; /* offset of the ifd, used for multipage - * Should be uint32 for libtiff 3.9.x - * uint64 for libtiff 4.0.x - */ + uint32_t ifd; /* offset of the ifd, used for multipage + * Should be uint32 for libtiff 3.9.x + * uint64 for libtiff 4.0.x + */ TIFF *tiff; /* Used in write */ toff_t eof; int flrealloc; /* may we realloc */ } TIFFSTATE; extern int -ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset); +ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int diff --git a/src/outline.c b/src/outline.c index ba3e056cc..0a9a3646e 100644 --- a/src/outline.c +++ b/src/outline.c @@ -145,11 +145,11 @@ _outline_transform(OutlineObject *self, PyObject *args) { } static struct PyMethodDef _outline_methods[] = { - {"line", (PyCFunction)_outline_line, 1}, - {"curve", (PyCFunction)_outline_curve, 1}, - {"move", (PyCFunction)_outline_move, 1}, - {"close", (PyCFunction)_outline_close, 1}, - {"transform", (PyCFunction)_outline_transform, 1}, + {"line", (PyCFunction)_outline_line, METH_VARARGS}, + {"curve", (PyCFunction)_outline_curve, METH_VARARGS}, + {"move", (PyCFunction)_outline_move, METH_VARARGS}, + {"close", (PyCFunction)_outline_close, METH_VARARGS}, + {"transform", (PyCFunction)_outline_transform, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/path.c b/src/path.c index 8d1f68e84..dea274ee3 100644 --- a/src/path.c +++ b/src/path.c @@ -57,7 +57,7 @@ alloc_array(Py_ssize_t count) { if ((unsigned long long)count > (SIZE_MAX / (2 * sizeof(double))) - 1) { return ImagingError_MemoryError(); } - xy = malloc(2 * count * sizeof(double) + 1); + xy = calloc(2 * count * sizeof(double) + 1, sizeof(double)); if (!xy) { ImagingError_MemoryError(); } @@ -327,21 +327,26 @@ path_getbbox(PyPathObject *self, PyObject *args) { xy = self->xy; - x0 = x1 = xy[0]; - y0 = y1 = xy[1]; + if (self->count == 0) { + x0 = x1 = 0; + y0 = y1 = 0; + } else { + x0 = x1 = xy[0]; + y0 = y1 = xy[1]; - for (i = 1; i < self->count; i++) { - if (xy[i + i] < x0) { - x0 = xy[i + i]; - } - if (xy[i + i] > x1) { - x1 = xy[i + i]; - } - if (xy[i + i + 1] < y0) { - y0 = xy[i + i + 1]; - } - if (xy[i + i + 1] > y1) { - y1 = xy[i + i + 1]; + for (i = 1; i < self->count; i++) { + if (xy[i + i] < x0) { + x0 = xy[i + i]; + } + if (xy[i + i] > x1) { + x1 = xy[i + i]; + } + if (xy[i + i + 1] < y0) { + y0 = xy[i + i + 1]; + } + if (xy[i + i + 1] > y1) { + y1 = xy[i + i + 1]; + } } } @@ -524,11 +529,11 @@ path_transform(PyPathObject *self, PyObject *args) { } static struct PyMethodDef methods[] = { - {"getbbox", (PyCFunction)path_getbbox, 1}, - {"tolist", (PyCFunction)path_tolist, 1}, - {"compact", (PyCFunction)path_compact, 1}, - {"map", (PyCFunction)path_map, 1}, - {"transform", (PyCFunction)path_transform, 1}, + {"getbbox", (PyCFunction)path_getbbox, METH_VARARGS}, + {"tolist", (PyCFunction)path_tolist, METH_VARARGS}, + {"compact", (PyCFunction)path_compact, METH_VARARGS}, + {"map", (PyCFunction)path_map, METH_VARARGS}, + {"transform", (PyCFunction)path_transform, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/thirdparty/fribidi-shim/fribidi.c b/src/thirdparty/fribidi-shim/fribidi.c index 55e2a6ab3..76fd5b9a4 100644 --- a/src/thirdparty/fribidi-shim/fribidi.c +++ b/src/thirdparty/fribidi-shim/fribidi.c @@ -12,7 +12,7 @@ /* FriBiDi>=1.0.0 adds bracket_types param, ignore and call legacy function */ -FriBidiLevel fribidi_get_par_embedding_levels_ex_compat( +static FriBidiLevel fribidi_get_par_embedding_levels_ex_compat( const FriBidiCharType *bidi_types, const FriBidiBracketType *bracket_types, const FriBidiStrIndex len, @@ -24,7 +24,7 @@ FriBidiLevel fribidi_get_par_embedding_levels_ex_compat( } /* FriBiDi>=1.0.0 gets bracket types here, ignore */ -void fribidi_get_bracket_types_compat( +static void fribidi_get_bracket_types_compat( const FriBidiChar *str, const FriBidiStrIndex len, const FriBidiCharType *types, @@ -56,6 +56,9 @@ int load_fribidi(void) { error = error || (func == 0); p_fribidi = LoadLibrary("fribidi"); + if (!p_fribidi) { + p_fribidi = LoadLibrary("fribidi-0"); + } /* MSYS2 */ if (!p_fribidi) { p_fribidi = LoadLibrary("libfribidi-0"); diff --git a/src/thirdparty/fribidi-shim/fribidi.h b/src/thirdparty/fribidi-shim/fribidi.h index 7712a5b22..7e175c3db 100644 --- a/src/thirdparty/fribidi-shim/fribidi.h +++ b/src/thirdparty/fribidi-shim/fribidi.h @@ -63,8 +63,12 @@ typedef uint32_t FriBidiParType; /* functions */ #ifdef FRIBIDI_SHIM_IMPLEMENTATION +#ifdef _MSC_VER #define FRIBIDI_ENTRY #else +#define FRIBIDI_ENTRY __attribute__((visibility ("hidden"))) +#endif +#else #define FRIBIDI_ENTRY extern #endif diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS index 29c9ae0e5..c49176a95 100644 --- a/src/thirdparty/raqm/NEWS +++ b/src/thirdparty/raqm/NEWS @@ -1,3 +1,17 @@ +Overview of changes leading to 0.7.1 +Monday, September 27, 2021 +==================================== + +Fix test failure with newer HarfBuzz versions. + +Apply FT_Face transformation matrix when built against FreeType 2.11 or later. + +Add meson build system. Autotools build system will be dropped in next release. + +Improve MSVC support. + +Build and documentation fixes. + Overview of changes leading to 0.7.1 Sunday, November 22, 2020 ==================================== diff --git a/src/thirdparty/raqm/README b/src/thirdparty/raqm/README.md similarity index 79% rename from src/thirdparty/raqm/README rename to src/thirdparty/raqm/README.md index 7940bf3b6..64937343a 100644 --- a/src/thirdparty/raqm/README +++ b/src/thirdparty/raqm/README.md @@ -1,8 +1,7 @@ Raqm ==== -[![Linux & macOS build](https://travis-ci.org/HOST-Oman/libraqm.svg?branch=master)](https://travis-ci.org/HOST-Oman/libraqm) -[![Windows build](https://img.shields.io/appveyor/ci/HOSTOman/libraqm/master.svg)](https://ci.appveyor.com/project/HOSTOman/libraqm) +[![Build](https://github.com/HOST-Oman/libraqm/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/HOST-Oman/libraqm/actions) Raqm is a small library that encapsulates the logic for complex text layout and provides a convenient API. @@ -15,7 +14,7 @@ The documentation can be accessed on the web at: > http://host-oman.github.io/libraqm/ Raqm (Arabic: رَقْم) is writing, also number or digit and the Arabic word for -digital (رَقَمِيّ) shares the same root, so it is a play on “digital writing”. +digital (رَقَمِيّ) shares the same root, so it is a play on “digital writing”. Building -------- @@ -30,31 +29,30 @@ To build the documentation you will also need: To install dependencies on Fedora: - sudo dnf install freetype-devel harfbuzz-devel fribidi-devel gtk-doc + sudo dnf install freetype-devel harfbuzz-devel fribidi-devel meson gtk-doc To install dependencies on Ubuntu: - sudo apt-get install libfreetype6-dev libharfbuzz-dev libfribidi-dev \ - gtk-doc-tools + sudo apt-get install libfreetype6-dev libharfbuzz-dev libfribidi-dev meson gtk-doc-tools On Mac OS X you can use Homebrew: - brew install freetype harfbuzz fribidi gtk-doc + brew install freetype harfbuzz fribidi meson gtk-doc export XML_CATALOG_FILES="/usr/local/etc/xml/catalog" # for the docs Once you have the source code and the dependencies, you can proceed to build. To do that, run the customary sequence of commands in the source code directory: - $ ./configure - $ make - $ make install + $ meson build + $ ninja -C build + $ ninja -C build install -To build the documentation, pass `--enable-gtk-doc` to the `configure` script. +To build the documentation, pass `-Ddocs=enable` to the `meson`. To run the tests: - $ make check + $ ninja -C test Contributing ------------ diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index 94b25ada7..8b115f612 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 7 -#define RAQM_VERSION_MICRO 1 +#define RAQM_VERSION_MICRO 2 -#define RAQM_VERSION_STRING "0.7.1" +#define RAQM_VERSION_STRING "0.7.2" #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 5a0b2078e..31161c9d9 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -39,6 +39,21 @@ #include #include +#if FREETYPE_MAJOR > 2 || \ + FREETYPE_MAJOR == 2 && FREETYPE_MINOR >= 11 +#define HAVE_FT_GET_TRANSFORM +#endif + +#if HB_VERSION_ATLEAST(2, 0, 0) +#define HAVE_HB_BUFFER_SET_INVISIBLE_GLYPH +#endif + +#if HB_VERSION_ATLEAST(1, 8, 0) +#define HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES 1 +#else +#define HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES 0 +#endif + #include "raqm.h" #if FRIBIDI_MAJOR_VERSION >= 1 @@ -455,8 +470,6 @@ raqm_set_text_utf8 (raqm_t *rq, return true; } - RAQM_TEST ("Text is: %s\n", text); - rq->flags |= RAQM_FLAG_UTF8; rq->text_utf8 = malloc (sizeof (char) * len); @@ -491,7 +504,7 @@ raqm_set_text_utf8 (raqm_t *rq, * * The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph * direction based on the first character with strong bidi type (see [rule - * P2](http://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm), + * P2](https://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm), * which can be good enough for many cases but has problems when a mainly * right-to-left paragraph starts with a left-to-right character and vice versa * as the detected paragraph direction will be the wrong one, or when text does @@ -1556,6 +1569,21 @@ _raqm_resolve_scripts (raqm_t *rq) return true; } +static void +_raqm_ft_transform (int *x, + int *y, + FT_Matrix matrix) +{ + FT_Vector vector; + vector.x = *x; + vector.y = *y; + + FT_Vector_Transform (&vector, &matrix); + + *x = vector.x; + *y = vector.y; +} + static bool _raqm_shape (raqm_t *rq) { @@ -1585,6 +1613,22 @@ _raqm_shape (raqm_t *rq) hb_shape_full (run->font, run->buffer, rq->features, rq->features_len, NULL); + +#ifdef HAVE_FT_GET_TRANSFORM + { + FT_Matrix matrix; + hb_glyph_position_t *pos; + unsigned int len; + + FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL); + pos = hb_buffer_get_glyph_positions (run->buffer, &len); + for (unsigned int i = 0; i < len; i++) + { + _raqm_ft_transform (&pos[i].x_advance, &pos[i].y_advance, matrix); + _raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix); + } + } +#endif } return true; diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h index 1a33fe8ba..342afc8b2 100644 --- a/src/thirdparty/raqm/raqm.h +++ b/src/thirdparty/raqm/raqm.h @@ -30,6 +30,10 @@ #include "config.h" #endif +#ifndef RAQM_API +#define RAQM_API +#endif + #include #include #include @@ -93,86 +97,86 @@ typedef struct raqm_glyph_t { FT_Face ftface; } raqm_glyph_t; -raqm_t * +RAQM_API raqm_t * raqm_create (void); -raqm_t * +RAQM_API raqm_t * raqm_reference (raqm_t *rq); -void +RAQM_API void raqm_destroy (raqm_t *rq); -bool +RAQM_API bool raqm_set_text (raqm_t *rq, const uint32_t *text, size_t len); -bool +RAQM_API bool raqm_set_text_utf8 (raqm_t *rq, const char *text, size_t len); -bool +RAQM_API bool raqm_set_par_direction (raqm_t *rq, raqm_direction_t dir); -bool +RAQM_API bool raqm_set_language (raqm_t *rq, const char *lang, size_t start, size_t len); -bool +RAQM_API bool raqm_add_font_feature (raqm_t *rq, const char *feature, int len); -bool +RAQM_API bool raqm_set_freetype_face (raqm_t *rq, FT_Face face); -bool +RAQM_API bool raqm_set_freetype_face_range (raqm_t *rq, FT_Face face, size_t start, size_t len); -bool +RAQM_API bool raqm_set_freetype_load_flags (raqm_t *rq, int flags); -bool +RAQM_API bool raqm_set_invisible_glyph (raqm_t *rq, int gid); -bool +RAQM_API bool raqm_layout (raqm_t *rq); -raqm_glyph_t * +RAQM_API raqm_glyph_t * raqm_get_glyphs (raqm_t *rq, size_t *length); -bool +RAQM_API bool raqm_index_to_position (raqm_t *rq, size_t *index, int *x, int *y); -bool +RAQM_API bool raqm_position_to_index (raqm_t *rq, int x, int y, size_t *index); -void +RAQM_API void raqm_version (unsigned int *major, unsigned int *minor, unsigned int *micro); -const char * +RAQM_API const char * raqm_version_string (void); -bool +RAQM_API bool raqm_version_atleast (unsigned int major, unsigned int minor, unsigned int micro); diff --git a/tox.ini b/tox.ini index 2557d5067..bdedc2bd5 100644 --- a/tox.ini +++ b/tox.ini @@ -6,13 +6,13 @@ [tox] envlist = lint - py{36,37,38,39,py3} + py{37,38,39,310,py3} minversion = 1.9 [testenv] commands = - {envpython} setup.py clean - {envpython} setup.py build_ext --inplace + make clean + {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} deps = diff --git a/winbuild/build.rst b/winbuild/build.rst index cd4a45e87..b30a94226 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -55,8 +55,8 @@ behaviour of ``build_prepare.py``: * ``-v`` will print generated scripts. * ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency -* ``--no-raqm`` will skip optional dependency Raqm (which itself depends on - LGPL-licensed ``fribidi``). +* ``--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`` @@ -87,7 +87,7 @@ and install Pillow in develop mode (instead of ``python3 -m pip install --editab Testing Pillow -------------- -Some binary dependencies (e.g. ``libraqm.dll``) will be stored in the +Some binary dependencies (e.g. ``fribidi.dll``) will be stored in the ``winbuild\build\bin`` directory; this directory should be added to ``PATH`` before running tests. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a91c86a73..0589baf21 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -105,9 +105,9 @@ header = [ # dependencies, listed in order of compilation deps = { "libjpeg": { - "url": SF_MIRROR + "/project/libjpeg-turbo/2.0.6/libjpeg-turbo-2.0.6.tar.gz", - "filename": "libjpeg-turbo-2.0.6.tar.gz", - "dir": "libjpeg-turbo-2.0.6", + "url": SF_MIRROR + "/project/libjpeg-turbo/2.1.2/libjpeg-turbo-2.1.2.tar.gz", + "filename": "libjpeg-turbo-2.1.2.tar.gz", + "dir": "libjpeg-turbo-2.1.2", "build": [ cmd_cmake( [ @@ -141,22 +141,22 @@ deps = { "libs": [r"*.lib"], }, "libtiff": { - "url": "https://download.osgeo.org/libtiff/tiff-4.2.0.tar.gz", - "filename": "tiff-4.2.0.tar.gz", - "dir": "tiff-4.2.0", + "url": "https://download.osgeo.org/libtiff/tiff-4.3.0.tar.gz", + "filename": "tiff-4.3.0.tar.gz", + "dir": "tiff-4.3.0", "build": [ - cmd_copy(r"{winbuild_dir}\tiff.opt", "nmake.opt"), - cmd_nmake("makefile.vc", "clean"), - cmd_nmake("makefile.vc", "lib"), + cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), + cmd_nmake(target="clean"), + cmd_nmake(target="tiff"), ], "headers": [r"libtiff\tiff*.h"], "libs": [r"libtiff\*.lib"], # "bins": [r"libtiff\*.dll"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.0.tar.gz", - "filename": "libwebp-1.2.0.tar.gz", - "dir": "libwebp-1.2.0", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.1.tar.gz", + "filename": "libwebp-1.2.1.tar.gz", + "dir": "libwebp-1.2.1", "build": [ cmd_rmdir(r"output\release-static"), # clean cmd_nmake( @@ -184,9 +184,9 @@ deps = { "libs": [r"libpng16.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.4.tar.gz", # noqa: E501 - "filename": "freetype-2.10.4.tar.gz", - "dir": "freetype-2.10.4", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.11.1.tar.gz", # noqa: E501 + "filename": "freetype-2.11.1.tar.gz", + "dir": "freetype-2.11.1", "patch": { r"builds\windows\vc2010\freetype.vcxproj": { # freetype setting is /MD for .dll and /MT for .lib, we need /MD @@ -236,7 +236,9 @@ deps = { cmd_rmdir("Lib"), cmd_rmdir(r"Projects\VC2017\Release"), cmd_msbuild(r"Projects\VC2017\lcms2.sln", "Release", "Clean"), - cmd_msbuild(r"Projects\VC2017\lcms2.sln", "Release", "lcms2_static"), + cmd_msbuild( + r"Projects\VC2017\lcms2.sln", "Release", "lcms2_static:Rebuild" + ), cmd_xcopy("include", "{inc_dir}"), ], "libs": [r"Lib\MS\*.lib"], @@ -255,29 +257,30 @@ deps = { "libs": [r"bin\*.lib"], }, "libimagequant": { - # Merge master into msvc (matches 2.14.1 except for version bump) - "url": "https://github.com/ImageOptim/libimagequant/archive/16adaded22d1f90db5c9154a06d00a8b672ca09a.zip", # noqa: E501 - "filename": "libimagequant-16adaded22d1f90db5c9154a06d00a8b672ca09a.zip", - "dir": "libimagequant-16adaded22d1f90db5c9154a06d00a8b672ca09a", + # commit: Merge branch 'master' into msvc (matches 2.17.0 tag) + "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", # noqa: E501 + "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", + "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab", "patch": { "CMakeLists.txt": { - "add_library": "add_compile_options(-openmp-)\r\nadd_library", - " SHARED": " STATIC", + "if(OPENMP_FOUND)": "if(false)", + "install": "#install", } }, "build": [ # lint: do not inline cmd_cmake(), cmd_nmake(target="clean"), - cmd_nmake(), + cmd_nmake(target="imagequant_a"), + cmd_copy("imagequant_a.lib", "imagequant.lib"), ], "headers": [r"*.h"], - "libs": [r"*.lib"], + "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/2.8.0.zip", - "filename": "harfbuzz-2.8.0.zip", - "dir": "harfbuzz-2.8.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/3.2.0.zip", + "filename": "harfbuzz-3.2.0.zip", + "dir": "harfbuzz-3.2.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), @@ -287,9 +290,9 @@ deps = { "libs": [r"*.lib"], }, "fribidi": { - "url": "https://github.com/fribidi/fribidi/archive/v1.0.10.zip", - "filename": "fribidi-1.0.10.zip", - "dir": "fribidi-1.0.10", + "url": "https://github.com/fribidi/fribidi/archive/v1.0.11.zip", + "filename": "fribidi-1.0.11.zip", + "dir": "fribidi-1.0.11", "build": [ cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), cmd_cmake(), @@ -435,6 +438,7 @@ def build_dep(name): assert patch_from in text text = text.replace(patch_from, patch_to) with open(patch_file, "w") as f: + print(f"Patching {patch_file}") f.write(text) banner = f"Building {name} ({dir})" @@ -470,8 +474,6 @@ def build_pillow(): cmd_cd("{pillow_dir}"), *prefs["header"], cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow - cmd_set("MSSdk", "1"), # for PyPy3.6 - cmd_set("py_vcruntime_redist", "true"), # use /MD, not /MT r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501 ] diff --git a/winbuild/raqm.cmake b/winbuild/raqm.cmake deleted file mode 100644 index 82c9cdc70..000000000 --- a/winbuild/raqm.cmake +++ /dev/null @@ -1,39 +0,0 @@ -cmake_minimum_required(VERSION 3.12) - -project(libraqm) - - -find_library(fribidi NAMES fribidi) -find_library(harfbuzz NAMES harfbuzz) -find_library(freetype NAMES freetype) - -add_definitions(-DFRIBIDI_LIB_STATIC) - - -function(raqm_conf) - file(READ configure.ac RAQM_CONF) - string(REGEX MATCH "\\[([0-9]+)\\.([0-9]+)\\.([0-9]+)\\]," _ "${RAQM_CONF}") - set(RAQM_VERSION_MAJOR "${CMAKE_MATCH_1}") - set(RAQM_VERSION_MINOR "${CMAKE_MATCH_2}") - set(RAQM_VERSION_MICRO "${CMAKE_MATCH_3}") - set(RAQM_VERSION "${RAQM_VERSION_MAJOR}.${RAQM_VERSION_MINOR}.${RAQM_VERSION_MICRO}") - message("detected libraqm version ${RAQM_VERSION}") - configure_file(src/raqm-version.h.in src/raqm-version.h @ONLY) -endfunction() -raqm_conf() - - -set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) -set(RAQM_SOURCES - src/raqm.c) -set(RAQM_HEADERS - src/raqm.h - src/raqm-version.h) - -add_library(libraqm SHARED - ${RAQM_SOURCES} - ${RAQM_HEADERS}) -target_link_libraries(libraqm - ${fribidi} - ${harfbuzz} - ${freetype}) diff --git a/winbuild/tiff.opt b/winbuild/tiff.opt deleted file mode 100644 index d82c51678..000000000 --- a/winbuild/tiff.opt +++ /dev/null @@ -1,220 +0,0 @@ -# $Id: nmake.opt,v 1.18 2006/06/07 16:33:45 dron Exp $ -# -# Copyright (C) 2004, Andrey Kiselev -# -# Permission to use, copy, modify, distribute, and sell this software and -# its documentation for any purpose is hereby granted without fee, provided -# that (i) the above copyright notices and this permission notice appear in -# all copies of the software and related documentation, and (ii) the names of -# Sam Leffler and Silicon Graphics may not be used in any advertising or -# publicity relating to the software without the specific, prior written -# permission of Sam Leffler and Silicon Graphics. -# -# THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, -# EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY -# WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. -# -# IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR -# ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, -# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF -# LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -# OF THIS SOFTWARE. - -# Compile time parameters for MS Visual C++ compiler. -# You may edit this file to specify building options. - -# -###### Edit the following lines to choose a feature set you need. ####### -# - -# -# Select WINMODE_CONSOLE to build a library which reports errors to stderr, or -# WINMODE_WINDOWED to build such that errors are reported via MessageBox(). -# -WINMODE_CONSOLE = 1 -#WINMODE_WINDOWED = 1 - -# -# Comment out the following lines to disable internal codecs. -# -# Support for CCITT Group 3 & 4 algorithms -CCITT_SUPPORT = 1 -# Support for Macintosh PackBits algorithm -PACKBITS_SUPPORT = 1 -# Support for LZW algorithm -LZW_SUPPORT = 1 -# Support for ThunderScan 4-bit RLE algorithm -THUNDER_SUPPORT = 1 -# Support for NeXT 2-bit RLE algorithm -NEXT_SUPPORT = 1 -# Support for LogLuv high dynamic range encoding -LOGLUV_SUPPORT = 1 - -# -# Uncomment and edit following lines to enable JPEG support. -# -JPEG_SUPPORT = 1 -JPEG_INCLUDE = -I$(INCLIB) -JPEG_LIB = $(INCLIB)/libjpeg.lib - -# -# Uncomment and edit following lines to enable ZIP support -# (required for Deflate compression and Pixar log-format) -# -ZIP_SUPPORT = 1 -ZLIB_INCLUDE = -I$(INCLIB) -ZLIB_LIB = $(INCLIB)/zlib.lib - -# Indicate if the compiler provides strtoll/strtoull (default 1) -# Users of MSVC++ 14.0 ("Visual Studio 2015") and later should set this to 1 -HAVE_STRTOLL = 1 - -# -# Uncomment and edit following lines to enable ISO JBIG support -# -#JBIG_SUPPORT = 1 -#JBIGDIR = d:/projects/jbigkit -#JBIG_INCLUDE = -I$(JBIGDIR)/libjbig -#JBIG_LIB = $(JBIGDIR)/libjbig/jbig.lib - -# -# Uncomment following line to enable Pixar log-format algorithm -# (Zlib required). -# -#PIXARLOG_SUPPORT = 1 - -# -# Comment out the following lines to disable strip chopping -# (whether or not to convert single-strip uncompressed images to multiple -# strips of specified size to reduce memory usage). Default strip size -# is 8192 bytes, it can be configured via the STRIP_SIZE_DEFAULT parameter -# -STRIPCHOP_SUPPORT = 1 -STRIP_SIZE_DEFAULT = 8192 - -# -# Comment out the following lines to disable treating the fourth sample with -# no EXTRASAMPLE_ value as being ASSOCALPHA. Many packages produce RGBA -# files but don't mark the alpha properly. -# -EXTRASAMPLE_AS_ALPHA_SUPPORT = 1 - -# -# Comment out the following lines to disable picking up YCbCr subsampling -# info from the JPEG data stream to support files lacking the tag. -# See Bug 168 in Bugzilla, and JPEGFixupTestSubsampling() for details. -# -CHECK_JPEG_YCBCR_SUBSAMPLING = 1 - -# -####################### Compiler related options. ####################### -# - -# -# Pick debug or optimized build flags. We default to an optimized build -# with no debugging information. -# NOTE: /EHsc option required if you want to build the C++ stream API -# -OPTFLAGS = /Ox /MD /EHsc /W3 /D_CRT_SECURE_NO_DEPRECATE -#OPTFLAGS = /Zi - -# -# Uncomment following line to enable using Windows Common RunTime Library -# instead of Windows specific system calls. See notes on top of tif_unix.c -# module for details. -# -USE_WIN_CRT_LIB = 1 - -# Compiler specific options. You may probably want to adjust compilation -# parameters in CFLAGS variable. Refer to your compiler documentation -# for the option reference. -# -MAKE = nmake /nologo -CC = cl /nologo -CXX = cl /nologo -AR = lib /nologo -LD = link /nologo - -CFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) -CXXFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) -EXTRAFLAGS = -LIBS = - -# Name of the output shared library -DLLNAME = libtiff.dll - -# -########### There is nothing to edit below this line normally. ########### -# - -# Set the native cpu bit order -EXTRAFLAGS = -DFILLODER_LSB2MSB $(EXTRAFLAGS) - -!IFDEF WINMODE_WINDOWED -EXTRAFLAGS = -DTIF_PLATFORM_WINDOWED $(EXTRAFLAGS) -LIBS = user32.lib $(LIBS) -!ELSE -EXTRAFLAGS = -DTIF_PLATFORM_CONSOLE $(EXTRAFLAGS) -!ENDIF - -# Codec stuff -!IFDEF CCITT_SUPPORT -EXTRAFLAGS = -DCCITT_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF PACKBITS_SUPPORT -EXTRAFLAGS = -DPACKBITS_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF LZW_SUPPORT -EXTRAFLAGS = -DLZW_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF THUNDER_SUPPORT -EXTRAFLAGS = -DTHUNDER_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF NEXT_SUPPORT -EXTRAFLAGS = -DNEXT_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF LOGLUV_SUPPORT -EXTRAFLAGS = -DLOGLUV_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF JPEG_SUPPORT -LIBS = $(LIBS) $(JPEG_LIB) -EXTRAFLAGS = -DJPEG_SUPPORT -DOJPEG_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF ZIP_SUPPORT -LIBS = $(LIBS) $(ZLIB_LIB) -EXTRAFLAGS = -DZIP_SUPPORT $(EXTRAFLAGS) -!IFDEF PIXARLOG_SUPPORT -EXTRAFLAGS = -DPIXARLOG_SUPPORT $(EXTRAFLAGS) -!ENDIF -!ENDIF - -!IFDEF JBIG_SUPPORT -LIBS = $(LIBS) $(JBIG_LIB) -EXTRAFLAGS = -DJBIG_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF STRIPCHOP_SUPPORT -EXTRAFLAGS = -DSTRIPCHOP_DEFAULT=TIFF_STRIPCHOP -DSTRIP_SIZE_DEFAULT=$(STRIP_SIZE_DEFAULT) $(EXTRAFLAGS) -!ENDIF - -!IFDEF EXTRASAMPLE_AS_ALPHA_SUPPORT -EXTRAFLAGS = -DDEFAULT_EXTRASAMPLE_AS_ALPHA $(EXTRAFLAGS) -!ENDIF - -!IFDEF CHECK_JPEG_YCBCR_SUBSAMPLING -EXTRAFLAGS = -DCHECK_JPEG_YCBCR_SUBSAMPLING $(EXTRAFLAGS) -!ENDIF - -!IFDEF USE_WIN_CRT_LIB -EXTRAFLAGS = -DAVOID_WIN32_FILEIO $(EXTRAFLAGS) -!ELSE -EXTRAFLAGS = -DUSE_WIN32_FILEIO $(EXTRAFLAGS) -!ENDIF