diff --git a/.appveyor.yml b/.appveyor.yml index b817cd9d8..36f5bd0ad 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,7 +13,7 @@ environment: - PYTHON: C:/Python311 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - - PYTHON: C:/Python37-x64 + - PYTHON: C:/Python38-x64 ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 @@ -21,15 +21,17 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip +- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-depends.zip -oc:\ +- 7z x pillow-test-images.zip -oc:\ - mv c:\pillow-depends-main c:\pillow-depends -- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images -- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ -- ..\pillow-depends\gs1000w32.exe /S -- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% +- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images +- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\ +- choco install ghostscript --version=10.0.0.20230317 +- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | - c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ + c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\pillow\winbuild\build\build_dep_all.cmd $host.SetShouldExit(0) - path C:\pillow\winbuild\build\bin;%PATH% @@ -50,8 +52,8 @@ test_script: #- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? after_test: -- python -m pip install codecov -- codecov --file coverage.xml --name %PYTHON% --flags AppVeyor +- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe +- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor matrix: fast_finish: true diff --git a/.ci/after_success.sh b/.ci/after_success.sh index 23a6fcd4d..c71546f00 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -1,7 +1,7 @@ #!/bin/bash # gather the coverage data -python3 -m pip install codecov +python3 -m pip install coverage if [[ $MATRIX_DOCKER ]]; then python3 -m coverage xml --ignore-errors else diff --git a/.ci/install.sh b/.ci/install.sh index 518b66acc..17c349ab1 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -37,11 +37,12 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy + # TODO Remove condition when NumPy supports 3.12 + if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 + sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index db0307046..560d6c7df 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -3,10 +3,12 @@ name: CIFuzz on: push: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" workflow_dispatch: @@ -14,7 +16,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..81ba8ef15 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,55 @@ +name: Docs + +on: + push: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + build: + + runs-on: ubuntu-latest + name: Docs + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: ".ci/*.sh" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Install Linux dependencies + run: | + .ci/install.sh + env: + GHA_PYTHON_VERSION: "3.x" + + - name: Build + run: | + .ci/build.sh + + - name: Docs + run: | + make doccheck diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index dfd7d0553..1fc6262f4 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -13,7 +13,8 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install numpy +# TODO Remove condition when NumPy supports 3.12 +if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8c210bc90..24b8f85d1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v7 + uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7b8070d34..9a1e46705 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -1,6 +1,15 @@ name: Test Cygwin -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -30,22 +39,37 @@ jobs: uses: actions/checkout@v3 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v3 + uses: cygwin/cygwin-install-action@v4 with: platform: x86_64 packages: > - ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel - libimagequant-devel libjpeg-devel liblapack-devel - liblcms2-devel libopenjp2-devel libraqm-devel - libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 - make netpbm perl + gcc-g++ + ghostscript + ImageMagick + jpeg + libfreetype-devel + libimagequant-devel + libjpeg-devel + liblapack-devel + liblcms2-devel + libopenjp2-devel + libraqm-devel + libtiff-devel + libwebp-devel + libxcb-devel + libxcb-xinerama0 + make + netpbm + perl python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools subversion xorg-server-extra zlib-devel + wget + xorg-server-extra + zlib-devel - name: Add Lapack to PATH uses: egor-tensin/cleanup-path@v3 @@ -60,6 +84,10 @@ jobs: restore-keys: | ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- + - name: Select Python version + run: | + ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + - name: Build system information run: | dash.exe -c "python3 .github/workflows/system-info.py" @@ -71,7 +99,7 @@ jobs: - name: Install a different NumPy shell: dash.exe -l "{0}" run: | - python3 -m pip install -U 'numpy!=1.21.*' + python3 -m pip install -U numpy - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 7331cf8ee..4f01abe44 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -1,6 +1,15 @@ name: Test Docker -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -24,16 +33,15 @@ jobs: # Then run the remainder alpine, amazon-2-amd64, + amazon-2023-amd64, arch, centos-7-amd64, centos-stream-8-amd64, centos-stream-9-amd64, - debian-10-buster-x86, debian-11-bullseye-x86, - fedora-36-amd64, fedora-37-amd64, + fedora-38-amd64, gentoo, - ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, ] @@ -87,6 +95,7 @@ jobs: with: flags: GHA_Docker name: ${{ matrix.docker }} + gcov: true success: permissions: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index ccf6e193a..a109ec0d8 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -1,11 +1,20 @@ name: Test MinGW -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -45,12 +54,6 @@ jobs: - 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 }}-gcc \ ${{ matrix.package }}-ghostscript \ @@ -61,14 +64,23 @@ jobs: ${{ matrix.package }}-libtiff \ ${{ matrix.package }}-libwebp \ ${{ matrix.package }}-openjpeg2 \ - subversion + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-setuptools + + if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then + pacman -S --noconfirm \ + ${{ matrix.package }}-python-pyqt6 + fi python3 -m pip install pyroma pytest pytest-cov pytest-timeout pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . + run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . - name: Test Pillow run: | diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index f8b050f76..6fab0ecd2 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -5,10 +5,12 @@ name: Test Valgrind on: push: paths: + - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" workflow_dispatch: @@ -16,7 +18,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 487c3586f..a00880111 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -1,6 +1,15 @@ name: Test Windows -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -15,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev"] architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows @@ -38,6 +47,12 @@ jobs: repository: python-pillow/pillow-depends path: winbuild\depends + - name: Checkout extra test images + uses: actions/checkout@v3 + with: + repository: python-pillow/test-images + path: Tests\test-images + # sets env: pythonLocation - name: Set up Python uses: actions/setup-python@v4 @@ -56,13 +71,14 @@ jobs: - name: Install dependencies id: install run: | - 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" - echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH + 7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\" + echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH - winbuild\depends\gs1000w32.exe /S - echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.0.0.20230317 + echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH - xcopy /S /Y winbuild\depends\test_images\* Tests\images\ + # Install extra test images + xcopy /S /Y Tests\test-images\* Tests\images # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` @@ -81,7 +97,7 @@ jobs: - name: Prepare build if: steps.build-cache.outputs.cache-hit != 'true' run: | - & python.exe winbuild\build_prepare.py -v --python=$env:pythonLocation --srcdir + & python.exe winbuild\build_prepare.py -v --python $env:pythonLocation shell: pwsh - name: Build dependencies / libjpeg-turbo diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7b77be..fced6113b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,15 @@ name: Test -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -22,14 +31,14 @@ jobs: python-version: [ "pypy3.9", "pypy3.8", + "3.12-dev", "3.11", "3.10", "3.9", "3.8", - "3.7", ] include: - - python-version: "3.7" + - python-version: "3.9" PYTHONOPTIMIZE: 1 REVERSE: "--reverse" - python-version: "3.8" @@ -95,11 +104,6 @@ jobs: name: errors path: Tests/errors - - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11 - run: | - make doccheck - - name: After success run: | .ci/after_success.sh @@ -107,9 +111,9 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v3 with: - file: ./coverage.xml flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} + gcov: true success: permissions: diff --git a/.gitignore b/.gitignore index 790404535..1dd6c9175 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ docs/_build/ # JetBrains .idea -# Extra test images installed from pillow-depends/test_images +# Extra test images installed from python-pillow/test-images Tests/images/README.md Tests/images/crash_1.tif Tests/images/crash_2.tif diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d790e7850..4882a317f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,20 @@ repos: - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.3.0 hooks: - id: black - args: [--target-version=py37] + args: [--target-version=py38] # Only .py files, until https://github.com/psf/black/issues/402 resolved files: \.py$ types: [] - repo: https://github.com/PyCQA/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/PyCQA/bandit - rev: 1.7.4 + rev: 1.7.5 hooks: - id: bandit args: [--severity-level=high] @@ -26,10 +26,10 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.3.1 + rev: v1.5.1 hooks: - id: remove-tabs - exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 @@ -39,7 +39,7 @@ repos: [flake8-2020, flake8-errmsg, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-check-blanket-noqa - id: rst-backticks @@ -57,7 +57,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.5.2 + rev: 1.3.0 hooks: - id: tox-ini-fmt diff --git a/.readthedocs.yml b/.readthedocs.yml index 0f581ebba..ec3300dd1 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,12 @@ version: 2 +formats: all + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + python: install: - method: pip diff --git a/CHANGES.rst b/CHANGES.rst index 7ec7b936d..f8844daca 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,147 @@ Changelog (Pillow) ================== +10.0.0 (unreleased) +------------------- + +- Support reading signed 8-bit TIFF images #7111 + [radarhere] + +- Added width argument to ImageDraw regular_polygon #7132 + [radarhere] + +- Support I mode for ImageFilter.BuiltinFilter #7108 + [radarhere] + +- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112 + [radarhere] + +- Added unpacker from I;16B to I;16 #7125 + [radarhere] + +- Support float font sizes #7107 + [radarhere] + +- Use later value for duplicate xref entries in PdfParser #7102 + [radarhere] + +- Load before getting size in __getstate__ #7105 + [bigcat88, radarhere] + +- Fixed type handling for include and lib directories #7069 + [adisbladis, radarhere] + +- Remove deprecations for Pillow 10.0.0 #7059, #7080 + [hugovk, radarhere] + +- Drop support for soon-EOL Python 3.7 #7058 + [hugovk, radarhere] + +9.5.0 (2023-04-01) +------------------ + +- Added ImageSourceData to TAGS_V2 #7053 + [radarhere] + +- Clear PPM half token after use #7052 + [radarhere] + +- Removed absolute path to ldconfig #7044 + [radarhere] + +- Support custom comments and PLT markers when saving JPEG2000 images #6903 + [joshware, radarhere, hugovk] + +- Load before getting size in __array_interface__ #7034 + [radarhere] + +- Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 #7010 + [radarhere] + +- Consider transparency when applying APNG blend mask #7018 + [radarhere] + +- Round duration when saving animated WebP images #6996 + [radarhere] + +- Added reading of JPEG2000 comments #6909 + [radarhere] + +- Decrement reference count #7003 + [radarhere, nulano] + +- Allow libtiff_support_custom_tags to be missing #7020 + [radarhere] + +- Improved I;16N support #6834 + [radarhere] + +- Added QOI reading #6852 + [radarhere, hugovk] + +- Added saving RGBA images as PDFs #6925 + [radarhere] + +- Do not raise an error if os.environ does not contain PATH #6935 + [radarhere, hugovk] + +- Close OleFileIO instance when closing or exiting FPX or MIC #7005 + [radarhere] + +- Added __int__ to IFDRational for Python >= 3.11 #6998 + [radarhere] + +- Added memoryview support to Dib.frombytes() #6988 + [radarhere, nulano] + +- Close file pointer copy in the libtiff encoder if still open #6986 + [fcarron, radarhere] + +- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 + [radarhere] + +- Added "corners" argument to ImageDraw rounded_rectangle() #6954 + [radarhere] + +- Added memoryview support to frombytes() #6974 + [radarhere] + +- Allow comments in FITS images #6973 + [radarhere] + +- Support saving PDF with different X and Y resolutions #6961 + [jvanderneutstulen, radarhere, hugovk] + +- Fixed writing int as UNDEFINED tag #6950 + [radarhere] + +- Raise an error if EXIF data is too long when saving JPEG #6939 + [radarhere] + +- Handle more than one directory returned by pkg-config #6896 + [sebastic, radarhere] + +- Do not retry past formats when loading all formats for the first time #6902 + [radarhere] + +- Do not retry specified formats if they failed when opening #6893 + [radarhere] + +- Do not unintentionally load TIFF format at first #6892 + [radarhere] + +- Stop reading when EPS line becomes too long #6897 + [radarhere] + +- Allow writing IFDRational to BYTE tag #6890 + [radarhere] + +- Raise ValueError for BoxBlur filter with negative radius #6874 + [hugovk, radarhere] + +- Support arbitrary number of loaded modules on Windows #6761 + [javidcf, radarhere, nulano] + 9.4.0 (2023-01-02) ------------------ diff --git a/LICENSE b/LICENSE index 616808a48..cf65e86d7 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-2023 by Alex Clark and contributors + Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors. Like PIL, Pillow is licensed under the open source HPND License: @@ -13,8 +13,8 @@ By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: -Permission to use, copy, modify, and distribute this software and its -associated documentation for any purpose and without fee is hereby granted, +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be diff --git a/Makefile b/Makefile index a2545b54e..e41f36411 100644 --- a/Makefile +++ b/Makefile @@ -16,10 +16,16 @@ coverage: python3 -m coverage report .PHONY: doc -doc: +.PHONY: html +doc html: python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html +.PHONY: htmlview +htmlview: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs htmlview + .PHONY: doccheck doccheck: $(MAKE) doc @@ -38,7 +44,8 @@ help: @echo " coverage run coverage test (in progress)" @echo " doc make HTML docs" @echo " docserve run an HTTP server on the docs directory" - @echo " html to make standalone HTML files" + @echo " html make HTML docs" + @echo " htmlview open the index page built by the html target in your browser" @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" @@ -71,6 +78,7 @@ debug: .PHONY: release-test release-test: + python3 Tests/check_release_notes.py python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests @@ -116,5 +124,5 @@ lint: lint-fix: python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort - python3 -m black --target-version py37 . + python3 -m black --target-version py38 . python3 -m isort . diff --git a/README.md b/README.md index 8ee68f9b8..af1ca57c2 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ ## Python Imaging Library (Fork) -Pillow is the friendly PIL fork by [Alex Clark and -Contributors](https://github.com/python-pillow/Pillow/graphs/contributors). +Pillow is the friendly PIL fork by [Jeffrey A. Clark (Alex) and +contributors](https://github.com/python-pillow/Pillow/graphs/contributors). PIL is the Python Imaging Library by Fredrik Lundh and Contributors. As of 2019, Pillow development is [supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). @@ -88,6 +88,10 @@ As of 2019, Pillow development is Follow on https://twitter.com/PythonPillow + Follow on https://fosstodon.org/@pillow diff --git a/RELEASING.md b/RELEASING.md index b05067484..604bb1b8c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -11,14 +11,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Develop and prepare release in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions. -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: ```bash git branch 5.2.x git tag 5.2.0 - git push --all git push --tags ``` * [ ] Create and check source distribution: @@ -32,8 +31,11 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. python3 -m twine upload dist/Pillow-5.2.0* ``` * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` - +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), + increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: + ```bash + git push --all + ``` ## Point Release Released as needed for security, installation or critical bug fixes. @@ -45,16 +47,12 @@ Released as needed for security, installation or critical bug fixes. git checkout -t remotes/origin/5.2.x ``` * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. - - - * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: ```bash git tag 5.2.1 - git push git push --tags ``` * [ ] Create and check source distribution: @@ -67,7 +65,10 @@ Released as needed for security, installation or critical bug fixes. python3 -m twine check --strict dist/* python3 -m twine upload dist/Pillow-5.2.1* ``` -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: + ```bash + git push + ``` ## Embargoed Release @@ -83,7 +84,6 @@ Released as needed privately to individual vendors for critical security-related ```bash git checkout 2.5.x git tag 2.5.3 - git push origin 2.5.x git push origin --tags ``` * [ ] Create and check source distribution: @@ -91,15 +91,14 @@ Released as needed privately to individual vendors for critical security-related make sdist ``` * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: + ```bash + git push origin 2.5.x + ``` ## Binary Distributions -### Windows -* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) - and copy into `dist/` - -### Mac and Linux +### macOS and Linux * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): ```bash git clone https://github.com/python-pillow/pillow-wheels @@ -107,11 +106,22 @@ Released as needed privately to individual vendors for critical security-related ./update-pillow-tag.sh [[release tag]] ``` * [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases) - and copy into `dist/` + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo: + ```bash + gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels + ``` + +### Windows +* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): + ```bash + gh run download --dir dist + # select dist-x.y.z + ``` ## Publicize Release -* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 +* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 ## Documentation diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 08a55d349..c600c45ed 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -4,7 +4,6 @@ TEST_FILE = "Tests/images/fli_overflow.fli" def test_fli_overflow(): - # this should not crash with a malloc error or access violation with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index d8d645189..f4a129f50 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -23,7 +23,6 @@ def test_ignore_dos_text(): def test_dos_text(): - try: im = Image.open(TEST_FILE) im.load() diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py new file mode 100644 index 000000000..0a9a898d7 --- /dev/null +++ b/Tests/check_release_notes.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +for rst in Path("docs/releasenotes").glob("[1-9]*.rst"): + if "TODO" in open(rst).read(): + sys.exit(f"Error: remove TODO from {rst}") diff --git a/Tests/helper.py b/Tests/helper.py index 0d1d03ac8..69246bfcf 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) HAS_UPLOADER = False -if os.environ.get("SHOW_ERRORS", None): +if os.environ.get("SHOW_ERRORS"): # local img.show for errors. HAS_UPLOADER = True @@ -271,7 +271,7 @@ def netpbm_available(): def magick_command(): if sys.platform == "win32": - magickhome = os.environ.get("MAGICK_HOME", "") + magickhome = os.environ.get("MAGICK_HOME") if magickhome: imagemagick = [os.path.join(magickhome, "convert.exe")] graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] diff --git a/Tests/images/8bit.s.tif b/Tests/images/8bit.s.tif new file mode 100644 index 000000000..043cba6af Binary files /dev/null and b/Tests/images/8bit.s.tif differ diff --git a/Tests/images/blend_transparency.png b/Tests/images/blend_transparency.png new file mode 100644 index 000000000..cef0a16de Binary files /dev/null and b/Tests/images/blend_transparency.png differ diff --git a/Tests/images/comment.jp2 b/Tests/images/comment.jp2 new file mode 100644 index 000000000..4bdf91760 Binary files /dev/null and b/Tests/images/comment.jp2 differ diff --git a/Tests/images/duplicate_xref_entry.pdf b/Tests/images/duplicate_xref_entry.pdf new file mode 100644 index 000000000..f57a57d61 Binary files /dev/null and b/Tests/images/duplicate_xref_entry.pdf differ diff --git a/Tests/images/hopper.qoi b/Tests/images/hopper.qoi new file mode 100644 index 000000000..6b255aba1 Binary files /dev/null and b/Tests/images/hopper.qoi differ diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png new file mode 100644 index 000000000..f4dab388f Binary files /dev/null and b/Tests/images/hopper_emboss_I.png differ diff --git a/Tests/images/hopper_emboss_more_I.png b/Tests/images/hopper_emboss_more_I.png new file mode 100644 index 000000000..c417c915f Binary files /dev/null and b/Tests/images/hopper_emboss_more_I.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png index 11a1be6fa..5e3cf22b4 100644 Binary files a/Tests/images/imagedraw_ellipse_various_sizes.png and b/Tests/images/imagedraw_ellipse_various_sizes.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes_filled.png b/Tests/images/imagedraw_ellipse_various_sizes_filled.png index d71e175b8..dd2f641f1 100644 Binary files a/Tests/images/imagedraw_ellipse_various_sizes_filled.png and b/Tests/images/imagedraw_ellipse_various_sizes_filled.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png new file mode 100644 index 000000000..3e79e21ae Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnny.png new file mode 100644 index 000000000..7fa09a3c0 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png new file mode 100644 index 000000000..d825ad263 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png new file mode 100644 index 000000000..c19da698e Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png new file mode 100644 index 000000000..f3e95d487 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png new file mode 100644 index 000000000..274d27984 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png new file mode 100644 index 000000000..c5f40bfdb Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png new file mode 100644 index 000000000..01bfd1750 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png new file mode 100644 index 000000000..efd27be4f Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png new file mode 100644 index 000000000..d3acd01ab Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png new file mode 100644 index 000000000..55ddbc033 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png new file mode 100644 index 000000000..c000b26e9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png new file mode 100644 index 000000000..7056b4fd9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyny.png new file mode 100644 index 000000000..5eca030b9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png new file mode 100644 index 000000000..7f1f00344 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png new file mode 100644 index 000000000..2e815f4ad Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png differ diff --git a/Tests/images/imagedraw_triangle_width.png b/Tests/images/imagedraw_triangle_width.png new file mode 100644 index 000000000..3d35326e7 Binary files /dev/null and b/Tests/images/imagedraw_triangle_width.png differ diff --git a/Tests/images/pil123rgba.qoi b/Tests/images/pil123rgba.qoi new file mode 100644 index 000000000..1e46036c7 Binary files /dev/null and b/Tests/images/pil123rgba.qoi differ diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index ed9aff9cc..002a44a4f 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -18,7 +18,6 @@ def test_bad(): """These shouldn't crash/dos, but they shouldn't return anything either""" for f in get_files("b"): - # Assert that there is no unclosed file warning with warnings.catch_warnings(): try: diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 385192a3c..9021a9fb3 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -177,13 +177,14 @@ class TestEnvVars: Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) assert Image.core.get_block_size() == 2 * 1024 * 1024 - def test_warnings(self): - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"} - ) + @pytest.mark.parametrize( + "var", + ( + {"PILLOW_ALIGNMENT": "15"}, + {"PILLOW_BLOCK_SIZE": "1024"}, + {"PILLOW_BLOCKS_MAX": "wat"}, + ), + ) + def test_warnings(self, var): + with pytest.warns(UserWarning): + Image._apply_env_variables(var) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 63071b78c..4fd02449c 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -36,12 +36,10 @@ class TestDecompressionBomb: Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 - def open(): + with pytest.warns(Image.DecompressionBombWarning): with Image.open(TEST_FILE): pass - pytest.warns(Image.DecompressionBombWarning, open) - def test_exception(self): # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 @@ -87,7 +85,8 @@ class TestDecompressionCrop: # same decompression bomb warnings on them. with hopper() as src: box = (0, 0, src.width * 2, src.height * 2) - pytest.warns(Image.DecompressionBombWarning, src.crop, box) + with pytest.warns(Image.DecompressionBombWarning): + src.crop(box) def test_crop_decompression_checks(self): im = Image.new("RGB", (100, 100)) @@ -95,7 +94,8 @@ class TestDecompressionCrop: for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): assert im.crop(value).size == (9, 9) - pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99)) + with pytest.warns(Image.DecompressionBombWarning): + im.crop((-160, -160, 99, 99)) with pytest.raises(Image.DecompressionBombError): im.crop((-99909, -99990, 99999, 99999)) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 30ed4a808..f175b90af 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -7,9 +7,9 @@ from PIL import _deprecate "version, expected", [ ( - 10, - "Old thing is deprecated and will be removed in Pillow 10 " - r"\(2023-07-01\)\. Use new thing instead\.", + 11, + "Old thing is deprecated and will be removed in Pillow 11 " + r"\(2024-10-15\)\. Use new thing instead\.", ), ( None, @@ -24,7 +24,7 @@ def test_version(version, expected): def test_unknown_version(): - expected = r"Unknown removal version, update PIL\._deprecate\?" + expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" with pytest.raises(ValueError, match=expected): _deprecate.deprecate("Old thing", 12345, "new thing") @@ -52,18 +52,18 @@ def test_old_version(deprecated, plural, expected): def test_plural(): expected = ( - r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Use new thing instead\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old things", 10, "new thing", plural=True) + _deprecate.deprecate("Old things", 11, "new thing", plural=True) def test_replacement_and_action(): expected = "Use only one of 'replacement' and 'action'" with pytest.raises(ValueError, match=expected): _deprecate.deprecate( - "Old thing", 10, replacement="new thing", action="Upgrade to new thing" + "Old thing", 11, replacement="new thing", action="Upgrade to new thing" ) @@ -76,16 +76,16 @@ def test_replacement_and_action(): ) def test_action(action): expected = ( - r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 10, action=action) + _deprecate.deprecate("Old thing", 11, action=action) def test_no_replacement_or_action(): expected = ( - r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)" + r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 10) + _deprecate.deprecate("Old thing", 11) diff --git a/Tests/test_deprecated_imageqt.py b/Tests/test_deprecated_imageqt.py deleted file mode 100644 index 2528ff3f7..000000000 --- a/Tests/test_deprecated_imageqt.py +++ /dev/null @@ -1,18 +0,0 @@ -import warnings - -with warnings.catch_warnings(record=True) as w: - # Arrange: cause all warnings to always be triggered - warnings.simplefilter("always") - - # Act: trigger a warning with Qt5 - from PIL import ImageQt - - -def test_deprecated(): - # Assert - if ImageQt.qt_version in ("5", "side2"): - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "deprecated" in str(w[0].message) - else: - assert len(w) == 0 diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 51637c786..f78c086eb 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -163,6 +163,12 @@ def test_apng_blend(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) +def test_apng_blend_transparency(): + with Image.open("Tests/images/blend_transparency.png") as im: + im.seek(1) + assert im.getpixel((0, 0)) == (255, 0, 0) + + def test_apng_chunk_order(): with Image.open("Tests/images/apng/fctl_actl.png") as im: im.seek(im.n_frames - 1) @@ -263,13 +269,11 @@ def test_apng_chunk_errors(): with Image.open("Tests/images/apng/chunk_no_actl.png") as im: assert not im.is_animated - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() assert not im.is_animated - pytest.warns(UserWarning, open) - with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: assert not im.is_animated @@ -287,21 +291,17 @@ def test_apng_chunk_errors(): def test_apng_syntax_errors(): - def open_frames_zero(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert not im.is_animated with pytest.raises(OSError): im.load() - pytest.warns(UserWarning, open_frames_zero) - - def open_frames_zero_default(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open_frames_zero_default) - # we can handle this case gracefully exception = None with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: @@ -316,13 +316,11 @@ def test_apng_syntax_errors(): im.seek(im.n_frames - 1) im.load() - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open) - @pytest.mark.parametrize( "test_file", @@ -657,13 +655,3 @@ def test_different_modes_in_later_frames(mode, tmp_path): im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))]) with Image.open(test_file) as reloaded: assert reloaded.mode == mode - - -def test_constants_deprecation(): - for enum, prefix in { - PngImagePlugin.Disposal: "APNG_DISPOSE_", - PngImagePlugin.Blend: "APNG_BLEND_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(PngImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index ba2781820..8b1355b62 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,6 +1,6 @@ import pytest -from PIL import BlpImagePlugin, Image +from PIL import Image from .helper import ( assert_image_equal, @@ -72,14 +72,3 @@ def test_crashes(test_file): with Image.open(f) as im: with pytest.raises(OSError): im.load() - - -def test_constants_deprecation(): - for enum, prefix in { - BlpImagePlugin.Format: "BLP_FORMAT_", - BlpImagePlugin.Encoding: "BLP_ENCODING_", - BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(BlpImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 5f6d52355..9e79937e9 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -141,7 +141,6 @@ def test_rgba_bitfields(): # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: - # So before the comparing the image, swap the channels b, g, r = im.split()[1:] im = Image.merge("RGB", (r, g, b)) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index e330404d6..a7714c92c 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "BUFR" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() @@ -58,6 +56,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 0f09c4b99..22686af34 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -15,7 +15,6 @@ def test_sanity(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.size == (128, 128) assert isinstance(im, DcxImagePlugin.DcxImageFile) @@ -29,7 +28,8 @@ def test_unclosed_file(): im = Image.open(TEST_FILE) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -54,7 +54,6 @@ def test_invalid_file(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 015dda992..26adfff87 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -28,34 +28,65 @@ FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" # EPS test files with binary preview FILE3 = "Tests/images/binary_preview_map.eps" +# Three unsigned 32bit little-endian values: +# 0xC6D3D0C5 magic number +# byte position of start of postscript section (12) +# byte length of postscript section (0) +# this byte length isn't valid, but we don't read it +simple_binary_header = b"\xc5\xd0\xd3\xc6\x0c\x00\x00\x00\x00\x00\x00\x00" + +# taken from page 8 of the specification +# https://web.archive.org/web/20220120164601/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf +simple_eps_file = ( + b"%!PS-Adobe-3.0 EPSF-3.0", + b"%%BoundingBox: 5 5 105 105", + b"10 setlinewidth", + b"10 10 moveto", + b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", + b"stroke", +) +simple_eps_file_with_comments = ( + simple_eps_file[:1] + + ( + b"%%Comment1: Some Value", + b"%%SecondComment: Another Value", + ) + + simple_eps_file[1:] +) +simple_eps_file_without_version = simple_eps_file[1:] +simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] +simple_eps_file_with_invalid_boundingbox = ( + simple_eps_file[:1] + (b"%%BoundingBox: a b c d",) + simple_eps_file[2:] +) +simple_eps_file_with_invalid_boundingbox_valid_imagedata = ( + simple_eps_file_with_invalid_boundingbox + (b"%ImageData: 100 100 8 3",) +) +simple_eps_file_with_long_ascii_comment = ( + simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:] +) +simple_eps_file_with_long_binary_data = ( + simple_eps_file[:2] + + ( + b"%%BeginBinary: 300", + b"\0" * 300, + b"%%EndBinary", + ) + + simple_eps_file[2:] +) + @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_sanity(): - # Regular scale - with Image.open(FILE1) as image1: - image1.load() - assert image1.mode == "RGB" - assert image1.size == (460, 352) - assert image1.format == "EPS" - - with Image.open(FILE2) as image2: - image2.load() - assert image2.mode == "RGB" - assert image2.size == (360, 252) - assert image2.format == "EPS" - - # Double scale - with Image.open(FILE1) as image1_scale2: - image1_scale2.load(scale=2) - assert image1_scale2.mode == "RGB" - assert image1_scale2.size == (920, 704) - assert image1_scale2.format == "EPS" - - with Image.open(FILE2) as image2_scale2: - image2_scale2.load(scale=2) - assert image2_scale2.mode == "RGB" - assert image2_scale2.size == (720, 504) - assert image2_scale2.format == "EPS" +@pytest.mark.parametrize( + ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) +) +@pytest.mark.parametrize("scale", (1, 2)) +def test_sanity(filename, size, scale): + expected_size = tuple(s * scale for s in size) + with Image.open(filename) as image: + image.load(scale=scale) + assert image.mode == "RGB" + assert image.size == expected_size + assert image.format == "EPS" @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -69,18 +100,78 @@ def test_load(): def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(invalid_file) +def test_binary_header_only(): + data = io.BytesIO(simple_binary_header) + with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_version_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) + with pytest.raises(SyntaxError): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_boundingbox_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) + with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) + with pytest.raises(OSError, match="cannot determine EPS bounding box"): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): + data = io.BytesIO( + prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) + ) + with Image.open(data) as img: + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_ascii_comment_too_long(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) + with pytest.raises(SyntaxError, match="not an EPS file"): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_long_binary_data(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_load_long_binary_data(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + with Image.open(data) as img: + img.load() + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @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: - assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) assert cmyk_image.format == "EPS" @@ -101,7 +192,7 @@ def test_showpage(): with Image.open("Tests/images/reqd_showpage.png") as target: # should not crash/hang plot_image.load() - # fonts could be slightly different + # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -112,7 +203,7 @@ def test_transparency(): assert plot_image.mode == "RGBA" with Image.open("Tests/images/reqd_showpage_transparency.png") as target: - # fonts could be slightly different + # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -207,7 +298,6 @@ def test_resize(filename): @pytest.mark.parametrize("filename", (FILE1, FILE2)) def test_thumbnail(filename): # Issue #619 - # Arrange with Image.open(filename) as im: new_size = (100, 100) im.thumbnail(new_size) @@ -221,7 +311,7 @@ def test_read_binary_preview(): pass -def test_readline(tmp_path): +def test_readline_psfile(tmp_path): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' line_endings = ["\r\n", "\n", "\n\r", "\r"] @@ -238,7 +328,8 @@ def test_readline(tmp_path): def _test_readline_io_psfile(test_string, ending): f = io.BytesIO(test_string.encode("latin-1")) - t = EpsImagePlugin.PSFile(f) + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) def _test_readline_file_psfile(test_string, ending): @@ -247,7 +338,8 @@ def test_readline(tmp_path): w.write(test_string.encode("latin-1")) with open(f, "rb") as r: - t = EpsImagePlugin.PSFile(r) + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(r) _test_readline(t, ending) for ending in line_endings: @@ -256,6 +348,25 @@ def test_readline(tmp_path): _test_readline_file_psfile(s, ending) +def test_psfile_deprecation(): + with pytest.warns(DeprecationWarning): + EpsImagePlugin.PSFile(None) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +@pytest.mark.parametrize( + "line_ending", + (b"\r\n", b"\n", b"\n\r", b"\r"), +) +def test_readline(prefix, line_ending): + simple_file = prefix + line_ending.join(simple_eps_file_with_comments) + data = io.BytesIO(simple_file) + test_file = EpsImagePlugin.EpsImageFile(data) + assert test_file.info["Comment1"] == "Some Value" + assert test_file.info["SecondComment"] == "Another Value" + assert test_file.size == (100, 100) + + @pytest.mark.parametrize( "filename", ( diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 447888acd..68b3eb567 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -2,7 +2,7 @@ from io import BytesIO import pytest -from PIL import FitsImagePlugin, FitsStubImagePlugin, Image +from PIL import FitsImagePlugin, Image from .helper import assert_image_equal, hopper @@ -12,7 +12,6 @@ TEST_FILE = "Tests/images/hopper.fits" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "FITS" assert im.size == (128, 128) @@ -45,36 +44,7 @@ def test_naxis_zero(): pass -def test_stub_deprecated(): - class Handler: - opened = False - loaded = False - - def open(self, im): - self.opened = True - - def load(self, im): - self.loaded = True - return Image.new("RGB", (1, 1)) - - handler = Handler() - with pytest.warns(DeprecationWarning): - FitsStubImagePlugin.register_handler(handler) - - with Image.open(TEST_FILE) as im: - assert im.format == "FITS" - assert im.size == (128, 128) - assert im.mode == "L" - - assert handler.opened - assert not handler.loaded - - im.load() - assert handler.loaded - - FitsStubImagePlugin._handler = None - Image.register_open( - FitsImagePlugin.FitsImageFile.format, - FitsImagePlugin.FitsImageFile, - FitsImagePlugin._accept, - ) +def test_comment(): + image_data = b"SIMPLE = T / comment string" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index b8b999d70..f96afdc95 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -36,7 +36,8 @@ def test_unclosed_file(): im = Image.open(static_test_file) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -64,7 +65,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(static_test_file) as im: - # Act frame = im.tell() @@ -110,7 +110,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(animated_test_file) as im: - layer_number = im.tell() assert layer_number == 0 diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index fa22e90f6..9a1784d31 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -18,6 +18,16 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") +def test_close(): + with Image.open("Tests/images/input_bw_one_band.fpx") as im: + pass + assert im.ole.fp.closed + + im = Image.open("Tests/images/input_bw_one_band.fpx") + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index cae20fa46..ac6253db0 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -21,12 +21,3 @@ def test_invalid_file(): with pytest.raises(SyntaxError): FtexImagePlugin.FtexImageFile(invalid_file) - - -def test_constants_deprecation(): - for enum, prefix in { - FtexImagePlugin.Format: "FORMAT_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(FtexImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index d48fc1442..8522f486a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -36,7 +36,8 @@ def test_unclosed_file(): im = Image.open(TEST_GIF) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -158,39 +159,42 @@ def test_optimize(): assert test_bilevel(1) == 799 -def test_optimize_correctness(): - # 256 color Palette image, posterize to > 128 and < 128 levels - # Size bigger and smaller than 512x512 +@pytest.mark.parametrize( + "colors, size, expected_palette_length", + ( + # These do optimize the palette + (256, 511, 256), + (255, 511, 255), + (129, 511, 129), + (128, 511, 128), + (64, 511, 64), + (4, 511, 4), + # These don't optimize the palette + (128, 513, 256), + (64, 513, 256), + (4, 513, 256), + ), +) +def test_optimize_correctness(colors, size, expected_palette_length): + # 256 color Palette image, posterize to > 128 and < 128 levels. + # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. - # Check for correctness after conversion back to RGB - def check(colors, size, expected_palette_length): - # make an image with empty colors in the start of the palette range - im = Image.frombytes( - "P", (colors, colors), bytes(range(256 - colors, 256)) * colors - ) - im = im.resize((size, size)) - outfile = BytesIO() - im.save(outfile, "GIF") - outfile.seek(0) - with Image.open(outfile) as reloaded: - # check palette length - palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) - assert expected_palette_length == palette_length + # Check for correctness after conversion back to RGB. - assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + # make an image with empty colors in the start of the palette range + im = Image.frombytes( + "P", (colors, colors), bytes(range(256 - colors, 256)) * colors + ) + im = im.resize((size, size)) + outfile = BytesIO() + im.save(outfile, "GIF") + outfile.seek(0) + with Image.open(outfile) as reloaded: + # check palette length + palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) + assert expected_palette_length == palette_length - # These do optimize the palette - check(256, 511, 256) - check(255, 511, 255) - check(129, 511, 129) - check(128, 511, 128) - check(64, 511, 64) - check(4, 511, 4) - - # These don't optimize the palette - check(128, 513, 256) - check(64, 513, 256) - check(4, 513, 256) + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) def test_optimize_full_l(): @@ -206,7 +210,7 @@ def test_optimize_if_palette_can_be_reduced_by_half(): im = im.resize((591, 443)) im_rgb = im.convert("RGB") - for (optimize, colors) in ((False, 256), (True, 8)): + for optimize, colors in ((False, 256), (True, 8)): out = BytesIO() im_rgb.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: @@ -218,7 +222,6 @@ def test_roundtrip(tmp_path): im = hopper() im.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) @@ -229,7 +232,6 @@ def test_roundtrip2(tmp_path): im2 = im.copy() im2.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), hopper(), 50) @@ -239,7 +241,6 @@ def test_roundtrip_save_all(tmp_path): im = hopper() im.save(out, save_all=True) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) # Multiframe image @@ -281,13 +282,11 @@ def test_headers_saving_for_animated_gifs(tmp_path): important_headers = ["background", "version", "duration", "loop"] # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - info = im.info.copy() out = str(tmp_path / "temp.gif") im.save(out, save_all=True) with Image.open(out) as reread: - for header in important_headers: assert info[header] == reread.info[header] @@ -305,7 +304,6 @@ def test_palette_handling(tmp_path): im2.save(f, optimize=True) with Image.open(f) as reloaded: - assert_image_similar(im, reloaded.convert("RGB"), 10) @@ -321,7 +319,6 @@ def test_palette_434(tmp_path): orig = "Tests/images/test.colors.gif" with Image.open(orig) as im: - with roundtrip(im) as reloaded: assert_image_similar(im, reloaded, 1) with roundtrip(im, optimize=True) as reloaded: @@ -572,7 +569,6 @@ def test_save_dispose(tmp_path): ) with Image.open(out) as img: - for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 @@ -770,7 +766,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -783,7 +778,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -841,7 +835,6 @@ def test_identical_frames(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - # Assert that the first three frames were combined assert reread.n_frames == 2 @@ -1095,7 +1088,8 @@ def test_rgb_transparency(tmp_path): im = Image.new("RGB", (1, 1)) im.info["transparency"] = b"" ims = [Image.new("RGB", (1, 1))] - pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims) + with pytest.warns(UserWarning): + im.save(out, save_all=True, append_images=ims) with Image.open(out) as reloaded: assert "transparency" not in reloaded.info diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index fd427746e..dd1c5e7d2 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "GRIB" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() @@ -58,6 +56,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 20b4b9619..7ca10fac5 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -8,7 +8,6 @@ TEST_FILE = "Tests/images/hdf5.h5" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "HDF5" @@ -29,7 +28,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() @@ -59,6 +57,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 55632909c..42275424d 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -16,7 +16,6 @@ def test_sanity(): # Loading this icon by default should result in the largest size # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: - # Assert that there is no unclosed file warning with warnings.catch_warnings(): im.load() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index afb17b1af..4e6dbe6ed 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -175,7 +175,6 @@ def test_save_256x256(tmp_path): # Act im.save(outfile) with Image.open(outfile) as im_saved: - # Assert assert im_saved.size == (256, 256) @@ -213,12 +212,10 @@ def test_save_append_images(tmp_path): def test_unexpected_size(): # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/hopper_unexpected.ico") as im: assert im.size == (16, 16) - pytest.warns(UserWarning, open) - def test_draw_reloaded(tmp_path): with Image.open(TEST_ICO_FILE) as im: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 5cf93713b..fd00f260e 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -32,7 +32,8 @@ def test_unclosed_file(): im = Image.open(TEST_IM) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -51,7 +52,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(TEST_IM) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 2d0e6977a..2d99528d3 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -11,7 +11,6 @@ TEST_FILE = "Tests/images/iptc.jpg" def test_getiptcinfo_jpg_none(): # Arrange with hopper() as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -22,7 +21,6 @@ def test_getiptcinfo_jpg_none(): def test_getiptcinfo_jpg_found(): # Arrange with Image.open(TEST_FILE) as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -35,7 +33,6 @@ def test_getiptcinfo_jpg_found(): def test_getiptcinfo_tiff_none(): # Arrange with Image.open("Tests/images/hopper.tif") as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eabc6bf75..73a00386f 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -57,7 +57,6 @@ class TestFileJpeg: return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) def test_sanity(self): - # internal version number assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) @@ -271,7 +270,10 @@ class TestFileJpeg: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() - im.save(f, "JPEG", quality=90, exif=b"1" * 65532) + im.save(f, "JPEG", quality=90, exif=b"1" * 65533) + + with pytest.raises(ValueError): + im.save(f, "JPEG", quality=90, exif=b"1" * 65534) def test_exif_typeerror(self): with Image.open("Tests/images/exif_typeerror.jpg") as im: @@ -368,7 +370,6 @@ class TestFileJpeg: def test_exif_gps_typeerror(self): with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: - # Should not raise a TypeError im._getexif() @@ -447,7 +448,7 @@ class TestFileJpeg: ims = im.get_child_images() assert len(ims) == 1 - assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") + assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: @@ -635,12 +636,6 @@ class TestFileJpeg: assert max(im2.quantization[0]) <= 255 assert max(im2.quantization[1]) <= 255 - def test_convert_dict_qtables_deprecation(self): - with pytest.warns(DeprecationWarning): - qtable = {0: [1, 2, 3, 4]} - qtable2 = JpegImagePlugin.convert_dict_qtables(qtable) - assert qtable == qtable2 - @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self): with Image.open(TEST_FILE) as img: @@ -682,7 +677,6 @@ class TestFileJpeg: # Shouldn't raise error fn = "Tests/images/sugarshack_bad_mpo_header.jpg" with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert assert im.format == "JPEG" @@ -704,7 +698,6 @@ class TestFileJpeg: # Arrange outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: - # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -731,7 +724,6 @@ class TestFileJpeg: # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (200, 200) @@ -740,7 +732,6 @@ class TestFileJpeg: # This image has DPI in EXIF not metadata # EXIF XResolution is 72 with Image.open("Tests/images/exif-72dpi-int.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (72, 72) @@ -749,7 +740,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg with Image.open("Tests/images/exif-200dpcm.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (508, 508) @@ -758,7 +748,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: - # Act / Assert # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) @@ -767,7 +756,6 @@ class TestFileJpeg: # 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) @@ -777,7 +765,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: - # Act / Assert # "When the image resolution is unknown, 72 [dpi] is designated." # https://exiv2.org/tags.html @@ -787,7 +774,6 @@ class TestFileJpeg: # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF with Image.open("Tests/images/invalid-exif.jpg") as im: - # This should return the default, and not a SyntaxError or # OSError for unidentified image. assert im.info.get("dpi") == (72, 72) @@ -810,7 +796,6 @@ class TestFileJpeg: def test_invalid_exif_x_resolution(self): # When no x or y resolution is defined in EXIF with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: - # This should return the default, and not a ValueError or # OSError for an unidentified image. assert im.info.get("dpi") == (72, 72) @@ -820,7 +805,6 @@ class TestFileJpeg: # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 with Image.open("Tests/images/exif-ifd-offset.jpg") as im: - # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 0229b2243..b6e8215f7 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -4,13 +4,21 @@ from io import BytesIO import pytest -from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features +from PIL import ( + Image, + ImageFile, + Jpeg2KImagePlugin, + UnidentifiedImageError, + _binary, + features, +) from .helper import ( assert_image_equal, assert_image_similar, assert_image_similar_tofile, skip_unless_feature, + skip_unless_feature_version, ) EXTRA_DIR = "Tests/images/jpeg2000" @@ -270,7 +278,6 @@ def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Act j2k.load() jp2.load() @@ -354,6 +361,35 @@ def test_subsampling_decode(name): assert_image_similar(im, expected, epsilon) +def test_comment(): + with Image.open("Tests/images/comment.jp2") as im: + assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" + + # Test an image that is truncated partway through a codestream + with open("Tests/images/comment.jp2", "rb") as fp: + b = BytesIO(fp.read(130)) + with Image.open(b) as im: + pass + + +def test_save_comment(): + for comment in ("Created by Pillow", b"Created by Pillow"): + out = BytesIO() + test_card.save(out, "JPEG2000", comment=comment) + + with Image.open(out) as im: + assert im.info["comment"] == b"Created by Pillow" + + out = BytesIO() + long_comment = b" " * 65531 + test_card.save(out, "JPEG2000", comment=long_comment) + with Image.open(out) as im: + assert im.info["comment"] == long_comment + + with pytest.raises(ValueError): + test_card.save(out, "JPEG2000", comment=long_comment + b" ") + + @pytest.mark.parametrize( "test_file", [ @@ -371,3 +407,29 @@ def test_crashes(test_file): im.load() except OSError: pass + + +@skip_unless_feature_version("jpg_2000", "2.4.0") +def test_plt_marker(): + # Search the start of the codesteam for PLT + out = BytesIO() + test_card.save(out, "JPEG2000", no_jp2=True, plt=True) + out.seek(0) + while True: + marker = out.read(2) + if not marker: + assert False, "End of stream without PLT" + + jp2_boxid = _binary.i16be(marker) + if jp2_boxid == 0xFF4F: + # SOC has no length + continue + elif jp2_boxid == 0xFF58: + # PLT + return + elif jp2_boxid == 0xFF93: + assert False, "SOD without finding PLT first" + + hdr = out.read(2) + length = _binary.i16be(hdr) + out.seek(length - 2, os.SEEK_CUR) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1109cd15e..ac78b0869 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -645,7 +645,6 @@ class TestFileLibTiff(LibTiffTestCase): pilim = hopper() def save_bytesio(compression=None): - buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -669,6 +668,16 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) + def test_exif_ifd(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert im.tag_v2[34665] == 125456 + im.save(outfile) + + with Image.open(outfile) as reloaded: + if Image.core.libtiff_support_custom_tags: + assert reloaded.tag_v2[34665] == 125456 + def test_crashing_metadata(self, tmp_path): # issue 1597 with Image.open("Tests/images/rdf.tif") as im: @@ -740,7 +749,6 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage_compression(self): with Image.open("Tests/images/compression.tif") as im: - im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) @@ -986,6 +994,36 @@ class TestFileLibTiff(LibTiffTestCase): ) as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + @pytest.mark.parametrize( + "file_name, mode, size, tile", + [ + ( + "tiff_wrong_bits_per_sample.tiff", + "RGBA", + (52, 53), + [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_2.tiff", + "RGB", + (16, 16), + [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_3.tiff", + "RGBA", + (512, 256), + [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], + ), + ], + ) + def test_wrong_bits_per_sample(self, file_name, mode, size, tile): + with Image.open("Tests/images/" + file_name) as im: + assert im.mode == mode + assert im.size == size + assert im.tile == tile + im.load() + def test_no_rows_per_strip(self): # This image does not have a RowsPerStrip TIFF tag infile = "Tests/images/no_rows_per_strip.tif" @@ -1067,3 +1105,27 @@ class TestFileLibTiff(LibTiffTestCase): out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): im.save(out, compression=compression) + + def test_save_many_compressed(self, tmp_path): + im = hopper() + out = str(tmp_path / "temp.tif") + for _ in range(10000): + im.save(out, compression="jpeg") + + @pytest.mark.parametrize( + "path, sizes", + ( + ("Tests/images/hopper.tif", ()), + ("Tests/images/child_ifd.tiff", (16, 8)), + ("Tests/images/child_ifd_jpeg.tiff", (20,)), + ), + ) + def test_get_child_images(self, path, sizes): + with Image.open(path) as im: + ims = im.get_child_images() + + assert len(ims) == len(sizes) + for i, im in enumerate(ims): + w = sizes[i] + expected = Image.new("RGB", (w, w), "#f00") + assert_image_similar(im, expected, 1) diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 464d138e2..2588d3a05 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -51,6 +51,16 @@ def test_seek(): assert im.tell() == 0 +def test_close(): + with Image.open(TEST_FILE) as im: + pass + assert im.ole.fp.closed + + im = Image.open(TEST_FILE) + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 3e5476222..2e921e467 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -42,7 +42,8 @@ def test_unclosed_file(): im = Image.open(test_files[0]) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -168,8 +169,7 @@ def test_mp_no_data(): def test_mp_attribute(test_file): with Image.open(test_file) as im: mpinfo = im._getmp() - frame_number = 0 - for mpentry in mpinfo[0xB002]: + for frame_number, mpentry in enumerate(mpinfo[0xB002]): mpattr = mpentry["Attribute"] if frame_number: assert not mpattr["RepresentativeImageFlag"] @@ -180,7 +180,6 @@ def test_mp_attribute(test_file): assert mpattr["ImageDataFormat"] == "JPEG" assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" assert mpattr["Reserved"] == 0 - frame_number += 1 @pytest.mark.parametrize("test_file", test_files) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 50d7c590b..497052b05 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -44,7 +44,6 @@ def test_open_windows_v1(): # Arrange # Act with Image.open(TEST_FILE) as im: - # Assert assert_image_equal(im, hopper("1")) assert isinstance(im, MspImagePlugin.MspImageFile) @@ -59,7 +58,6 @@ def _assert_file_image_equal(source_path, target_path): not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) def test_open_windows_v2(): - files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 5299febe9..967f5c35e 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -8,7 +8,7 @@ import pytest from PIL import Image, PdfParser, features -from .helper import hopper, mark_if_feature_version +from .helper import hopper, mark_if_feature_version, skip_unless_feature def helper_save_as_pdf(tmp_path, mode, **kwargs): @@ -42,6 +42,11 @@ def test_save(tmp_path, mode): helper_save_as_pdf(tmp_path, mode) +@skip_unless_feature("jpg_2000") +def test_save_rgba(tmp_path): + helper_save_as_pdf(tmp_path, "RGBA") + + def test_monochrome(tmp_path): # Arrange mode = "1" @@ -80,6 +85,34 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) +@pytest.mark.parametrize( + "params", + ( + {"dpi": (75, 150)}, + {"dpi": (75, 150), "resolution": 200}, + ), +) +def test_dpi(params, tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, **params) + + 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 == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @@ -89,7 +122,6 @@ def test_save_all(tmp_path): # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile, save_all=True) @@ -123,7 +155,6 @@ def test_save_all(tmp_path): def test_multiframe_normal_save(tmp_path): # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 9481cd5dd..c4db97905 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -78,7 +78,6 @@ class TestFilePng: return chunks def test_sanity(self, tmp_path): - # internal version number assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) @@ -156,7 +155,6 @@ class TestFilePng: assert im.info == {"spam": "egg"} def test_bad_itxt(self): - im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -201,7 +199,6 @@ class TestFilePng: assert im.info["spam"].tkey == "Spam" def test_interlace(self): - test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -495,7 +492,6 @@ class TestFilePng: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: - assert im.info["transparency"] == 0 def test_save_icc_profile(self): @@ -593,7 +589,7 @@ class TestFilePng: def test_textual_chunks_after_idat(self): with Image.open("Tests/images/hopper.png") as im: - assert "comment" in im.text.keys() + assert "comment" in im.text for k, v in { "date:create": "2014-09-04T09:37:08+03:00", "date:modify": "2014-09-04T09:37:08+03:00", diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fbcbea6c6..292642ca9 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -256,6 +256,16 @@ def test_truncated_file(tmp_path): im.load() +def test_not_enough_image_data(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P2 1 2 255 255") + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + @pytest.mark.parametrize("maxval", (b"0", b"65536")) def test_invalid_maxval(maxval, tmp_path): path = str(tmp_path / "temp.ppm") diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 4f934375c..e405834b5 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -27,7 +27,8 @@ def test_unclosed_file(): im = Image.open(test_file) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -77,7 +78,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(test_file) as im: - layer_number = im.tell() assert layer_number == 1 @@ -95,7 +95,6 @@ def test_seek_tell(): def test_seek_eoferror(): with Image.open(test_file) as im: - with pytest.raises(EOFError): im.seek(-1) diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py new file mode 100644 index 000000000..f33eada61 --- /dev/null +++ b/Tests/test_file_qoi.py @@ -0,0 +1,28 @@ +import pytest + +from PIL import Image, QoiImagePlugin + +from .helper import assert_image_equal_tofile, assert_image_similar_tofile + + +def test_sanity(): + with Image.open("Tests/images/hopper.qoi") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "QOI" + + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + with Image.open("Tests/images/pil123rgba.qoi") as im: + assert im.mode == "RGBA" + assert im.size == (162, 150) + assert im.format == "QOI" + + assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03) + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + QoiImagePlugin.QoiImageFile(invalid_file) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 0e3b705a2..09f1ef8e4 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -25,7 +25,8 @@ def test_unclosed_file(): im = Image.open(TEST_FILE) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -79,7 +80,6 @@ def test_is_spider_image(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act index = im.tell() diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 05c78c316..edb320603 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -16,7 +16,6 @@ def test_sanity(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (128, 128) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 5daab47fc..b27fa25f3 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -10,27 +10,28 @@ from .helper import is_pypy TEST_TAR_FILE = "Tests/images/hopper.tar" -def test_sanity(): - for codec, test_path, format in [ - ["zlib", "hopper.png", "PNG"], - ["jpg", "hopper.jpg", "JPEG"], - ]: - if features.check(codec): - with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: - with Image.open(tar) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == format +@pytest.mark.parametrize( + "codec, test_path, format", + ( + ("zlib", "hopper.png", "PNG"), + ("jpg", "hopper.jpg", "JPEG"), + ), +) +def test_sanity(codec, test_path, format): + if features.check(codec): + with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: + with Image.open(tar) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == format @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - pytest.warns(ResourceWarning, open) - def test_close(): with warnings.catch_warnings(): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 7d8b5139a..1a5730f49 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -78,7 +78,6 @@ def test_id_field(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (100, 100) @@ -89,7 +88,6 @@ def test_id_field_rle(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (199, 199) @@ -165,13 +163,14 @@ def test_save_id_section(tmp_path): # Save with custom id section greater than 255 characters id_section = b"Test content" * 25 - pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section)) + with pytest.warns(UserWarning): + im.save(out, id_section=id_section) + with Image.open(out) as test_im: assert test_im.info["id_section"] == id_section[:255] test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: - # Save with no id section im.save(out, id_section="") with Image.open(out) as test_im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 4f3c8e390..30c6303a2 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -25,7 +25,6 @@ except ImportError: class TestFileTiff: def test_sanity(self, tmp_path): - filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -62,7 +61,8 @@ class TestFileTiff: im = Image.open("Tests/images/multipage.tiff") im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(self): with warnings.catch_warnings(): @@ -84,24 +84,6 @@ class TestFileTiff: with Image.open("Tests/images/multipage.tiff") as im: im.load() - @pytest.mark.parametrize( - "path, sizes", - ( - ("Tests/images/hopper.tif", ()), - ("Tests/images/child_ifd.tiff", (16, 8)), - ("Tests/images/child_ifd_jpeg.tiff", (20,)), - ), - ) - def test_get_child_images(self, path, sizes): - with Image.open(path) as im: - ims = im.get_child_images() - - assert len(ims) == len(sizes) - for i, im in enumerate(ims): - w = sizes[i] - expected = Image.new("RGB", (w, w), "#f00") - assert_image_similar(im, expected, 1) - def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] @@ -118,36 +100,6 @@ class TestFileTiff: with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") - @pytest.mark.parametrize( - "file_name,mode,size,tile", - [ - ( - "tiff_wrong_bits_per_sample.tiff", - "RGBA", - (52, 53), - [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_2.tiff", - "RGB", - (16, 16), - [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_3.tiff", - "RGBA", - (512, 256), - [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], - ), - ], - ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile): - with Image.open("Tests/images/" + file_name) as im: - assert im.mode == mode - assert im.size == size - assert im.tile == tile - im.load() - def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: @@ -157,7 +109,6 @@ class TestFileTiff: def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -171,7 +122,6 @@ class TestFileTiff: def test_xyres_fallback_tiff(self): filename = "Tests/images/compression.tif" with Image.open(filename) as im: - # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -186,7 +136,6 @@ class TestFileTiff: def test_int_resolution(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -235,7 +184,8 @@ class TestFileTiff: def test_bad_exif(self): with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. - pytest.warns(UserWarning, i._getexif) + with pytest.warns(UserWarning): + i._getexif() def test_save_rgba(self, tmp_path): im = hopper("RGBA") @@ -248,6 +198,12 @@ class TestFileTiff: with pytest.raises(OSError): im.save(outfile) + def test_8bit_s(self): + with Image.open("Tests/images/8bit.s.tif") as im: + im.load() + assert im.mode == "L" + assert im.getpixel((50, 50)) == 184 + def test_little_endian(self): with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 @@ -381,7 +337,6 @@ class TestFileTiff: def test___str__(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # Act ret = str(im.ifd) @@ -392,7 +347,6 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # v2 interface v2_tags = { 256: 55, @@ -630,7 +584,6 @@ class TestFileTiff: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) with Image.open(filename) as im: - # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 48797ea08..b7d100e7a 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -54,7 +54,6 @@ def test_rt_metadata(tmp_path): img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -74,14 +73,12 @@ def test_rt_metadata(tmp_path): info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata(): with Image.open("Tests/images/hopper_g4.tif") as img: - assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, @@ -202,14 +199,15 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path): assert reloaded.tag_v2[271] == expected -def test_writing_int_to_bytes(tmp_path): +@pytest.mark.parametrize("value", (1, IFDRational(1))) +def test_writing_other_types_to_bytes(value, tmp_path): im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[700] assert tag.type == TiffTags.BYTE - info[700] = 1 + info[700] = value out = str(tmp_path / "temp.tiff") im.save(out, tiffinfo=info) @@ -218,6 +216,22 @@ def test_writing_int_to_bytes(tmp_path): assert reloaded.tag_v2[700] == b"\x01" +def test_writing_other_types_to_undefined(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[33723] + assert tag.type == TiffTags.UNDEFINED + + info[33723] = 1 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[33723] == b"1" + + def test_undefined_zero(tmp_path): # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] @@ -238,7 +252,8 @@ def test_empty_metadata(): head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) # Should not raise struct.error. - pytest.warns(UserWarning, info.load, f) + with pytest.warns(UserWarning): + info.load(f) def test_iccprofile(tmp_path): @@ -404,11 +419,12 @@ def test_too_many_entries(): ifd = TiffImagePlugin.ImageFileDirectory_v2() # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack("hh", 4, 4) + ifd._tagdata[277] = struct.pack(">> im.save('Tests/images/hopper_45.png') with Image.open("Tests/images/hopper_45.png") as target: - for (resample, epsilon) in ( + for resample, epsilon in ( (Image.Resampling.NEAREST, 10), (Image.Resampling.BILINEAR, 5), (Image.Resampling.BICUBIC, 0), diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 178cfcef3..a12ce329f 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -4,7 +4,6 @@ from .helper import assert_image_equal, fromstring, hopper def test_sanity(): - with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index a78349801..64a5c9459 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -42,12 +42,12 @@ class TestImageTransform: def test_extent(self): im = hopper("RGB") (w, h) = im.size - # fmt: off - transformed = im.transform(im.size, Image.Transform.EXTENT, - (0, 0, - w//2, h//2), # ul -> lr - Image.Resampling.BILINEAR) - # fmt: on + transformed = im.transform( + im.size, + Image.Transform.EXTENT, + (0, 0, w // 2, h // 2), # ul -> lr + Image.Resampling.BILINEAR, + ) scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h)) @@ -58,13 +58,12 @@ class TestImageTransform: # one simple quad transform, equivalent to scale & crop upper left quad im = hopper("RGB") (w, h) = im.size - # fmt: off - transformed = im.transform(im.size, Image.Transform.QUAD, - (0, 0, 0, h//2, - # ul -> ccw around quad: - w//2, h//2, w//2, 0), - Image.Resampling.BILINEAR) - # fmt: on + transformed = im.transform( + im.size, + Image.Transform.QUAD, + (0, 0, 0, h // 2, w // 2, h // 2, w // 2, 0), # ul -> ccw around quad + Image.Resampling.BILINEAR, + ) scaled = im.transform( (w, h), @@ -99,16 +98,21 @@ class TestImageTransform: # this should be a checkerboard of halfsized hoppers in ul, lr im = hopper("RGBA") (w, h) = im.size - # fmt: off - transformed = im.transform(im.size, Image.Transform.MESH, - [((0, 0, w//2, h//2), # box - (0, 0, 0, h, - w, h, w, 0)), # ul -> ccw around quad - ((w//2, h//2, w, h), # box - (0, 0, 0, h, - w, h, w, 0))], # ul -> ccw around quad - Image.Resampling.BILINEAR) - # fmt: on + transformed = im.transform( + im.size, + Image.Transform.MESH, + ( + ( + (0, 0, w // 2, h // 2), # box + (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad + ), + ( + (w // 2, h // 2, w, h), # box + (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad + ), + ), + Image.Resampling.BILINEAR, + ) scaled = im.transform( (w // 2, h // 2), @@ -174,11 +178,13 @@ class TestImageTransform: im = op(im, (40, 10)) - colors = im.getcolors() - assert colors == [ - (20 * 10, opaque), - (20 * 10, transparent), - ] + colors = sorted(im.getcolors()) + assert colors == sorted( + ( + (20 * 10, opaque), + (20 * 10, transparent), + ) + ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) def test_nearest_resize(self, mode): diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index b839a7b14..d0fea3854 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -50,7 +50,6 @@ def test_add(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2) @@ -63,7 +62,6 @@ def test_add_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2, scale=2.5, offset=100) @@ -87,7 +85,6 @@ def test_add_modulo(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add_modulo(im1, im2) @@ -111,7 +108,6 @@ def test_blend(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.blend(im1, im2, 0.5) @@ -137,7 +133,6 @@ def test_darker_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -149,7 +144,6 @@ def test_darker_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -161,7 +155,6 @@ def test_difference(): # Arrange with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -173,7 +166,6 @@ def test_difference_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -195,7 +187,6 @@ def test_duplicate(): def test_invert(): # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: - # Act new = ImageChops.invert(im) @@ -209,7 +200,6 @@ def test_lighter_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -221,7 +211,6 @@ def test_lighter_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -275,7 +264,6 @@ def test_offset(): xoffset = 45 yoffset = 20 with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: - # Act new = ImageChops.offset(im, xoffset, yoffset) @@ -292,7 +280,6 @@ def test_screen(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.screen(im1, im2) @@ -305,7 +292,6 @@ def test_subtract(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -319,7 +305,6 @@ def test_subtract_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) @@ -332,7 +317,6 @@ def test_subtract_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -344,7 +328,6 @@ def test_subtract_modulo(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -358,7 +341,6 @@ def test_subtract_modulo_no_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -370,7 +352,6 @@ def test_soft_light(): # Arrange 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) @@ -383,7 +364,6 @@ def test_hard_light(): # Arrange 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) @@ -396,7 +376,6 @@ def test_overlay(): # Arrange 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) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 3d8dbe6bb..8efe063c1 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -617,11 +617,12 @@ def test_auxiliary_channels_isolated(): assert_image_equal(test_image.convert(dst_format[2]), reference_image) -def test_constants_deprecation(): - for enum, prefix in { - ImageCms.Intent: "INTENT_", - ImageCms.Direction: "DIRECTION_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(ImageCms, prefix + name) == enum[name] +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) +def test_rgb_lab(mode): + im = Image.new(mode, (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (0, 128, 128) + + im = Image.new("LAB", (1, 1), (255, 0, 0)) + converted_im = im.convert(mode) + assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4c4c41b7b..406f44c06 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -52,7 +52,6 @@ def test_sanity(): def test_valueerror(): with Image.open("Tests/images/chi.gif") as im: - draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) @@ -356,7 +355,13 @@ def ellipse_various_sizes_helper(filled): for w in ellipse_sizes: y = 1 for h in ellipse_sizes: - border = [x, y, x + w - 1, y + h - 1] + x1 = x + w + if w: + x1 -= 1 + y1 = y + h + if h: + y1 -= 1 + border = [x, y, x1, y1] if filled: draw.ellipse(border, fill="white") else: @@ -736,6 +741,36 @@ def test_rounded_rectangle(xy): assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") +@pytest.mark.parametrize("top_left", (True, False)) +@pytest.mark.parametrize("top_right", (True, False)) +@pytest.mark.parametrize("bottom_right", (True, False)) +@pytest.mark.parametrize("bottom_left", (True, False)) +def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_left): + corners = (top_left, top_right, bottom_right, bottom_left) + + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle( + (10, 20, 190, 180), 30, fill="red", outline="green", width=5, corners=corners + ) + + # Assert + suffix = "".join( + ( + ("y" if top_left else "n"), + ("y" if top_right else "n"), + ("y" if bottom_right else "n"), + ("y" if bottom_left else "n"), + ) + ) + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_corners_" + suffix + ".png" + ) + + @pytest.mark.parametrize( "xy, radius, type", [ @@ -903,9 +938,6 @@ def test_square(): img, draw = create_base_image_draw((10, 10)) draw.rectangle((2, 2, 7, 7), BLACK) assert_image_equal_tofile(img, expected, "square as normal rectangle failed") - img, draw = create_base_image_draw((10, 10)) - draw.rectangle((7, 7, 2, 2), BLACK) - assert_image_equal_tofile(img, expected, "square as inverted rectangle failed") def test_triangle_right(): @@ -1192,21 +1224,6 @@ def test_textbbox_stroke(): assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) -def test_textsize_deprecation(): - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - - with pytest.warns(DeprecationWarning) as log: - draw.textsize("Hello") - assert len(log) == 1 - with pytest.warns(DeprecationWarning) as log: - draw.textsize("Hello\nWorld") - assert len(log) == 1 - with pytest.warns(DeprecationWarning) as log: - draw.multiline_textsize("Hello\nWorld") - assert len(log) == 1 - - @skip_unless_feature("freetype2") def test_stroke(): for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): @@ -1330,20 +1347,20 @@ def test_same_color_outline(): @pytest.mark.parametrize( - "n_sides, rotation, polygon_name", - [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], + "n_sides, polygon_name, args", + [ + (4, "square", {}), + (8, "regular_octagon", {}), + (4, "square_rotate_45", {"rotation": 45}), + (3, "triangle_width", {"width": 5, "outline": "yellow"}), + ], ) -def test_draw_regular_polygon(n_sides, rotation, polygon_name): +def test_draw_regular_polygon(n_sides, polygon_name, args): im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) - filename_base = f"Tests/images/imagedraw_{polygon_name}" - filename = ( - f"{filename_base}.png" - if rotation == 0 - else f"{filename_base}_rotate_{rotation}.png" - ) + filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) bounding_circle = ((W // 2, H // 2), 25) - draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") + draw.regular_polygon(bounding_circle, n_sides, fill="red", **args) assert_image_equal_tofile(im, filename) @@ -1470,3 +1487,21 @@ def test_polygon2(): draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") expected = "Tests/images/imagedraw_outline_polygon_RGB.png" assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) +def test_incorrectly_ordered_coordinates(xy): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with pytest.raises(ValueError): + draw.arc(xy, 10, 260) + with pytest.raises(ValueError): + draw.chord(xy, 10, 260) + with pytest.raises(ValueError): + draw.ellipse(xy) + with pytest.raises(ValueError): + draw.pieslice(xy, 10, 260) + with pytest.raises(ValueError): + draw.rectangle(xy) + with pytest.raises(ValueError): + draw.rounded_rectangle(xy) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 6fc829f1a..a8a2ee1fc 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -2,7 +2,7 @@ import os.path import pytest -from PIL import Image, ImageDraw, ImageDraw2 +from PIL import Image, ImageDraw, ImageDraw2, features from .helper import ( assert_image_equal, @@ -171,19 +171,18 @@ def test_text(): @skip_unless_feature("freetype2") -def test_textsize(): +def test_textbbox(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) font = ImageDraw2.Font("white", FONT_PATH) # Act - with pytest.warns(DeprecationWarning) as log: - size = draw.textsize("ImageDraw2", font) - assert len(log) == 1 + bbox = draw.textbbox((0, 0), "ImageDraw2", font) # Assert - assert size[1] == 12 + right = 72 if features.check_feature("raqm") else 70 + assert bbox == (0, 2, right, 12) @skip_unless_feature("freetype2") diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index fc0fbfb9b..412bc10d9 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -30,7 +30,6 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile: def test_parser(self): def roundtrip(format): - im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 306a2f1bf..7ea485a55 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -191,6 +191,16 @@ def test_getlength( assert length == length_raqm +def test_float_size(): + lengths = [] + for size in (48, 48.5, 49): + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine + ) + lengths.append(f.getlength("text")) + assert lengths[0] != lengths[1] != lengths[2] + + def test_render_multiline(font): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -251,27 +261,6 @@ def test_draw_align(font): draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_size(font): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - with pytest.warns(DeprecationWarning) as log: - # Test that textsize() correctly connects to multiline_textsize() - assert draw.textsize(TEST_TEXT, font=font) == draw.multiline_textsize( - TEST_TEXT, font=font - ) - - # Test that multiline_textsize corresponds to ImageFont.textsize() - # for single line text - assert font.getsize("A") == draw.multiline_textsize("A", font=font) - - # Test that textsize() can pass on additional arguments - # to multiline_textsize() - draw.textsize(TEST_TEXT, font=font, spacing=4) - draw.textsize(TEST_TEXT, font, 4) - assert len(log) == 6 - - def test_multiline_bbox(font): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -298,12 +287,6 @@ def test_multiline_width(font): draw.textbbox((0, 0), "longest line", font=font)[2] == draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2] ) - with pytest.warns(DeprecationWarning) as log: - assert ( - draw.textsize("longest line", font=font)[0] - == draw.multiline_textsize("longest line\nline", font=font)[0] - ) - assert len(log) == 2 def test_multiline_spacing(font): @@ -326,32 +309,27 @@ def test_rotated_transposed_font(font, orientation): # Original font draw.font = font - with pytest.warns(DeprecationWarning) as log: - box_size_a = draw.textsize(word) - assert box_size_a == font.getsize(word) - assert len(log) == 2 bbox_a = draw.textbbox((10, 10), word) # Rotated font draw.font = transposed_font - with pytest.warns(DeprecationWarning) as log: - box_size_b = draw.textsize(word) - assert box_size_b == transposed_font.getsize(word) - assert len(log) == 2 bbox_b = draw.textbbox((20, 20), word) - # Check (w,h) of box a is (h,w) of box b - assert box_size_a[0] == box_size_b[1] - assert box_size_a[1] == box_size_b[0] + # Check (w, h) of box a is (h, w) of box b + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( + bbox_b[3] - bbox_b[1], + bbox_b[2] - bbox_b[0], + ) - # Check bbox b is (20, 20, 20 + h, 20 + w) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] - assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) # text length is undefined for vertical text - pytest.raises(ValueError, draw.textlength, word) + with pytest.raises(ValueError): + draw.textlength(word) @pytest.mark.parametrize( @@ -372,28 +350,25 @@ def test_unrotated_transposed_font(font, orientation): # Original font draw.font = font - with pytest.warns(DeprecationWarning) as log: - box_size_a = draw.textsize(word) - assert len(log) == 1 bbox_a = draw.textbbox((10, 10), word) length_a = draw.textlength(word) # Rotated font draw.font = transposed_font - with pytest.warns(DeprecationWarning) as log: - box_size_b = draw.textsize(word) - assert len(log) == 1 bbox_b = draw.textbbox((20, 20), word) length_b = draw.textlength(word) # Check boxes a and b are same size - assert box_size_a == box_size_b + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( + bbox_b[2] - bbox_b[0], + bbox_b[3] - bbox_b[1], + ) - # Check bbox b is (20, 20, 20 + w, 20 + h) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] - assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) assert length_a == length_b @@ -446,19 +421,6 @@ def test_free_type_font_get_metrics(font): assert (ascent, descent) == (16, 4) -def test_free_type_font_get_offset(font): - # Arrange - text = "offset this" - - # Act - with pytest.warns(DeprecationWarning) as log: - offset = font.getoffset(text) - - # Assert - assert len(log) == 1 - assert offset == (0, 3) - - def test_free_type_font_get_mask(font): # Arrange text = "mask this" @@ -617,19 +579,6 @@ def test_imagefont_getters(font): assert font.getlength("M") == 12 assert font.getlength("y") == 12 assert font.getlength("a") == 12 - with pytest.warns(DeprecationWarning) as log: - assert font.getsize("A") == (12, 16) - assert font.getsize("AB") == (24, 16) - assert font.getsize("M") == (12, 16) - assert font.getsize("y") == (12, 20) - assert font.getsize("a") == (12, 16) - assert font.getsize_multiline("A") == (12, 16) - assert font.getsize_multiline("AB") == (24, 16) - assert font.getsize_multiline("a") == (12, 16) - assert font.getsize_multiline("ABC\n") == (36, 36) - assert font.getsize_multiline("ABC\nA") == (36, 36) - assert font.getsize_multiline("ABC\nAaaa") == (48, 36) - assert len(log) == 11 @pytest.mark.parametrize("stroke_width", (0, 2)) @@ -640,16 +589,6 @@ def test_getsize_stroke(font, stroke_width): 12 + stroke_width, 16 + stroke_width, ) - with pytest.warns(DeprecationWarning) as log: - assert font.getsize("A", stroke_width=stroke_width) == ( - 12 + stroke_width * 2, - 16 + stroke_width * 2, - ) - assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( - 48 + stroke_width * 2, - 36 + stroke_width * 4, - ) - assert len(log) == 2 def test_complex_font_settings(): @@ -780,11 +719,8 @@ def test_textbbox_non_freetypefont(): im = Image.new("RGB", (200, 200)) d = ImageDraw.Draw(im) default_font = ImageFont.load_default() - with pytest.warns(DeprecationWarning) as log: - width, height = d.textsize("test", font=default_font) - assert len(log) == 1 - assert d.textlength("test", font=default_font) == width - assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) + assert d.textlength("test", font=default_font) == 24 + assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) @pytest.mark.parametrize( @@ -872,25 +808,23 @@ def test_anchor_invalid(font): d.font = font for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: - pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)) - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor) + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor) + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) for anchor in ["lt", "lb"]: - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) @@ -1084,14 +1018,6 @@ def test_woff2(layout_engine): assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) -def test_fill_deprecation(font): - with pytest.warns(DeprecationWarning): - font.getmask2("Hello world", fill=Image.core.fill) - with pytest.warns(DeprecationWarning): - with pytest.raises(TypeError): - font.getmask2("Hello world", fill=None) - - def test_render_mono_size(): # issue 4177 @@ -1131,12 +1057,3 @@ def test_raqm_missing_warning(monkeypatch): "Raqm layout was requested, but Raqm is not available. " "Falling back to basic layout." ) - - -def test_constants_deprecation(): - for enum, prefix in { - ImageFont.Layout: "LAYOUT_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(ImageFont, prefix + name) == enum[name] diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index cf039e86e..6099b04e4 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -360,37 +360,20 @@ def test_anchor_invalid_ttb(): d.font = font for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: - pytest.raises( - ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, lambda: font.getbbox("hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, - lambda: d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb"), - ) - pytest.raises( - ValueError, - lambda: d.multiline_text( - (0, 0), "foo\nbar", anchor=anchor, direction="ttb" - ), - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox( - (0, 0), "foo\nbar", anchor=anchor, direction="ttb" - ), - ) + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") # ttb multiline text does not support anchors at all - pytest.raises( - ValueError, - lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"), - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb"), - ) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb") diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 317db4c01..fa88065f4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -64,7 +64,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 ) p.communicate() else: - if not shutil.which("wl-paste"): + if not shutil.which("wl-paste") and not shutil.which("xclip"): with pytest.raises( NotImplementedError, match="wl-paste or xclip is required for" diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index c9b2fd865..d390f3c1e 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -21,7 +21,6 @@ deformer = Deformer() def test_sanity(): - ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -419,7 +418,6 @@ def test_autocontrast_cutoff(): def test_autocontrast_mask_toy_input(): # Test the mask argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0 = img.size[0] // 4 @@ -439,7 +437,6 @@ def test_autocontrast_mask_toy_input(): def test_autocontrast_mask_real_input(): # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0, y0 = img.size[0] // 2, img.size[1] // 2 diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 5bda28117..baa698bb4 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -6,14 +6,9 @@ from .helper import assert_image_equal, assert_image_equal_tofile def test_sanity(): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError): - ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10) - def test_reload(): with Image.open("Tests/images/hopper.gif") as im: @@ -23,7 +18,6 @@ def test_reload(): def test_getcolor(): - palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 assert len(palette.colors) == 0 @@ -84,7 +78,6 @@ def test_getcolor_not_special(index, palette): def test_file(tmp_path): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = str(tmp_path / "temp.lut") diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 2f2b07918..2c73a2094 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -2,13 +2,10 @@ import warnings import pytest +from PIL import ImageQt + from .helper import assert_image_similar, hopper -with warnings.catch_warnings() as w: - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt - - pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" ) @@ -26,10 +23,6 @@ def test_rgb(): from PyQt6.QtGui import qRgb elif ImageQt.qt_version == "side6": from PySide6.QtGui import qRgb - elif ImageQt.qt_version == "5": - from PyQt5.QtGui import qRgb - elif ImageQt.qt_version == "side2": - from PySide2.QtGui import qRgb assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6af7e7602..62f528332 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -6,7 +6,6 @@ from .helper import assert_image_equal, hopper, skip_unless_feature def test_sanity(tmp_path): - test_file = str(tmp_path / "temp.im") im = hopper("RGB") diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 3e147a9ef..e54372b60 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -55,8 +55,8 @@ def test_show_without_viewers(): viewers = ImageShow._viewers ImageShow._viewers = [] - im = hopper() - assert not ImageShow.show(im) + with hopper() as im: + assert not ImageShow.show(im) ImageShow._viewers = viewers @@ -89,20 +89,3 @@ def test_ipythonviewer(): im = hopper() assert test_viewer.show(im) == 1 - - -@pytest.mark.skipif( - not on_ci() or is_win32(), - reason="Only run on CIs; hangs on Windows CIs", -) -@pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_file_deprecated(tmp_path, viewer): - f = str(tmp_path / "temp.jpg") - hopper().save(f) - with pytest.warns(DeprecationWarning): - try: - viewer.show_file(file=f) - except NotImplementedError: - pass - with pytest.raises(TypeError): - viewer.show_file() diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 5717fe150..b3b5db13f 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -6,7 +6,6 @@ from .helper import hopper def test_sanity(): - im = hopper() st = ImageStat.Stat(im) @@ -31,7 +30,6 @@ def test_sanity(): def test_hopper(): - im = hopper() st = ImageStat.Stat(im) @@ -45,7 +43,6 @@ def test_hopper(): def test_constant(): - im = Image.new("L", (128, 128), 128) st = ImageStat.Stat(im) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index 995d0ee1f..a0c9574ba 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -89,13 +89,6 @@ def test_photoimage_blank(mode): assert_image_equal(reloaded.convert(mode), im) -def test_box_deprecation(): - im = hopper() - im_tk = ImageTk.PhotoImage(im) - with pytest.warns(DeprecationWarning): - im_tk.paste(im, (0, 0, 128, 128)) - - def test_bitmapimage(): im = hopper("1") diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 9d64d17a3..5e489284f 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -100,8 +100,11 @@ class TestImageWinDib: # Act # Make one the same as the using tobytes()/frombytes() test_buffer = dib1.tobytes() - dib2.frombytes(test_buffer) + for datatype in ("bytes", "memoryview"): + if datatype == "memoryview": + test_buffer = memoryview(test_buffer) + dib2.frombytes(test_buffer) - # Assert - # Confirm they're the same - assert dib1.tobytes() == dib2.tobytes() + # Assert + # Confirm they're the same + assert dib1.tobytes() == dib2.tobytes() diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 37ed3659d..f6818be46 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -4,7 +4,6 @@ from PIL import Image def test_setmode(): - im = Image.new("L", (1, 1), 255) im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 979806cae..f7812f62b 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -207,6 +207,9 @@ class TestLibPack: 0x01000083, ) + def test_I16(self): + self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + def test_F_float(self): self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) @@ -754,6 +757,7 @@ class TestLibUnpack: def test_I16(self): self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) @@ -761,10 +765,12 @@ class TestLibUnpack: self.assert_unpack("I;16", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16B", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16L", "I;16N", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) else: self.assert_unpack("I;16", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) + self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) def test_CMYK16(self): self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index efcdab9ec..1786dba38 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -42,7 +42,6 @@ def test_basic(tmp_path, mode): im_in.save(filename) with Image.open(filename) as im_out: - verify(im_in) verify(im_out) @@ -87,13 +86,9 @@ def test_tobytes(): def test_convert(): - im = original.copy() - verify(im.convert("I;16")) - verify(im.convert("I;16").convert("L")) - verify(im.convert("I;16").convert("I")) - - verify(im.convert("I;16B")) - verify(im.convert("I;16B").convert("L")) - verify(im.convert("I;16B").convert("I")) + for mode in ("I;16", "I;16B", "I;16N"): + verify(im.convert(mode)) + verify(im.convert(mode).convert("L")) + verify(im.convert(mode).convert("I")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 3de7ec30f..147f94a71 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -4,7 +4,7 @@ import pytest from PIL import Image -from .helper import assert_deep_equal, assert_image, hopper +from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature numpy = pytest.importorskip("numpy", reason="NumPy not installed") @@ -219,6 +219,13 @@ def test_zero_size(): assert im.size == (0, 0) +@skip_unless_feature("libtiff") +def test_load_first(): + with Image.open("Tests/images/g4_orientation_5.tif") as im: + a = numpy.array(im) + assert a.shape == (88, 590) + + def test_bool(): # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) @@ -235,7 +242,6 @@ def test_no_resource_warning_for_numpy_array(): test_file = "Tests/images/hopper.png" with Image.open(test_file) as im: - # Act/Assert with warnings.catch_warnings(): array(im) diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index ea9b33dfc..105a838d9 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -88,9 +88,8 @@ def test_parsing(): b"D:20180729214124+08'00'": "20180729134124", b"D:20180729214124-05'00'": "20180730024124", }.items(): - d = PdfParser.get_value(b"<>", 0)[ - 0 - ] + b = b"<>" + d = PdfParser.get_value(b, 0)[0] assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value @@ -118,3 +117,9 @@ def test_pdf_repr(): assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" + + +def test_duplicate_xref_entry(): + pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") + assert pdf.xref_table.existing_entries[6][0] == 1197 + pdf.close() diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 23eb9e39f..2f6d05888 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -89,7 +89,6 @@ def test_pickle_la_mode_with_palette(tmp_path): def test_pickle_tell(): # Arrange with Image.open("Tests/images/hopper.webp") as image: - # Act: roundtrip unpickled_image = pickle.loads(pickle.dumps(image)) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 1fc816146..5d2e41212 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,12 +1,8 @@ -import warnings - import pytest -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt +from PIL import ImageQt -from .helper import assert_image_equal, assert_image_equal_tofile, hopper +from .helper import assert_image_equal_tofile, assert_image_similar, hopper if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -19,14 +15,6 @@ if ImageQt.qt_is_installed: from PySide6.QtCore import QPoint from PySide6.QtGui import QImage, QPainter, QRegion from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - elif ImageQt.qt_version == "5": - from PyQt5.QtCore import QPoint - from PyQt5.QtGui import QImage, QPainter, QRegion - from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - elif ImageQt.qt_version == "side2": - from PySide2.QtCore import QPoint - from PySide2.QtGui import QImage, QPainter, QRegion - from PySide2.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget class Example(QWidget): def __init__(self): @@ -48,7 +36,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_equal(result, expected.convert("RGB")) + assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index c1983031a..95c13ba75 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,10 +1,6 @@ -import warnings - import pytest -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt +from PIL import ImageQt from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -32,7 +28,7 @@ def test_sanity(mode, tmp_path): assert_image_equal(rt, src) if mode == "1": - # BW appears to not save correctly on QT5 + # BW appears to not save correctly on Qt # kicks out errors on console: # libpng warning: Invalid color type/bit depth combination # in IHDR diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 12f475df0..6e3fcec90 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -7,7 +7,6 @@ from .helper import hopper def _test_equal(num, denom, target): - t = IFDRational(num, denom) assert target == t @@ -15,7 +14,6 @@ def _test_equal(num, denom, target): def test_sanity(): - _test_equal(1, 1, 1) _test_equal(1, 1, Fraction(1, 1)) diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 34197c14f..5bd9bacdb 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -9,7 +9,6 @@ test_file = "Tests/images/hopper.webp" @skip_unless_feature("webp") class TestWebPLeaks(PillowLeakTestCase): - mem_limit = 3 * 1024 # kb iterations = 100 diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index d9608e782..a318bfafd 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -8,5 +8,5 @@ if [ ! -f $archive.tar.gz ]; then wget -O $archive.tar.gz $url fi -rm -r $archive +rmdir $archive tar -xvzf $archive.tar.gz diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 02da12d61..1ef6f4e97 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,15 +1,12 @@ -#!/bin/bash +#!/usr/bin/env bash # install extra test images -# Use SVN to just fetch a single Git subdirectory -svn_export() -{ - if [ ! -z $1 ]; then - echo "" - echo "Retrying svn export..." - echo "" - fi +archive=test-images-main - svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images -} -svn_export || svn_export retry || svn_export retry || svn_export retry +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz + +mv $archive/* ../Tests/images/ + +# Cleanup old tarball and empty directory +rm $archive.tar.gz +rmdir $archive diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 64dd024bd..fd6000ee1 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.4 +archive=libimagequant-4.2.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 992503650..24c1f9c30 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=libraqm-0.9.0 +archive=libraqm-0.10.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 05867b7d4..f8b985a7a 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.2.4 +archive=libwebp-1.3.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/COPYING b/docs/COPYING index b400381d3..bc44ba388 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-2023 by Alex Clark and contributors + Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/Makefile b/docs/Makefile index d32d25a3c..3b4deb9bf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -19,6 +19,7 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" + @echo " htmlview to open the index page built by the html target in your browser" @echo " serve to start a local server for viewing docs" @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @@ -45,7 +46,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph + $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-removed-in sphinxext-opengraph .PHONY: html html: @@ -196,6 +197,10 @@ doctest: @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." +.PHONY: htmlview +htmlview: html + $(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" + .PHONY: livehtml livehtml: html livereload $(BUILDDIR)/html -p 33233 diff --git a/docs/conf.py b/docs/conf.py index fb58d25ed..2ebcd6b2e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,11 +28,11 @@ needs_sphinx = "2.4" # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_inline_tabs", - "sphinx_issues", "sphinx_removed_in", "sphinxext.opengraph", ] @@ -52,8 +52,10 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2023 Alex Clark and Contributors" -author = "Fredrik Lundh, Alex Clark and Contributors" +copyright = ( + "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" +) +author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -243,7 +245,7 @@ latex_documents = [ master_doc, "PillowPILFork.tex", "Pillow (PIL Fork) Documentation", - "Alex Clark", + "Jeffrey A. Clark (Alex)", "manual", ) ] @@ -293,7 +295,7 @@ texinfo_documents = [ "Pillow (PIL Fork) Documentation", author, "PillowPILFork", - "Pillow is the friendly PIL fork by Alex Clark and Contributors.", + "Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors.", "Miscellaneous", ) ] @@ -315,8 +317,17 @@ def setup(app): app.add_css_file("css/dark.css") -# GitHub repo for sphinx-issues -issues_github_path = "python-pillow/Pillow" +# sphinx.ext.extlinks +# This config is a dictionary of external sites, +# mapping unique short aliases to a base URL and a prefix. +# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html +_repo = "https://github.com/python-pillow/Pillow/" +extlinks = { + "cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"), + "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), + "issue": (_repo + "issues/%s", "#%s"), + "pr": (_repo + "pull/%s", "#%s"), +} # sphinxext.opengraph ogp_image = ( diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a..45b2f4200 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,22 +12,38 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. +PSFile +~~~~~~ + +.. deprecated:: 9.5.0 + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + +Removed features +---------------- + +Deprecated features are only removed in major releases after an appropriate +period of deprecation has passed. + Tk/Tcl 8.4 ~~~~~~~~~~ .. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -when Tk/Tcl 8.5 will be the minimum supported. +Support for Tk/Tcl 8.4 was removed in Pillow 10.0.0 (2023-07-01). Categories ~~~~~~~~~~ .. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and -``Image.CONTAINER`` attributes. +``im.category`` was removed along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. To determine if an image has multiple frames or not, ``getattr(im, "is_animated", False)`` can be used instead. @@ -36,43 +52,40 @@ JpegImagePlugin.convert_dict_qtables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 8.3.0 +.. versionremoved:: 10.0.0 -JPEG ``quantization`` is now automatically converted, but still returned as a -dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer -performs any operations on the data given to it, has been deprecated and will be -removed in Pillow 10.0.0 (2023-07-01). +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. ImagePalette size parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 8.4.0 - -The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). +.. versionremoved:: 10.0.0 Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by -default, and the size parameter could be used to override that. Pillow 8.3.0 removed -the default required length, also removing the need for the size parameter. +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. ImageShow.Viewer.show_file file argument ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been -deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by -``path``. +removed and replaced by ``path``. In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. -``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest -``viewer.show_file(path="test.jpg")`` instead. Constants ~~~~~~~~~ .. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 -A number of constants have been deprecated and will be removed in Pillow 10.0.0 -(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. +A number of constants have been removed. +Instead, ``enum.IntEnum`` classes have been added. .. note:: @@ -81,7 +94,7 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 See :ref:`restored-image-constants` ===================================================== ============================================================ -Deprecated Use instead +Removed Use instead ===================================================== ============================================================ ``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` ``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` @@ -115,71 +128,31 @@ FitsStubImagePlugin ~~~~~~~~~~~~~~~~~~~ .. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 -The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in -Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through -:mod:`~PIL.FitsImagePlugin` instead. - -FreeTypeFont.getmask2 fill parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been -deprecated and will be removed in Pillow 10 (2023-07-01). - -PhotoImage.paste box parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). - -PyQt5 and PySide2 -~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed -in Pillow 10 (2023-07-01). Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. - -Image.coerce_e -~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -This undocumented method has been deprecated and will be removed in Pillow 10 -(2023-07-01). - -.. _Font size and offset methods: +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. Font size and offset methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 -Several functions for computing the size and offset of rendered text -have been deprecated and will be removed in Pillow 10 (2023-07-01): +Several functions for computing the size and offset of rendered text have been removed: -=========================================================================== ============================================================================================================= -Deprecated Use instead -=========================================================================== ============================================================================================================= -:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` -:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` -:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` -:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` -:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` -:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` -=========================================================================== ============================================================================================================= +=============================================================== ============================================================================================================= +Removed Use instead +=============================================================== ============================================================================================================= +``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` +``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=============================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -194,9 +167,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont @@ -211,11 +182,43 @@ Use instead: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") width, height = right - left, bottom - top -Removed features ----------------- +FreeTypeFont.getmask2 fill parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Deprecated features are only removed in major releases after an appropriate -period of deprecation has passed. +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + +PhotoImage.paste box parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The ``box`` parameter was unused and has been removed. + +PyQt5 and PySide2 +~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +This undocumented method has been removed. PILLOW_VERSION constant ~~~~~~~~~~~~~~~~~~~~~~~ @@ -265,7 +268,7 @@ FreeType 2.7 Support for FreeType 2.7 has been removed. We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _FreeType: https://freetype.org/ @@ -336,16 +339,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 01f75e9a3..e40ed4687 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -31,7 +31,7 @@ INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current re supports the following standard modes: * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) - * ``L`` (8-bit pixels, black and white) + * ``L`` (8-bit pixels, grayscale) * ``P`` (8-bit pixels, mapped to any other mode using a color palette) * ``RGB`` (3x8-bit pixels, true color) * ``RGBA`` (4x8-bit pixels, true color with transparency mask) @@ -62,7 +62,6 @@ Pillow also provides limited support for a few additional modes, including: * ``BGR;15`` (15-bit reversed true colour) * ``BGR;16`` (16-bit reversed true colour) * ``BGR;24`` (24-bit reversed true colour) - * ``BGR;32`` (32-bit reversed true colour) Premultiplied alpha is where the values for each other channel have been multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` @@ -148,44 +147,44 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. py:currentmodule:: PIL.Image -.. data:: NEAREST +.. data:: Resampling.NEAREST Pick one nearest pixel from the input image. Ignore all other input pixels. -.. data:: BOX +.. data:: Resampling.BOX Each pixel of source image contributes to one pixel of the destination image with identical weights. - For upscaling is equivalent of :data:`NEAREST`. + For upscaling is equivalent of :data:`Resampling.NEAREST`. This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` methods. .. versionadded:: 3.4.0 -.. data:: BILINEAR +.. data:: Resampling.BILINEAR For resize calculate the output pixel value using linear interpolation on all pixels that may contribute to the output value. For other transformations linear interpolation over a 2x2 environment in the input image is used. -.. data:: HAMMING +.. data:: Resampling.HAMMING - Produces a sharper image than :data:`BILINEAR`, doesn't have dislocations - on local level like with :data:`BOX`. + Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have + dislocations on local level like with :data:`Resampling.BOX`. This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` methods. .. versionadded:: 3.4.0 -.. data:: BICUBIC +.. data:: Resampling.BICUBIC For resize calculate the output pixel value using cubic interpolation on all pixels that may contribute to the output value. For other transformations cubic interpolation over a 4x4 environment in the input image is used. -.. data:: LANCZOS +.. data:: Resampling.LANCZOS Calculate the output pixel value using a high-quality Lanczos filter (a truncated sinc) on all pixels that may contribute to the output value. @@ -198,19 +197,19 @@ pixel, the Python Imaging Library provides different resampling *filters*. Filters comparison table ~~~~~~~~~~~~~~~~~~~~~~~~ -+----------------+-------------+-----------+-------------+ -| Filter | Downscaling | Upscaling | Performance | -| | quality | quality | | -+================+=============+===========+=============+ -|:data:`NEAREST` | | | ⭐⭐⭐⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`BOX` | ⭐ | | ⭐⭐⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`BILINEAR`| ⭐ | ⭐ | ⭐⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`HAMMING` | ⭐⭐ | | ⭐⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | -+----------------+-------------+-----------+-------------+ -|:data:`LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | -+----------------+-------------+-----------+-------------+ ++---------------------------+-------------+-----------+-------------+ +| Filter | Downscaling | Upscaling | Performance | +| | quality | quality | | ++===========================+=============+===========+=============+ +|:data:`Resampling.NEAREST` | | | ⭐⭐⭐⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BOX` | ⭐ | | ⭐⭐⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BILINEAR`| ⭐ | ⭐ | ⭐⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.HAMMING` | ⭐⭐ | | ⭐⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ++---------------------------+-------------+-----------+-------------+ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..74ba883b1 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -589,6 +589,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.1.0 +**comment** + Adds a custom comment to the file, replacing the default + "Created by OpenJPEG version" comment. + + .. versionadded:: 9.5.0 + +**plt** + If ``True`` and OpenJPEG 2.4.0 or later is available, then include a PLT + (packet length, tile-part header) marker in the produced file. + Defaults to ``False``. + + .. versionadded:: 9.5.0 + .. note:: To enable JPEG 2000 support, you need to build and install the OpenJPEG @@ -1104,7 +1117,7 @@ using the general tags available through tiffinfo. Either an integer or a float. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. For consistency with other image formats, the x and y resolutions of the dpi will be rounded to the nearest integer. @@ -1126,7 +1139,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If present and true, instructs the WebP writer to use lossless compression. **quality** - Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest + Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest size and 100 the largest. For lossless, this parameter is the amount of effort put into the compression: 0 is the fastest, but gives larger files compared to the slowest, but best, 100. @@ -1147,6 +1160,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: The exif data to include in the saved file. Only supported if the system WebP library was built with webpmux support. +**xmp** + The XMP data to include in the saved file. Only supported if + the system WebP library was built with webpmux support. + Saving sequences ~~~~~~~~~~~~~~~~ @@ -1389,9 +1406,7 @@ WMF, EMF Pillow can identify WMF and EMF files. On Windows, it can read WMF and EMF files. By default, it will load the image -at 72 dpi. To load it at another resolution: - -.. code-block:: python +at 72 dpi. To load it at another resolution:: from PIL import Image @@ -1400,9 +1415,7 @@ at 72 dpi. To load it at another resolution: To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF -handler. - -.. code-block:: python +handler. :: from PIL import Image from PIL import WmfImagePlugin @@ -1457,8 +1470,13 @@ PDF ^^^ Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4 -files, using either JPEG or HEX encoding depending on the image mode (and -whether JPEG support is available or not). +files. Different encoding methods are used, depending on the image mode. + +* 1 mode images are saved using TIFF encoding, or JPEG encoding if libtiff support is + unavailable +* L, RGB and CMYK mode images use JPEG encoding +* P mode images use HEX encoding +* RGBA mode images use JPEG2000 encoding .. _pdf-saving: @@ -1493,6 +1511,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum image, will determine the physical dimensions of the page that will be saved in the PDF. +**dpi** + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution + unit. If both the ``resolution`` parameter and the ``dpi`` parameter are + present, ``resolution`` will be ignored. + **title** The document’s title. If not appending to an existing PDF file, this will default to the filename. @@ -1539,6 +1562,13 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 5.3.0 +QOI +^^^ + +.. versionadded:: 9.5.0 + +Pillow identifies and reads images in Quite OK Image format. + XV Thumbnails ^^^^^^^^^^^^^ diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst index 0aecd3483..3a9572ab2 100644 --- a/docs/handbook/text-anchors.rst +++ b/docs/handbook/text-anchors.rst @@ -29,7 +29,7 @@ For example, in the following image, the text is ``ms`` (middle-baseline) aligne :alt: ms (middle-baseline) aligned text. :align: left -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 59dfac588..75604e17a 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -108,9 +108,7 @@ Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. -Once the plugin has been imported, it can be used: - -.. code-block:: python +Once the plugin has been imported, it can be used:: from PIL import Image import SpamImagePlugin @@ -169,9 +167,7 @@ The raw decoder The ``raw`` decoder is used to read uncompressed data from an image file. It can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.frombytes` function, use the following syntax: - -.. code-block:: python +:py:func:`PIL.Image.frombytes` function, use the following syntax:: image = Image.frombytes( mode, size, data, "raw", @@ -281,9 +277,7 @@ decoder that can be used to read various packed formats into a floating point image memory. To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use -the following syntax: - -.. code-block:: python +the following syntax:: image = Image.frombytes( mode, size, data, "bit", diff --git a/docs/index.rst b/docs/index.rst index 5bcd5afa5..418844ba7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ Pillow ====== -Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Pillow is the friendly PIL fork by `Jeffrey A. Clark (Alex) and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors. Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. @@ -73,6 +73,22 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more + Overview ======== diff --git a/docs/installation.rst b/docs/installation.rst index 42fe8c254..ad27b67ee 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -68,6 +68,18 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow + While we provide binaries for both x86-64 and arm64, we do not provide universal2 + binaries. However, it is simple to combine our current binaries to create one:: + + python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow + python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow + python3 -m pip install delocate + + Then, with the names of the downloaded wheels, use Python to combine them:: + + from delocate.fuse import fuse_wheels + fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') + .. tab:: Windows We provide Pillow binaries for Windows compiled for the matrix of @@ -150,7 +162,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.14**. + above uses liblcms2. Tested with **1.19** and **2.7-2.15**. * **libwebp** provides the WebP format. @@ -169,7 +181,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.4** + * Pillow has been tested with libimagequant **2.6-4.2** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. @@ -186,8 +198,8 @@ Many of Pillow's features require external libraries: * Pillow wheels since version 8.2.0 include a modified version of libraqm that loads libfribidi at runtime if it is installed. On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` - into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs) - `_ + into a directory listed in the `Dynamic-link library search order (Microsoft Learn) + `_ (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). See `Build Options`_ to see how to build this version. * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. @@ -369,21 +381,21 @@ Build Options available, as many as are present. * Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, - ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, - ``--disable-imagequant``, ``--disable-xcb``. + ``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``, + ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``, + ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, - ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, - ``--enable-imagequant``, ``--enable-xcb``. + ``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``, + ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``, + ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Build flags: ``--vendor-raqm --vendor-fribidi`` +* 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 @@ -422,7 +434,9 @@ These platforms are built and tested for every change. +==================================+============================+=====================+ | Alpine | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2 | 3.7 | x86-64 | +| Amazon Linux 2 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2023 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Arch | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -432,31 +446,29 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 10 Buster | 3.7 | x86 | -+----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ -| Fedora 36 | 3.10 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 38 | 3.11 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | +| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.10 | arm64v8, ppc64le, | +| | | s390x | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, | -| | | s390x, x86-64 | +| Windows Server 2016 | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2016 | 3.7 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | -| | PyPy3 | | +| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | +| | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | | +----------------------------+---------------------+ @@ -478,13 +490,13 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | +---------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |x86-64 | +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | | +---------------------------+------------------+ | | | 3.6 | 8.4.0 | | +----------------------------------+---------------------------+------------------+--------------+ diff --git a/docs/make.bat b/docs/make.bat index c943319ad..0ed5ee1a5 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -19,6 +19,7 @@ if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files + echo. htmlview to open the index page built by the html target in your browser echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files @@ -44,12 +45,23 @@ if "%1" == "clean" ( goto end ) -if "%1" == "html" ( +set html=false +if "%1%" == "html" set html=true +if "%1%" == "htmlview" set html=true +if "%html%" == "true" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end + + if "%1" == "htmlview" ( + if EXIST "%BUILDDIR%\html\index.html" ( + echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... + start "" "%BUILDDIR%\html\index.html" + ) + ) + + goto end ) if "%1" == "dirhtml" ( diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index ed2369259..d53947ff5 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,5 +1,6 @@ Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,, +Pillow >= 10,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,Yes,Yes,Yes,Yes,Yes,, Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 7f6f666c3..41d3b8fce 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -17,9 +17,7 @@ Open, rotate, and display an image (using the default viewer) The following script loads an image, rotates it 45 degrees, and displays it using an external viewer (usually xv on Unix, and the Paint program on -Windows). - -.. code-block:: python +Windows). :: from PIL import Image with Image.open("hopper.jpg") as im: @@ -29,9 +27,7 @@ Create thumbnails ^^^^^^^^^^^^^^^^^ The following script creates nice thumbnails of all JPEG images in the -current directory preserving aspect ratios with 128x128 max resolution. - -.. code-block:: python +current directory preserving aspect ratios with 128x128 max resolution. :: from PIL import Image import glob, os @@ -127,9 +123,7 @@ methods. Unless otherwise stated, all methods return a new instance of the .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to -ITU-R 709, using the D65 luminant) to the CIE XYZ color space: - -.. code-block:: python +ITU-R 709, using the D65 luminant) to the CIE XYZ color space:: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, @@ -140,9 +134,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop -This crops the input image with the provided coordinates: - -.. code-block:: python +This crops the input image with the provided coordinates:: from PIL import Image @@ -162,9 +154,7 @@ This crops the input image with the provided coordinates: .. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter -This blurs the input image using a filter from the ``ImageFilter`` module: - -.. code-block:: python +This blurs the input image using a filter from the ``ImageFilter`` module:: from PIL import Image, ImageFilter @@ -176,9 +166,7 @@ This blurs the input image using a filter from the ``ImageFilter`` module: .. automethod:: PIL.Image.Image.frombytes .. automethod:: PIL.Image.Image.getbands -This helps to get the bands of the input image: - -.. code-block:: python +This helps to get the bands of the input image:: from PIL import Image @@ -187,9 +175,7 @@ This helps to get the bands of the input image: .. automethod:: PIL.Image.Image.getbbox -This helps to get the bounding box coordinates of the input image: - -.. code-block:: python +This helps to get the bounding box coordinates of the input image:: from PIL import Image @@ -217,9 +203,7 @@ This helps to get the bounding box coordinates of the input image: .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.resize -This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: - -.. code-block:: python +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``:: from PIL import Image @@ -231,9 +215,7 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` .. automethod:: PIL.Image.Image.rotate -This rotates the input image by ``theta`` degrees counter clockwise: - -.. code-block:: python +This rotates the input image by ``theta`` degrees counter clockwise:: from PIL import Image @@ -256,9 +238,7 @@ This rotates the input image by ``theta`` degrees counter clockwise: .. automethod:: PIL.Image.Image.transpose This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` -method. - -.. code-block:: python +method. :: from PIL import Image @@ -430,18 +410,7 @@ See :ref:`concept-filters` for details. .. autoclass:: Resampling :members: :undoc-members: - -Some deprecated filters are also available under the following names: - -.. data:: NONE :noindex: - :value: Resampling.NEAREST -.. data:: LINEAR - :value: Resampling.BILINEAR -.. data:: CUBIC - :value: Resampling.BICUBIC -.. data:: ANTIALIAS - :value: Resampling.LANCZOS Dither modes ^^^^^^^^^^^^ @@ -470,7 +439,7 @@ Used to specify the dithering method to use for the Palettes ^^^^^^^^ -Used to specify the pallete to use for the :meth:`~Image.convert` method. +Used to specify the palette to use for the :meth:`~Image.convert` method. .. autoclass:: Palette :members: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9aa26916a..29115120c 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -16,7 +16,7 @@ For a more advanced drawing library for PIL, see the `aggdraw module`_. Example: Draw a gray cross over an image ---------------------------------------- -.. code-block:: python +:: import sys from PIL import Image, ImageDraw @@ -78,7 +78,7 @@ libraries, and may not available in all PIL builds. Example: Draw Partial Opacity Text ---------------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -105,7 +105,7 @@ Example: Draw Partial Opacity Text Example: Draw Multiline Text ---------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -296,7 +296,7 @@ Methods :param width: The line width, in pixels. -.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1) Draws a regular polygon inscribed in ``bounding_circle``, with ``n_sides``, and rotation of ``rotation`` degrees. @@ -311,6 +311,7 @@ Methods (e.g. ``rotation=90``, applies a 90 degree rotation). :param fill: Color to use for the fill. :param outline: Color to use for the outline. + :param width: The line width, in pixels. .. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) @@ -318,10 +319,10 @@ Methods Draws a rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. - :param outline: Color to use for the outline. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. .. versionadded:: 5.3.0 @@ -331,12 +332,14 @@ Methods Draws a rounded rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param radius: Radius of the corners. - :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. + :param corners: A tuple of whether to round each corner, + ``(top_left, top_right, bottom_right, bottom_left)``. .. versionadded:: 8.2.0 @@ -472,116 +475,6 @@ Methods .. versionadded:: 8.0.0 -.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) - - .. deprecated:: 9.2.0 - - See :ref:`deprecations ` for more information. - - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. - - Return the size of the given string, in pixels. - - .. note:: For historical reasons this function measures text height from - the ascender line instead of the top, see :ref:`text-anchors`. - If you wish to measure text height from the top, it is recommended - to use :meth:`textbbox` with ``anchor='lt'`` instead. - - :param text: Text to be measured. If it contains any newline characters, - the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: If the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`, - the number of pixels between lines. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - -.. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) - - .. deprecated:: 9.2.0 - - See :ref:`deprecations ` for more information. - - Use :py:meth:`.multiline_textbbox` instead. - - Return the size of the given string, in pixels. - - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. - - .. note:: For historical reasons this function measures text height as the - distance between the top ascender line and bottom descender line, - not the top and bottom of the text, see :ref:`text-anchors`. - If you wish to measure text height from the top to the bottom of text, - it is recommended to use :meth:`multiline_textbbox` instead. - - :param text: Text to be measured. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: The number of pixels between lines. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - .. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False) Returns length (in pixels with 1/64 precision) of given text when rendered @@ -597,18 +490,14 @@ Methods string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = draw.textlength("Hello", font) world = draw.textlength("World", font) hello_world = hello + world # not adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # may fail - use - - .. code-block:: python + use :: hello = draw.textlength("HelloW", font) - draw.textlength( "W", font @@ -617,9 +506,7 @@ Methods hello_world = hello + world # adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 29ceee314..457f0d4df 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -10,7 +10,7 @@ for image enhancement. Example: Vary the sharpness of an image --------------------------------------- -.. code-block:: python +:: from PIL import ImageEnhance @@ -29,6 +29,8 @@ Classes All enhancement classes implement a common interface, containing a single method: +.. _enhancement-factor: + .. py:class:: _Enhance .. py:method:: enhance(factor) @@ -45,31 +47,35 @@ method: Adjust image color balance. - This class can be used to adjust the colour balance of an image, in - a manner similar to the controls on a colour TV set. An enhancement - factor of 0.0 gives a black and white image. A factor of 1.0 gives - the original image. + This class can be used to adjust the colour balance of an image, in a + manner similar to the controls on a colour TV set. An + :ref:`enhancement factor ` of 0.0 gives a black and + white image. A factor of 1.0 gives the original image. .. py:class:: Contrast(image) Adjust image contrast. - This class can be used to control the contrast of an image, similar - to the contrast control on a TV set. An enhancement factor of 0.0 - gives a solid grey image. A factor of 1.0 gives the original image. + This class can be used to control the contrast of an image, similar to the + contrast control on a TV set. An + :ref:`enhancement factor ` of 0.0 gives a solid grey + image, a factor of 1.0 gives the original image, and greater values + increase the contrast of the image. .. py:class:: Brightness(image) Adjust image brightness. - This class can be used to control the brightness of an image. An - enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the - original image. + This class can be used to control the brightness of an image. An + :ref:`enhancement factor ` of 0.0 gives a black image, + a factor of 1.0 gives the original image, and greater values increase the + brightness of the image. .. py:class:: Sharpness(image) Adjust image sharpness. This class can be used to adjust the sharpness of an image. An - enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the - original image, and a factor of 2.0 gives a sharpened image. + :ref:`enhancement factor ` of 0.0 gives a blurred + image, a factor of 1.0 gives the original image, and a factor of 2.0 gives + a sharpened image. diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 3cf59c610..047990f1c 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -15,7 +15,7 @@ and **xmllib** modules. Example: Parse an image ----------------------- -.. code-block:: python +:: from PIL import ImageFile diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index c85da4fb5..044aede62 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -11,7 +11,7 @@ filters, which can be be used with the :py:meth:`Image.filter() Example: Filter an image ------------------------ -.. code-block:: python +:: from PIL import ImageFilter diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 516fa63a7..946bd3c4b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -21,7 +21,7 @@ the imToolkit package. Example ------- -.. code-block:: python +:: from PIL import ImageFont, ImageDraw diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 63f88fddd..118d988d6 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -11,7 +11,7 @@ an expression string and one or more images. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- -.. code-block:: python +:: from PIL import Image, ImageMath diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index b9bdfc507..7c1a3ad70 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -60,9 +60,7 @@ vector data. Path objects can be passed to the methods on the .. py:method:: PIL.ImagePath.Path.transform(matrix) Transforms the path in place, using an affine transform. The matrix is a - 6-tuple (a, b, c, d, e, f), and each point is mapped as follows: - - .. code-block:: python + 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:: xOut = xIn * a + yIn * b + c yOut = xIn * d + yIn * e + f diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 15d052d1c..7e67a44d3 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -4,16 +4,8 @@ :py:mod:`~PIL.ImageQt` Module ============================= -The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5 -or PySide2 QImage objects from PIL images. - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed -in Pillow 10 (2023-07-01). Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. +The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6 +QImage objects from PIL images. .. versionadded:: 1.1.6 @@ -22,7 +14,7 @@ in Pillow 10 (2023-07-01). Upgrade to Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt6/PySide6/PyQt5/PySide2 API functions and methods. + to PyQt6/PySide6 API functions and methods. This operation is currently supported for mode 1, L, P, RGB, and RGBA images. To handle other modes, you need to convert the image first. diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index f2e7d9edd..a27b2fb4e 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -10,7 +10,7 @@ iterate over the frames of an image sequence. Extracting frames from an animation ----------------------------------- -.. code-block:: python +:: from PIL import Image, ImageSequence diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 2ee3cadb7..4151be4a7 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -9,9 +9,7 @@ Windows. ImageWin can be used with PythonWin and other user interface toolkits that provide access to Windows device contexts or window handles. For example, -Tkinter makes the window handle available via the winfo_id method: - -.. code-block:: python +Tkinter makes the window handle available via the winfo_id method:: from PIL import ImageWin diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index b234b7b4e..04d6f5dcd 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -18,9 +18,7 @@ Example ------- The following script loads an image, accesses one pixel from it, then -changes it. - -.. code-block:: python +changes it. :: from PIL import Image @@ -35,9 +33,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index f9eb9b524..ed58ca3a5 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -17,9 +17,7 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the Example ------- -The following script loads an image, accesses one pixel from it, then changes it. - -.. code-block:: python +The following script loads an image, accesses one pixel from it, then changes it. :: from PIL import Image @@ -34,9 +32,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index dc4c2bf94..5e8586905 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -10,19 +10,13 @@ distributions. - ``python3-dbg`` package for the gdb extensions and python symbols - ``gdb`` and ``valgrind`` -- Potentially debug symbols for libraries. On ubuntu they're shipped - in package-dbgsym packages, from a different repo. +- Potentially debug symbols for libraries. On Ubuntu you can follow those + instructions to install the corresponding packages: `Debug Symbol Packages `_ -:: +Then ``sudo apt-get install libtiff5-dbgsym`` - deb http://ddebs.ubuntu.com focal main restricted universe multiverse - deb http://ddebs.ubuntu.com focal-updates main restricted universe multiverse - deb http://ddebs.ubuntu.com focal-proposed main restricted universe multiverse - -Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym`` - -- There's a bug with the dbg package for at least python 3.8 on ubuntu - 20.04, and you need to add a new link or two to make it autoload when +- There's a bug with the ``python3-dbg`` package for at least Python 3.8 on + Ubuntu 20.04, and you need to add a new link or two to make it autoload when running python: :: diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 6bfd50588..f31941c9a 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -61,9 +61,7 @@ Image Lifecycle * ``Image.Image.close()`` Closes the file and destroys the core image object. The Pillow context manager will also close the file, but will not destroy - the core image object. e.g.: - -.. code-block:: python + the core image object. e.g.:: with Image.open("test.jpg") as img: img.load() diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst new file mode 100644 index 000000000..e2005b710 --- /dev/null +++ b/docs/releasenotes/10.0.0.rst @@ -0,0 +1,167 @@ +10.0.0 +------ + +Backwards Incompatible Changes +============================== + +Categories +^^^^^^^^^^ + +``im.category`` has been removed, along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +Tk/Tcl 8.4 +^^^^^^^^^^ + +Support for Tk/Tcl 8.4 has been removed. + +JpegImagePlugin.convert_dict_qtables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. + +ImagePalette size parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. + +ImageShow.Viewer.show_file file argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +removed and replaced by ``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. + +Constants +^^^^^^^^^ + +A number of constants have been removed. +Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Removed Use instead +===================================================== ============================================================ +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +FitsStubImagePlugin +^^^^^^^^^^^^^^^^^^^ + +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. + +Font size and offset methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several functions for computing the size and offset of rendered text have been removed: + +=============================================================== ============================================================================================================= +Removed Use instead +=============================================================== ============================================================================================================= +``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` +``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=============================================================== ============================================================================================================= + +FreeTypeFont.getmask2 fill parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + +PhotoImage.paste box parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``box`` parameter was unused and has been removed. + +PyQt5 and PySide2 +^^^^^^^^^^^^^^^^^ + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +Image.coerce_e +^^^^^^^^^^^^^^ + +This undocumented method has been removed. + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +Added line width parameter to ImageDraw regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to +``ImageDraw.Draw.regular_polygon``. + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +Support reading signed 8-bit TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TIFF images with signed integer data, 8 bits per sample and a photometric +interpretaton of BlackIsZero can now be read. diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index dda814c1f..0b3eeeb49 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -29,84 +29,78 @@ Image resizing filters Image resizing methods :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells which filter should be used for resampling. Possible values are: -:py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BILINEAR`, -:py:data:`PIL.Image.BICUBIC` and :py:data:`PIL.Image.ANTIALIAS`. -Almost all of them were changed in this version. +``NEAREST``, ``BILINEAR``, ``BICUBIC`` and ``ANTIALIAS``. Almost all of them +were changed in this version. Bicubic and bilinear downscaling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -From the beginning :py:data:`~PIL.Image.BILINEAR` and -:py:data:`~PIL.Image.BICUBIC` filters were based on affine transformations -and used a fixed number of pixels from the source image for every destination -pixel (2x2 pixels for :py:data:`~PIL.Image.BILINEAR` and 4x4 for -:py:data:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for -downscaling. At the same time, a high quality convolutions-based algorithm with -flexible kernel was used for :py:data:`~PIL.Image.ANTIALIAS` filter. +From the beginning ``BILINEAR`` and ``BICUBIC`` filters were based on affine +transformations and used a fixed number of pixels from the source image for +every destination pixel (2x2 pixels for ``BILINEAR`` and 4x4 for ``BICUBIC``). +This gave an unsatisfactory result for downscaling. At the same time, a high +quality convolutions-based algorithm with flexible kernel was used for +``ANTIALIAS`` filter. Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used for all of these three filters. If you have previously used any tricks to maintain quality when downscaling with -:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters -(for example, reducing within several steps), they are unnecessary now. +``BILINEAR`` and ``BICUBIC`` filters (for example, reducing within several +steps), they are unnecessary now. Antialias renamed to Lanczos ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A new :py:data:`PIL.Image.LANCZOS` constant was added instead of -:py:data:`~PIL.Image.ANTIALIAS`. +A new ``LANCZOS`` constant was added instead of ``ANTIALIAS``. -When :py:data:`~PIL.Image.ANTIALIAS` was initially added, it was the only -high-quality filter based on convolutions. It's name was supposed to reflect -this. Starting from Pillow 2.7.0 all resize method are based on convolutions. -All of them are antialias from now on. And the real name of the -:py:data:`~PIL.Image.ANTIALIAS` filter is Lanczos filter. +When ``ANTIALIAS`` was initially added, it was the only high-quality filter +based on convolutions. It's name was supposed to reflect this. Starting from +Pillow 2.7.0 all resize method are based on convolutions. All of them are +antialias from now on. And the real name of the ``ANTIALIAS`` filter is Lanczos +filter. -The :py:data:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility -and is an alias for :py:data:`~PIL.Image.LANCZOS`. +The ``ANTIALIAS`` constant is left for backward compatibility and is an alias +for ``LANCZOS``. Lanczos upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The image upscaling quality with :py:data:`~PIL.Image.LANCZOS` filter was -almost the same as :py:data:`~PIL.Image.BILINEAR` due to bug. This has been fixed. +The image upscaling quality with ``LANCZOS`` filter was almost the same as +``BILINEAR`` due to a bug. This has been fixed. Bicubic upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The :py:data:`~PIL.Image.BICUBIC` filter for affine transformations produced -sharp, slightly pixelated image for upscaling. Bicubic for convolutions is -more soft. +The ``BICUBIC`` filter for affine transformations produced sharp, slightly +pixelated image for upscaling. Bicubic for convolutions is more soft. Resize performance ^^^^^^^^^^^^^^^^^^ In most cases, convolution is more a expensive algorithm for downscaling because it takes into account all the pixels of source image. Therefore -:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters' -performance can be lower than before. On the other hand the quality of -:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` was close to -:py:data:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks -you can switch to :py:data:`~PIL.Image.NEAREST` filter for downscaling, -which will give a huge improvement in performance. +``BILINEAR`` and ``BICUBIC`` filters' performance can be lower than before. +On the other hand the quality of ``BILINEAR`` and ``BICUBIC`` was close to +``NEAREST``. So if such quality is suitable for your tasks you can switch to +``NEAREST`` filter for downscaling, which will give a huge improvement in +performance. At the same time performance of convolution resampling for downscaling has been improved by around a factor of two compared to the previous version. -The upscaling performance of the :py:data:`~PIL.Image.LANCZOS` filter has -remained the same. For :py:data:`~PIL.Image.BILINEAR` filter it has improved by -1.5 times and for :py:data:`~PIL.Image.BICUBIC` by four times. +The upscaling performance of the ``LANCZOS`` filter has remained the same. For +``BILINEAR`` filter it has improved by 1.5 times and for ``BICUBIC`` by four +times. Default filter for thumbnails ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was -changed from :py:data:`~PIL.Image.NEAREST` to :py:data:`~PIL.Image.ANTIALIAS`. -Antialias was chosen because all the other filters gave poor quality for -reduction. Starting from Pillow 2.7.0, :py:data:`~PIL.Image.ANTIALIAS` has been -replaced with :py:data:`~PIL.Image.BICUBIC`, because it's faster and -:py:data:`~PIL.Image.ANTIALIAS` doesn't give any advantages after -downscaling with libjpeg, which uses supersampling internally, not convolutions. +changed from ``NEAREST`` to ``ANTIALIAS``. Antialias was chosen because all the +other filters gave poor quality for reduction. Starting from Pillow 2.7.0, +``ANTIALIAS`` has been replaced with ``BICUBIC``, because it's faster and +``ANTIALIAS`` doesn't give any advantages after downscaling with libjpeg, which +uses supersampling internally, not convolutions. Image transposition ------------------- diff --git a/docs/releasenotes/3.1.1.rst b/docs/releasenotes/3.1.1.rst index 38118ea39..5d60e116c 100644 --- a/docs/releasenotes/3.1.1.rst +++ b/docs/releasenotes/3.1.1.rst @@ -6,7 +6,7 @@ CVE-2016-0740 -- Buffer overflow in TiffDecode.c ------------------------------------------------ Pillow 3.1.0 and earlier when linked against libtiff >= 4.0.0 on x64 -may overflow a buffer when reading a specially crafted tiff file (:cve:`CVE-2016-0740`). +may overflow a buffer when reading a specially crafted tiff file (:cve:`2016-0740`). Specifically, libtiff >= 4.0.0 changed the return type of ``TIFFScanlineSize`` from ``int32`` to machine dependent @@ -24,7 +24,7 @@ CVE-2016-0775 -- Buffer overflow in FliDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, FliDecode.c has a buffer overflow error (:cve:`CVE-2016-0775`). +release, FliDecode.c has a buffer overflow error (:cve:`2016-0775`). Around line 192: @@ -53,7 +53,7 @@ CVE-2016-2533 -- Buffer overflow in PcdDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, ``PcdDecode.c`` has a buffer overflow error (:cve:`CVE-2016-2533`). +release, ``PcdDecode.c`` has a buffer overflow error (:cve:`2016-2533`). The ``state.buffer`` for ``PcdDecode.c`` is allocated based on a 3 bytes per pixel sizing, where ``PcdDecode.c`` wrote into the buffer diff --git a/docs/releasenotes/3.1.2.rst b/docs/releasenotes/3.1.2.rst index b5f7cfe99..04325ad86 100644 --- a/docs/releasenotes/3.1.2.rst +++ b/docs/releasenotes/3.1.2.rst @@ -7,7 +7,7 @@ CVE-2016-3076 -- Buffer overflow in Jpeg2KEncode.c Pillow between 2.5.0 and 3.1.1 may overflow a buffer when writing large Jpeg2000 files, allowing for code execution or other memory -corruption (:cve:`CVE-2016-3076`). +corruption (:cve:`2016-3076`). This occurs specifically in the function ``j2k_encode_entry``, at the line: diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index eb4304843..76e13b061 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -13,16 +13,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been dep Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: - -.. code-block:: python +Deprecated:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") @@ -79,9 +75,7 @@ Image quality for JPEG compressed TIFF The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG -encoder. The default is 75. For example: - -.. code-block:: python +encoder. The default is 75. For example:: im.save("out.tif", compression="jpeg", quality=85) diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 20a009cc1..7daac1b19 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -10,9 +10,7 @@ Text stroking ``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing operations. They allow text to be outlined, setting the width of the stroke and and the color respectively. If not provided, ``stroke_fill`` will default to -the ``fill`` parameter. - -.. code-block:: python +the ``fill`` parameter. :: from PIL import Image, ImageDraw, ImageFont @@ -28,9 +26,7 @@ the ``fill`` parameter. draw.multiline_text((10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0") -For example, - -.. code-block:: python +For example, :: from PIL import Image, ImageDraw, ImageFont @@ -73,7 +69,7 @@ Security ======== This release catches several buffer overruns, as well as addressing -:cve:`CVE-2019-16865`. The CVE is regarding DOS problems, such as consuming large +:cve:`2019-16865`. The CVE is regarding DOS problems, such as consuming large amounts of memory, or taking a large amount of time to process an image. In RawDecode.c, an error is now thrown if skip is calculated to be less than diff --git a/docs/releasenotes/6.2.2.rst b/docs/releasenotes/6.2.2.rst index 79d4b88aa..47692a3de 100644 --- a/docs/releasenotes/6.2.2.rst +++ b/docs/releasenotes/6.2.2.rst @@ -6,13 +6,13 @@ Security This release addresses several security problems. -:cve:`CVE-2019-19911` is regarding FPX images. If an image reports that it has a large +:cve:`2019-19911` is regarding FPX images. If an image reports that it has a large number of bands, a large amount of resources will be used when trying to process the image. This is fixed by limiting the number of bands to those usable by Pillow. -Buffer overruns were found when processing an SGI (:cve:`CVE-2020-5311`), -PCX (:cve:`CVE-2020-5312`) or FLI image (:cve:`CVE-2020-5313`). Checks have been added +Buffer overruns were found when processing an SGI (:cve:`2020-5311`), +PCX (:cve:`2020-5312`) or FLI image (:cve:`2020-5313`). Checks have been added to prevent this. -:cve:`CVE-2020-5310`: Overflow checks have been added when calculating the size of a +:cve:`2020-5310`: Overflow checks have been added when calculating the size of a memory block to be reallocated in the processing of a TIFF image. diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 80002b0ce..f2e235289 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -118,9 +118,7 @@ Loading WMF images at a given DPI ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ On Windows, Pillow can read WMF files, with a default DPI of 72. An image can -now also be loaded at another resolution: - -.. code-block:: python +now also be loaded at another resolution:: from PIL import Image with Image.open("drawing.wmf") as im: @@ -136,16 +134,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0024a537d..6e231464e 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -10,9 +10,7 @@ Allow saving of zero quality JPEG images If no quality was specified when saving a JPEG, Pillow internally used a value of zero to indicate that the default quality should be used. However, this removed the ability to actually save a JPEG with zero quality. This has now -been resolved. - -.. code-block:: python +been resolved. :: from PIL import Image im = Image.open("hopper.jpg") @@ -74,11 +72,11 @@ Security This release includes security fixes. -* :cve:`CVE-2020-10177` Fix multiple out-of-bounds reads in FLI decoding -* :cve:`CVE-2020-10378` Fix bounds overflow in PCX decoding -* :cve:`CVE-2020-10379` Fix two buffer overflows in TIFF decoding -* :cve:`CVE-2020-10994` Fix bounds overflow in JPEG 2000 decoding -* :cve:`CVE-2020-11538` Fix buffer overflow in SGI-RLE decoding +* :cve:`2020-10177` Fix multiple out-of-bounds reads in FLI decoding +* :cve:`2020-10378` Fix bounds overflow in PCX decoding +* :cve:`2020-10379` Fix two buffer overflows in TIFF decoding +* :cve:`2020-10994` Fix bounds overflow in JPEG 2000 decoding +* :cve:`2020-11538` Fix buffer overflow in SGI-RLE decoding Other Changes ============= diff --git a/docs/releasenotes/8.0.1.rst b/docs/releasenotes/8.0.1.rst index 3584a5d72..f7a1cea65 100644 --- a/docs/releasenotes/8.0.1.rst +++ b/docs/releasenotes/8.0.1.rst @@ -4,7 +4,7 @@ Security ======== -Update FreeType used in binary wheels to `2.10.4`_ to fix :cve:`CVE-2020-15999`: +Update FreeType used in binary wheels to `2.10.4`_ to fix :cve:`2020-15999`: - A heap buffer overflow has been found in the handling of embedded PNG bitmaps, introduced in FreeType version 2.6. diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst index 8ed1d9d85..69726e628 100644 --- a/docs/releasenotes/8.1.0.rst +++ b/docs/releasenotes/8.1.0.rst @@ -11,7 +11,7 @@ Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022 when FreeType 2.8 will be the minimum supported. We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ @@ -40,13 +40,13 @@ This release includes security fixes. * An out-of-bounds read when saving TIFFs with custom metadata through LibTIFF * An out-of-bounds read when saving a GIF of 1px width -* :cve:`CVE-2020-35653` Buffer read overrun in PCX decoding +* :cve:`2020-35653` Buffer read overrun in PCX decoding The PCX image decoder used the reported image stride to calculate the row buffer, rather than calculating it from the image size. This issue dates back to the PIL fork. Thanks to Google's `OSS-Fuzz`_ project for finding this. -* :cve:`CVE-2020-35654` Fix TIFF out-of-bounds write error +* :cve:`2020-35654` Fix TIFF out-of-bounds write error Out-of-bounds write in ``TiffDecode.c`` when reading corrupt YCbCr files in some LibTIFF versions (4.1.0/Ubuntu 20.04, but not 4.0.9/Ubuntu 18.04). In some cases @@ -55,7 +55,7 @@ an out-of-bounds write in ``TiffDecode.c``. This potentially affects Pillow vers from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through `Tidelift`_. -* :cve:`CVE-2020-35655` Fix for SGI Decode buffer overrun +* :cve:`2020-35655` Fix for SGI Decode buffer overrun 4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly checking the offsets and length tables. Independently reported through `Tidelift`_ and Google's diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst index 4081c49ca..18d0a33f1 100644 --- a/docs/releasenotes/8.1.1.rst +++ b/docs/releasenotes/8.1.1.rst @@ -4,19 +4,19 @@ Security ======== -:cve:`CVE-2021-25289`: The previous fix for :cve:`CVE-2020-35654` was insufficient +:cve:`2021-25289`: The previous fix for :cve:`2020-35654` was insufficient due to incorrect error checking in ``TiffDecode.c``. -:cve:`CVE-2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` +:cve:`2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` with an invalid size. -:cve:`CVE-2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to +:cve:`2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to an out-of-bounds read in ``TIFFReadRGBATile``. -:cve:`CVE-2021-25292`: The PDF parser has a catastrophic backtracking regex +:cve:`2021-25292`: The PDF parser has a catastrophic backtracking regex that could be used as a DOS attack. -:cve:`CVE-2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, +:cve:`2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, since Pillow 4.3.0. diff --git a/docs/releasenotes/8.1.2.rst b/docs/releasenotes/8.1.2.rst index 50d132f33..de50a3f1d 100644 --- a/docs/releasenotes/8.1.2.rst +++ b/docs/releasenotes/8.1.2.rst @@ -4,8 +4,8 @@ Security ======== -There is an exhaustion of memory DOS in the BLP (:cve:`CVE-2021-27921`), -ICNS (:cve:`CVE-2021-27922`) and ICO (:cve:`CVE-2021-27923`) container formats +There is an exhaustion of memory DOS in the BLP (:cve:`2021-27921`), +ICNS (:cve:`2021-27922`) and ICO (:cve:`2021-27923`) container formats where Pillow did not properly check the reported size of the contained image. These images could cause arbitrarily large memory allocations. This was reported by Jiayi Lin, Luke Shaffer, Xinran Xie, and Akshay Ajayan of diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index c902ccf71..452077f1a 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -76,9 +76,7 @@ ImageDraw.rounded_rectangle Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as :py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` argument. ``radius`` is limited to half of the width or the height, so that users can -create a circle, but not any other ellipse. - -.. code-block:: python +create a circle, but not any other ellipse. :: from PIL import Image, ImageDraw im = Image.new("RGB", (200, 200)) @@ -131,15 +129,15 @@ Security These were all found with `OSS-Fuzz`_. -:cve:`CVE-2021-25287`, :cve:`CVE-2021-25288`: Fix OOB read in Jpeg2KDecode -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-25287`, :cve:`2021-25288`: Fix OOB read in Jpeg2KDecode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * For J2k images with multiple bands, it's legal to have different widths for each band, e.g. 1 byte for ``L``, 4 bytes for ``A``. * This dates to Pillow 2.4.0. -:cve:`CVE-2021-28675`: Fix DOS in PsdImagePlugin -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28675`: Fix DOS in PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input layers with regard to the size of the data block, this could lead to a @@ -147,15 +145,15 @@ These were all found with `OSS-Fuzz`_. :py:meth:`~PIL.Image.Image.load`. * This dates to the PIL fork. -:cve:`CVE-2021-28676`: Fix FLI DOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28676`: Fix FLI DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * ``FliDecode.c`` did not properly check that the block advance was non-zero, potentially leading to an infinite loop on load. * This dates to the PIL fork. -:cve:`CVE-2021-28677`: Fix EPS DOS on _open -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28677`: Fix EPS DOS on _open +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line endings. It accidentally used a quadratic method of accumulating lines while looking @@ -164,8 +162,8 @@ These were all found with `OSS-Fuzz`_. open phase, before an image was accepted for opening. * This dates to the PIL fork. -:cve:`CVE-2021-28678`: Fix BLP DOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28678`: Fix BLP DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets returned data. This could lead to a denial-of-service where the decoder could be run a diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index 0bfead144..e74880f6f 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -8,7 +8,7 @@ JpegImagePlugin.convert_dict_qtables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ JPEG ``quantization`` is now automatically converted, but still returned as a -dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer +dictionary. The ``convert_dict_qtables`` method no longer performs any operations on the data given to it, has been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). @@ -85,7 +85,7 @@ Security Buffer overflow ^^^^^^^^^^^^^^^ -This release addresses :cve:`CVE-2021-34552`. PIL since 1.1.4 and Pillow since 1.0 +This release addresses :cve:`2021-34552`. PIL since 1.1.4 and Pillow since 1.0 allowed parameters passed into a convert function to trigger buffer overflow in Convert.c. diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst index 6b5c759fc..3333d63a1 100644 --- a/docs/releasenotes/8.3.2.rst +++ b/docs/releasenotes/8.3.2.rst @@ -4,7 +4,7 @@ Security ======== -* :cve:`CVE-2021-23437`: Avoid a potential ReDoS (regular expression denial of service) +* :cve:`2021-23437`: Avoid a potential ReDoS (regular expression denial of service) in :py:class:`~PIL.ImageColor`'s :py:meth:`~PIL.ImageColor.getrgb` by raising :py:exc:`ValueError` if the color specifier is too long. Present since Pillow 5.2.0. diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index 9becf9146..e61471e72 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -24,9 +24,7 @@ Added "transparency" argument for loading EPS images This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", generating an RGBA image with a transparent background instead of an RGB image with a -white background. - -.. code-block:: python +white background. :: with Image.open("sample.eps") as im: im.load(transparency=True) diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index a19da361a..73e77ad3e 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -43,7 +43,7 @@ FreeType 2.7 Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported. We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _FreeType: https://freetype.org/ @@ -119,7 +119,7 @@ Google's `OSS-Fuzz`_ project for finding this issue. Restrict builtins available to ImageMath.eval ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:cve:`CVE-2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow +:cve:`2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow will now restrict the builtins available to :py:meth:`PIL.ImageMath.eval`. This will help prevent problems arising if users evaluate arbitrary expressions, such as ``ImageMath.eval("exec(exit())")``. @@ -127,7 +127,7 @@ help prevent problems arising if users evaluate arbitrary expressions, such as Fixed ImagePath.Path array handling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:cve:`CVE-2022-22815` (:cwe:`CWE-126`) and :cve:`CVE-2022-22816` (:cwe:`CWE-665`) were +:cve:`2022-22815` (:cwe:`126`) and :cve:`2022-22816` (:cwe:`665`) were found when initializing ``ImagePath.Path``. .. _OSS-Fuzz: https://github.com/google/oss-fuzz @@ -155,9 +155,7 @@ altered slightly with this change. Added support for pickling TrueType fonts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TrueType fonts may now be pickled and unpickled. For example: - -.. code-block:: python +TrueType fonts may now be pickled and unpickled. For example:: import pickle from PIL import ImageFont diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst index c1feee088..acb92dc41 100644 --- a/docs/releasenotes/9.0.1.rst +++ b/docs/releasenotes/9.0.1.rst @@ -6,12 +6,12 @@ Security This release addresses several security problems. -:cve:`CVE-2022-24303`: If the path to the temporary directory on Linux or macOS +:cve:`2022-24303`: If the path to the temporary directory on Linux or macOS contained a space, this would break removal of the temporary image file after ``im.show()`` (and related actions), and potentially remove an unrelated file. This has been present since PIL. -:cve:`CVE-2022-22817`: While Pillow 9.0 restricted top-level builtins available to +:cve:`2022-22817`: While Pillow 9.0 restricted top-level builtins available to :py:meth:`PIL.ImageMath.eval`, it did not prevent builtins available to lambda expressions. These are now also restricted. diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index e97b58a41..19690ca59 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -182,17 +182,13 @@ GifImagePlugin loading strategy Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as -well. - -.. code-block:: python +well. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS Or subsequent frames can be kept in ``P`` mode as long as there is only a single -palette. - -.. code-block:: python +palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY diff --git a/docs/releasenotes/9.1.1.rst b/docs/releasenotes/9.1.1.rst index f8b155f3d..bab70f8f9 100644 --- a/docs/releasenotes/9.1.1.rst +++ b/docs/releasenotes/9.1.1.rst @@ -6,7 +6,7 @@ Security This release addresses several security problems. -:cve:`CVE-2022-30595`: When reading a TGA file with RLE packets that cross scan lines, +:cve:`2022-30595`: When reading a TGA file with RLE packets that cross scan lines, Pillow reads the information past the end of the first line without deducting that from the length of the remaining file data. This vulnerability was introduced in Pillow 9.1.0, and can cause a heap buffer overflow. diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6dbfa2702..8d8bfc9f8 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -48,20 +48,18 @@ Font size and offset methods Several functions for computing the size and offset of rendered text have been deprecated and will be removed in Pillow 10 (2023-07-01): -=========================================================================== ============================================================================================================= -Deprecated Use instead -=========================================================================== ============================================================================================================= -:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` -:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` -:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` -:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` -:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` -:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` -=========================================================================== ============================================================================================================= +=============================================================== ============================================================================================================= +Deprecated Use instead +=============================================================== ============================================================================================================= +``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox` +``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=============================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -76,9 +74,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst new file mode 100644 index 000000000..b1e982fcc --- /dev/null +++ b/docs/releasenotes/9.5.0.rst @@ -0,0 +1,96 @@ +9.5.0 +----- + +Deprecations +============ + +PSFile +^^^^^^ + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + +API Additions +============= + +QOI file format +^^^^^^^^^^^^^^^ + +Pillow can now read images in Quite OK Image format. + +Added ``dpi`` argument when saving PDFs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving a PDF, resolution could already be specified using the +``resolution`` argument. Now, a tuple of ``(x_resolution, y_resolution)`` can +be provided as ``dpi``. If both are provided, ``dpi`` will override +``resolution``. + +Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`.ImageDraw.rounded_rectangle` now accepts a keyword argument of +``corners``. This a tuple of Booleans, specifying whether to round each corner, +``(top_left, top_right, bottom_right, bottom_left)``. + +JPEG2000 comments and PLT marker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG2000 image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info`. The ``comment`` keyword argument can be used +to save it back again. + +If OpenJPEG 2.4.0 or later is available and the ``plt`` keyword argument +is present and true when saving JPEG2000 images, tell the encoder to generate +PLT markers. + +Security +======== + +Clear PPM half token after use +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Image files that are small on disk are often prevented from expanding to be +big images consuming a large amount of resources simply because they lack the +data to populate those resources. + +PpmImagePlugin might hold onto the last data read for a pixel value in case the +pixel value has not been finished yet. However, that data was not being cleared +afterwards, meaning that infinite data could be available to fill any image +size. This has been present since Pillow 9.2.0. + +That data is now cleared after use. + +Saving TIFF tag ImageSourceData +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If Pillow incorrectly saved the TIFF tag ImageSourceData as ASCII instead of +UNDEFINED, a segmentation fault was triggered. + +The correct tag type will now be used by default instead. + +Other Changes +============= + +Added support for saving PDFs in RGBA mode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Using the JPXDecode filter, PDFs can now be saved in RGBA mode. + +Improved I;16N support +^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for I;16N access, packing and unpacking. Conversion to +and from L mode has also been added. + +BGR;* modes +^^^^^^^^^^^ + +It is now possible to create new BGR;15, BGR;16 and BGR;24 images. Conversely, BGR;32 +has been removed from ImageMode and its associated methods, dropping the little support +Pillow had for the mode. + +With that, all modes listed under :ref:`concept-modes` can now be used to create a new +image. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index a2b588696..9bca98541 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,8 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.0.0 + 9.5.0 9.4.0 9.3.0 9.2.0 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index f7271ae2b..440d04b1c 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -1,5 +1,5 @@ -x.y.z ------ +xx.y.z +------ Backwards Incompatible Changes ============================== diff --git a/setup.cfg b/setup.cfg index b562e2934..06e95d7cc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,15 +4,14 @@ 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 +author = Jeffrey A. Clark (Alex) +author_email = aclark@aclark.net 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 @@ -32,10 +31,11 @@ project_urls = 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 + Mastodon=https://fosstodon.org/@pillow [options] packages = PIL -python_requires = >=3.7 +python_requires = >=3.8 include_package_data = True package_dir = = src @@ -47,7 +47,6 @@ docs = sphinx>=2.4 sphinx-copybutton sphinx-inline-tabs - sphinx-issues>=3.0.1 sphinx-removed-in sphinxext-opengraph tests = diff --git a/setup.py b/setup.py index 243365681..4aed148e8 100755 --- a/setup.py +++ b/setup.py @@ -166,14 +166,14 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache - args = ["/sbin/ldconfig", "-p"] + args = ["ldconfig", "-p"] expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" elif sys.platform.startswith("freebsd"): - args = ["/sbin/ldconfig", "-r"] + args = ["ldconfig", "-r"] expr = r".* => (.*)" env = {} @@ -242,7 +242,9 @@ def _find_include_dir(self, dirname, include): return subdir -def _cmd_exists(cmd): +def _cmd_exists(cmd: str) -> bool: + if "PATH" not in os.environ: + return False return any( os.access(os.path.join(path, cmd), os.X_OK) for path in os.environ["PATH"].split(os.pathsep) @@ -263,18 +265,16 @@ def _pkg_config(name): if not DEBUG: command_libs.append("--silence-errors") command_cflags.append("--silence-errors") - libs = ( + libs = re.split( + r"(^|\s+)-L", subprocess.check_output(command_libs, stderr=stderr) .decode("utf8") - .strip() - .replace("-L", "") - ) - cflags = ( - subprocess.check_output(command_cflags) - .decode("utf8") - .strip() - .replace("-I", "") - ) + .strip(), + )[::2][1:] + cflags = re.split( + r"(^|\s+)-I", + subprocess.check_output(command_cflags).decode("utf8").strip(), + )[::2][1:] return libs, cflags except Exception: pass @@ -430,7 +430,6 @@ class pil_build_ext(build_ext): return sdk_path def build_extensions(self): - library_dirs = [] include_dirs = [] @@ -473,8 +472,16 @@ class pil_build_ext(build_ext): else: lib_root = include_root = root - _add_directory(library_dirs, lib_root) - _add_directory(include_dirs, include_root) + if lib_root is not None: + if not isinstance(lib_root, (tuple, list)): + lib_root = (lib_root,) + for lib_dir in lib_root: + _add_directory(library_dirs, lib_dir) + if include_root is not None: + if not isinstance(include_root, (tuple, list)): + include_root = (include_root,) + for include_dir in include_root: + _add_directory(include_dirs, include_dir) # respect CFLAGS/CPPFLAGS/LDFLAGS for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): @@ -567,9 +574,7 @@ class pil_build_ext(build_ext): ): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) - if sys.platform.startswith("linux") and os.environ.get( - "ANDROID_ROOT", None - ): + if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): # termux support for android. # system libraries (zlib) are installed in /system/lib # headers are at $PREFIX/include @@ -680,10 +685,6 @@ class pil_build_ext(build_ext): # Add the directory to the include path so we can include # rather than having to cope with the versioned # include path - # FIXME (melvyn-sopacua): - # At this point it's possible that best_path is already in - # self.compiler.include_dirs. Should investigate how that is - # possible. _add_directory(self.compiler.include_dirs, best_path, 0) feature.jpeg2000 = "openjp2" feature.openjpeg_version = ".".join(str(x) for x in best_version) @@ -913,7 +914,6 @@ class pil_build_ext(build_ext): self.summary_report(feature) def summary_report(self, feature): - print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index e0dd4dede..075d46290 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,27 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - [x, y, l, d] = [int(p) for p in props["BBX"].split()] - [dx, dy] = [int(p) for p in props["DWIDTH"].split()] + # The word BBX + # followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBxoff0, BByoff0) + # of the lower left corner from the origin of the character. + width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y) + # The word DWIDTH + # followed by the width in x and y of the character in device pixels. + dwx, dwy = [int(p) for p in props["DWIDTH"].split()] + + bbox = ( + (dwx, dwy), + (x_disp, -y_disp - height, width + x_disp, -y_disp), + (0, 0, width, height), + ) try: - im = Image.frombytes("1", (x, y), bitmap, "hex", "1") + im = Image.frombytes("1", (width, height), bitmap, "hex", "1") except ValueError: # deal with zero-width characters - im = Image.new("1", (x, y)) + im = Image.new("1", (width, height)) return id, int(props["ENCODING"]), bbox, im diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 1cc0d4b3c..0ca60ff24 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -35,7 +35,6 @@ from enum import IntEnum from io import BytesIO from . import Image, ImageFile -from ._deprecate import deprecate class Format(IntEnum): @@ -54,21 +53,6 @@ class AlphaEncoding(IntEnum): DXT5 = 7 -def __getattr__(name): - for enum, prefix in { - Format: "BLP_FORMAT_", - Encoding: "BLP_ENCODING_", - AlphaEncoding: "BLP_ALPHA_ENCODING_", - }.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - def unpack_565(i): return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index e13b18f27..5bda0a5b0 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -223,7 +223,6 @@ class BmpImageFile(ImageFile.ImageFile): # --------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images - # ---------------------------------------------------- 1-bit images if not (0 < file_info["colors"] <= 65536): msg = f"Unsupported BMP Palette size ({file_info['colors']})" @@ -360,7 +359,6 @@ class BmpRleDecoder(ImageFile.PyDecoder): # Image plugin for the DIB format (BMP alias) # ============================================================================= class DibImageFile(BmpImageFile): - format = "DIB" format_description = "Windows Bitmap" diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index a0da1b786..0425bbd75 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class BufrStubImageFile(ImageFile.StubImageFile): - format = "BUFR" format_description = "BUFR" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(4)): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index aedc6ce7f..94efff341 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -32,12 +32,10 @@ def _accept(prefix): class CurImageFile(BmpImagePlugin.BmpImageFile): - format = "CUR" format_description = "Windows Cursor" def _open(self): - offset = self.fp.tell() # check magic diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 81c0314f0..cde9d42f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -37,13 +37,11 @@ def _accept(prefix): class DcxImageFile(PcxImageFile): - format = "DCX" format_description = "Intel DCX" _close_exclusive_fp_after_loading = False def _open(self): - # Header s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 016e3c135..1c88d22c7 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -29,10 +29,11 @@ import tempfile from . import Image, ImageFile from ._binary import i32le as i32 +from ._deprecate import deprecate -# # -------------------------------------------------------------------- + split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") @@ -162,9 +163,16 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. + This class is no longer used internally, but kept for backwards compatibility. """ def __init__(self, fp): + deprecate( + "PSFile", + 11, + action="If you need the functionality of this class " + "you will need to implement it yourself.", + ) self.fp = fp self.char = None @@ -194,7 +202,7 @@ def _accept(prefix): ## -# Image plugin for Encapsulated PostScript. This plugin supports only +# Image plugin for Encapsulated PostScript. This plugin supports only # a few variants of this format. @@ -209,29 +217,69 @@ class EpsImageFile(ImageFile.ImageFile): def _open(self): (length, offset) = self._find_offset(self.fp) - # Rewrap the open file pointer in something that will - # convert line endings and decode to latin-1. - fp = PSFile(self.fp) - # go to offset - start of "%!PS" - fp.seek(offset) - - box = None + self.fp.seek(offset) self.mode = "RGB" - self._size = 1, 1 # FIXME: huh? + self._size = None - # - # Load EPS header + byte_arr = bytearray(255) + bytes_mv = memoryview(byte_arr) + bytes_read = 0 + reading_comments = True - s_raw = fp.readline() - s = s_raw.strip("\r\n") + def check_required_header_comments(): + if "PS-Adobe" not in self.info: + msg = 'EPS header missing "%!PS-Adobe" comment' + raise SyntaxError(msg) + if "BoundingBox" not in self.info: + msg = 'EPS header missing "%%BoundingBox" comment' + raise SyntaxError(msg) - while s_raw: - if s: - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) + while True: + byte = self.fp.read(1) + if byte == b"": + # if we didn't read a byte we must be at the end of the file + if bytes_read == 0: + break + elif byte in b"\r\n": + # if we read a line ending character, ignore it and parse what + # we have already read. if we haven't read any other characters, + # continue reading + if bytes_read == 0: + continue + else: + # ASCII/hexadecimal lines in an EPS file must not exceed + # 255 characters, not including line ending characters + if bytes_read >= 255: + # only enforce this for lines starting with a "%", + # otherwise assume it's binary data + if byte_arr[0] == ord("%"): + msg = "not an EPS file" + raise SyntaxError(msg) + else: + if reading_comments: + check_required_header_comments() + reading_comments = False + # reset bytes_read so we can keep reading + # data until the end of the line + bytes_read = 0 + byte_arr[bytes_read] = byte[0] + bytes_read += 1 + continue + + if reading_comments: + # Load EPS header + + # if this line doesn't start with a "%", + # or does start with "%%EndComments", + # then we've reached the end of the header/comments + if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": + check_required_header_comments() + reading_comments = False + continue + + s = str(bytes_mv[:bytes_read], "latin-1") try: m = split.match(s) @@ -254,16 +302,12 @@ class EpsImageFile(ImageFile.ImageFile): ] except Exception: pass - else: m = field.match(s) if m: k = m.group(1) - - if k == "EndComments": - break if k[:8] == "PS-Adobe": - self.info[k[:8]] = k[9:] + self.info["PS-Adobe"] = k[9:] else: self.info[k] = "" elif s[0] == "%": @@ -273,64 +317,64 @@ class EpsImageFile(ImageFile.ImageFile): else: msg = "bad EPS header" raise OSError(msg) + elif bytes_mv[:11] == b"%ImageData:": + # Check for an "ImageData" descriptor + # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 - s_raw = fp.readline() - s = s_raw.strip("\r\n") + # Values: + # columns + # rows + # bit depth (1 or 8) + # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) + # number of padding channels + # block size (number of bytes per row per channel) + # binary/ascii (1: binary, 2: ascii) + # data start identifier (the image data follows after a single line + # consisting only of this quoted value) + image_data_values = byte_arr[11:bytes_read].split(None, 7) + columns, rows, bit_depth, mode_id = [ + int(value) for value in image_data_values[:4] + ] - if s and s[:1] != "%": - break - - # - # Scan for an "ImageData" descriptor - - while s[:1] == "%": - - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) - - if s[:11] == "%ImageData:": - # Encoded bitmapped image. - x, y, bi, mo = s[11:].split(None, 7)[:4] - - if int(bi) == 1: + if bit_depth == 1: self.mode = "1" - elif int(bi) == 8: + elif bit_depth == 8: try: - self.mode = self.mode_map[int(mo)] + self.mode = self.mode_map[mode_id] except ValueError: break else: break - self._size = int(x), int(y) + self._size = columns, rows return - s = fp.readline().strip("\r\n") - if not s: - break + bytes_read = 0 - if not box: + check_required_header_comments() + + if not self._size: + self._size = 1, 1 # errors if this isn't set. why (1,1)? msg = "cannot determine EPS bounding box" raise OSError(msg) def _find_offset(self, fp): + s = fp.read(4) - s = fp.read(160) - - if s[:4] == b"%!PS": + if s == b"%!PS": # for HEAD without binary preview fp.seek(0, io.SEEK_END) length = fp.tell() offset = 0 - elif i32(s, 0) == 0xC6D3D0C5: + elif i32(s) == 0xC6D3D0C5: # FIX for: Some EPS file not handled correctly / issue #302 # EPS can contain binary data # or start directly with latin coding # more info see: # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - offset = i32(s, 4) - length = i32(s, 8) + s = fp.read(8) + offset = i32(s) + length = i32(s, 4) else: msg = "not an EPS file" raise SyntaxError(msg) @@ -352,18 +396,15 @@ class EpsImageFile(ImageFile.ImageFile): pass -# # -------------------------------------------------------------------- def _save(im, fp, filename, eps=1): """EPS Writer for the Python Imaging Library.""" - # # make sure image data is available im.load() - # # determine PostScript image mode if im.mode == "L": operator = (8, 1, b"image") @@ -376,7 +417,6 @@ def _save(im, fp, filename, eps=1): raise ValueError(msg) if eps: - # # write EPS header fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") @@ -388,7 +428,6 @@ def _save(im, fp, filename, eps=1): fp.write(b"%%ImageData: %d %d " % im.size) fp.write(b'%d %d 0 1 1 "%s"\n' % operator) - # # image header fp.write(b"gsave\n") fp.write(b"10 dict begin\n") @@ -409,7 +448,6 @@ def _save(im, fp, filename, eps=1): fp.flush() -# # -------------------------------------------------------------------- diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 536bc1fe6..1359aeb12 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -19,7 +19,6 @@ def _accept(prefix): class FitsImageFile(ImageFile.ImageFile): - format = "FITS" format_description = "FITS" @@ -33,7 +32,7 @@ class FitsImageFile(ImageFile.ImageFile): keyword = header[:8].strip() if keyword == b"END": break - value = header[8:].strip() + value = header[8:].split(b"/")[0].strip() if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py deleted file mode 100644 index 86eb2d5a2..000000000 --- a/src/PIL/FitsStubImagePlugin.py +++ /dev/null @@ -1,77 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# FITS stub adapter -# -# Copyright (c) 1998-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from . import FitsImagePlugin, Image, ImageFile -from ._deprecate import deprecate - -_handler = None - - -def register_handler(handler): - """ - Install application-specific FITS image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - deprecate( - "FitsStubImagePlugin", - 10, - action="FITS images can now be read without " - "a handler through FitsImagePlugin instead", - ) - - # Override FitsImagePlugin with this handler - # for backwards compatibility - try: - Image.ID.remove(FITSStubImageFile.format) - except ValueError: - pass - - Image.register_open( - FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept - ) - - -class FITSStubImageFile(ImageFile.StubImageFile): - - format = FitsImagePlugin.FitsImageFile.format - format_description = FitsImagePlugin.FitsImageFile.format_description - - def _open(self): - offset = self.fp.tell() - - im = FitsImagePlugin.FitsImageFile(self.fp) - self._size = im.size - self.mode = im.mode - self.tile = [] - - self.fp.seek(offset) - - loader = self._load() - if loader: - loader.open(self) - - def _load(self): - return _handler - - -def _save(im, fp, filename): - msg = "FITS save handler not installed" - raise OSError(msg) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_save(FITSStubImageFile.format, _save) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 66681939d..f4e89a03e 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -40,13 +40,11 @@ def _accept(prefix): class FliImageFile(ImageFile.ImageFile): - format = "FLI" format_description = "Autodesk FLI/FLC Animation" _close_exclusive_fp_after_loading = False def _open(self): - # HEAD s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index c5fc80b37..5ec0a6632 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -36,7 +36,6 @@ class FontFile: bitmap = None def __init__(self): - self.info = {} self.glyph = [None] * 256 diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8ddc6b40b..2450c67e9 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -48,7 +48,6 @@ def _accept(prefix): class FpxImageFile(ImageFile.ImageFile): - format = "FPX" format_description = "FlashPix" @@ -157,7 +156,6 @@ class FpxImageFile(ImageFile.ImageFile): self.tile = [] for i in range(0, len(s), length): - x1 = min(xsize, x + xtile) y1 = min(ysize, y + ytile) @@ -174,7 +172,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 1: - # FIXME: the fill decoder is not implemented self.tile.append( ( @@ -186,7 +183,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 2: - internal_color_conversion = s[14] jpeg_tables = s[15] rawmode = self.rawmode @@ -234,12 +230,19 @@ class FpxImageFile(ImageFile.ImageFile): self.fp = None def load(self): - if not self.fp: self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) return ImageFile.ImageFile.load(self) + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index c7c32252b..c46b2f28b 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -56,7 +56,6 @@ from enum import IntEnum from io import BytesIO from . import Image, ImageFile -from ._deprecate import deprecate MAGIC = b"FTEX" @@ -66,17 +65,6 @@ class Format(IntEnum): UNCOMPRESSED = 1 -def __getattr__(name): - for enum, prefix in {Format: "FORMAT_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - class FtexImageFile(ImageFile.ImageFile): format = "FTEX" format_description = "Texture File Format (IW2:EOC)" diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 828a45ced..994a6e8eb 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -37,7 +37,6 @@ def _accept(prefix): class GbrImageFile(ImageFile.ImageFile): - format = "GBR" format_description = "GIMP brush file" diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 3875dc866..7dda4f143 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -44,7 +44,6 @@ class GdImageFile(ImageFile.ImageFile): format_description = "GD uncompressed images" def _open(self): - # Header s = self.fp.read(1037) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d01315b20..eadee1560 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -61,7 +61,6 @@ def _accept(prefix): class GifImageFile(ImageFile.ImageFile): - format = "GIF" format_description = "Compuserve GIF" _close_exclusive_fp_after_loading = False @@ -81,7 +80,6 @@ class GifImageFile(ImageFile.ImageFile): return False def _open(self): - # Screen s = self.fp.read(13) if not _accept(s): @@ -157,7 +155,6 @@ class GifImageFile(ImageFile.ImageFile): raise EOFError(msg) from e def _seek(self, frame, update_image=True): - if frame == 0: # rewind self.__offset = 0 @@ -195,7 +192,6 @@ class GifImageFile(ImageFile.ImageFile): interlace = None frame_dispose_extent = None while True: - if not s: s = self.fp.read(1) if not s or s == b";": @@ -487,7 +483,7 @@ def _normalize_mode(im): if Image.getmodebase(im.mode) == "RGB": im = im.convert("P", palette=Image.Palette.ADAPTIVE) if im.palette.mode == "RGBA": - for rgba in im.palette.colors.keys(): + for rgba in im.palette.colors: if rgba[3] == 0: im.info["transparency"] = im.palette.colors[rgba] break @@ -579,7 +575,6 @@ def _getbbox(base_im, im_frame): def _write_multiple_frames(im, fp, palette): - duration = im.encoderinfo.get("duration") disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) @@ -752,7 +747,6 @@ def _write_local_header(fp, im, offset, flags): def _save_netpbm(im, fp, filename): - # Unused by default. # To use, uncomment the register_save call at the end of the file. # diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index b5c5e3ca4..8e801be0b 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -64,18 +64,15 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] class GradientFile: - gradient = None def getpalette(self, entries=256): - palette = [] ix = 0 x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] for i in range(entries): - x = i / (entries - 1) while x1 < x: @@ -105,7 +102,6 @@ class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" def __init__(self, fp): - if fp.readline()[:13] != b"GIMP Gradient": msg = "not a GIMP gradient file" raise SyntaxError(msg) @@ -121,7 +117,6 @@ class GimpGradientFile(GradientFile): gradient = [] for i in range(count): - s = fp.readline().split() w = [float(x) for x in s[:11]] diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 2e9cbe58d..d38892894 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -25,7 +25,6 @@ class GimpPaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [o8(i) * 3 for i in range(256)] if fp.readline()[:12] != b"GIMP Palette": @@ -33,7 +32,6 @@ class GimpPaletteFile: raise SyntaxError(msg) for i in range(256): - s = fp.readline() if not s: break @@ -55,5 +53,4 @@ class GimpPaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 2088eb7b0..8a799f19c 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class GribStubImageFile(ImageFile.StubImageFile): - format = "GRIB" format_description = "GRIB" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index d6f283739..bba05ed65 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class HDF5StubImageFile(ImageFile.StubImageFile): - format = "HDF5" format_description = "HDF5" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index e76d0c35a..c2f050edd 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -135,7 +135,6 @@ def read_png_or_jpeg2000(fobj, start_length, size): class IcnsFile: - SIZES = { (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], @@ -189,7 +188,7 @@ class IcnsFile: def itersizes(self): sizes = [] for size, fmts in self.SIZES.items(): - for (fmt, reader) in fmts: + for fmt, reader in fmts: if fmt in self.dct: sizes.append(size) break diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 568e6d38d..a188f8fdc 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -185,7 +185,7 @@ class IcoFile: return {(h["width"], h["height"]) for h in self.entry} def getentryindex(self, size, bpp=False): - for (i, h) in enumerate(self.entry): + for i, h in enumerate(self.entry): if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): return i return 0 diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 875a20326..746743f65 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -115,13 +115,11 @@ def number(s): class ImImageFile(ImageFile.ImageFile): - format = "IM" format_description = "IFUNC Image Memory" _close_exclusive_fp_after_loading = False def _open(self): - # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. @@ -140,7 +138,6 @@ class ImImageFile(ImageFile.ImageFile): self.rawmode = "L" while True: - s = self.fp.read(1) # Some versions of IFUNC uses \n\r instead of \r\n... @@ -169,7 +166,6 @@ class ImImageFile(ImageFile.ImageFile): raise SyntaxError(msg) from e if m: - k, v = m.group(1, 2) # Don't know if this is the correct encoding, @@ -200,7 +196,6 @@ class ImImageFile(ImageFile.ImageFile): n += 1 else: - msg = "Syntax error in IM header: " + s.decode("ascii", "replace") raise SyntaxError(msg) @@ -252,7 +247,6 @@ class ImImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack if self.rawmode[:2] == "F;": - # ifunc95 formats try: # use bit decoder (if necessary) @@ -332,7 +326,6 @@ SAVE = { def _save(im, fp, filename): - try: image_type, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4e1c3a021..bee9e23d0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -56,29 +56,8 @@ from . import ( _plugins, ) from ._binary import i32le, o32be, o32le -from ._deprecate import deprecate from ._util import DeferredError, is_path - -def __getattr__(name): - categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} - if name in categories: - deprecate("Image categories", 10, "is_animated", plural=True) - return categories[name] - old_resampling = { - "LINEAR": "BILINEAR", - "CUBIC": "BICUBIC", - "ANTIALIAS": "LANCZOS", - } - if name in old_resampling: - deprecate( - name, 10, f"{old_resampling[name]} or Resampling.{old_resampling[name]}" - ) - return Resampling[old_resampling[name]] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - logger = logging.getLogger(__name__) @@ -153,6 +132,7 @@ def isImageType(t): # # Constants + # transpose class Transpose(IntEnum): FLIP_LEFT_RIGHT = 0 @@ -391,7 +371,6 @@ def init(): def _getdecoder(mode, decoder_name, args, extra=()): - # tweak arguments if args is None: args = () @@ -415,7 +394,6 @@ def _getdecoder(mode, decoder_name, args, extra=()): def _getencoder(mode, encoder_name, args, extra=()): - # tweak arguments if args is None: args = () @@ -442,26 +420,18 @@ def _getencoder(mode, encoder_name, args, extra=()): # Simple expression analyzer -def coerce_e(value): - deprecate("coerce_e", 10) - return value if isinstance(value, _E) else _E(1, value) - - -# _E(scale, offset) represents the affine transformation scale * x + offset. -# The "data" field is named for compatibility with the old implementation, -# and should be renamed once coerce_e is removed. class _E: - def __init__(self, scale, data): + def __init__(self, scale, offset): self.scale = scale - self.data = data + self.offset = offset def __neg__(self): - return _E(-self.scale, -self.data) + return _E(-self.scale, -self.offset) def __add__(self, other): if isinstance(other, _E): - return _E(self.scale + other.scale, self.data + other.data) - return _E(self.scale, self.data + other) + return _E(self.scale + other.scale, self.offset + other.offset) + return _E(self.scale, self.offset + other) __radd__ = __add__ @@ -474,19 +444,19 @@ class _E: def __mul__(self, other): if isinstance(other, _E): return NotImplemented - return _E(self.scale * other, self.data * other) + return _E(self.scale * other, self.offset * other) __rmul__ = __mul__ def __truediv__(self, other): if isinstance(other, _E): return NotImplemented - return _E(self.scale / other, self.data / other) + return _E(self.scale / other, self.offset / other) def _getscaleoffset(expr): a = expr(_E(1, 0)) - return (a.scale, a.data) if isinstance(a, _E) else (0, a) + return (a.scale, a.offset) if isinstance(a, _E) else (0, a) # -------------------------------------------------------------------- @@ -517,17 +487,10 @@ class Image: self._size = (0, 0) self.palette = None self.info = {} - self._category = 0 self.readonly = 0 self.pyaccess = None self._exif = None - def __getattr__(self, name): - if name == "category": - deprecate("Image categories", 10, "is_animated", plural=True) - return self._category - raise AttributeError(name) - @property def width(self): return self.size[0] @@ -640,7 +603,6 @@ class Image: and self.mode == other.mode and self.size == other.size and self.info == other.info - and self._category == other._category and self.getpalette() == other.getpalette() and self.tobytes() == other.tobytes() ) @@ -687,11 +649,7 @@ class Image: @property def __array_interface__(self): # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new["shape"] = shape - new["typestr"] = typestr - new["version"] = 3 + new = {"version": 3} try: if self.mode == "1": # Binary images need to be extended from bits to bytes @@ -710,10 +668,12 @@ class Image: if parse_version(numpy.__version__) < parse_version("1.23"): warnings.warn(e) raise + new["shape"], new["typestr"] = _conv_type_shape(self) return new def __getstate__(self): - return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] + im_data = self.tobytes() # load image first + return [self.info, self.mode, self.size, self.getpalette(), im_data] def __setstate__(self, state): Image.__init__(self) @@ -766,17 +726,17 @@ class Image: bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - data = [] + output = [] while True: - l, s, d = e.encode(bufsize) - data.append(d) - if s: + bytes_consumed, errcode, data = e.encode(bufsize) + output.append(data) + if errcode: break - if s < 0: - msg = f"encoder error {s} in tobytes" + if errcode < 0: + msg = f"encoder error {errcode} in tobytes" raise RuntimeError(msg) - return b"".join(data) + return b"".join(output) def tobitmap(self, name="image"): """ @@ -1433,6 +1393,11 @@ class Image: return {get_name(root.tag): get_value(root)} def getexif(self): + """ + Gets EXIF data from the image. + + :returns: an :py:class:`~PIL.Image.Exif` object. + """ if self._exif is None: self._exif = Exif() self._exif._loaded = False @@ -3030,21 +2995,29 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): def fromarray(obj, mode=None): """ Creates an image memory from an object exporting the array interface - (using the buffer protocol). + (using the buffer protocol):: + + from PIL import Image + import numpy as np + a = np.zeros((5, 5)) + im = Image.fromarray(a) If ``obj`` is not contiguous, then the ``tobytes`` method is called and :py:func:`~PIL.Image.frombuffer` is used. - If you have an image in NumPy:: + In the case of NumPy, be aware that Pillow modes do not always correspond + to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, + 32-bit signed integer pixels, and 32-bit floating point pixels. + + Pillow images can also be converted to arrays:: from PIL import Image import numpy as np im = Image.open("hopper.jpg") a = np.asarray(im) - Then this can be used to convert it to a Pillow image:: - - im = Image.fromarray(a) + When converting Pillow images to arrays however, only pixel values are + transferred. This means that P and PA mode images will lose their palette. :param obj: Object with array interface :param mode: Optional mode to use when reading ``obj``. Will be determined from @@ -3267,9 +3240,15 @@ def open(fp, mode="r", formats=None): im = _open_core(fp, filename, prefix, formats) - if im is None: + if im is None and formats is ID: + checked_formats = formats.copy() if init(): - im = _open_core(fp, filename, prefix, formats) + im = _open_core( + fp, + filename, + prefix, + tuple(format for format in formats if format not in checked_formats), + ) if im: im._exclusive_fp = exclusive_fp @@ -3400,7 +3379,8 @@ def register_open(id, factory, accept=None): reject images having another format. """ id = id.upper() - ID.append(id) + if id not in ID: + ID.append(id) OPEN[id] = factory, accept @@ -3595,6 +3575,39 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): + """ + This class provides read and write access to EXIF image data:: + + from PIL import Image + im = Image.open("exif.png") + exif = im.getexif() # Returns an instance of this class + + Information can be read and written, iterated over or deleted:: + + print(exif[274]) # 1 + exif[274] = 2 + for k, v in exif.items(): + print("Tag", k, "Value", v) # Tag 274 Value 2 + del exif[274] + + To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` + returns a dictionary:: + + from PIL import ExifTags + im = Image.open("exif_gps.jpg") + exif = im.getexif() + gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) + print(gps_ifd) + + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. + + :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: + + print(exif[ExifTags.Base.Software]) # PIL + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 + """ + endian = None bigtiff = False @@ -3841,7 +3854,7 @@ class Exif(MutableMapping): def __str__(self): if self._info is not None: # Load all keys into self._data - for tag in self._info.keys(): + for tag in self._info: self[tag] return str(self._data) diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index fec4694b2..701200317 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -38,9 +38,7 @@ def duplicate(image): def invert(image): """ - Invert an image (channel). - - .. code-block:: python + Invert an image (channel). :: out = MAX - image @@ -54,9 +52,7 @@ def invert(image): def lighter(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. - - .. code-block:: python + the lighter values. :: out = max(image1, image2) @@ -71,9 +67,7 @@ def lighter(image1, image2): def darker(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the darker values. - - .. code-block:: python + the darker values. :: out = min(image1, image2) @@ -88,9 +82,7 @@ def darker(image1, image2): def difference(image1, image2): """ Returns the absolute value of the pixel-by-pixel difference between the two - images. - - .. code-block:: python + images. :: out = abs(image1 - image2) @@ -107,9 +99,7 @@ def multiply(image1, image2): Superimposes two images on top of each other. If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. - - .. code-block:: python + you multiply with a solid white image, the image is unaffected. :: out = image1 * image2 / MAX @@ -123,9 +113,7 @@ def multiply(image1, image2): def screen(image1, image2): """ - Superimposes two inverted images on top of each other. - - .. code-block:: python + Superimposes two inverted images on top of each other. :: out = MAX - ((MAX - image1) * (MAX - image2) / MAX) @@ -176,9 +164,7 @@ def overlay(image1, image2): def add(image1, image2, scale=1.0, offset=0): """ Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 + image2) / scale + offset) @@ -193,9 +179,7 @@ def add(image1, image2, scale=1.0, offset=0): def subtract(image1, image2, scale=1.0, offset=0): """ Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 - image2) / scale + offset) @@ -208,9 +192,7 @@ def subtract(image1, image2, scale=1.0, offset=0): def add_modulo(image1, image2): - """Add two images, without clipping the result. - - .. code-block:: python + """Add two images, without clipping the result. :: out = ((image1 + image2) % MAX) @@ -223,9 +205,7 @@ def add_modulo(image1, image2): def subtract_modulo(image1, image2): - """Subtract two images, without clipping the result. - - .. code-block:: python + """Subtract two images, without clipping the result. :: out = ((image1 - image2) % MAX) @@ -243,9 +223,7 @@ def logical_and(image1, image2): Both of the images must have mode "1". If you would like to perform a logical AND on an image with a mode other than "1", try :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. - - .. code-block:: python + as the second image. :: out = ((image1 and image2) % MAX) @@ -260,9 +238,7 @@ def logical_and(image1, image2): def logical_or(image1, image2): """Logical OR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((image1 or image2) % MAX) @@ -277,9 +253,7 @@ def logical_or(image1, image2): def logical_xor(image1, image2): """Logical XOR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((bool(image1) != bool(image2)) % MAX) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index f87849680..38cbab19c 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -20,8 +20,6 @@ from enum import IntEnum from PIL import Image -from ._deprecate import deprecate - try: from PIL import _imagingcms except ImportError as ex: @@ -117,17 +115,6 @@ class Direction(IntEnum): PROOF = 2 -def __getattr__(name): - for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # # flags @@ -198,12 +185,8 @@ class ImageCmsProfile: def _set(self, profile, filename=None): self.profile = profile self.filename = filename - if profile: - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info - else: - self.product_name = None - self.product_info = None + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info def tobytes(self): """ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ce29a163b..1e4eeab25 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -32,10 +32,8 @@ import math import numbers -import warnings from . import Image, ImageColor -from ._deprecate import deprecate """ A simple 2D drawing interface for PIL images. @@ -281,11 +279,11 @@ class ImageDraw: self.im.paste(im.im, (0, 0) + im.size, mask.im) def regular_polygon( - self, bounding_circle, n_sides, rotation=0, fill=None, outline=None + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 ): """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - self.polygon(xy, fill, outline) + self.polygon(xy, fill, outline, width) def rectangle(self, xy, fill=None, outline=None, width=1): """Draw a rectangle.""" @@ -295,29 +293,43 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_rectangle(xy, ink, 0, width) - def rounded_rectangle(self, xy, radius=0, fill=None, outline=None, width=1): + def rounded_rectangle( + self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None + ): """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): (x0, y0), (x1, y1) = xy else: x0, y0, x1, y1 = xy + if x1 < x0: + msg = "x1 must be greater than or equal to x0" + raise ValueError(msg) + if y1 < y0: + msg = "y1 must be greater than or equal to y0" + raise ValueError(msg) + if corners is None: + corners = (True, True, True, True) d = radius * 2 - full_x = d >= x1 - x0 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) + full_x, full_y = False, False + if all(corners): + full_x = d >= x1 - x0 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) - if d == 0: - # If the corners have no curve, that is a rectangle + if d == 0 or not any(corners): + # If the corners have no curve, + # or there are no corners, + # that is a rectangle return self.rectangle(xy, fill, outline, width) r = d // 2 @@ -338,12 +350,17 @@ class ImageDraw: ) else: # Draw four separate corners - parts = ( - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ((x0, y0, x0 + d, y0 + d), 180, 270), - ) + parts = [] + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) + ): + if corners[i]: + parts.append(part) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -358,28 +375,52 @@ class ImageDraw: else: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) if not full_x and not full_y: - self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) - self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) + left = [x0, y0, x0 + r, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, fill, 1) + + right = [x1 - r, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, fill, 1) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: - self.draw.draw_rectangle( - (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 - ) + top = [x0, y0, x1, y0 + width - 1] + if corners[0]: + top[0] += r + 1 + if corners[1]: + top[2] -= r + 1 + self.draw.draw_rectangle(top, ink, 1) + + bottom = [x0, y1 - width + 1, x1, y1] + if corners[3]: + bottom[0] += r + 1 + if corners[2]: + bottom[2] -= r + 1 + self.draw.draw_rectangle(bottom, ink, 1) if not full_y: - self.draw.draw_rectangle( - (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 - ) + left = [x0, y0, x0 + width - 1, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, ink, 1) + + right = [x1 - width + 1, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text): - """Draw text.""" split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text @@ -390,17 +431,11 @@ class ImageDraw: return text.split(split_character) def _multiline_spacing(self, font, spacing, stroke_width): - # this can be replaced with self.textbbox(...)[3] when textsize is removed - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return ( - self.textsize( - "A", - font=font, - stroke_width=stroke_width, - )[1] - + spacing - ) + return ( + self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + + stroke_width + + spacing + ) def text( self, @@ -420,6 +455,7 @@ class ImageDraw: *args, **kwargs, ): + """Draw text.""" if self._multiline_check(text): return self.multiline_text( xy, @@ -601,72 +637,6 @@ class ImageDraw: ) top += line_spacing - def textsize( - self, - text, - font=None, - spacing=4, - direction=None, - features=None, - language=None, - stroke_width=0, - ): - """Get the size of a given string, in pixels.""" - deprecate("textsize", 10, "textbbox or textlength") - if self._multiline_check(text): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return self.multiline_textsize( - text, - font, - spacing, - direction, - features, - language, - stroke_width, - ) - - if font is None: - font = self.getfont() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return font.getsize( - text, - direction, - features, - language, - stroke_width, - ) - - def multiline_textsize( - self, - text, - font=None, - spacing=4, - direction=None, - features=None, - language=None, - stroke_width=0, - ): - deprecate("multiline_textsize", 10, "multiline_textbbox") - max_width = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - for line in lines: - line_width, line_height = self.textsize( - line, - font, - spacing, - direction, - features, - language, - stroke_width, - ) - max_width = max(max_width, line_width) - return max_width, len(lines) * line_spacing - spacing - def textlength( self, text, @@ -687,22 +657,7 @@ class ImageDraw: if font is None: font = self.getfont() mode = "RGBA" if embedded_color else self.fontmode - try: - return font.getlength(text, mode, direction, features, language) - except AttributeError: - deprecate("textlength support for fonts without getlength", 10) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - size = self.textsize( - text, - font, - direction=direction, - features=features, - language=language, - ) - if direction == "ttb": - return size[1] - return size[0] + return font.getlength(text, mode, direction, features, language) def textbbox( self, @@ -928,8 +883,8 @@ def floodfill(image, xy, value, border=None, thresh=0): full_edge = set() while edge: new_edge = set() - for (x, y) in edge: # 4 adjacent method - for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + for x, y in edge: # 4 adjacent method + for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): # If already processed, or if a coordinate is negative, skip if (s, t) in full_edge or s < 0 or t < 0: continue diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 2667b77dd..7ce0224a6 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -24,10 +24,7 @@ """ -import warnings - from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath -from ._deprecate import deprecate class Pen: @@ -173,19 +170,6 @@ class Draw: xy.transform(self.transform) self.draw.text(xy, text, font=font.font, fill=font.color) - def textsize(self, text, font): - """ - .. deprecated:: 9.2.0 - - Return the size of the given string, in pixels. - - .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize` - """ - deprecate("textsize", 10, "textbbox or textlength") - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - return self.draw.textsize(text, font=font.font) - def textbbox(self, xy, text, font): """ Returns bounding box (in pixels) of given text. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 12391955f..8e4f7dfb2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -395,7 +395,6 @@ class Parser: # parse what we have if self.decoder: - if self.offset > 0: # skip header skip = min(len(self.data), self.offset) @@ -420,14 +419,12 @@ class Parser: self.data = self.data[n:] elif self.image: - # if we end up here with no decoder, this file cannot # be incrementally parsed. wait until we've gotten all # available data pass else: - # attempt to open this file try: with io.BytesIO(self.data) as fp: @@ -533,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - l, s = encoder.encode_to_pyfd() + errcode = encoder.encode_to_pyfd()[1] else: if exc: # compress to Python file-compatible object while True: - l, s, d = encoder.encode(bufsize) - fp.write(d) - if s: + errcode, data = encoder.encode(bufsize)[1:] + fp.write(data) + if errcode: break else: # slight speedup: compress to real file object - s = encoder.encode_to_file(fh, bufsize) - if s < 0: - msg = f"encoder error {s} when writing image file" + errcode = encoder.encode_to_file(fh, bufsize) + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 59e2c18b9..63d6dcf5c 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -183,6 +183,9 @@ class BoxBlur(MultibandFilter): name = "BoxBlur" def __init__(self, radius): + if radius < 0: + msg = "radius must be >= 0" + raise ValueError(msg) self.radius = radius def filter(self, image): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b144c3dd2..ea4549cf5 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,7 +34,6 @@ from enum import IntEnum from io import BytesIO from . import Image -from ._deprecate import deprecate from ._util import is_directory, is_path @@ -43,31 +42,12 @@ class Layout(IntEnum): RAQM = 1 -def __getattr__(name): - for enum, prefix in {Layout: "LAYOUT_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - -class _ImagingFtNotInstalled: - # module placeholder - def __getattr__(self, id): - msg = "The _imagingft C module is not installed" - raise ImportError(msg) - - try: from . import _imagingft as core -except ImportError: - core = _ImagingFtNotInstalled() +except ImportError as ex: + from ._util import DeferredError - -_UNSPECIFIED = object() + core = DeferredError(ex) # FIXME: add support for pilfont2 format (see FontFile.py) @@ -90,7 +70,6 @@ class ImageFont: """PIL font wrapper""" def _load_pilfont(self, filename): - with open(filename, "rb") as fp: image = None for ext in (".png", ".gif", ".pbm"): @@ -116,7 +95,6 @@ class ImageFont: image.close() def _load_pilfont_data(self, file, image): - # read PILfont header if file.readline() != b"PILfont\n": msg = "Not a PILfont file" @@ -141,23 +119,6 @@ class ImageFont: self.font = Image.core.font(image.im, data) - def getsize(self, text, *args, **kwargs): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text. - - :param text: Text to measure. - - :return: (width, height) - """ - deprecate("getsize", 10, "getbbox or getlength") - return self.font.getsize(text) - def getmask(self, text, mode="", *args, **kwargs): """ Create a bitmap for the text. @@ -299,27 +260,21 @@ class FreeTypeFont: string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = font.getlength("Hello") world = font.getlength("World") hello_world = hello + world # not adjusted for kerning assert hello_world == font.getlength("HelloWorld") # may fail - use - - .. code-block:: python + use :: hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning world = font.getlength("World") hello_world = hello + world # adjusted for kerning assert hello_world == font.getlength("HelloWorld") # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) @@ -425,165 +380,6 @@ class FreeTypeFont: width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width return left, top, left + width, top + height - def getsize( - self, - text, - direction=None, - features=None, - language=None, - stroke_width=0, - ): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`getlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text if rendered in font with - provided direction, features, and language. - - .. note:: For historical reasons this function measures text height from - the ascender line instead of the top, see :ref:`text-anchors`. - If you wish to measure text height from the top, it is recommended - to use the bottom value of :meth:`getbbox` with ``anchor='lt'`` instead. - - :param text: Text to measure. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - """ - deprecate("getsize", 10, "getbbox or getlength") - # vertical offset is added for historical reasons - # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 - size, offset = self.font.getsize(text, "L", direction, features, language) - return ( - size[0] + stroke_width * 2, - size[1] + stroke_width * 2 + offset[1], - ) - - def getsize_multiline( - self, - text, - direction=None, - spacing=4, - features=None, - language=None, - stroke_width=0, - ): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.ImageDraw.multiline_textbbox` instead. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text if rendered in font - with provided direction, features, and language, while respecting - newline characters. - - :param text: Text to measure. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param spacing: The vertical gap between lines, defaulting to 4 pixels. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - """ - deprecate("getsize_multiline", 10, "ImageDraw.multiline_textbbox") - max_width = 0 - lines = self._multiline_split(text) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing - for line in lines: - line_width, line_height = self.getsize( - line, direction, features, language, stroke_width - ) - max_width = max(max_width, line_width) - - return max_width, len(lines) * line_spacing - spacing - - def getoffset(self, text): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` instead. - - See :ref:`deprecations ` for more information. - - Returns the offset of given text. This is the gap between the - starting coordinate and the first marking. Note that this gap is - included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`. - - :param text: Text to measure. - - :return: A tuple of the x and y offset - """ - deprecate("getoffset", 10, "getbbox") - return self.font.getsize(text)[1] - def getmask( self, text, @@ -678,7 +474,6 @@ class FreeTypeFont: self, text, mode="", - fill=_UNSPECIFIED, direction=None, features=None, language=None, @@ -704,12 +499,6 @@ class FreeTypeFont: .. versionadded:: 1.1.5 - :param fill: Optional fill function. By default, an internal Pillow function - will be used. - - Deprecated. This parameter will be removed in Pillow 10 - (2023-07-01). - :param direction: Direction of the text. It can be 'rtl' (right to left), 'ltr' (left to right) or 'ttb' (top to bottom). Requires libraqm. @@ -762,10 +551,6 @@ class FreeTypeFont: :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - if fill is _UNSPECIFIED: - fill = Image.core.fill - else: - deprecate("fill", 10) size, offset = self.font.getsize( text, mode, direction, features, language, anchor ) @@ -774,19 +559,20 @@ class FreeTypeFont: size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) offset = offset[0] - stroke_width, offset[1] - stroke_width Image._decompression_bomb_check(size) - im = fill("RGBA" if mode == "RGBA" else "L", size, 0) - self.font.render( - text, - im.id, - mode, - direction, - features, - language, - stroke_width, - ink, - start[0], - start[1], - ) + im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0) + if min(size): + self.font.render( + text, + im.id, + mode, + direction, + features, + language, + stroke_width, + ink, + start[0], + start[1], + ) return im, offset def font_variant( @@ -888,22 +674,6 @@ class TransposedFont: self.font = font self.orientation = orientation # any 'transpose' argument, or None - def getsize(self, text, *args, **kwargs): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. - - See :ref:`deprecations ` for more information. - """ - deprecate("getsize", 10, "getbbox or getlength") - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w, h = self.font.getsize(text) - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - return h, w - return w, h - def getmask(self, text, mode="", *args, **kwargs): im = self.font.getmask(text, mode, *args, **kwargs) if self.orientation is not None: @@ -1020,7 +790,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): if windir: dirs.append(os.path.join(windir, "fonts")) elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS", "") + lindirs = os.environ.get("XDG_DATA_DIRS") if not lindirs: # According to the freedesktop spec, XDG_DATA_DIRS should # default to /usr/share diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 982f77f20..2592ba2df 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -141,8 +141,11 @@ def grabclipboard(): msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) fh, filepath = tempfile.mkstemp() - subprocess.call(args, stdout=fh) + err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr os.close(fh) + if err: + msg = f"{args[0]} error: {err.strip().decode()}" + raise ChildProcessError(msg) im = Image.open(filepath) im.load() os.unlink(filepath) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 0973536c9..a0b335142 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -58,10 +58,9 @@ def getmode(mode): "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), # extra experimental modes "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), endian + "u2"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), endian + "u2"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), endian + "u3"), - "BGR;32": ("RGB", "L", ("B", "G", "R"), endian + "u4"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), "LA": ("L", "L", ("L", "A"), "|u1"), "La": ("L", "L", ("L", "a"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"), diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index e2168ce62..301c593c7 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -205,7 +205,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (2-color) if mid is None: - range_map = range(0, whitepoint - blackpoint) for i in range_map: @@ -215,7 +214,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (3-color) else: - range_map1 = range(0, midpoint - blackpoint) range_map2 = range(0, whitepoint - midpoint) @@ -248,7 +246,8 @@ def contain(image, size, method=Image.Resampling.BICUBIC): :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`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :return: An image. """ @@ -276,7 +275,8 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 :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`. + :py:attr:`~PIL.Image.Resampling.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 padded version. @@ -328,7 +328,8 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): :param image: The image to rescale. :param factor: The expansion factor, as a float. :param resample: Resampling method to use. Default is - :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ if factor == 1: @@ -425,7 +426,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, :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`. + :py:attr:`~PIL.Image.Resampling.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 one percent). The default value is 0 (no border). diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fe0d32155..f0c094708 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -19,7 +19,6 @@ import array from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile -from ._deprecate import deprecate class ImagePalette: @@ -34,16 +33,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None, size=0): + def __init__(self, mode="RGB", palette=None): self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() self.dirty = None - if size != 0: - deprecate("The size parameter", 10, None) - if size != len(self.palette): - msg = "wrong palette size" - raise ValueError(msg) @property def palette(self): @@ -248,11 +242,9 @@ def wedge(mode="RGB"): def load(filename): - # FIXME: supports GIMP gradients only with open(filename, "rb") as fp: - for paletteHandler in [ GimpPaletteFile.GimpPaletteFile, GimpGradientFile.GimpGradientFile, diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index ad607a97b..9b7245454 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -20,14 +20,11 @@ import sys from io import BytesIO from . import Image -from ._deprecate import deprecate from ._util import is_path qt_versions = [ ["6", "PyQt6"], ["side6", "PySide6"], - ["5", "PyQt5"], - ["side2", "PySide2"], ] # If a version has already been imported, attempt it first @@ -40,16 +37,6 @@ for qt_version, qt_module in qt_versions: elif qt_module == "PySide6": from PySide6.QtCore import QBuffer, QIODevice from PySide6.QtGui import QImage, QPixmap, qRgba - elif qt_module == "PyQt5": - from PyQt5.QtCore import QBuffer, QIODevice - from PyQt5.QtGui import QImage, QPixmap, qRgba - - deprecate("Support for PyQt5", 10, "PyQt6 or PySide6") - elif qt_module == "PySide2": - from PySide2.QtCore import QBuffer, QIODevice - from PySide2.QtGui import QImage, QPixmap, qRgba - - deprecate("Support for PySide2", 10, "PyQt6 or PySide6") except (ImportError, RuntimeError): continue qt_is_installed = True diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 29d900bef..3f68a2696 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -19,8 +19,6 @@ from shlex import quote from PIL import Image -from ._deprecate import deprecate - _viewers = [] @@ -111,21 +109,10 @@ class Viewer: """Display the given image.""" return self.show_file(self.save_image(image), **options) - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) os.system(self.get_command(path, **options)) # nosec return 1 @@ -164,21 +151,10 @@ class MacViewer(Viewer): command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" return command - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.call(["open", "-a", "Preview.app", path]) executable = sys.executable or shutil.which("python3") if executable: @@ -215,21 +191,10 @@ class XDGViewer(UnixViewer): command = executable = "xdg-open" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["xdg-open", path]) return 1 @@ -246,20 +211,10 @@ class DisplayViewer(UnixViewer): command += f" -title {quote(title)}" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) args = ["display"] title = options.get("title") if title: @@ -278,20 +233,10 @@ class GmDisplayViewer(UnixViewer): command = "gm display" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["gm", "display", path]) return 1 @@ -304,20 +249,10 @@ class EogViewer(UnixViewer): command = "eog -n" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["eog", "-n", path]) return 1 @@ -336,20 +271,10 @@ class XVViewer(UnixViewer): command += f" -name {quote(title)}" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) args = ["xv"] title = options.get("title") if title: @@ -390,7 +315,6 @@ else: if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 ImageShow.py imagefile [title]") sys.exit() diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 09a6356fa..bf98eb2c8 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -29,7 +29,6 @@ import tkinter from io import BytesIO from . import Image -from ._deprecate import deprecate # -------------------------------------------------------------------- # Check for Tkinter interface hooks @@ -97,7 +96,6 @@ class PhotoImage: """ def __init__(self, image=None, size=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) @@ -163,7 +161,7 @@ class PhotoImage: """ return self.__size[1] - def paste(self, im, box=None): + def paste(self, im): """ Paste a PIL image into the photo image. Note that this can be very slow if the photo image is displayed. @@ -171,13 +169,7 @@ class PhotoImage: :param im: A PIL image. The size must match the target region. If the mode does not match, the image is converted to the mode of the bitmap image. - :param box: Deprecated. This parameter will be removed in Pillow 10 - (2023-07-01). """ - - if box is not None: - deprecate("The box parameter", 10, None) - # convert to blittable im.load() image = im.im @@ -209,7 +201,6 @@ class BitmapImage: """ def __init__(self, image=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index cfeadd53c..ac267457b 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -30,12 +30,10 @@ field = re.compile(rb"([a-z]*) ([^ \r\n]*)") class ImtImageFile(ImageFile.ImageFile): - format = "IMT" format_description = "IM Tools" def _open(self): - # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. @@ -47,7 +45,6 @@ class ImtImageFile(ImageFile.ImageFile): xsize = ysize = 0 while True: - if buffer: s = buffer[:1] buffer = buffer[1:] @@ -57,7 +54,6 @@ class ImtImageFile(ImageFile.ImageFile): break if s == b"\x0C": - # image data begins self.tile = [ ( @@ -71,7 +67,6 @@ class ImtImageFile(ImageFile.ImageFile): break else: - # read key/value pair if b"\n" not in buffer: buffer += self.fp.read(100) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 774817569..4c47b55c1 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -48,7 +48,6 @@ def dump(c): class IptcImageFile(ImageFile.ImageFile): - format = "IPTC" format_description = "IPTC/NAA" @@ -84,7 +83,6 @@ class IptcImageFile(ImageFile.ImageFile): return tag, size def _open(self): - # load descriptive fields while True: offset = self.fp.tell() @@ -134,7 +132,6 @@ class IptcImageFile(ImageFile.ImageFile): ] def load(self): - if len(self.tile) != 1 or self.tile[0][0] != "iptc": return ImageFile.ImageFile.load(self) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 7457874c1..9309768ba 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -17,7 +17,7 @@ import io import os import struct -from . import Image, ImageFile +from . import Image, ImageFile, _binary class BoxReader: @@ -99,7 +99,7 @@ def _parse_codestream(fp): count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" hdr = fp.read(2) - lsiz = struct.unpack(">H", hdr)[0] + lsiz = _binary.i16be(hdr) siz = hdr + fp.read(lsiz - 2) lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( ">HHIIIIIIIIH", siz @@ -218,6 +218,8 @@ class Jpeg2KImageFile(ImageFile.ImageFile): self._size, self.mode, self.custom_mimetype, dpi = header if dpi is not None: self.info["dpi"] = dpi + if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + self._parse_comment() else: msg = "not a JPEG 2000 file" raise SyntaxError(msg) @@ -254,6 +256,28 @@ class Jpeg2KImageFile(ImageFile.ImageFile): ) ] + def _parse_comment(self): + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + self.fp.seek(length - 2, os.SEEK_CUR) + + while True: + marker = self.fp.read(2) + if not marker: + break + typ = marker[1] + if typ in (0x90, 0xD9): + # Start of tile or end of codestream + break + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + if typ == 0x64: + # Comment + self.info["comment"] = self.fp.read(length - 2)[2:] + break + else: + self.fp.seek(length - 2, os.SEEK_CUR) + @property def reduce(self): # https://github.com/python-pillow/Pillow/issues/4343 found that the @@ -327,8 +351,12 @@ def _save(im, fp, filename): cinema_mode = info.get("cinema_mode", "no") mct = info.get("mct", 0) signed = info.get("signed", False) - fd = -1 + comment = info.get("comment") + if isinstance(comment, str): + comment = comment.encode() + plt = info.get("plt", False) + fd = -1 if hasattr(fp, "fileno"): try: fd = fp.fileno() @@ -350,6 +378,8 @@ def _save(im, fp, filename): mct, signed, fd, + comment, + plt, ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 9657ae9d0..5dd1a61af 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -41,12 +41,11 @@ import sys import tempfile import warnings -from . import Image, ImageFile, TiffImagePlugin +from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 -from ._deprecate import deprecate from .JpegPresets import presets # @@ -344,12 +343,10 @@ def _accept(prefix): class JpegImageFile(ImageFile.ImageFile): - format = "JPEG" format_description = "JPEG (ISO 10918)" def _open(self): - s = self.fp.read(3) if not _accept(s): @@ -370,7 +367,6 @@ class JpegImageFile(ImageFile.ImageFile): self.icclist = [] while True: - i = s[0] if i == 0xFF: s = s + self.fp.read(1) @@ -418,7 +414,6 @@ class JpegImageFile(ImageFile.ImageFile): return s def draft(self, mode, size): - if len(self.tile) != 1: return @@ -455,7 +450,6 @@ class JpegImageFile(ImageFile.ImageFile): return self.mode, box def load_djpeg(self): - # ALTERNATIVE: handle JPEGs via the IJG command line utilities f, path = tempfile.mkstemp() @@ -524,6 +518,8 @@ def _getmp(self): head = file_contents.read(8) endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<" # process dictionary + from . import TiffImagePlugin + try: info = TiffImagePlugin.ImageFileDirectory_v2(head) file_contents.seek(info.next) @@ -615,11 +611,6 @@ samplings = { # fmt: on -def convert_dict_qtables(qtables): - deprecate("convert_dict_qtables", 10, action="Conversion is no longer needed") - return qtables - - def get_sampling(im): # There's no subsampling when images have only 1 layer # (grayscale images) or when they are CMYK (4 layers), @@ -733,10 +724,10 @@ def _save(im, fp, filename): extra = info.get("extra", b"") + MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 - MAX_BYTES_IN_MARKER = 65533 MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN markers = [] while icc_profile: @@ -767,6 +758,9 @@ def _save(im, fp, filename): exif = info.get("exif", b"") if isinstance(exif, Image.Exif): exif = exif.tobytes() + if len(exif) > MAX_BYTES_IN_MARKER: + msg = "EXIF data is too long" + raise ValueError(msg) # get keyword arguments im.encoderconfig = ( diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 8d4d826aa..17c008b9a 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -30,12 +30,10 @@ def _accept(s): class McIdasImageFile(ImageFile.ImageFile): - format = "MCIDAS" format_description = "McIdas area file" def _open(self): - # parse area file directory s = self.fp.read(256) if not _accept(s) or len(s) != 256: diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index e7e1054a3..801318930 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -34,13 +34,11 @@ def _accept(prefix): class MicImageFile(TiffImagePlugin.TiffImageFile): - format = "MIC" format_description = "Microsoft Image Composer" _close_exclusive_fp_after_loading = False def _open(self): - # read the OLE directory and see if this is a likely # to be a Microsoft Image Composer file @@ -68,9 +66,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 - if len(self.images) > 1: - self._category = Image.CONTAINER - self.seek(0) def seek(self, frame): @@ -91,6 +86,14 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def tell(self): return self.frame + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 2d799d6d8..d96d3a11c 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -58,12 +58,10 @@ class BitStream: class MpegImageFile(ImageFile.ImageFile): - format = "MPEG" format_description = "MPEG" def _open(self): - s = BitStream(self.fp) if s.read(32) != 0x1B3: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index b1ec2c7bc..f9261c77d 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -101,7 +101,6 @@ def _save_all(im, fp, filename): class MpoImageFile(JpegImagePlugin.JpegImageFile): - format = "MPO" format_description = "MPO (CIPA DC-007)" _close_exclusive_fp_after_loading = False diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 5420894dc..c6567b2ae 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class MspImageFile(ImageFile.ImageFile): - format = "MSP" format_description = "Windows Paint" def _open(self): - # Header s = self.fp.read(32) if not _accept(s): @@ -111,7 +109,6 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True def decode(self, buffer): - img = io.BytesIO() blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: @@ -162,7 +159,6 @@ Image.register_decoder("MSP", MspDecoder) def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 07acd5580..4a2c497fc 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -22,11 +22,9 @@ class PaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [(i, i, i) for i in range(256)] while True: - s = fp.readline() if not s: @@ -50,5 +48,4 @@ class PaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 109aad9ab..a88a90791 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -112,9 +112,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} def _save(im, fp, filename): - if im.mode == "P": - # we assume this is a color Palm image with the standard colormap, # unless the "info" dict has a "custom-colormap" field @@ -147,14 +145,12 @@ def _save(im, fp, filename): version = 1 elif im.mode == "1": - # monochrome -- write it inverted, as is the Palm standard rawmode = "1;I" bpp = 1 version = 0 else: - msg = f"cannot write mode {im.mode} as Palm" raise OSError(msg) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 5802d386a..e390f3fe5 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -24,12 +24,10 @@ from . import Image, ImageFile class PcdImageFile(ImageFile.ImageFile): - format = "PCD" format_description = "Kodak PhotoCD" def _open(self): - # rough self.fp.seek(2048) s = self.fp.read(2048) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index ecce1b097..8db5822fe 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -58,7 +58,6 @@ class PcfFontFile(FontFile.FontFile): name = "name" def __init__(self, fp, charset_encoding="iso8859-1"): - self.charset_encoding = charset_encoding magic = l32(fp.read(4)) @@ -87,12 +86,24 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - x, y, l, r, w, a, d, f = metrics[ix] - glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix] - self.glyph[ch] = glyph + ( + xsize, + ysize, + left, + right, + width, + ascent, + descent, + attributes, + ) = metrics[ix] + self.glyph[ch] = ( + (width, 0), + (left, descent - ysize, xsize + left, descent), + (0, 0, xsize, ysize), + bitmaps[ix], + ) def _getformat(self, tag): - format, size, offset = self.toc[tag] fp = self.fp @@ -108,7 +119,6 @@ class PcfFontFile(FontFile.FontFile): return fp, format, i16, i32 def _load_properties(self): - # # font properties @@ -136,7 +146,6 @@ class PcfFontFile(FontFile.FontFile): return properties def _load_metrics(self): - # # font metrics @@ -147,7 +156,6 @@ class PcfFontFile(FontFile.FontFile): append = metrics.append if (format & 0xFF00) == 0x100: - # "compressed" metrics for i in range(i16(fp.read(2))): left = i8(fp.read(1)) - 128 @@ -160,7 +168,6 @@ class PcfFontFile(FontFile.FontFile): append((xsize, ysize, left, right, width, ascent, descent, 0)) else: - # "jumbo" metrics for i in range(i32(fp.read(4))): left = i16(fp.read(2)) @@ -176,7 +183,6 @@ class PcfFontFile(FontFile.FontFile): return metrics def _load_bitmaps(self, metrics): - # # bitmap data @@ -213,9 +219,11 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y, l, r, w, a, d, f = metrics[i] - b, e = offsets[i], offsets[i + 1] - bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) + xsize, ysize = metrics[i][:2] + b, e = offsets[i : i + 2] + bitmaps.append( + Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) + ) return bitmaps diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3202475dc..f42c2456b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -45,12 +45,10 @@ def _accept(prefix): class PcxImageFile(ImageFile.ImageFile): - format = "PCX" format_description = "Paintbrush" def _open(self): - # header s = self.fp.read(128) if not _accept(s): @@ -143,7 +141,6 @@ SAVE = { def _save(im, fp, filename): - try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index baad4939f..c41f8aee0 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,7 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - resolution = im.encoderinfo.get("resolution", 72.0) + dpi = im.encoderinfo.get("dpi") + if dpi: + x_resolution = dpi[0] + y_resolution = dpi[1] + else: + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { "title": None @@ -168,6 +173,10 @@ def _save(im, fp, filename, save_all=False): filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images + elif im.mode == "RGBA": + filter = "JPXDecode" + colorspace = PdfParser.PdfName("DeviceRGB") + procset = "ImageC" # color images elif im.mode == "CMYK": filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceCMYK") @@ -194,6 +203,8 @@ def _save(im, fp, filename, save_all=False): ) elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) + elif filter == "JPXDecode": + Image.SAVE["JPEG2000"](im, op, filename) elif filter == "FlateDecode": ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) elif filter == "RunLengthDecode": @@ -214,8 +225,8 @@ def _save(im, fp, filename, save_all=False): stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, Filter=filter, BitsPerComponent=bits, Decode=decode, @@ -235,8 +246,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ], Contents=contents_refs[page_number], ) @@ -245,8 +256,8 @@ def _save(im, fp, filename, save_all=False): # page contents page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index aa5ea2fbb..dc1012f54 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -328,9 +328,7 @@ def pdf_repr(x): return b"null" elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): return bytes(x) - elif isinstance(x, int): - return str(x).encode("us-ascii") - elif isinstance(x, float): + elif isinstance(x, (int, 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")" @@ -959,14 +957,11 @@ class PdfParser: check_format_condition(m, "xref entry not found") offset = m.end() is_free = m.group(3) == b"f" - generation = int(m.group(2)) if not is_free: + generation = int(m.group(2)) new_entry = (int(m.group(1)), generation) - check_format_condition( - i not in self.xref_table or self.xref_table[i] == new_entry, - "xref entry duplicated (and not identical)", - ) - self.xref_table[i] = new_entry + if i not in self.xref_table: + self.xref_table[i] = new_entry return offset def read_indirect(self, ref, max_nesting=-1): diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 8d0a34dba..7eb82228a 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -35,12 +35,10 @@ def _accept(prefix): class PixarImageFile(ImageFile.ImageFile): - format = "PIXAR" format_description = "PIXAR raster image" def _open(self): - # assuming a 4-byte magic label s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index b6626bbc5..82a74b267 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -45,7 +45,6 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 -from ._deprecate import deprecate logger = logging.getLogger(__name__) @@ -131,17 +130,6 @@ class Blend(IntEnum): """ -def __getattr__(name): - for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - def _safe_zlib_decompress(s): dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) @@ -161,7 +149,6 @@ def _crc32(data, seed=0): class ChunkStream: def __init__(self, fp): - self.fp = fp self.queue = [] @@ -195,7 +182,6 @@ class ChunkStream: self.queue = self.fp = None def push(self, cid, pos, length): - self.queue.append((cid, pos, length)) def call(self, cid, pos, length): @@ -230,7 +216,6 @@ class ChunkStream: self.fp.read(4) def verify(self, endchunk=b"IEND"): - # Simple approach; just calculate checksum for all remaining # blocks. Must be called directly after open. @@ -397,7 +382,6 @@ class PngStream(ChunkStream): self._seq_num = self.rewind_state["seq_num"] def chunk_iCCP(self, pos, length): - # ICC profile s = ImageFile._safe_read(self.fp, length) # according to PNG spec, the iCCP chunk contains: @@ -425,7 +409,6 @@ class PngStream(ChunkStream): return s def chunk_IHDR(self, pos, length): - # image header s = ImageFile._safe_read(self.fp, length) if length < 13: @@ -446,7 +429,6 @@ class PngStream(ChunkStream): return s def chunk_IDAT(self, pos, length): - # image data if "bbox" in self.im_info: tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] @@ -459,12 +441,10 @@ class PngStream(ChunkStream): raise EOFError def chunk_IEND(self, pos, length): - # end of PNG image raise EOFError def chunk_PLTE(self, pos, length): - # palette s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -472,7 +452,6 @@ class PngStream(ChunkStream): return s def chunk_tRNS(self, pos, length): - # transparency s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -524,7 +503,6 @@ class PngStream(ChunkStream): return s def chunk_pHYs(self, pos, length): - # pixels per unit s = ImageFile._safe_read(self.fp, length) if length < 9: @@ -542,7 +520,6 @@ class PngStream(ChunkStream): return s def chunk_tEXt(self, pos, length): - # text s = ImageFile._safe_read(self.fp, length) try: @@ -562,7 +539,6 @@ class PngStream(ChunkStream): return s def chunk_zTXt(self, pos, length): - # compressed text s = ImageFile._safe_read(self.fp, length) try: @@ -597,7 +573,6 @@ class PngStream(ChunkStream): return s def chunk_iTXt(self, pos, length): - # international text r = s = ImageFile._safe_read(self.fp, length) try: @@ -721,12 +696,10 @@ def _accept(prefix): class PngImageFile(ImageFile.ImageFile): - format = "PNG" format_description = "Portable network graphics" def _open(self): - if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -740,7 +713,6 @@ class PngImageFile(ImageFile.ImageFile): self.png = PngStream(self.fp) while True: - # # get next chunk @@ -1019,9 +991,13 @@ class PngImageFile(ImageFile.ImageFile): else: if self._prev_im and self.blend_op == Blend.OP_OVER: updated = self._crop(self.im, self.dispose_extent) - self._prev_im.paste( - updated, self.dispose_extent, updated.convert("RGBA") - ) + if self.im.mode == "RGB" and "transparency" in self.info: + mask = updated.convert_transparent( + "RGBA", self.info["transparency"] + ) + else: + mask = updated.convert("RGBA") + self._prev_im.paste(updated, self.dispose_extent, mask) self.im = self._prev_im if self.pyaccess: self.pyaccess = None @@ -1264,7 +1240,6 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): mode = im.mode if mode == "P": - # # attempt to minimize storage requirements for palette images if "bits" in im.encoderinfo: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index dee2f1e15..2cb1e5636 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -51,7 +51,6 @@ def _accept(prefix): class PpmImageFile(ImageFile.ImageFile): - format = "PPM" format_description = "Pbmplus image" @@ -238,6 +237,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): if half_token: block = half_token + block # stitch half_token to new block + half_token = False tokens = block.split() diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index c1ca30a03..5a5d60d56 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -51,13 +51,11 @@ def _accept(prefix): class PsdImageFile(ImageFile.ImageFile): - format = "PSD" format_description = "Adobe Photoshop" _close_exclusive_fp_after_loading = False def _open(self): - read = self.fp.read # @@ -177,7 +175,6 @@ def _layerinfo(fp, ct_bytes): raise SyntaxError(msg) for _ in range(abs(ct)): - # bounding box y0 = i32(read(4)) x0 = i32(read(4)) @@ -238,21 +235,18 @@ def _layerinfo(fp, ct_bytes): layers.append((name, mode, (x0, y0, x1, y1))) # get tiles - i = 0 - for name, mode, bbox in layers: + for i, (name, mode, bbox) in enumerate(layers): tile = [] for m in mode: t = _maketile(fp, m, bbox, 1) if t: tile.extend(t) layers[i] = name, mode, bbox, tile - i += 1 return layers def _maketile(file, mode, bbox, channels): - tile = None read = file.read diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index e9cb34ced..39747b4f3 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -320,6 +320,7 @@ mode_map = { "1": _PyAccess8, "L": _PyAccess8, "P": _PyAccess8, + "I;16N": _PyAccessI16_N, "LA": _PyAccess32_2, "La": _PyAccess32_2, "PA": _PyAccess32_2, diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py new file mode 100644 index 000000000..ef91b90ab --- /dev/null +++ b/src/PIL/QoiImagePlugin.py @@ -0,0 +1,105 @@ +# +# The Python Imaging Library. +# +# QOI support for PIL +# +# See the README file for information on usage and redistribution. +# + +import os + +from . import Image, ImageFile +from ._binary import i32be as i32 +from ._binary import o8 + + +def _accept(prefix): + return prefix[:4] == b"qoif" + + +class QoiImageFile(ImageFile.ImageFile): + format = "QOI" + format_description = "Quite OK Image" + + def _open(self): + if not _accept(self.fp.read(4)): + msg = "not a QOI file" + raise SyntaxError(msg) + + self._size = tuple(i32(self.fp.read(4)) for i in range(2)) + + channels = self.fp.read(1)[0] + self.mode = "RGB" if channels == 3 else "RGBA" + + self.fp.seek(1, os.SEEK_CUR) # colorspace + self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)] + + +class QoiDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def _add_to_previous_pixels(self, value): + self._previous_pixel = value + + r, g, b, a = value + hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + self._previously_seen_pixels[hash_value] = value + + def decode(self, buffer): + self._previously_seen_pixels = {} + self._previous_pixel = None + self._add_to_previous_pixels(b"".join(o8(i) for i in (0, 0, 0, 255))) + + data = bytearray() + bands = Image.getmodebands(self.mode) + while len(data) < self.state.xsize * self.state.ysize * bands: + byte = self.fd.read(1)[0] + if byte == 0b11111110: # QOI_OP_RGB + value = self.fd.read(3) + o8(255) + elif byte == 0b11111111: # QOI_OP_RGBA + value = self.fd.read(4) + else: + op = byte >> 6 + if op == 0: # QOI_OP_INDEX + op_index = byte & 0b00111111 + value = self._previously_seen_pixels.get(op_index, (0, 0, 0, 0)) + elif op == 1: # QOI_OP_DIFF + value = ( + (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) + % 256, + (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2) + % 256, + (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256, + ) + value += (self._previous_pixel[3],) + elif op == 2: # QOI_OP_LUMA + second_byte = self.fd.read(1)[0] + diff_green = (byte & 0b00111111) - 32 + diff_red = ((second_byte & 0b11110000) >> 4) - 8 + diff_blue = (second_byte & 0b00001111) - 8 + + value = tuple( + (self._previous_pixel[i] + diff_green + diff) % 256 + for i, diff in enumerate((diff_red, 0, diff_blue)) + ) + value += (self._previous_pixel[3],) + elif op == 3: # QOI_OP_RUN + run_length = (byte & 0b00111111) + 1 + value = self._previous_pixel + if bands == 3: + value = value[:3] + data += value * run_length + continue + value = b"".join(o8(i) for i in value) + self._add_to_previous_pixels(value) + + if bands == 3: + value = value[:3] + data += value + self.set_as_raw(bytes(data)) + return -1, 0 + + +Image.register_open(QoiImageFile.format, QoiImageFile, _accept) +Image.register_decoder("qoi", QoiDecoder) +Image.register_extension(QoiImageFile.format, ".qoi") diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index d533c55e5..3662ffd15 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -49,12 +49,10 @@ MODES = { ## # Image plugin for SGI images. class SgiImageFile(ImageFile.ImageFile): - format = "SGI" format_description = "SGI Image File Format" def _open(self): - # HEAD headlen = 512 s = self.fp.read(headlen) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 1192c2d73..eac27e679 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -91,7 +91,6 @@ def isSpiderImage(filename): class SpiderImageFile(ImageFile.ImageFile): - format = "SPIDER" format_description = "Spider 2D image" _close_exclusive_fp_after_loading = False @@ -200,6 +199,7 @@ class SpiderImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Image series + # given a list of filenames, return a list of images def loadImageSeries(filelist=None): """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" @@ -289,7 +289,6 @@ Image.register_open(SpiderImageFile.format, SpiderImageFile) Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") sys.exit() diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index c64de4444..6712583d7 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -30,12 +30,10 @@ def _accept(prefix): class SunImageFile(ImageFile.ImageFile): - format = "SUN" format_description = "Sun Raster File" def _open(self): - # The Sun Raster file header is 32 bytes in length # and has the following format: diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 20e8a083f..32928f6af 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -32,7 +32,6 @@ class TarIO(ContainerIO.ContainerIO): self.fh = open(tarfile, "rb") while True: - s = self.fh.read(512) if len(s) != 512: msg = "unexpected end of tar file" diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 53fe6ef5c..67dfc3d3c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -46,12 +46,10 @@ MODES = { class TgaImageFile(ImageFile.ImageFile): - format = "TGA" format_description = "Targa" def _open(self): - # process header s = self.fp.read(18) @@ -174,7 +172,6 @@ SAVE = { def _save(im, fp, filename): - try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 431edfd9b..1ca1b6ea9 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -170,6 +170,8 @@ OPEN_INFO = { (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (II, 1, (1,), 1, (8,), ()): ("L", "L"), (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (2,), 1, (8,), ()): ("L", "L"), + (MM, 1, (2,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), @@ -257,7 +259,7 @@ OPEN_INFO = { (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } -MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys()) +MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) PREFIXES = [ b"MM\x00\x2A", # Valid TIFF header with big-endian byte order @@ -425,6 +427,9 @@ class IFDRational(Rational): __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") + # Python >= 3.11 + if hasattr(Fraction, "__int__"): + __int__ = _delegate("__int__") class ImageFileDirectory_v2(MutableMapping): @@ -722,6 +727,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(1) # Basic type, except for the legacy API. def write_byte(self, data): + if isinstance(data, IFDRational): + data = int(data) if isinstance(data, int): data = bytes((data,)) return data @@ -762,6 +769,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(7) def write_undefined(self, value): + if isinstance(value, int): + value = str(value).encode("ascii", "replace") return value @_register_loader(10, 8) @@ -791,7 +800,6 @@ class ImageFileDirectory_v2(MutableMapping): return ret def load(self, fp): - self.reset() self._offset = fp.tell() @@ -936,7 +944,6 @@ class ImageFileDirectory_v2(MutableMapping): return result def save(self, fp): - if fp.tell() == 0: # skip TIFF header on subsequent pages # tiff header -- PIL always starts the first IFD at offset 8 fp.write(self._prefix + self._pack("HL", 42, 8)) @@ -1057,7 +1064,6 @@ ImageFileDirectory = ImageFileDirectory_v1 class TiffImageFile(ImageFile.ImageFile): - format = "TIFF" format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False @@ -1222,7 +1228,7 @@ class TiffImageFile(ImageFile.ImageFile): # load IFD data from fp before it is closed exif = self.getexif() - for key in TiffTags.TAGS_V2_GROUPS.keys(): + for key in TiffTags.TAGS_V2_GROUPS: if key not in exif: continue exif.get_ifd(key) @@ -1580,7 +1586,6 @@ SAVE_INFO = { def _save(im, fp, filename): - try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: @@ -1629,7 +1634,7 @@ def _save(im, fp, filename): if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() for key in info: - if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys(): + if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: ifd[key] = info.get_ifd(key) else: ifd[key] = info.get(key) @@ -1804,7 +1809,7 @@ def _save(im, fp, filename): # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - if not Image.core.libtiff_support_custom_tags: + if not getattr(Image.core, "libtiff_support_custom_tags", False): continue if tag in ifd.tagtype: @@ -1845,13 +1850,18 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16 * 1024) + errcode, data = e.encode(16 * 1024)[1:] if not _fp: - fp.write(d) - if s: + fp.write(data) + if errcode: break - if s < 0: - msg = f"encoder error {s} when writing image file" + if _fp: + try: + os.close(_fp) + except OSError: + pass + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) else: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 9b5277138..30b05e4e1 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -195,6 +195,7 @@ TAGS_V2 = { 34675: ("ICCProfile", UNDEFINED, 1), 34853: ("GPSInfoIFD", LONG, 1), 36864: ("ExifVersion", UNDEFINED, 1), + 37724: ("ImageSourceData", UNDEFINED, 1), 40965: ("InteroperabilityIFD", LONG, 1), 41730: ("CFAPattern", UNDEFINED, 1), # MPInfo @@ -312,7 +313,7 @@ TAGS = { 34910: "HylaFAX FaxRecvTime", 36864: "ExifVersion", 36867: "DateTimeOriginal", - 36868: "DateTImeDigitized", + 36868: "DateTimeDigitized", 37121: "ComponentsConfiguration", 37122: "CompressedBitsPerPixel", 37724: "ImageSourceData", diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 0dc695a88..e4f47aa04 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -28,7 +28,6 @@ from ._binary import i32le as i32 class WalImageFile(ImageFile.ImageFile): - format = "WAL" format_description = "Quake2 Texture" diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1d074f78c..ce8e05fcb 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -35,7 +35,6 @@ def _accept(prefix): class WebPImageFile(ImageFile.ImageFile): - format = "WEBP" format_description = "WebP image" __loaded = 0 @@ -286,7 +285,7 @@ def _save_all(im, fp, filename): # Append the frame to the animation encoder enc.add( frame.tobytes("raw", rawmode), - timestamp, + round(timestamp), frame.size[0], frame.size[1], rawmode, @@ -306,7 +305,7 @@ def _save_all(im, fp, filename): im.seek(cur_idx) # Force encoder to flush frames - enc.add(None, timestamp, 0, 0, "", lossless, quality, 0) + enc.add(None, round(timestamp), 0, 0, "", lossless, quality, 0) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 639730b8e..0ecab56a8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -75,7 +75,6 @@ def _accept(prefix): class WmfStubImageFile(ImageFile.StubImageFile): - format = "WMF" format_description = "Windows Metafile" @@ -86,7 +85,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): s = self.fp.read(80) if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": - # placeable windows metafile # get units per inch diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index f0e05e867..aa4a01f4e 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -41,12 +41,10 @@ def _accept(prefix): class XVThumbImageFile(ImageFile.ImageFile): - format = "XVThumb" format_description = "XV thumbnail image" def _open(self): - # check magic if not _accept(self.fp.read(6)): msg = "not an XV thumbnail file" diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index ad18e0031..3c12564c9 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class XbmImageFile(ImageFile.ImageFile): - format = "XBM" format_description = "X11 Bitmap" def _open(self): - m = xbm_head.match(self.fp.read(512)) if not m: @@ -69,7 +67,6 @@ class XbmImageFile(ImageFile.ImageFile): def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 5fae4cd68..5d5bdc3ed 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class XpmImageFile(ImageFile.ImageFile): - format = "XPM" format_description = "X11 Pixel Map" def _open(self): - if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) @@ -68,7 +66,6 @@ class XpmImageFile(ImageFile.ImageFile): palette = [b"\0\0\0"] * 256 for _ in range(pal): - s = self.fp.readline() if s[-2:] == b"\r\n": s = s[:-2] @@ -79,9 +76,7 @@ class XpmImageFile(ImageFile.ImageFile): s = s[2:-2].split() for i in range(0, len(s), 2): - if s[i] == b"c": - # process colour key rgb = s[i + 1] if rgb == b"None": @@ -99,7 +94,6 @@ class XpmImageFile(ImageFile.ImageFile): break else: - # missing colour key msg = "cannot read this XPM file" raise ValueError(msg) @@ -110,7 +104,6 @@ class XpmImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] def load_read(self, bytes): - # # load all image data in one chunk diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index e65b155b2..2bb8f6d7f 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -1,11 +1,11 @@ """Pillow (Fork of the Python Imaging Library) -Pillow is the friendly PIL fork by Alex Clark and Contributors. +Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors. https://github.com/python-pillow/Pillow/ Pillow is forked from PIL 1.1.7. -PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +PIL is the Python Imaging Library by Fredrik Lundh and contributors. Copyright (c) 1999 by Secret Labs AB. Use PIL.__version__ for this Pillow version. @@ -31,7 +31,6 @@ _plugins = [ "DdsImagePlugin", "EpsImagePlugin", "FitsImagePlugin", - "FitsStubImagePlugin", "FliImagePlugin", "FpxImagePlugin", "FtexImagePlugin", @@ -59,6 +58,7 @@ _plugins = [ "PngImagePlugin", "PpmImagePlugin", "PsdImagePlugin", + "QoiImagePlugin", "SgiImagePlugin", "SpiderImagePlugin", "SunImagePlugin", @@ -75,6 +75,10 @@ _plugins = [ class UnidentifiedImageError(OSError): """ Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. + + If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` + to true may allow the image to be opened after all. The setting will ignore missing + data and checksum failures. """ pass diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 7c4b1623d..2f2a3df13 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -45,10 +45,10 @@ def deprecate( elif when <= int(__version__.split(".")[0]): msg = f"{deprecated} {is_} deprecated and should be removed." raise RuntimeError(msg) - elif when == 10: - removed = "Pillow 10 (2023-07-01)" + elif when == 11: + removed = "Pillow 11 (2024-10-15)" else: - msg = f"Unknown removal version, update {__name__}?" + msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg) if replacement and action: diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 5cd7e9b1f..597c21b5e 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -4,8 +4,6 @@ import sys import tkinter from tkinter import _tkinter as tk -from ._deprecate import deprecate - try: if hasattr(sys, "pypy_find_executable"): TKINTER_LIB = tk.tklib_cffi.__file__ @@ -17,7 +15,3 @@ except AttributeError: TKINTER_LIB = None tk_version = str(tkinter.TkVersion) -if tk_version == "8.4": - deprecate( - "Support for Tk/Tcl 8.4", 10, action="Please upgrade to Tk/Tcl 8.5 or newer" - ) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 7baa9fb6c..800203d51 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.5.0.dev0" +__version__ = "10.0.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 6f9d99e76..80a16a75e 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -33,7 +33,10 @@ def check_module(feature): try: __import__(module) return True - except ImportError: + except ModuleNotFoundError: + return False + except ImportError as ex: + warnings.warn(str(ex)) return False @@ -145,7 +148,10 @@ def check_feature(feature): try: imported_module = __import__(module, fromlist=["PIL"]) return getattr(imported_module, flag) - except ImportError: + except ModuleNotFoundError: + return None + except ImportError as ex: + warnings.warn(str(ex)) return None diff --git a/src/Tk/_tkmini.h b/src/Tk/_tkmini.h index 9852fc9d6..68247bc47 100644 --- a/src/Tk/_tkmini.h +++ b/src/Tk/_tkmini.h @@ -119,17 +119,7 @@ typedef struct Tk_PhotoImageBlock { } Tk_PhotoImageBlock; /* Typedefs derived from function signatures in Tk header */ -/* Tk_PhotoPutBlock for Tk <= 8.4 */ -typedef void (*Tk_PhotoPutBlock_84_t)( - Tk_PhotoHandle handle, - Tk_PhotoImageBlock *blockPtr, - int x, - int y, - int width, - int height, - int compRule); -/* Tk_PhotoPutBlock for Tk >= 8.5 */ -typedef int (*Tk_PhotoPutBlock_85_t)( +typedef int (*Tk_PhotoPutBlock_t)( Tcl_Interp *interp, Tk_PhotoHandle handle, Tk_PhotoImageBlock *blockPtr, @@ -138,8 +128,6 @@ typedef int (*Tk_PhotoPutBlock_85_t)( int width, int height, int compRule); -/* Tk_PhotoSetSize for Tk <= 8.4 */ -typedef void (*Tk_PhotoSetSize_84_t)(Tk_PhotoHandle handle, int width, int height); /* Tk_FindPhoto */ typedef Tk_PhotoHandle (*Tk_FindPhoto_t)(Tcl_Interp *interp, const char *imageName); /* Tk_PhotoGetImage */ diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 16b9a2edd..bd3cafe95 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -48,14 +48,11 @@ * Global vars for Tcl / Tk functions. We load these symbols from the tkinter * extension module or loaded Tcl / Tk libraries at run-time. */ -static int TK_LT_85 = 0; static Tcl_CreateCommand_t TCL_CREATE_COMMAND; static Tcl_AppendResult_t TCL_APPEND_RESULT; static Tk_FindPhoto_t TK_FIND_PHOTO; static Tk_PhotoGetImage_t TK_PHOTO_GET_IMAGE; -static Tk_PhotoPutBlock_84_t TK_PHOTO_PUT_BLOCK_84; -static Tk_PhotoSetSize_84_t TK_PHOTO_SET_SIZE_84; -static Tk_PhotoPutBlock_85_t TK_PHOTO_PUT_BLOCK_85; +static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; static Imaging ImagingFind(const char *name) { @@ -130,26 +127,15 @@ PyImagingPhotoPut( block.pitch = im->linesize; block.pixelPtr = (unsigned char *)im->block; - if (TK_LT_85) { /* Tk 8.4 */ - TK_PHOTO_PUT_BLOCK_84( - photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); - if (strcmp(im->mode, "RGBA") == 0) { - /* Tk workaround: we need apply ToggleComplexAlphaIfNeeded */ - /* (fixed in Tk 8.5a3) */ - TK_PHOTO_SET_SIZE_84(photo, block.width, block.height); - } - } else { - /* Tk >=8.5 */ - TK_PHOTO_PUT_BLOCK_85( - interp, - photo, - &block, - 0, - 0, - block.width, - block.height, - TK_PHOTO_COMPOSITE_SET); - } + TK_PHOTO_PUT_BLOCK( + interp, + photo, + &block, + 0, + 0, + block.width, + block.height, + TK_PHOTO_COMPOSITE_SET); return TCL_OK; } @@ -290,16 +276,7 @@ get_tk(HMODULE hMod) { if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(hMod, "Tk_FindPhoto")) == NULL) { return -1; }; - TK_LT_85 = GetProcAddress(hMod, "Tk_PhotoPutBlock_Panic") == NULL; - /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ - if (TK_LT_85) { - TK_PHOTO_PUT_BLOCK_84 = (Tk_PhotoPutBlock_84_t)func; - return ((TK_PHOTO_SET_SIZE_84 = - (Tk_PhotoSetSize_84_t)_dfunc(hMod, "Tk_PhotoSetSize")) == NULL) - ? -1 - : 1; - } - TK_PHOTO_PUT_BLOCK_85 = (Tk_PhotoPutBlock_85_t)func; + TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)func; return 1; } @@ -310,7 +287,7 @@ load_tkinter_funcs(void) { * Return 0 for success, non-zero for failure. */ - HMODULE hMods[1024]; + HMODULE* hMods = NULL; HANDLE hProcess; DWORD cbNeeded; unsigned int i; @@ -327,33 +304,48 @@ load_tkinter_funcs(void) { /* Returns pseudo-handle that does not need to be closed */ hProcess = GetCurrentProcess(); + /* Allocate module handlers array */ + if (!EnumProcessModules(hProcess, NULL, 0, &cbNeeded)) { +#if defined(__CYGWIN__) + PyErr_SetString(PyExc_OSError, "Call to EnumProcessModules failed"); +#else + PyErr_SetFromWindowsErr(0); +#endif + return 1; + } + if (!(hMods = (HMODULE*) malloc(cbNeeded))) { + PyErr_NoMemory(); + return 1; + } + /* Iterate through modules in this process looking for Tcl / Tk names */ - if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) { + if (EnumProcessModules(hProcess, hMods, cbNeeded, &cbNeeded)) { for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) { if (!found_tcl) { found_tcl = get_tcl(hMods[i]); if (found_tcl == -1) { - return 1; + break; } } if (!found_tk) { found_tk = get_tk(hMods[i]); if (found_tk == -1) { - return 1; + break; } } if (found_tcl && found_tk) { - return 0; + break; } } } + free(hMods); if (found_tcl == 0) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); - } else { + } else if (found_tk == 0) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); } - return 1; + return (int) ((found_tcl != 1) || (found_tk != 1)); } #else /* not Windows */ @@ -407,18 +399,9 @@ _func_loader(void *lib) { if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(lib, "Tk_FindPhoto")) == NULL) { return 1; } - /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ - TK_LT_85 = (dlsym(lib, "Tk_PhotoPutBlock_Panic") == NULL); - if (TK_LT_85) { - return ( - ((TK_PHOTO_PUT_BLOCK_84 = - (Tk_PhotoPutBlock_84_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL) || - ((TK_PHOTO_SET_SIZE_84 = - (Tk_PhotoSetSize_84_t)_dfunc(lib, "Tk_PhotoSetSize")) == NULL)); - } return ( - (TK_PHOTO_PUT_BLOCK_85 = - (Tk_PhotoPutBlock_85_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); + (TK_PHOTO_PUT_BLOCK = + (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); } int diff --git a/src/_imaging.c b/src/_imaging.c index 05e1370f6..281f3a4d2 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -251,6 +251,10 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { static const char *must_be_sequence = "argument must be a sequence"; static const char *must_be_two_coordinates = "coordinate list must contain exactly 2 coordinates"; +static const char *incorrectly_ordered_x_coordinate = + "x1 must be greater than or equal to x0"; +static const char *incorrectly_ordered_y_coordinate = + "y1 must be greater than or equal to y0"; static const char *wrong_mode = "unrecognized image mode"; static const char *wrong_raw_mode = "unrecognized raw mode"; static const char *outside_image = "image index out of range"; @@ -487,7 +491,7 @@ getink(PyObject *color, Imaging im, char *ink) { int g = 0, b = 0, a = 0; double f = 0; /* Windows 64 bit longs are 32 bits, and 0xFFFFFFFF (white) is a - python long (not int) that raises an overflow error when trying + Python long (not int) that raises an overflow error when trying to return it into a 32 bit C long */ PY_LONG_LONG r = 0; @@ -498,7 +502,8 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - if (PyTuple_Check(color) && PyTuple_GET_SIZE(color) == 1) { + int tupleSize = PyTuple_Check(color) ? PyTuple_GET_SIZE(color) : -1; + if (tupleSize == 1) { color = PyTuple_GetItem(color, 0); } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || @@ -509,15 +514,13 @@ getink(PyObject *color, Imaging im, char *ink) { return NULL; } rIsInt = 1; - } else if (im->type == IMAGING_TYPE_UINT8) { - if (!PyTuple_Check(color)) { - PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); - return NULL; - } - } else { + } else if (im->bands == 1) { PyErr_SetString( PyExc_TypeError, "color must be int or single-element tuple"); return NULL; + } else if (tupleSize == -1) { + PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); + return NULL; } } @@ -527,7 +530,7 @@ getink(PyObject *color, Imaging im, char *ink) { if (im->bands == 1) { /* unsigned integer, single layer */ if (rIsInt != 1) { - if (PyTuple_GET_SIZE(color) != 1) { + if (tupleSize != 1) { PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); return NULL; } else if (!PyArg_ParseTuple(color, "L", &r)) { @@ -537,7 +540,6 @@ getink(PyObject *color, Imaging im, char *ink) { ink[0] = (char)CLIP8(r); ink[1] = ink[2] = ink[3] = 0; } else { - a = 255; if (rIsInt) { /* compatibility: ABGR */ a = (UINT8)(r >> 24); @@ -545,7 +547,7 @@ getink(PyObject *color, Imaging im, char *ink) { g = (UINT8)(r >> 8); r = (UINT8)r; } else { - int tupleSize = PyTuple_GET_SIZE(color); + a = 255; if (im->bands == 2) { if (tupleSize != 1 && tupleSize != 2) { PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements"); @@ -589,6 +591,41 @@ getink(PyObject *color, Imaging im, char *ink) { ink[1] = (UINT8)(r >> 8); ink[2] = ink[3] = 0; return ink; + } else { + if (rIsInt) { + b = (UINT8)(r >> 16); + g = (UINT8)(r >> 8); + r = (UINT8)r; + } else if (tupleSize != 3) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or three elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "Lii", &r, &g, &b)) { + return NULL; + } + if (!strcmp(im->mode, "BGR;15")) { + UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + + ((((UINT16)g) << 2) & 0x03e0) + + ((((UINT16)b) >> 3) & 0x001f); + + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;16")) { + UINT16 v = ((((UINT16)r) << 8) & 0xf800) + + ((((UINT16)g) << 3) & 0x07e0) + + ((((UINT16)b) >> 3) & 0x001f); + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;24")) { + ink[0] = (UINT8)b; + ink[1] = (UINT8)g; + ink[2] = (UINT8)r; + ink[3] = 0; + return ink; + } } } @@ -1245,6 +1282,10 @@ _histogram(ImagingObject *self, PyObject *args) { /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); + if (list == NULL) { + ImagingHistogramDelete(h); + return NULL; + } for (i = 0; i < h->bands * 256; i++) { PyObject *item; item = PyLong_FromLong(h->histogram[i]); @@ -2150,6 +2191,10 @@ _getcolors(ImagingObject *self, PyObject *args) { Py_INCREF(out); } else { out = PyList_New(colors); + if (out == NULL) { + free(items); + return NULL; + } for (i = 0; i < colors; i++) { ImagingColorItem *v = &items[i]; PyObject *item = Py_BuildValue( @@ -2805,6 +2850,16 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawArc( self->image->image, @@ -2886,6 +2941,16 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawChord( self->image->image, @@ -2932,6 +2997,16 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawEllipse( self->image->image, @@ -3101,6 +3176,16 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawPieslice( self->image->image, @@ -3197,6 +3282,16 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawRectangle( self->image->image, @@ -3756,6 +3851,7 @@ static PyTypeObject PixelAccess_Type = { static PyObject * _get_stats(PyObject *self, PyObject *args) { PyObject *d; + PyObject *v; ImagingMemoryArena arena = &ImagingDefaultArena; if (!PyArg_ParseTuple(args, ":get_stats")) { @@ -3766,15 +3862,29 @@ _get_stats(PyObject *self, PyObject *args) { if (!d) { return NULL; } - PyDict_SetItemString(d, "new_count", PyLong_FromLong(arena->stats_new_count)); - PyDict_SetItemString( - d, "allocated_blocks", PyLong_FromLong(arena->stats_allocated_blocks)); - PyDict_SetItemString( - d, "reused_blocks", PyLong_FromLong(arena->stats_reused_blocks)); - PyDict_SetItemString( - d, "reallocated_blocks", PyLong_FromLong(arena->stats_reallocated_blocks)); - PyDict_SetItemString(d, "freed_blocks", PyLong_FromLong(arena->stats_freed_blocks)); - PyDict_SetItemString(d, "blocks_cached", PyLong_FromLong(arena->blocks_cached)); + v = PyLong_FromLong(arena->stats_new_count); + PyDict_SetItemString(d, "new_count", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_allocated_blocks); + PyDict_SetItemString(d, "allocated_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_reused_blocks); + PyDict_SetItemString(d, "reused_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_reallocated_blocks); + PyDict_SetItemString(d, "reallocated_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->stats_freed_blocks); + PyDict_SetItemString(d, "freed_blocks", v ? v : Py_None); + Py_XDECREF(v); + + v = PyLong_FromLong(arena->blocks_cached); + PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); + Py_XDECREF(v); return d; } @@ -3984,8 +4094,6 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args); extern PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args); -extern PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_DrawWmf(PyObject *self, PyObject *args); @@ -4069,7 +4177,6 @@ static PyMethodDef functions[] = { {"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 @@ -4146,28 +4253,33 @@ setup_module(PyObject *m) { #ifdef HAVE_LIBJPEG { extern const char *ImagingJpegVersion(void); - PyDict_SetItemString( - d, "jpeglib_version", PyUnicode_FromString(ImagingJpegVersion())); + PyObject *v = PyUnicode_FromString(ImagingJpegVersion()); + PyDict_SetItemString(d, "jpeglib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif #ifdef HAVE_OPENJPEG { extern const char *ImagingJpeg2KVersion(void); - PyDict_SetItemString( - d, "jp2klib_version", PyUnicode_FromString(ImagingJpeg2KVersion())); + PyObject *v = PyUnicode_FromString(ImagingJpeg2KVersion()); + PyDict_SetItemString(d, "jp2klib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif PyObject *have_libjpegturbo; #ifdef LIBJPEG_TURBO_VERSION have_libjpegturbo = Py_True; + { #define tostr1(a) #a #define tostr(a) tostr1(a) - PyDict_SetItemString( - d, "libjpeg_turbo_version", PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION))); + PyObject *v = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); + PyDict_SetItemString(d, "libjpeg_turbo_version", v ? v : Py_None); + Py_XDECREF(v); #undef tostr #undef tostr1 + } #else have_libjpegturbo = Py_False; #endif @@ -4179,8 +4291,9 @@ setup_module(PyObject *m) { have_libimagequant = Py_True; { extern const char *ImagingImageQuantVersion(void); - PyDict_SetItemString( - d, "imagequant_version", PyUnicode_FromString(ImagingImageQuantVersion())); + PyObject *v = PyUnicode_FromString(ImagingImageQuantVersion()); + PyDict_SetItemString(d, "imagequant_version", v ? v : Py_None); + Py_XDECREF(v); } #else have_libimagequant = Py_False; @@ -4197,16 +4310,18 @@ setup_module(PyObject *m) { PyModule_AddIntConstant(m, "FIXED", Z_FIXED); { extern const char *ImagingZipVersion(void); - PyDict_SetItemString( - d, "zlib_version", PyUnicode_FromString(ImagingZipVersion())); + PyObject *v = PyUnicode_FromString(ImagingZipVersion()); + PyDict_SetItemString(d, "zlib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); - PyDict_SetItemString( - d, "libtiff_version", PyUnicode_FromString(ImagingTiffVersion())); + PyObject *v = PyUnicode_FromString(ImagingTiffVersion()); + PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None); + Py_XDECREF(v); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 PyObject *support_custom_tags; @@ -4229,7 +4344,9 @@ setup_module(PyObject *m) { Py_INCREF(have_xcb); PyModule_AddObject(m, "HAVE_XCB", have_xcb); - PyDict_SetItemString(d, "PILLOW_VERSION", PyUnicode_FromString(version)); + PyObject *pillow_version = PyUnicode_FromString(version); + PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); + Py_XDECREF(pillow_version); return 0; } @@ -4249,6 +4366,7 @@ PyInit__imaging(void) { m = PyModule_Create(&module_def); if (setup_module(m) < 0) { + Py_DECREF(m); return NULL; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 9b5a121d7..ddfe6ad64 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -116,7 +116,7 @@ cms_profile_open(PyObject *self, PyObject *args) { } static PyObject * -cms_profile_fromstring(PyObject *self, PyObject *args) { +cms_profile_frombytes(PyObject *self, PyObject *args) { cmsHPROFILE hProfile; char *pProfile; @@ -950,6 +950,8 @@ _is_intent_supported(CmsProfileObject *self, int clut) { return Py_None; } PyDict_SetItem(result, id, entry); + Py_DECREF(id); + Py_DECREF(entry); } return result; } @@ -960,8 +962,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { static PyMethodDef pyCMSdll_methods[] = { {"profile_open", cms_profile_open, METH_VARARGS}, - {"profile_frombytes", cms_profile_fromstring, METH_VARARGS}, - {"profile_fromstring", cms_profile_fromstring, METH_VARARGS}, + {"profile_frombytes", cms_profile_frombytes, METH_VARARGS}, {"profile_tobytes", cms_profile_tobytes, METH_VARARGS}, /* profile and transform functions */ @@ -1532,7 +1533,8 @@ setup_module(PyObject *m) { } else { v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); } - PyDict_SetItemString(d, "littlecms_version", v); + PyDict_SetItemString(d, "littlecms_version", v ? v : Py_None); + Py_XDECREF(v); return 0; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 0db17a5a6..78e3f7f10 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -33,12 +33,6 @@ #include FT_COLOR_H #endif -#define KEEP_PY_UNICODE - -#if !defined(FT_LOAD_TARGET_MONO) -#define FT_LOAD_TARGET_MONO FT_LOAD_MONOCHROME -#endif - /* -------------------------------------------------------------------- */ /* error table */ @@ -122,7 +116,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { int error = 0; char *filename = NULL; - Py_ssize_t size; + float size; + FT_Size_RequestRec req; + FT_Long width; Py_ssize_t index = 0; Py_ssize_t layout_engine = 0; unsigned char *encoding; @@ -139,7 +135,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { if (!PyArg_ParseTupleAndKeywords( args, kw, - "etn|nsy#n", + "etf|nsy#n", kwlist, Py_FileSystemDefaultEncoding, &filename, @@ -185,7 +181,13 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { } if (!error) { - error = FT_Set_Pixel_Sizes(self->face, 0, size); + width = size * 64; + req.type = FT_SIZE_REQUEST_TYPE_NOMINAL; + req.width = width; + req.height = width; + req.horiResolution = 0; + req.vertResolution = 0; + error = FT_Request_Size(self->face, &req); } if (!error && encoding && strlen((char *)encoding) == 4) { @@ -420,11 +422,9 @@ text_layout_fallback( if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif for (i = 0; font_getchar(string, i, &ch); i++) { (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); @@ -581,11 +581,9 @@ font_getsize(FontObject *self, PyObject *args) { if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif /* * text bounds are given by: @@ -844,11 +842,9 @@ font_render(FontObject *self, PyObject *args) { if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif /* * calculate x_min and y_max @@ -958,13 +954,11 @@ font_render(FontObject *self, PyObject *args) { /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ case FT_PIXEL_MODE_GRAY: break; -#ifdef FT_LOAD_COLOR case FT_PIXEL_MODE_BGRA: if (color) { break; } /* we didn't ask for color, fall through to default */ -#endif default: PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; @@ -995,7 +989,6 @@ font_render(FontObject *self, PyObject *args) { } else { target = im->image8[yy] + xx; } -#ifdef FT_LOAD_COLOR if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { /* paste color glyph */ for (k = x0; k < x1; k++) { @@ -1010,9 +1003,7 @@ font_render(FontObject *self, PyObject *args) { target[k * 4 + 3] = source[k * 4 + 3]; } } - } else -#endif - if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { if (color) { unsigned char *ink = (unsigned char *)&foreground_ink; for (k = x0; k < x1; k++) { @@ -1082,11 +1073,17 @@ font_getvarnames(FontObject *self) { num_namedstyles = master->num_namedstyles; list_names = PyList_New(num_namedstyles); + if (list_names == NULL) { + FT_Done_MM_Var(library, master); + return NULL; + } name_count = FT_Get_Sfnt_Name_Count(self->face); for (i = 0; i < name_count; i++) { error = FT_Get_Sfnt_Name(self->face, i, &name); if (error) { + Py_DECREF(list_names); + FT_Done_MM_Var(library, master); return geterror(error); } @@ -1125,25 +1122,44 @@ font_getvaraxes(FontObject *self) { name_count = FT_Get_Sfnt_Name_Count(self->face); list_axes = PyList_New(num_axis); + if (list_axes == NULL) { + FT_Done_MM_Var(library, master); + return NULL; + } for (i = 0; i < num_axis; i++) { axis = master->axis[i]; list_axis = PyDict_New(); - PyDict_SetItemString( - list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536)); - PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536)); - PyDict_SetItemString( - list_axis, "maximum", PyLong_FromLong(axis.maximum / 65536)); + if (list_axis == NULL) { + Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); + return NULL; + } + PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); + PyDict_SetItemString(list_axis, "minimum", minimum ? minimum : Py_None); + Py_XDECREF(minimum); + + PyObject *def = PyLong_FromLong(axis.def / 65536); + PyDict_SetItemString(list_axis, "default", def ? def : Py_None); + Py_XDECREF(def); + + PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); + PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); + Py_XDECREF(maximum); for (j = 0; j < name_count; j++) { error = FT_Get_Sfnt_Name(self->face, j, &name); if (error) { + Py_DECREF(list_axis); + Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); return geterror(error); } if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString(list_axis, "name", axis_name); + PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None); + Py_XDECREF(axis_name); break; } } @@ -1358,7 +1374,8 @@ setup_module(PyObject *m) { FT_Library_Version(library, &major, &minor, &patch); v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); - PyDict_SetItemString(d, "freetype2_version", v); + PyDict_SetItemString(d, "freetype2_version", v ? v : Py_None); + Py_XDECREF(v); #ifdef HAVE_RAQM #if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) @@ -1376,35 +1393,34 @@ setup_module(PyObject *m) { PyDict_SetItemString(d, "HAVE_RAQM", v); PyDict_SetItemString(d, "HAVE_FRIBIDI", v); PyDict_SetItemString(d, "HAVE_HARFBUZZ", v); + Py_DECREF(v); if (have_raqm) { + v = NULL; #ifdef RAQM_VERSION_MAJOR v = PyUnicode_FromString(raqm_version_string()); -#else - v = Py_None; #endif - PyDict_SetItemString(d, "raqm_version", v); + PyDict_SetItemString(d, "raqm_version", v ? v : Py_None); + Py_XDECREF(v); + v = NULL; #ifdef FRIBIDI_MAJOR_VERSION { const char *a = strchr(fribidi_version_info, ')'); const char *b = strchr(fribidi_version_info, '\n'); if (a && b && a + 2 < b) { v = PyUnicode_FromStringAndSize(a + 2, b - (a + 2)); - } else { - v = Py_None; } } -#else - v = Py_None; #endif - PyDict_SetItemString(d, "fribidi_version", v); + PyDict_SetItemString(d, "fribidi_version", v ? v : Py_None); + Py_XDECREF(v); + v = NULL; #ifdef HB_VERSION_STRING v = PyUnicode_FromString(hb_version_string()); -#else - v = Py_None; #endif - PyDict_SetItemString(d, "harfbuzz_version", v); + PyDict_SetItemString(d, "harfbuzz_version", v ? v : Py_None); + Py_XDECREF(v); } return 0; diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c0644b616..8815c2b7e 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -136,13 +136,18 @@ match(PyObject *self, PyObject *args) { int row_idx, col_idx; UINT8 **inrows; PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } if (!PyBytes_Check(py_lut)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); return NULL; } @@ -150,6 +155,7 @@ match(PyObject *self, PyObject *args) { lut_len = PyBytes_Size(py_lut); if (lut_len < LUT_SIZE) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } @@ -158,6 +164,7 @@ match(PyObject *self, PyObject *args) { imgin = (Imaging)i0; if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } @@ -194,6 +201,7 @@ match(PyObject *self, PyObject *args) { if (lut[lut_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); } } } @@ -213,10 +221,13 @@ get_on_pixels(PyObject *self, PyObject *args) { int row_idx, col_idx; int width, height; PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } if (!PyArg_ParseTuple(args, "n", &i0)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; } img = (Imaging)i0; @@ -230,21 +241,13 @@ get_on_pixels(PyObject *self, PyObject *args) { if (row[col_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); } } } return ret; } -static int -setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); - - PyDict_SetItemString(d, "__version", PyUnicode_FromString("0.1")); - - return 0; -} - static PyMethodDef functions[] = { /* Functions */ {"apply", (PyCFunction)apply, METH_VARARGS, NULL}, @@ -266,9 +269,5 @@ PyInit__imagingmorph(void) { m = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - return NULL; - } - return m; } diff --git a/src/_imagingtk.c b/src/_imagingtk.c index b9273b0b8..efa7fc1b6 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -58,5 +58,9 @@ PyInit__imagingtk(void) { }; PyObject *m; m = PyModule_Create(&module_def); - return (load_tkinter_funcs() == 0) ? m : NULL; + if (load_tkinter_funcs() != 0) { + Py_DECREF(m); + return NULL; + } + return m; } diff --git a/src/_webp.c b/src/_webp.c index 493e0709c..fe63027fb 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -949,20 +949,14 @@ addAnimFlagToModule(PyObject *m) { void addTransparencyFlagToModule(PyObject *m) { - PyModule_AddObject( - m, "HAVE_TRANSPARENCY", PyBool_FromLong(!WebPDecoderBuggyAlpha())); + PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); + if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { + Py_DECREF(have_transparency); + } } static int setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); - addMuxFlagToModule(m); - addAnimFlagToModule(m); - addTransparencyFlagToModule(m); - - PyDict_SetItemString( - d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); - #ifdef HAVE_WEBPANIM /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || @@ -970,6 +964,15 @@ setup_module(PyObject *m) { return -1; } #endif + PyObject *d = PyModule_GetDict(m); + addMuxFlagToModule(m); + addAnimFlagToModule(m); + addTransparencyFlagToModule(m); + + PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); + PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); + Py_XDECREF(v); + return 0; } @@ -987,6 +990,7 @@ PyInit__webp(void) { m = PyModule_Create(&module_def); if (setup_module(m) < 0) { + Py_DECREF(m); return NULL; } diff --git a/src/decode.c b/src/decode.c index 7a9b956c5..7e3fadc04 100644 --- a/src/decode.c +++ b/src/decode.c @@ -116,12 +116,11 @@ _dealloc(ImagingDecoderObject *decoder) { static PyObject * _decode(ImagingDecoderObject *decoder, PyObject *args) { - UINT8 *buffer; - Py_ssize_t bufsize; + Py_buffer buffer; int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) { + if (!PyArg_ParseTuple(args, "y*", &buffer)) { return NULL; } @@ -129,12 +128,13 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionEnter(&cookie); } - status = decoder->decode(decoder->im, &decoder->state, buffer, bufsize); + status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len); if (!decoder->pulls_fd) { ImagingSectionLeave(&cookie); } + PyBuffer_Release(&buffer); return Py_BuildValue("ii", status, decoder->state.errcode); } diff --git a/src/display.c b/src/display.c index 0ce10e249..e8e7b62c2 100644 --- a/src/display.c +++ b/src/display.c @@ -195,20 +195,21 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) { static PyObject * _frombytes(ImagingDisplayObject *display, PyObject *args) { - char *ptr; - Py_ssize_t bytes; + Py_buffer buffer; - if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) { + if (!PyArg_ParseTuple(args, "y*:frombytes", &buffer)) { return NULL; } - if (display->dib->ysize * display->dib->linesize != bytes) { + if (display->dib->ysize * display->dib->linesize != buffer.len) { + PyBuffer_Release(&buffer); PyErr_SetString(PyExc_ValueError, "wrong size"); return NULL; } - memcpy(display->dib->bits, ptr, bytes); + memcpy(display->dib->bits, buffer.buf, buffer.len); + PyBuffer_Release(&buffer); Py_INCREF(Py_None); return Py_None; } @@ -421,79 +422,6 @@ error: return NULL; } -static BOOL CALLBACK -list_windows_callback(HWND hwnd, LPARAM lParam) { - PyObject *window_list = (PyObject *)lParam; - PyObject *item; - PyObject *title; - RECT inner, outer; - int title_size; - int status; - - /* get window title */ - title_size = GetWindowTextLength(hwnd); - if (title_size > 0) { - title = PyUnicode_FromStringAndSize(NULL, title_size); - if (title) { - GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size + 1); - } - } else { - title = PyUnicode_FromString(""); - } - if (!title) { - return 0; - } - - /* get bounding boxes */ - GetClientRect(hwnd, &inner); - GetWindowRect(hwnd, &outer); - - item = Py_BuildValue( - F_HANDLE "N(iiii)(iiii)", - hwnd, - title, - inner.left, - inner.top, - inner.right, - inner.bottom, - outer.left, - outer.top, - outer.right, - outer.bottom); - if (!item) { - return 0; - } - - status = PyList_Append(window_list, item); - - Py_DECREF(item); - - if (status < 0) { - return 0; - } - - return 1; -} - -PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args) { - PyObject *window_list; - - window_list = PyList_New(0); - if (!window_list) { - return NULL; - } - - EnumWindows(list_windows_callback, (LPARAM)window_list); - - if (PyErr_Occurred()) { - Py_DECREF(window_list); - return NULL; - } - - return window_list; -} - /* -------------------------------------------------------------------- */ /* Windows clipboard grabber */ diff --git a/src/encode.c b/src/encode.c index 21c42d915..308bd2059 100644 --- a/src/encode.c +++ b/src/encode.c @@ -904,7 +904,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value)); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)); + &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value)); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (INT16)PyLong_AsLong(value)); @@ -1214,10 +1214,13 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; + char *comment; + Py_ssize_t comment_size; + int plt = 0; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbn", + "ss|OOOsOnOOOssbbnz#p", &mode, &format, &offset, @@ -1233,7 +1236,10 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &cinema_mode, &mct, &sgnd, - &fd)) { + &fd, + &comment, + &comment_size, + &plt)) { return NULL; } @@ -1315,6 +1321,26 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } } + if (comment && comment_size > 0) { + /* Size is stored as as an uint16, subtract 4 bytes for the header */ + if (comment_size >= 65532) { + PyErr_SetString( + PyExc_ValueError, + "JPEG 2000 comment is too long"); + Py_DECREF(encoder); + return NULL; + } + + char *p = malloc(comment_size + 1); + if (!p) { + Py_DECREF(encoder); + return ImagingError_MemoryError(); + } + memcpy(p, comment, comment_size); + p[comment_size] = '\0'; + context->comment = p; + } + if (quality_layers && PySequence_Check(quality_layers)) { context->quality_is_in_db = strcmp(quality_mode, "dB") == 0; context->quality_layers = quality_layers; @@ -1332,6 +1358,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->cinema_mode = cine_mode; context->mct = mct; context->sgnd = sgnd; + context->plt = plt; return (PyObject *)encoder; } diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 83860c38a..f00939da0 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -13,7 +13,7 @@ /* use make_hash.py from the pillow-scripts repository to calculate these values */ #define ACCESS_TABLE_SIZE 27 -#define ACCESS_TABLE_HASH 3078 +#define ACCESS_TABLE_HASH 33051 static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; @@ -92,6 +92,12 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } +static void +get_pixel_16(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x + x]; + memcpy(color, in, sizeof(UINT16)); +} + static void get_pixel_32(Imaging im, int x, int y, void *color) { memcpy(color, &im->image32[y][x], sizeof(INT32)); @@ -186,6 +192,7 @@ ImagingAccessInit() { ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); ADD("I;16B", get_pixel_16B, put_pixel_16B); + ADD("I;16N", get_pixel_16, put_pixel_16L); ADD("I;32L", get_pixel_32L, put_pixel_32L); ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 2e45a3358..5afe7cf50 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -237,6 +237,9 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { if (n < 1) { return ImagingError_ValueError("number of passes must be greater than zero"); } + if (radius < 0) { + return ImagingError_ValueError("radius must be >= 0"); + } if (strcmp(imIn->mode, imOut->mode) || imIn->type != imOut->type || imIn->bands != imOut->bands || imIn->xsize != imOut->xsize || diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index b03bd02af..7fe24a639 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -990,6 +990,13 @@ static struct { {"I;16L", "L", I16L_L}, {"L", "I;16B", L_I16B}, {"I;16B", "L", I16B_L}, +#ifdef WORDS_BIGENDIAN + {"L", "I;16N", L_I16B}, + {"I;16N", "L", I16B_L}, +#else + {"L", "I;16N", L_I16L}, + {"I;16N", "L", I16L_L}, +#endif {"I;16", "F", I16L_F}, {"I;16L", "F", I16L_F}, diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 77343e583..82f290bd0 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -85,25 +85,22 @@ point32(Imaging im, int x, int y, int ink) { static inline void point32rgba(Imaging im, int x, int y, int ink) { - unsigned int tmp1; + unsigned int tmp; if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { UINT8 *out = (UINT8 *)im->image[y] + x * 4; UINT8 *in = (UINT8 *)&ink; - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); } } static inline void hline8(Imaging im, int x0, int y0, int x1, int ink) { - int tmp, pixelwidth; + int pixelwidth; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -126,13 +123,9 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; INT32 *p; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -152,13 +145,9 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; - unsigned int tmp1; + unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -173,9 +162,9 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); x0++; out += 4; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index fab3b4948..4b8d2bf05 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -37,6 +37,17 @@ clip8(float in) { return (UINT8)in; } +static inline INT32 +clip32(float in) { + if (in <= 0.0) { + return 0; + } + if (in >= pow(2, 31) - 1) { + return pow(2, 31) - 1; + } + return (INT32)in; +} + Imaging ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { Imaging imOut; @@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[2]) + (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ + _i2f(in0[x + d]) * (kernel)[2]) int x = 0, y = 0; @@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 1; y < im->ysize - 1; y++) { - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 1; y < im->ysize - 1; y++) { + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - for (x = 1; x < im->xsize - 1; x++) { - float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip32(ss); + } + out[x] = in0[x]; + } + } else { + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip8(ss); + } + out[x] = in0[x]; } - out[x] = in0[x]; } } else { // Add one time for rounding @@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { void ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x5(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \ - _i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[3] + \ - _i2f((UINT8)in0[x + d + d]) * (kernel)[4]) + (_i2f(in0[x - d - d]) * (kernel)[0] + \ + _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \ + _i2f(in0[x + d]) * (kernel)[3] + \ + _i2f(in0[x + d + d]) * (kernel)[4]) int x = 0, y = 0; @@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 2; y < im->ysize - 2; y++) { - UINT8 *in_2 = (UINT8 *)im->image[y - 2]; - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *in2 = (UINT8 *)im->image[y + 2]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 2; y < im->ysize - 2; y++) { + INT32 *in_2 = (INT32 *)im->image[y - 2]; + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *in2 = (INT32 *)im->image[y + 2]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - out[1] = in0[1]; - for (x = 2; x < im->xsize - 2; x++) { - float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip32(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; + } + } else { + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; } } else { // Add one time for rounding @@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; ImagingSectionCookie cookie; - if (!im || im->type != IMAGING_TYPE_UINT8) { + if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { return (Imaging)ImagingError_ModeError(); } diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index b28a0440a..e8d92f7b6 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -97,6 +97,12 @@ typedef struct { /* PRIVATE CONTEXT (set by decoder) */ const char *error_msg; + /* Custom comment */ + char *comment; + + /* Include PLT marker segment */ + int plt; + } JPEG2KENCODESTATE; /* diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index db1c5c0c9..0d7e896b7 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -281,7 +281,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { int ret = -1; unsigned prec = 8; - unsigned bpp = 8; unsigned _overflow_scale_factor; stream = opj_stream_create(BUFFER_SIZE, OPJ_FALSE); @@ -313,7 +312,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_i16; prec = 16; - bpp = 12; } else if (strcmp(im->mode, "LA") == 0) { components = 2; color_space = OPJ_CLRSPC_GRAY; @@ -342,7 +340,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { image_params[n].h = im->ysize; image_params[n].x0 = image_params[n].y0 = 0; image_params[n].prec = prec; - image_params[n].bpp = bpp; image_params[n].sgnd = context->sgnd == 0 ? 0 : 1; } @@ -439,6 +436,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { params.tcp_mct = context->mct; } + if (context->comment) { + params.cp_comment = context->comment; + } + params.prog_order = context->progression; params.cp_cinema = context->cinema_mode; @@ -487,11 +488,23 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } + if (strcmp(im->mode, "RGBA") == 0) { + image->comps[3].alpha = 1; + } + opj_set_error_handler(codec, j2k_error, context); opj_set_info_handler(codec, j2k_warn, context); opj_set_warning_handler(codec, j2k_warn, context); opj_setup_encoder(codec, ¶ms, image); + /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ +#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) + if (context->plt) { + const char *plt_option[2] = {"PLT=YES", NULL}; + opj_encoder_set_extra_options(codec, plt_option); + } +#endif + /* Start encoding */ if (!opj_start_compress(codec, image, stream)) { state->errcode = IMAGING_CODEC_BROKEN; @@ -624,7 +637,12 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { free((void *)context->error_msg); } + if (context->comment) { + free((void *)context->comment); + } + context->error_msg = NULL; + context->comment = NULL; return -1; } diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 01760e742..14c8f1461 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -664,6 +664,7 @@ static struct { #endif {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, packI16N_I16}, {"I;16B", "I;16N", 16, packI16N_I16B}, diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 783852c24..02a4a5c76 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -341,7 +341,10 @@ splitlists( PixelList *l, *r, *c, *n; int i; - int nRight, nLeft; + int nRight; +#ifndef NO_OUTPUT + int nLeft; +#endif int splitColourVal; #ifdef TEST_SPLIT @@ -396,12 +399,17 @@ splitlists( } #endif nCount[0] = nCount[1] = 0; - nLeft = nRight = 0; + nRight = 0; +#ifndef NO_OUTPUT + nLeft = 0; +#endif for (left = 0, c = h[axis]; c;) { left = left + c->count; nCount[0] += c->count; c->flag = 0; +#ifndef NO_OUTPUT nLeft++; +#endif c = c->next[axis]; if (left * 2 > pixelCount) { break; @@ -414,7 +422,9 @@ splitlists( break; } c->flag = 0; +#ifndef NO_OUTPUT nLeft++; +#endif nCount[0] += c->count; } } @@ -430,7 +440,9 @@ splitlists( } c->flag = 1; nRight++; +#ifndef NO_OUTPUT nLeft--; +#endif nCount[0] -= c->count; nCount[1] += c->count; } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 76750aaf7..7cf00ef35 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -131,7 +131,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ /* 15-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; @@ -139,7 +139,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;16") == 0) { /* EXPERIMENTAL */ /* 16-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; @@ -147,19 +147,11 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;24") == 0) { /* EXPERIMENTAL */ /* 24-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 3; im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; - } else if (strcmp(mode, "BGR;32") == 0) { - /* EXPERIMENTAL */ - /* 32-bit reversed true colour */ - im->bands = 1; - im->pixelsize = 4; - im->linesize = (xsize * 4 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index e426ed74f..a0fa22c7d 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1149,6 +1149,16 @@ unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { } } static void +unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + out[0] = in[1]; + out[1] = in[0]; + in += 2; + out += 2; + } +} +static void unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) { int i; for (i = 0; i < pixels; i++) { @@ -1762,7 +1772,9 @@ static struct { {"I;16", "I;16", 16, copy2}, {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, + {"I;16", "I;16B", 16, unpackI16B_I16}, {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16B", "I;16N", 16, unpackI16N_I16B}, diff --git a/src/path.c b/src/path.c index 3e3431575..e17580fa2 100644 --- a/src/path.c +++ b/src/path.c @@ -439,6 +439,9 @@ path_tolist(PyPathObject *self, PyObject *args) { if (flat) { list = PyList_New(self->count * 2); + if (list == NULL) { + return NULL; + } for (i = 0; i < self->count * 2; i++) { PyObject *item; item = PyFloat_FromDouble(self->xy[i]); @@ -449,6 +452,9 @@ path_tolist(PyPathObject *self, PyObject *args) { } } else { list = PyList_New(self->count); + if (list == NULL) { + return NULL; + } for (i = 0; i < self->count; i++) { PyObject *item; item = Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING index c605a5dc6..97e2489b7 100644 --- a/src/thirdparty/raqm/COPYING +++ b/src/thirdparty/raqm/COPYING @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright © 2015 Information Technology Authority (ITA) -Copyright © 2016-2022 Khaled Hosny +Copyright © 2016-2023 Khaled Hosny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS index ae1128485..e8bf32e0b 100644 --- a/src/thirdparty/raqm/NEWS +++ b/src/thirdparty/raqm/NEWS @@ -1,3 +1,38 @@ +Overview of changes leading to 0.10.1 +Wednesday, April 12, 2023 +==================================== + +Make combining marks always inherit the script of their base. + +Overview of changes leading to 0.10.0 +Wednesday, January 11, 2023 +==================================== + +Fix font feature ranges. + +Fix resolved direction for all-neutral text. + +Implement letter and word spacing support. + +New API: + * raqm_set_text_utf16 + +Overview of changes leading to 0.9.0 +Sunday, January 30, 2022 +==================================== + +Raise the minimum versions of Raqm dependencies: no longer conditionally +enabling any features based on specific dependency version. + +raqm_t objects can now be reused by calling raqm_clear_contents() before +re-use, to potentially reduce the number memory allocations. + +Don't hardcode python3 in tests. + +New API: + * raqm_set_freetype_load_flags_range + * raqm_clear_contents + Overview of changes leading to 0.8.0 Monday, December 13, 2021 ==================================== diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md index 3354a4d25..ab729cdc0 100644 --- a/src/thirdparty/raqm/README.md +++ b/src/thirdparty/raqm/README.md @@ -11,7 +11,7 @@ It currently provides bidirectional text support (using [FriBiDi][1] or As a result, Raqm can support most writing systems covered by Unicode. The documentation can be accessed on the web at: -> http://host-oman.github.io/libraqm/ +> https://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”. diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index 78b70a561..62d2d2064 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -32,10 +32,10 @@ #define _RAQM_VERSION_H_ #define RAQM_VERSION_MAJOR 0 -#define RAQM_VERSION_MINOR 9 -#define RAQM_VERSION_MICRO 0 +#define RAQM_VERSION_MINOR 10 +#define RAQM_VERSION_MICRO 1 -#define RAQM_VERSION_STRING "0.9.0" +#define RAQM_VERSION_STRING "0.10.1" #define RAQM_VERSION_ATLEAST(major,minor,micro) \ ((major)*10000+(minor)*100+(micro) <= \ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 13f6e1f02..2b331e1af 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -1,6 +1,6 @@ /* * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2022 Khaled Hosny + * Copyright © 2016-2023 Khaled Hosny * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -171,19 +171,23 @@ typedef FriBidiLevel _raqm_bidi_level_t; #endif -typedef struct { +typedef struct +{ FT_Face ftface; int ftloadflags; hb_language_t lang; hb_script_t script; + int spacing_after; } _raqm_text_info; typedef struct _raqm_run raqm_run_t; -struct _raqm { +struct _raqm +{ int ref_count; uint32_t *text; + uint16_t *text_utf16; char *text_utf8; size_t text_len; size_t text_capacity_bytes; @@ -205,7 +209,8 @@ struct _raqm { int invisible_glyph; }; -struct _raqm_run { +struct _raqm_run +{ uint32_t pos; uint32_t len; @@ -217,9 +222,13 @@ struct _raqm_run { raqm_run_t *next; }; -static uint32_t -_raqm_u8_to_u32_index (raqm_t *rq, - uint32_t index); +static size_t +_raqm_encoding_to_u32_index (raqm_t *rq, + size_t index); + +static bool +_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, + hb_codepoint_t r_char); static void _raqm_init_text_info (raqm_t *rq) @@ -231,6 +240,7 @@ _raqm_init_text_info (raqm_t *rq) rq->text_info[i].ftloadflags = -1; rq->text_info[i].lang = default_lang; rq->text_info[i].script = HB_SCRIPT_INVALID; + rq->text_info[i].spacing_after = 0; } } @@ -263,6 +273,8 @@ _raqm_compare_text_info (_raqm_text_info a, if (a.script != b.script) return false; + /* Spacing shouldn't break runs, so we don't compare them here. */ + return true; } @@ -273,6 +285,7 @@ _raqm_free_text(raqm_t* rq) rq->text = NULL; rq->text_info = NULL; rq->text_utf8 = NULL; + rq->text_utf16 = NULL; rq->text_len = 0; rq->text_capacity_bytes = 0; } @@ -280,12 +293,15 @@ _raqm_free_text(raqm_t* rq) static bool _raqm_alloc_text(raqm_t *rq, size_t len, - bool need_utf8) + bool need_utf8, + bool need_utf16) { /* Allocate contiguous memory block for texts and text_info */ size_t mem_size = (sizeof (uint32_t) + sizeof (_raqm_text_info)) * len; if (need_utf8) mem_size += sizeof (char) * len; + else if (need_utf16) + mem_size += sizeof (uint16_t) * len; if (mem_size > rq->text_capacity_bytes) { @@ -302,6 +318,7 @@ _raqm_alloc_text(raqm_t *rq, rq->text_info = (_raqm_text_info*)(rq->text + len); rq->text_utf8 = need_utf8 ? (char*)(rq->text_info + len) : NULL; + rq->text_utf16 = need_utf16 ? (uint16_t*)(rq->text_info + len) : NULL; return true; } @@ -357,7 +374,7 @@ _raqm_free_runs (raqm_run_t *runs) * Return value: * A newly allocated #raqm_t with a reference count of 1. The initial reference * count should be released with raqm_destroy() when you are done using the - * #raqm_t. Returns %NULL in case of error. + * #raqm_t. Returns `NULL` in case of error. * * Since: 0.1 */ @@ -381,6 +398,7 @@ raqm_create (void) rq->invisible_glyph = 0; rq->text = NULL; + rq->text_utf16 = NULL; rq->text_utf8 = NULL; rq->text_info = NULL; rq->text_capacity_bytes = 0; @@ -498,7 +516,7 @@ raqm_clear_contents (raqm_t *rq) * separately can give improper output. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -518,7 +536,7 @@ raqm_set_text (raqm_t *rq, if (!len) return true; - if (!_raqm_alloc_text(rq, len, false)) + if (!_raqm_alloc_text(rq, len, false, false)) return false; rq->text_len = len; @@ -575,6 +593,53 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) return (out_utf32 - unicode); } +static void * +_raqm_get_utf16_codepoint (const void *str, + uint32_t *out_codepoint) +{ + const uint16_t *s = (const uint16_t *)str; + + if (s[0] > 0xD800 && s[0] < 0xDBFF) + { + if (s[1] > 0xDC00 && s[1] < 0xDFFF) + { + uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1)); + uint32_t W = (s[0] >> 6) & ((1 << 5) - 1); + *out_codepoint = (W+1) << 16 | X; + s += 2; + } + else + { + /* A single high surrogate, this is an error. */ + *out_codepoint = s[0]; + s += 1; + } + } + else + { + *out_codepoint = s[0]; + s += 1; + } + return (void *)s; +} + +static size_t +_raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode) +{ + size_t in_len = 0; + uint32_t *out_utf32 = unicode; + const uint16_t *in_utf16 = text; + + while ((*in_utf16 != '\0') && (in_len < len)) + { + in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); + ++out_utf32; + ++in_len; + } + + return (out_utf32 - unicode); +} + /** * raqm_set_text_utf8: * @rq: a #raqm_t. @@ -584,7 +649,7 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) * Same as raqm_set_text(), but for text encoded in UTF-8 encoding. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -604,7 +669,7 @@ raqm_set_text_utf8 (raqm_t *rq, if (!len) return true; - if (!_raqm_alloc_text(rq, len, true)) + if (!_raqm_alloc_text(rq, len, true, false)) return false; rq->text_len = _raqm_u8_to_u32 (text, len, rq->text); @@ -614,6 +679,44 @@ raqm_set_text_utf8 (raqm_t *rq, return true; } +/** + * raqm_set_text_utf16: + * @rq: a #raqm_t. + * @text: a UTF-16 encoded text string. + * @len: the length of @text in UTF-16 shorts. + * + * Same as raqm_set_text(), but for text encoded in UTF-16 encoding. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_text_utf16 (raqm_t *rq, + const uint16_t *text, + size_t len) +{ + if (!rq || !text) + return false; + + /* Call raqm_clear_contents to reuse this raqm_t */ + if (rq->text_len) + return false; + + /* Empty string, don’t fail but do nothing */ + if (!len) + return true; + + if (!_raqm_alloc_text(rq, len, false, true)) + return false; + + rq->text_len = _raqm_u16_to_u32 (text, len, rq->text); + memcpy (rq->text_utf16, text, sizeof (uint16_t) * len); + _raqm_init_text_info (rq); + + return true; +} /** * raqm_set_par_direction: * @rq: a #raqm_t. @@ -640,7 +743,7 @@ raqm_set_text_utf8 (raqm_t *rq, * text. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -673,7 +776,7 @@ raqm_set_par_direction (raqm_t *rq, * parts of the text. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Stability: * Unstable @@ -687,7 +790,7 @@ raqm_set_language (raqm_t *rq, size_t len) { hb_language_t language; - size_t end = start + len; + size_t end; if (!rq) return false; @@ -695,11 +798,8 @@ raqm_set_language (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) - { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); - } + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); if (start >= rq->text_len || end > rq->text_len) return false; @@ -716,11 +816,37 @@ raqm_set_language (raqm_t *rq, return true; } +static bool +_raqm_add_font_feature (raqm_t *rq, + hb_feature_t fea) +{ + void* new_features; + + if (!rq) + return false; + + new_features = realloc (rq->features, + sizeof (hb_feature_t) * (rq->features_len + 1)); + if (!new_features) + return false; + + if (fea.start != HB_FEATURE_GLOBAL_START) + fea.start = _raqm_encoding_to_u32_index (rq, fea.start); + if (fea.end != HB_FEATURE_GLOBAL_END) + fea.end = _raqm_encoding_to_u32_index (rq, fea.end); + + rq->features = new_features; + rq->features[rq->features_len] = fea; + rq->features_len++; + + return true; +} + /** * raqm_add_font_feature: * @rq: a #raqm_t. * @feature: (transfer none): a font feature string. - * @len: length of @feature, -1 for %NULL-terminated. + * @len: length of @feature, -1 for `NULL`-terminated. * * Adds a font feature to be used by the #raqm_t during text layout. This is * usually used to turn on optional font features that are not enabled by @@ -734,7 +860,7 @@ raqm_set_language (raqm_t *rq, * end of the features list and can potentially override previous features. * * Return value: - * %true if parsing @feature succeeded, %false otherwise. + * `true` if parsing @feature succeeded, `false` otherwise. * * Since: 0.1 */ @@ -751,16 +877,7 @@ raqm_add_font_feature (raqm_t *rq, ok = hb_feature_from_string (feature, len, &fea); if (ok) - { - void* new_features = realloc (rq->features, - sizeof (hb_feature_t) * (rq->features_len + 1)); - if (!new_features) - return false; - - rq->features = new_features; - rq->features[rq->features_len] = fea; - rq->features_len++; - } + _raqm_add_font_feature (rq, fea); return ok; } @@ -817,7 +934,7 @@ _raqm_set_freetype_face (raqm_t *rq, * See also raqm_set_freetype_face_range(). * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -832,21 +949,23 @@ raqm_set_freetype_face (raqm_t *rq, * raqm_set_freetype_face_range: * @rq: a #raqm_t. * @face: an #FT_Face. - * @start: index of first character that should use @face. - * @len: number of characters using @face. + * @start: index of first character that should use @face from the input string. + * @len: number of elements using @face. * * Sets an #FT_Face to be used for @len-number of characters staring at @start. - * The @start and @len are input string array indices (i.e. counting bytes in - * UTF-8 and scaler values in UTF-32). + * The @start and @len are input string array indices, counting elements + * according to the underlying encoding. @start must always be aligned to the + * start of an encoded codepoint, and @len must always end at a codepoint's + * final element. * * This method can be used repeatedly to set different faces for different * parts of the text. It is the responsibility of the client to make sure that - * face ranges cover the whole text. + * face ranges cover the whole text, and is properly aligned. * * See also raqm_set_freetype_face(). * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -856,7 +975,7 @@ raqm_set_freetype_face_range (raqm_t *rq, size_t start, size_t len) { - size_t end = start + len; + size_t end; if (!rq) return false; @@ -864,11 +983,8 @@ raqm_set_freetype_face_range (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) - { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); - } + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); return _raqm_set_freetype_face (rq, face, start, end); } @@ -909,7 +1025,7 @@ _raqm_set_freetype_load_flags (raqm_t *rq, * older version the flags will be ignored. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.3 */ @@ -943,7 +1059,7 @@ raqm_set_freetype_load_flags (raqm_t *rq, * See also raqm_set_freetype_load_flags(). * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.9 */ @@ -953,7 +1069,7 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, size_t start, size_t len) { - size_t end = start + len; + size_t end; if (!rq) return false; @@ -961,15 +1077,161 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) - { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); - } + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); return _raqm_set_freetype_load_flags (rq, flags, start, end); } +static bool +_raqm_set_spacing (raqm_t *rq, + int spacing, + bool word_spacing, + size_t start, + size_t end) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + for (size_t i = start; i < end; i++) + { + bool set_spacing = i == 0; + if (!set_spacing) + set_spacing = _raqm_allowed_grapheme_boundary (rq->text[i-1], rq->text[i]); + + if (set_spacing) + { + if (word_spacing) + { + if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1])) + { + /* CSS word seperators, word spacing is only applied on these.*/ + if (rq->text[i] == 0x0020 || /* Space */ + rq->text[i] == 0x00A0 || /* No Break Space */ + rq->text[i] == 0x1361 || /* Ethiopic Word Space */ + rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */ + rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */ + rq->text[i] == 0x1039F || /* Ugaric Word Divider */ + rq->text[i] == 0x1091F) /* Phoenician Word Separator */ + { + rq->text_info[i].spacing_after = spacing; + } + } + } + else + { + rq->text_info[i].spacing_after = spacing; + } + } + } + + return true; +} + +/** + * raqm_set_letter_spacing_range: + * @rq: a #raqm_t. + * @spacing: amount of spacing in Freetype Font Units (26.6 format). + * @start: index of first character that should use @spacing. + * @len: number of characters using @spacing. + * + * Set the letter spacing or tracking for a given range, the value + * will be added onto the advance and offset for RTL, and the advance for + * other directions. Letter spacing will be applied between characters, so + * the last character will not have spacing applied after it. + * Note that not all scripts have a letter-spacing tradition, + * for example, Arabic does not, while Devanagari does. + * + * This will also add “disable `liga`, `clig`, `hlig`, `dlig`, and `calt`” font + * features to the internal features list, so call this function after setting + * the font features for best spacing results. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_letter_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = start + len - 1; + + if (spacing != 0) + { +#define NUM_TAGS 5 + static char *tags[NUM_TAGS] = { "clig", "liga", "hlig", "dlig", "calt" }; + for (size_t i = 0; i < NUM_TAGS; i++) + { + hb_feature_t fea = { hb_tag_from_string(tags[i], 5), 0, start, end }; + _raqm_add_font_feature (rq, fea); + } +#undef NUM_TAGS + } + + start = _raqm_encoding_to_u32_index (rq, start); + end = _raqm_encoding_to_u32_index (rq, end); + + return _raqm_set_spacing (rq, spacing, false, start, end); +} + +/** + * raqm_set_word_spacing_range: + * @rq: a #raqm_t. + * @spacing: amount of spacing in Freetype Font Units (26.6 format). + * @start: index of first character that should use @spacing. + * @len: number of characters using @spacing. + * + * Set the word spacing for a given range. Word spacing will only be applied to + * 'word separator' characters, such as 'space', 'no break space' and + * Ethiopic word separator'. + * The value will be added onto the advance and offset for RTL, and the advance + * for other directions. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_word_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); + + return _raqm_set_spacing (rq, spacing, true, start, end); +} + /** * raqm_set_invisible_glyph: * @rq: a #raqm_t. @@ -984,7 +1246,7 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, * If @gid is a positive number, it will be used for invisible glyphs. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.6 */ @@ -1014,7 +1276,7 @@ _raqm_shape (raqm_t *rq); * text shaping, and any other part of the layout process. * * Return value: - * %true if the layout process was successful, %false otherwise. + * `true` if the layout process was successful, `false` otherwise. * * Since: 0.1 */ @@ -1048,7 +1310,9 @@ raqm_layout (raqm_t *rq) static uint32_t _raqm_u32_to_u8_index (raqm_t *rq, uint32_t index); - +static uint32_t +_raqm_u32_to_u16_index (raqm_t *rq, + uint32_t index); /** * raqm_get_glyphs: * @rq: a #raqm_t. @@ -1059,7 +1323,7 @@ _raqm_u32_to_u8_index (raqm_t *rq, * information. * * Return value: (transfer none): - * An array of #raqm_glyph_t, or %NULL in case of error. This is owned by @rq + * An array of #raqm_glyph_t, or `NULL` in case of error. This is owned by @rq * and must not be freed. * * Since: 0.1 @@ -1147,6 +1411,12 @@ raqm_get_glyphs (raqm_t *rq, RAQM_TEST ("\n"); #endif } + else if (rq->text_utf16) + { + for (size_t i = 0; i < count; i++) + rq->glyphs[i].cluster = _raqm_u32_to_u16_index (rq, + rq->glyphs[i].cluster); + } return rq->glyphs; } @@ -1162,7 +1432,7 @@ raqm_get_glyphs (raqm_t *rq, * * Since: 0.8 */ -RAQM_API raqm_direction_t +raqm_direction_t raqm_get_par_resolved_direction (raqm_t *rq) { if (!rq) @@ -1185,7 +1455,7 @@ raqm_get_par_resolved_direction (raqm_t *rq) * * Since: 0.8 */ -RAQM_API raqm_direction_t +raqm_direction_t raqm_get_direction_at_index (raqm_t *rq, size_t index) { @@ -1194,8 +1464,10 @@ raqm_get_direction_at_index (raqm_t *rq, for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) { - if (run->pos <= index && index < run->pos + run->len) { - switch (run->direction) { + if (run->pos <= index && index < run->pos + run->len) + { + switch (run->direction) + { case HB_DIRECTION_LTR: return RAQM_DIRECTION_LTR; case HB_DIRECTION_RTL: @@ -1227,7 +1499,8 @@ _raqm_hb_dir (raqm_t *rq, _raqm_bidi_level_t level) return dir; } -typedef struct { +typedef struct +{ size_t pos; size_t len; _raqm_bidi_level_t level; @@ -1264,10 +1537,10 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count) line = SBParagraphCreateLine (par, 0, par_len); *run_count = SBLineGetRunCount (line); - if (SBParagraphGetBaseLevel (par) == 0) - rq->resolved_dir = RAQM_DIRECTION_LTR; - else + if (SBParagraphGetBaseLevel (par) == 1) rq->resolved_dir = RAQM_DIRECTION_RTL; + else + rq->resolved_dir = RAQM_DIRECTION_LTR; runs = malloc (sizeof (_raqm_bidi_run) * (*run_count)); if (runs) @@ -1418,10 +1691,10 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count) rq->text_len, &par_type, levels); - if (par_type == FRIBIDI_PAR_LTR) - rq->resolved_dir = RAQM_DIRECTION_LTR; - else + if (par_type == FRIBIDI_PAR_RTL) rq->resolved_dir = RAQM_DIRECTION_RTL; + else + rq->resolved_dir = RAQM_DIRECTION_LTR; if (max_level == 0) goto done; @@ -1447,22 +1720,15 @@ _raqm_itemize (raqm_t *rq) bool ok = true; #ifdef RAQM_TESTING - switch (rq->base_dir) - { - case RAQM_DIRECTION_RTL: - RAQM_TEST ("Direction is: RTL\n\n"); - break; - case RAQM_DIRECTION_LTR: - RAQM_TEST ("Direction is: LTR\n\n"); - break; - case RAQM_DIRECTION_TTB: - RAQM_TEST ("Direction is: TTB\n\n"); - break; - case RAQM_DIRECTION_DEFAULT: - default: - RAQM_TEST ("Direction is: DEFAULT\n\n"); - break; - } + static char *dir_names[] = { + "DEFAULT", + "RTL", + "LTR", + "TTB" + }; + + assert (rq->base_dir < sizeof (dir_names)); + RAQM_TEST ("Direction is: %s\n\n", dir_names[rq->base_dir]); #endif if (!_raqm_resolve_scripts (rq)) @@ -1483,9 +1749,9 @@ _raqm_itemize (raqm_t *rq) runs->len = rq->text_len; runs->level = 0; } - } else { - runs = _raqm_bidi_itemize (rq, &run_count); } + else + runs = _raqm_bidi_itemize (rq, &run_count); if (!runs) { @@ -1494,6 +1760,9 @@ _raqm_itemize (raqm_t *rq) } #ifdef RAQM_TESTING + assert (rq->resolved_dir < sizeof (dir_names)); + if (rq->base_dir == RAQM_DIRECTION_DEFAULT) + RAQM_TEST ("Resolved direction is: %s\n\n", dir_names[rq->resolved_dir]); RAQM_TEST ("Number of runs before script itemization: %zu\n\n", run_count); RAQM_TEST ("BiDi Runs:\n"); @@ -1617,7 +1886,8 @@ done: } /* Stack to handle script detection */ -typedef struct { +typedef struct +{ size_t capacity; size_t size; int *pair_index; @@ -1751,6 +2021,22 @@ _get_pair_index (const uint32_t ch) #define STACK_IS_EMPTY(script) ((script)->size <= 0) #define IS_OPEN(pair_index) (((pair_index) & 1) == 0) +static hb_script_t +_raqm_unicode_script (hb_codepoint_t u) +{ + static hb_unicode_funcs_t* unicode_funcs; + + unicode_funcs = hb_unicode_funcs_get_default (); + + /* Make combining marks inherit the script of their bases, regardless of + * their own script. + */ + if (hb_unicode_general_category (unicode_funcs, u) == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) + return HB_SCRIPT_INHERITED; + + return hb_unicode_script (unicode_funcs, u); +} + /* Resolve the script for each character in the input string, if the character * script is common or inherited it takes the script of the character before it * except paired characters which we try to make them use the same script. We @@ -1763,10 +2049,9 @@ _raqm_resolve_scripts (raqm_t *rq) int last_set_index = -1; hb_script_t last_script = HB_SCRIPT_INVALID; _raqm_stack_t *stack = NULL; - hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default (); for (size_t i = 0; i < rq->text_len; ++i) - rq->text_info[i].script = hb_unicode_script (unicode_funcs, rq->text[i]); + rq->text_info[i].script = _raqm_unicode_script (rq->text[i]); #ifdef RAQM_TESTING RAQM_TEST ("Before script detection:\n"); @@ -1910,15 +2195,47 @@ _raqm_shape (raqm_t *rq) { FT_Matrix matrix; + hb_glyph_info_t *info; 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); + info = hb_buffer_get_glyph_infos (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); + + bool set_spacing = false; + if (run->direction == HB_DIRECTION_RTL) + { + set_spacing = i == 0; + if (!set_spacing) + set_spacing = info[i].cluster != info[i-1].cluster; + } + else + { + set_spacing = i == len - 1; + if (!set_spacing) + set_spacing = info[i].cluster != info[i+1].cluster; + } + + _raqm_text_info rq_info = rq->text_info[info[i].cluster]; + + if (rq_info.spacing_after != 0 && set_spacing) + { + if (run->direction == HB_DIRECTION_TTB) + pos[i].y_advance -= rq_info.spacing_after; + else if (run->direction == HB_DIRECTION_RTL) + { + pos[i].x_advance += rq_info.spacing_after; + pos[i].x_offset += rq_info.spacing_after; + } + else + pos[i].x_advance += rq_info.spacing_after; + } } } } @@ -1954,9 +2271,9 @@ _raqm_u32_to_u8_index (raqm_t *rq, } /* Convert index from UTF-8 to UTF-32 */ -static uint32_t +static size_t _raqm_u8_to_u32_index (raqm_t *rq, - uint32_t index) + size_t index) { const unsigned char *s = (const unsigned char *) rq->text_utf8; const unsigned char *t = s; @@ -1982,9 +2299,64 @@ _raqm_u8_to_u32_index (raqm_t *rq, return length; } -static bool -_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, - hb_codepoint_t r_char); +/* Count equivalent UTF-16 short in codepoint */ +static size_t +_raqm_count_codepoint_utf16_short (uint32_t chr) +{ + if (chr > 0x010000) + return 2; + else + return 1; +} + +/* Convert index from UTF-32 to UTF-16 */ +static uint32_t +_raqm_u32_to_u16_index (raqm_t *rq, + uint32_t index) +{ + size_t length = 0; + + for (uint32_t i = 0; i < index; ++i) + length += _raqm_count_codepoint_utf16_short (rq->text[i]); + + return length; +} + +/* Convert index from UTF-16 to UTF-32 */ +static size_t +_raqm_u16_to_u32_index (raqm_t *rq, + size_t index) +{ + const uint16_t *s = (const uint16_t *) rq->text_utf16; + const uint16_t *t = s; + size_t length = 0; + + while (((size_t) (s - t) < index) && ('\0' != *s)) + { + if (*s < 0xD800 || *s > 0xDBFF) + s += 1; + else + s += 2; + + length++; + } + + if ((size_t) (s-t) > index) + length--; + + return length; +} + +static inline size_t +_raqm_encoding_to_u32_index (raqm_t *rq, + size_t index) +{ + if (rq->text_utf8) + return _raqm_u8_to_u32_index (rq, index); + else if (rq->text_utf16) + return _raqm_u16_to_u32_index (rq, index); + return index; +} static bool _raqm_in_hangul_syllable (hb_codepoint_t ch); @@ -2001,7 +2373,7 @@ _raqm_in_hangul_syllable (hb_codepoint_t ch); * character is left-to-right, then the cursor will be at the right of it. * * Return value: - * %true if the process was successful, %false otherwise. + * `true` if the process was successful, `false` otherwise. * * Since: 0.2 */ @@ -2018,8 +2390,7 @@ raqm_index_to_position (raqm_t *rq, if (rq == NULL) return false; - if (rq->text_utf8) - *index = _raqm_u8_to_u32_index (rq, *index); + *index = _raqm_encoding_to_u32_index (rq, *index); if (*index >= rq->text_len) return false; @@ -2077,6 +2448,8 @@ raqm_index_to_position (raqm_t *rq, found: if (rq->text_utf8) *index = _raqm_u32_to_u8_index (rq, *index); + else if (rq->text_utf16) + *index = _raqm_u32_to_u16_index (rq, *index); RAQM_TEST ("The position is %d at index %zu\n",*x ,*index); return true; } @@ -2093,7 +2466,7 @@ found: * @index. * * Return value: - * %true if the process was successful, %false in case of error. + * `true` if the process was successful, `false` in case of error. * * Since: 0.2 */ @@ -2371,8 +2744,8 @@ raqm_version_string (void) * Checks if library version is less than or equal the specified version. * * Return value: - * %true if library version is less than or equal the specfied version, %false - * otherwise. + * `true` if library version is less than or equal the specified version, + * `false` otherwise. * * Since: 0.7 **/ @@ -2393,8 +2766,8 @@ raqm_version_atleast (unsigned int major, * Checks if library version is less than or equal the specified version. * * Return value: - * %true if library version is less than or equal the specfied version, %false - * otherwise. + * `true` if library version is less than or equal the specified version, + * `false` otherwise. * * Since: 0.7 **/ diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h index bdb5a50d8..6fd6089c7 100644 --- a/src/thirdparty/raqm/raqm.h +++ b/src/thirdparty/raqm/raqm.h @@ -1,6 +1,6 @@ /* * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2022 Khaled Hosny + * Copyright © 2016-2023 Khaled Hosny * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -118,6 +118,10 @@ RAQM_API bool raqm_set_text_utf8 (raqm_t *rq, const char *text, size_t len); +RAQM_API bool +raqm_set_text_utf16 (raqm_t *rq, + const uint16_t *text, + size_t len); RAQM_API bool raqm_set_par_direction (raqm_t *rq, @@ -154,6 +158,17 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, size_t start, size_t len); +RAQM_API bool +raqm_set_letter_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len); +RAQM_API bool +raqm_set_word_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len); + RAQM_API bool raqm_set_invisible_glyph (raqm_t *rq, int gid); diff --git a/tox.ini b/tox.ini index 9a41ca96b..458a00107 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,9 @@ [tox] -envlist = +requires = + tox>=4.2 +env_list = lint - py{py3, 311, 310, 39, 38, 37} -minversion = 1.9 + py{py3, 311, 310, 39, 38} [testenv] deps = @@ -15,15 +16,16 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -allowlist_externals = make +allowlist_externals = + make [testenv:lint] -passenv = - PRE_COMMIT_COLOR skip_install = true deps = check-manifest pre-commit +pass_env = + PRE_COMMIT_COLOR commands = pre-commit run --all-files --show-diff-on-failure check-manifest diff --git a/winbuild/README.md b/winbuild/README.md index d8538fbf3..2975acf28 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -10,7 +10,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. -* Requires CMake 3.12 or newer (available as Visual Studio component). +* Requires CMake 3.15 or newer (available as Visual Studio component). * Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). @@ -18,7 +18,7 @@ The following is a simplified version of the script used on AppVeyor: ``` set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild -C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends +C:\Python39\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends build\build_dep_all.cmd build\build_pillow.cmd install cd .. diff --git a/winbuild/build.rst b/winbuild/build.rst index 716669771..99dfad301 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -21,10 +21,13 @@ Download and install: `_ (MSVC C++ build tools, and any Windows SDK version required) -* `CMake 3.12 or newer `_ +* `CMake 3.15 or newer `_ (also available as Visual Studio component C++ CMake tools for Windows) -* x86/x64: `NASM `_ +* `Ninja `_ + (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) + +* x86/x64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -35,41 +38,50 @@ Visual Studio is found automatically with ``vswhere.exe``. Build configuration ------------------- -The following environment variables, if set, will override the default -behaviour of ``build_prepare.py``: +Run ``build_prepare.py`` to configure the build:: -* ``PYTHON`` + ``EXECUTABLE`` point to the target version of Python. - If ``PYTHON`` is unset, the version of Python used to run - ``build_prepare.py`` will be used. If only ``PYTHON`` is set, - ``EXECUTABLE`` defaults to ``python.exe``. -* ``ARCHITECTURE`` is used to select a ``x86``, ``x64`` or ``ARM64`` build. - By default, uses same architecture as the version of Python used to run ``build_prepare.py``. -* ``PILLOW_BUILD`` can be used to override the ``winbuild\build`` directory - path, used to store generated build scripts and compiled libraries. - **Warning:** This directory is wiped when ``build_prepare.py`` is run. -* ``PILLOW_DEPS`` points to the directory used to store downloaded - dependencies. By default ``winbuild\depends`` is used. + usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] + [--depends PILLOW_DEPS] + [--architecture {x86,x64,ARM64}] + [--python PYTHON] [--executable EXECUTABLE] + [--nmake] [--no-imagequant] [--no-fribidi] -``build_prepare.py`` also supports the following command line parameters: + Download dependencies and generate build scripts for Pillow. -* ``-v`` will print generated scripts. -* ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency -* ``--no-fribidi`` or ``--no-raqm`` will skip optional LGPL-licensed dependency FriBiDi - (required for Raqm text shaping). -* ``--python=`` and ``--executable=`` override ``PYTHON`` and ``EXECUTABLE``. -* ``--architecture=`` overrides ``ARCHITECTURE``. -* ``--dir=`` and ``--depends=`` override ``PILLOW_BUILD`` - and ``PILLOW_DEPS``. + options: + -h, --help show this help message and exit + -v, --verbose print generated scripts + -d PILLOW_BUILD, --dir PILLOW_BUILD, --build-dir PILLOW_BUILD + build directory (default: 'winbuild\build') + --depends PILLOW_DEPS + directory used to store cached dependencies (default: + 'winbuild\depends') + --architecture {x86,x64,ARM64} + build architecture (default: same as host Python) + --python PYTHON Python install directory (default: use host Python) + --executable EXECUTABLE + Python executable (default: use host Python) + --nmake build dependencies using NMake instead of Ninja + --no-imagequant skip GPL-licensed optional dependency libimagequant + --no-fribidi, --no-raqm + skip LGPL-licensed optional dependency FriBiDi + + Arguments can also be supplied using the environment variables PILLOW_BUILD, + PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. See winbuild\build.rst for more + information. + +**Warning:** The build directory is wiped when ``build_prepare.py`` is run. Dependencies ------------ Dependencies will be automatically downloaded by ``build_prepare.py``. By default, downloaded dependencies are stored in ``winbuild\depends``; -set the ``PILLOW_DEPS`` environment variable to override this location. +use the ``--depends`` argument or ``PILLOW_DEPS`` environment variable +to override this location. To build all dependencies, run ``winbuild\build\build_dep_all.cmd``, -or run the individual scripts to build each dependency separately. +or run the individual scripts in order to build each dependency separately. Building Pillow --------------- @@ -96,13 +108,11 @@ directory. Example ------- -The following is a simplified version of the script used on AppVeyor: - -.. code-block:: +The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild - C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends + C:\Python39\bin\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd build\build_pillow.cmd install cd .. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f5050946c..9b5fc5d18 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,3 +1,4 @@ +import argparse import os import platform import re @@ -39,7 +40,7 @@ def cmd_rmdir(path): def cmd_nmake(makefile=None, target="", params=None): if params is None: params = "" - elif isinstance(params, list) or isinstance(params, tuple): + elif isinstance(params, (list, tuple)): params = " ".join(params) else: params = str(params) @@ -55,24 +56,28 @@ def cmd_nmake(makefile=None, target="", params=None): ) -def cmd_cmake(params=None, file="."): - if params is None: - params = "" - elif isinstance(params, list) or isinstance(params, tuple): - params = " ".join(params) - else: - params = str(params) - return " ".join( - [ - "{cmake}", - "-DCMAKE_VERBOSE_MAKEFILE=ON", - "-DCMAKE_RULE_MESSAGES:BOOL=OFF", - "-DCMAKE_BUILD_TYPE=Release", - f"{params}", - '-G "NMake Makefiles"', - f'"{file}"', - ] - ) +def cmds_cmake(target, *params): + if not isinstance(target, str): + target = " ".join(target) + + return [ + " ".join( + [ + "{cmake}", + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake + "-DCMAKE_C_COMPILER=cl.exe", # for Ninja + "-DCMAKE_CXX_COMPILER=cl.exe", # for Ninja + "-DCMAKE_C_FLAGS=-nologo", + "-DCMAKE_CXX_FLAGS=-nologo", + *params, + '-G "{cmake_generator}"', + ".", + ] + ), + f"{{cmake}} --build . --clean-first --parallel --target {target}", + ] def cmd_msbuild( @@ -109,28 +114,23 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download", - "filename": "libjpeg-turbo-2.1.4.tar.gz", - "dir": "libjpeg-turbo-2.1.4", + + "/libjpeg-turbo/files/2.1.5.1/libjpeg-turbo-2.1.5.1.tar.gz/download", + "filename": "libjpeg-turbo-2.1.5.1.tar.gz", + "dir": "libjpeg-turbo-2.1.5.1", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" ".+(libjpeg-turbo Licenses\n======================\n\n.+)$" ), "build": [ - cmd_cmake( - [ - "-DENABLE_SHARED:BOOL=FALSE", - "-DWITH_JPEG8:BOOL=TRUE", - "-DWITH_CRT_DLL:BOOL=TRUE", - ] + *cmds_cmake( + ("jpeg-static", "cjpeg-static", "djpeg-static"), + "-DENABLE_SHARED:BOOL=FALSE", + "-DWITH_JPEG8:BOOL=TRUE", + "-DWITH_CRT_DLL:BOOL=TRUE", ), - cmd_nmake(target="clean"), - cmd_nmake(target="jpeg-static"), cmd_copy("jpeg-static.lib", "libjpeg.lib"), - cmd_nmake(target="cjpeg-static"), cmd_copy("cjpeg-static.exe", "cjpeg.exe"), - cmd_nmake(target="djpeg-static"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], "headers": ["j*.h"], @@ -152,34 +152,22 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.0.tar.gz/download", - "filename": "xz-5.4.0.tar.gz", - "dir": "xz-5.4.0", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.2.tar.gz/download", + "filename": "xz-5.4.2.tar.gz", + "dir": "xz-5.4.2", "license": "COPYING", - "patch": { - r"src\liblzma\api\lzma.h": { - "#ifndef LZMA_API_IMPORT": "#ifndef LZMA_API_IMPORT\n#define LZMA_API_STATIC", # noqa: E501 - }, - r"windows\vs2019\liblzma.vcxproj": { - # retarget to default toolset (selected by vcvarsall.bat) - "v142": "$(DefaultPlatformToolset)", # noqa: E501 - # retarget to latest (selected by vcvarsall.bat) - "10.0": "$(WindowsSDKVersion)", # noqa: E501 - }, - }, "build": [ - cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Clean"), - cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Build"), + *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], "headers": [r"src\liblzma\api\lzma.h"], - "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"], + "libs": [r"liblzma.lib"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz", - "filename": "libwebp-1.2.4.tar.gz", - "dir": "libwebp-1.2.4", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.0.tar.gz", + "filename": "libwebp-1.3.0.tar.gz", + "dir": "libwebp-1.3.0", "license": "COPYING", "build": [ cmd_rmdir(r"output\release-static"), # clean @@ -215,9 +203,11 @@ deps = { }, }, "build": [ - cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), - cmd_nmake(target="clean"), - cmd_nmake(target="tiff"), + *cmds_cmake( + "tiff", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', + ) ], "headers": [r"libtiff\tiff*.h"], "libs": [r"libtiff\*.lib"], @@ -229,10 +219,7 @@ deps = { "dir": "lpng1639", "license": "LICENSE", "build": [ - # lint: do not inline - cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), - cmd_nmake(target="clean"), - cmd_nmake(), + *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), cmd_copy("libpng16_static.lib", "libpng16.lib"), ], "headers": [r"png*.h"], @@ -244,18 +231,15 @@ deps = { "dir": "brotli-1.0.9", "license": "LICENSE", "build": [ - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="brotlicommon-static"), - cmd_nmake(target="brotlidec-static"), + *cmds_cmake(("brotlicommon-static", "brotlidec-static")), cmd_xcopy(r"c\include", "{inc_dir}"), ], "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501 - "filename": "freetype-2.12.1.tar.gz", - "dir": "freetype-2.12.1", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.0.tar.gz", # noqa: E501 + "filename": "freetype-2.13.0.tar.gz", + "dir": "freetype-2.13.0", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -289,9 +273,9 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download", - "filename": "lcms2-2.14.tar.gz", - "dir": "lcms2-2.14", + "url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download", + "filename": "lcms2-2.15.tar.gz", + "dir": "lcms2-2.15", "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -325,9 +309,9 @@ deps = { } }, "build": [ - cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), - cmd_nmake(target="clean"), - cmd_nmake(target="openjp2"), + *cmds_cmake( + "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" + ), cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"), ], @@ -346,25 +330,23 @@ deps = { } }, "build": [ - # lint: do not inline - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="imagequant_a"), + *cmds_cmake("imagequant_a"), cmd_copy("imagequant_a.lib", "imagequant.lib"), ], "headers": [r"*.h"], "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip", - "filename": "harfbuzz-6.0.0.zip", - "dir": "harfbuzz-6.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.2.0.zip", + "filename": "harfbuzz-7.2.0.zip", + "dir": "harfbuzz-7.2.0", "license": "COPYING", "build": [ - cmd_set("CXXFLAGS", "-d2FH4-"), - cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), - cmd_nmake(target="clean"), - cmd_nmake(target="harfbuzz"), + *cmds_cmake( + "harfbuzz", + "-DHB_HAVE_FREETYPE:BOOL=TRUE", + '-DCMAKE_CXX_FLAGS="-nologo -d2FH4-"', + ), ], "headers": [r"src\*.h"], "libs": [r"*.lib"], @@ -377,9 +359,7 @@ deps = { "build": [ cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="fribidi"), + *cmds_cmake("fribidi"), ], "bins": [r"*.dll"], }, @@ -455,7 +435,7 @@ def extract_dep(url, filename): import urllib.request import zipfile - file = os.path.join(depends_dir, filename) + file = os.path.join(args.depends_dir, filename) if not os.path.exists(file): ex = None for i in range(3): @@ -496,12 +476,12 @@ def extract_dep(url, filename): def write_script(name, lines): - name = os.path.join(build_dir, name) + name = os.path.join(args.build_dir, name) lines = [line.format(**prefs) for line in lines] print("Writing " + name) with open(name, "w", newline="") as f: f.write(os.linesep.join(lines)) - if verbose: + if args.verbose: for line in lines: print(" " + line) @@ -570,11 +550,14 @@ def build_dep(name): def build_dep_all(): lines = ["@echo on"] for dep_name in deps: + print() if dep_name in disabled: + print(f"Skipping disabled dependency {dep_name}") continue script = build_dep(dep_name) lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + print() lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines) @@ -593,56 +576,91 @@ def build_pillow(): if __name__ == "__main__": - # winbuild directory winbuild_dir = os.path.dirname(os.path.realpath(__file__)) + pillow_dir = os.path.realpath(os.path.join(winbuild_dir, "..")) - verbose = False - disabled = [] - depends_dir = os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")) - python_dir = os.environ.get("PYTHON") - python_exe = os.environ.get("EXECUTABLE", "python.exe") - architecture = os.environ.get( - "ARCHITECTURE", - "ARM64" - if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64"), + parser = argparse.ArgumentParser( + prog="winbuild\\build_prepare.py", + description="Download dependencies and generate build scripts for Pillow.", + epilog="""Arguments can also be supplied using the environment variables + PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. + See winbuild\\build.rst for more information.""", ) - build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")) - sources_dir = "" - for arg in sys.argv[1:]: - if arg == "-v": - verbose = True - elif arg == "--no-imagequant": - disabled += ["libimagequant"] - elif arg == "--no-raqm" or arg == "--no-fribidi": - disabled += ["fribidi"] - elif arg.startswith("--depends="): - depends_dir = arg[10:] - elif arg.startswith("--python="): - python_dir = arg[9:] - elif arg.startswith("--executable="): - python_exe = arg[13:] - elif arg.startswith("--architecture="): - architecture = arg[15:] - elif arg.startswith("--dir="): - build_dir = arg[6:] - elif arg == "--srcdir": - sources_dir = os.path.sep + "src" - else: - msg = "Unknown parameter: " + arg - raise ValueError(msg) + parser.add_argument( + "-v", "--verbose", action="store_true", help="print generated scripts" + ) + parser.add_argument( + "-d", + "--dir", + "--build-dir", + dest="build_dir", + metavar="PILLOW_BUILD", + default=os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")), + help="build directory (default: 'winbuild\\build')", + ) + parser.add_argument( + "--depends", + dest="depends_dir", + metavar="PILLOW_DEPS", + default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), + help="directory used to store cached dependencies " + "(default: 'winbuild\\depends')", + ) + parser.add_argument( + "--architecture", + choices=architectures, + default=os.environ.get( + "ARCHITECTURE", + ( + "ARM64" + if platform.machine() == "ARM64" + else ("x86" if struct.calcsize("P") == 4 else "x64") + ), + ), + help="build architecture (default: same as host Python)", + ) + parser.add_argument( + "--python", + dest="python_dir", + metavar="PYTHON", + default=os.environ.get("PYTHON"), + help="Python install directory (default: use host Python)", + ) + parser.add_argument( + "--executable", + dest="python_exe", + metavar="EXECUTABLE", + default=os.environ.get("EXECUTABLE", "python.exe"), + help="Python executable (default: use host Python)", + ) + parser.add_argument( + "--nmake", + dest="cmake_generator", + action="store_const", + const="NMake Makefiles", + default="Ninja", + help="build dependencies using NMake instead of Ninja", + ) + parser.add_argument( + "--no-imagequant", + action="store_true", + help="skip GPL-licensed optional dependency libimagequant", + ) + parser.add_argument( + "--no-fribidi", + "--no-raqm", + action="store_true", + help="skip LGPL-licensed optional dependency FriBiDi", + ) + args = parser.parse_args() - # dependency cache directory - os.makedirs(depends_dir, exist_ok=True) - print("Caching dependencies in:", depends_dir) + arch_prefs = architectures[args.architecture] + print("Target architecture:", args.architecture) - if python_dir is None: - python_dir = os.path.dirname(os.path.realpath(sys.executable)) - python_exe = os.path.basename(sys.executable) - print("Target Python:", os.path.join(python_dir, python_exe)) - - arch_prefs = architectures[architecture] - print("Target Architecture:", architecture) + if args.python_dir is None: + args.python_dir = os.path.dirname(os.path.realpath(sys.executable)) + args.python_exe = os.path.basename(sys.executable) + print("Target Python:", os.path.join(args.python_dir, args.python_exe)) msvs = find_msvs() if msvs is None: @@ -650,35 +668,47 @@ if __name__ == "__main__": raise RuntimeError(msg) print("Found Visual Studio at:", msvs["vs_dir"]) - print("Using output directory:", build_dir) + # dependency cache directory + args.depends_dir = os.path.abspath(args.depends_dir) + os.makedirs(args.depends_dir, exist_ok=True) + print("Caching dependencies in:", args.depends_dir) + + args.build_dir = os.path.abspath(args.build_dir) + print("Using output directory:", args.build_dir) # build directory for *.h files - inc_dir = os.path.join(build_dir, "inc") + inc_dir = os.path.join(args.build_dir, "inc") # build directory for *.lib files - lib_dir = os.path.join(build_dir, "lib") + lib_dir = os.path.join(args.build_dir, "lib") # build directory for *.bin files - bin_dir = os.path.join(build_dir, "bin") + bin_dir = os.path.join(args.build_dir, "bin") # directory for storing project files - sources_dir = build_dir + sources_dir + sources_dir = os.path.join(args.build_dir, "src") # copy dependency licenses to this directory - license_dir = os.path.join(build_dir, "license") + license_dir = os.path.join(args.build_dir, "license") - shutil.rmtree(build_dir, ignore_errors=True) - os.makedirs(build_dir, exist_ok=False) + shutil.rmtree(args.build_dir, ignore_errors=True) + os.makedirs(args.build_dir, exist_ok=False) for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]: os.makedirs(path, exist_ok=True) + disabled = [] + if args.no_imagequant: + disabled += ["libimagequant"] + if args.no_fribidi: + disabled += ["fribidi"] + prefs = { # Python paths / preferences - "python_dir": python_dir, - "python_exe": python_exe, - "architecture": architecture, + "python_dir": args.python_dir, + "python_exe": args.python_exe, + "architecture": args.architecture, **arch_prefs, # Pillow paths - "pillow_dir": os.path.realpath(os.path.join(winbuild_dir, "..")), + "pillow_dir": pillow_dir, "winbuild_dir": winbuild_dir, # Build paths - "build_dir": build_dir, + "build_dir": args.build_dir, "inc_dir": inc_dir, "lib_dir": lib_dir, "bin_dir": bin_dir, @@ -687,6 +717,7 @@ if __name__ == "__main__": # Compilers / Tools **msvs, "cmake": "cmake.exe", # TODO find CMAKE automatically + "cmake_generator": args.cmake_generator, # TODO find NASM automatically # script header "header": sum([header, msvs["header"], ["@echo on"]], []), @@ -699,4 +730,6 @@ if __name__ == "__main__": write_script(".gitignore", ["*"]) build_dep_all() + if args.verbose: + print() build_pillow()