diff --git a/.appveyor.yml b/.appveyor.yml
index 20908052bab..60132a9a35a 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -10,35 +10,37 @@ environment:
TEST_OPTIONS:
DEPLOY: YES
matrix:
- - PYTHON: C:/Python310
+ - PYTHON: C:/Python311
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- - PYTHON: C:/Python37-x64
+ - PYTHON: C:/Python38-x64
ARCHITECTURE: x64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
install:
- '%PYTHON%\%EXECUTABLE% --version'
+- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
+- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-depends.zip -oc:\
+- 7z x pillow-test-images.zip -oc:\
- mv c:\pillow-depends-main c:\pillow-depends
-- xcopy /S /Y c:\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%
build_script:
-- ps: |
- c:\pillow\winbuild\build\build_pillow.cmd install
- $host.SetShouldExit(0)
- cd c:\pillow
+- winbuild\build\build_env.cmd
+- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .'
- '%PYTHON%\%EXECUTABLE% selftest.py --installed'
test_script:
@@ -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
@@ -60,18 +62,15 @@ cache:
- '%LOCALAPPDATA%\pip\Cache'
artifacts:
-- path: pillow\dist\*.egg
+- path: pillow\*.egg
name: egg
-- path: pillow\dist\*.wheel
+- path: pillow\*.whl
name: wheel
before_deploy:
- cd c:\pillow
- - '%PYTHON%\%EXECUTABLE% -m pip install wheel'
- - cd c:\pillow\winbuild\
- - c:\pillow\winbuild\build\build_pillow.cmd bdist_wheel
- - cd c:\pillow
- - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
+ - '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .'
+ - ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
deploy:
provider: S3
diff --git a/.ci/after_success.sh b/.ci/after_success.sh
index 23a6fcd4d45..c71546f007b 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 518b66acc23..6e87d386dd0 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -22,7 +22,8 @@ set -e
if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
- cmake meson imagemagick libharfbuzz-dev libfribidi-dev
+ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
+ sway wl-clipboard
fi
python3 -m pip install --upgrade pip
@@ -37,11 +38,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
+ if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then
+ 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/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index ba2b7d8ed26..d03fcf0d9da 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -19,6 +19,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor.
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
+- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged.
## Reporting Issues
diff --git a/.github/mergify.yml b/.github/mergify.yml
index 8dfa07f4ec5..3c20661376f 100644
--- a/.github/mergify.yml
+++ b/.github/mergify.yml
@@ -7,7 +7,7 @@ pull_request_rules:
- status-success=Test Successful
- status-success=Docker Test Successful
- status-success=Windows Test Successful
- - status-success=MinGW Test Successful
+ - status-success=MinGW
- status-success=Cygwin Test Successful
- status-success=continuous-integration/appveyor/pr
actions:
diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml
index db030704607..560d6c7dfea 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 00000000000..81ba8ef1506
--- /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/lint.yml b/.github/workflows/lint.yml
index 6195f973b05..49611e2879e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -30,7 +30,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: "3.10"
+ python-version: "3.x"
cache: pip
cache-dependency-path: "setup.py"
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index 65f2b81d543..1fc6262f4fe 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -2,7 +2,7 @@
set -e
-brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas libraqm
+brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage
@@ -13,8 +13,8 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
-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 ffac91ceca7..24b8f85d119 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- name: "Check issues"
- uses: actions/stale@v6
+ 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 5b9ab0edab3..e7ab6466e4e 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -1,11 +1,20 @@
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
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -15,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-minor-version: [7, 8, 9]
+ python-minor-version: [8, 9]
timeout-minutes: 40
@@ -30,25 +39,40 @@ jobs:
uses: actions/checkout@v3
- name: Install Cygwin
- uses: cygwin/cygwin-install-action@v2
+ 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@v2
+ uses: egor-tensin/cleanup-path@v3
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
@@ -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 c68d43935e2..36d9c131d0b 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -1,11 +1,20 @@
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
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -24,16 +33,17 @@ jobs:
# Then run the remainder
alpine,
amazon-2-amd64,
+ amazon-2023-amd64,
arch,
centos-7-amd64,
centos-stream-8-amd64,
centos-stream-9-amd64,
- debian-10-buster-x86,
- debian-11-bullseye-x86,
- fedora-35-amd64,
- fedora-36-amd64,
+ debian-11-bullseye-amd64,
+ debian-12-bookworm-x86,
+ debian-12-bookworm-amd64,
+ fedora-37-amd64,
+ fedora-38-amd64,
gentoo,
- ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
]
@@ -87,6 +97,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 ccf6e193a6d..36bb38cd7bd 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -1,38 +1,36 @@
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
jobs:
build:
runs-on: windows-latest
- strategy:
- fail-fast: false
- matrix:
- mingw: ["MINGW32", "MINGW64"]
- include:
- - mingw: "MINGW32"
- name: "MSYS2 MinGW 32-bit"
- package: "mingw-w64-i686"
- - mingw: "MINGW64"
- name: "MSYS2 MinGW 64-bit"
- package: "mingw-w64-x86_64"
defaults:
run:
shell: bash.exe --login -eo pipefail "{0}"
env:
- MSYSTEM: ${{ matrix.mingw }}
+ MSYSTEM: MINGW64
CHERE_INVOKING: 1
timeout-minutes: 30
- name: ${{ matrix.name }}
+ name: "MinGW"
steps:
- name: Checkout Pillow
@@ -45,30 +43,29 @@ 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 \
- ${{ matrix.package }}-lcms2 \
- ${{ matrix.package }}-libimagequant \
- ${{ matrix.package }}-libjpeg-turbo \
- ${{ matrix.package }}-libraqm \
- ${{ matrix.package }}-libtiff \
- ${{ matrix.package }}-libwebp \
- ${{ matrix.package }}-openjpeg2 \
- subversion
+ mingw-w64-x86_64-freetype \
+ mingw-w64-x86_64-gcc \
+ mingw-w64-x86_64-ghostscript \
+ mingw-w64-x86_64-lcms2 \
+ mingw-w64-x86_64-libimagequant \
+ mingw-w64-x86_64-libjpeg-turbo \
+ mingw-w64-x86_64-libraqm \
+ mingw-w64-x86_64-libtiff \
+ mingw-w64-x86_64-libwebp \
+ mingw-w64-x86_64-openjpeg2 \
+ mingw-w64-x86_64-python3-cffi \
+ mingw-w64-x86_64-python3-numpy \
+ mingw-w64-x86_64-python3-olefile \
+ mingw-w64-x86_64-python3-pip \
+ mingw-w64-x86_64-python3-setuptools \
+ mingw-w64-x86_64-python-pyqt6
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
pushd depends && ./install_extra_test_images.sh && popd
- name: Build Pillow
- run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" .
+ run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install .
- name: Test Pillow
run: |
@@ -81,14 +78,4 @@ jobs:
with:
file: ./coverage.xml
flags: GHA_Windows
- name: ${{ matrix.name }}
-
- success:
- permissions:
- contents: none
- needs: build
- runs-on: ubuntu-latest
- name: MinGW Test Successful
- steps:
- - name: Success
- run: echo MinGW Test Successful
+ name: "MSYS2 MinGW"
diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml
index 219189cf208..6fab0ecd227 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
@@ -48,5 +50,5 @@ jobs:
run: |
# The Pillow user in the docker container is UID 1000
sudo chown -R 1000 $GITHUB_WORKSPACE
- docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
+ docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 6b7f62c237e..70afbab24ee 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -1,6 +1,15 @@
name: Test Windows
-on: [push, pull_request, workflow_dispatch]
+on:
+ push:
+ paths-ignore:
+ - ".github/workflows/docs.yml"
+ - "docs/**"
+ pull_request:
+ paths-ignore:
+ - ".github/workflows/docs.yml"
+ - "docs/**"
+ workflow_dispatch:
permissions:
contents: read
@@ -15,18 +24,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
- architecture: ["x86", "x64"]
- include:
- # PyPy 7.3.4+ only ships 64-bit binaries for Windows
- - python-version: "pypy-3.7"
- architecture: "x64"
- - python-version: "pypy-3.8"
- architecture: "x64"
+ python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
timeout-minutes: 30
- name: Python ${{ matrix.python-version }} ${{ matrix.architecture }}
+ name: Python ${{ matrix.python-version }}
steps:
- name: Checkout Pillow
@@ -38,31 +40,37 @@ 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
with:
python-version: ${{ matrix.python-version }}
- architecture: ${{ matrix.architecture }}
cache: pip
cache-dependency-path: ".github/workflows/test-windows.yml"
- name: Print build system information
run: python3 .github/workflows/system-info.py
- - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
- run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
+ - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
+ run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
- name: Install dependencies
id: install
run: |
- 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
- echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
+ 7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\"
+ echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH
- winbuild\depends\gs1000w32.exe /S
- echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH
+ choco install ghostscript --version=10.0.0.20230317
+ echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
- 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 +89,7 @@ jobs:
- name: Prepare build
if: steps.build-cache.outputs.cache-hit != 'true'
run: |
- & python.exe winbuild\build_prepare.py -v --python=$env:pythonLocation --srcdir
+ & python.exe winbuild\build_prepare.py -v
shell: pwsh
- name: Build dependencies / libjpeg-turbo
@@ -141,7 +149,7 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_fribidi.cmd"
- # trim ~150MB x 9
+ # trim ~150MB for each job
- name: Optimize build cache
if: steps.build-cache.outputs.cache-hit != 'true'
run: rmdir /S /Q winbuild\build\src
@@ -149,9 +157,9 @@ jobs:
- name: Build Pillow
run: |
- $FLAGS=""
- if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" }
- & winbuild\build\build_pillow.cmd $FLAGS install
+ $FLAGS="-C raqm=vendor -C fribidi=vendor"
+ if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" }
+ cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
& $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh
@@ -190,14 +198,14 @@ jobs:
with:
file: ./coverage.xml
flags: GHA_Windows
- name: ${{ runner.os }} Python ${{ matrix.python-version }} ${{ matrix.architecture }}
+ name: ${{ runner.os }} Python ${{ matrix.python-version }}
- name: Build wheel
id: wheel
if: "github.event_name != 'pull_request'"
run: |
- mkdir fribidi\${{ matrix.architecture }}
- copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }}
+ mkdir fribidi
+ copy winbuild\build\bin\fribidi* fribidi
setlocal EnableDelayedExpansion
for %%f in (winbuild\build\license\*) do (
set x=%%~nf
@@ -215,7 +223,8 @@ jobs:
)
)
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
- winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
+ call winbuild\\build\\build_env.cmd
+ %pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable .
shell: cmd
- name: Upload wheel
@@ -223,10 +232,10 @@ jobs:
if: "github.event_name != 'pull_request'"
with:
name: ${{ steps.wheel.outputs.dist }}
- path: dist\*.whl
+ path: "*.whl"
- name: Upload fribidi.dll
- if: "github.event_name != 'pull_request' && matrix.python-version == 3.10"
+ if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
uses: actions/upload-artifact@v3
with:
name: fribidi
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 645384c02d8..893c0d12c6d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,11 +1,20 @@
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
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -20,16 +29,16 @@ jobs:
"ubuntu-latest",
]
python-version: [
- "pypy-3.8",
- "pypy-3.7",
+ "pypy3.10",
+ "pypy3.9",
+ "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"
@@ -75,7 +84,9 @@ jobs:
python3 -m pip install pytest-reverse
fi
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
- xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
+ xvfb-run -s '-screen 0 1024x768x24' sway&
+ export WAYLAND_DISPLAY=wayland-1
+ .ci/test.sh
else
.ci/test.sh
fi
@@ -95,11 +106,6 @@ jobs:
name: errors
path: Tests/errors
- - name: Docs
- if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
- run: |
- make doccheck
-
- name: After success
run: |
.ci/after_success.sh
@@ -107,9 +113,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/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml
deleted file mode 100644
index 69f9e547602..00000000000
--- a/.github/workflows/tidelift.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: Tidelift Align
-
-on:
- schedule:
- - cron: "30 2 * * *" # daily at 02:30 UTC
- push:
- paths:
- - "Pipfile*"
- - ".github/workflows/tidelift.yml"
- pull_request:
- paths:
- - "Pipfile*"
- - ".github/workflows/tidelift.yml"
- workflow_dispatch:
-
-permissions:
- contents: read
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- build:
- if: github.repository_owner == 'python-pillow'
- name: Run Tidelift to ensure approved open source packages are in use
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v3
- - name: Scan
- uses: tidelift/alignment-action@main
- env:
- TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }}
- TIDELIFT_ORGANIZATION: team/aclark4life
- TIDELIFT_PROJECT: pillow
diff --git a/.gitignore b/.gitignore
index 790404535f7..1dd6c917524 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 f81bcb956fa..872c73843c6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,52 +1,73 @@
repos:
- repo: https://github.com/psf/black
- rev: 22.8.0
+ rev: 23.3.0
hooks:
- id: black
- args: ["--target-version", "py37"]
- # Only .py files, until https://github.com/psf/black/issues/402 resolved
- files: \.py$
- types: []
+ args: [--target-version=py38]
- repo: https://github.com/PyCQA/isort
- rev: 5.10.1
+ rev: 5.12.0
hooks:
- id: isort
+ - repo: https://github.com/PyCQA/bandit
+ rev: 1.7.5
+ hooks:
+ - id: bandit
+ args: [--severity-level=high]
+ files: ^src/
+
- repo: https://github.com/asottile/yesqa
rev: v1.4.0
hooks:
- 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: 5.0.4
+ rev: 6.0.0
hooks:
- id: flake8
- additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
+ additional_dependencies:
+ [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
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.3.0
+ rev: v4.4.0
hooks:
- id: check-merge-conflict
- id: check-json
+ - id: check-toml
- id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint
- rev: v0.6.1
+ rev: v0.6.7
hooks:
- id: sphinx-lint
+ - repo: https://github.com/tox-dev/pyproject-fmt
+ rev: 0.12.1
+ hooks:
+ - id: pyproject-fmt
+
+ - repo: https://github.com/abravalheri/validate-pyproject
+ rev: v0.13
+ hooks:
+ - id: validate-pyproject
+
+ - repo: https://github.com/tox-dev/tox-ini-fmt
+ rev: 1.3.0
+ hooks:
+ - id: tox-ini-fmt
+
ci:
autoupdate_schedule: monthly
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 0f581ebba90..bda03d94457 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -1,5 +1,12 @@
version: 2
+formats: [pdf]
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
python:
install:
- method: pip
diff --git a/CHANGES.rst b/CHANGES.rst
index 6f2ba569e0c..b4dc1d6646e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,345 @@
Changelog (Pillow)
==================
+10.0.1 (2023-09-15)
+-------------------
+
+- Updated libwebp to 1.3.2 #7395
+ [radarhere]
+
+- Updated zlib to 1.3 #7344
+ [radarhere]
+
+10.0.0 (2023-07-01)
+-------------------
+
+- Fixed deallocating mask images #7246
+ [radarhere]
+
+- Added ImageFont.MAX_STRING_LENGTH #7244
+ [radarhere, hugovk]
+
+- Fix Windows build with pyproject.toml #7230
+ [hugovk, nulano, radarhere]
+
+- Do not close provided file handles with libtiff #7199
+ [radarhere]
+
+- Convert to HSV if mode is HSV in getcolor() #7226
+ [radarhere]
+
+- Added alpha_only argument to getbbox() #7123
+ [radarhere. hugovk]
+
+- Prioritise speed in _repr_png_ #7242
+ [radarhere]
+
+- Do not use CFFI access by default on PyPy #7236
+ [radarhere]
+
+- Limit size even if one dimension is zero in decompression bomb check #7235
+ [radarhere]
+
+- Use --config-settings instead of deprecated --global-option #7171
+ [radarhere]
+
+- Better C integer definitions #6645
+ [Yay295, hugovk]
+
+- Fixed finding dependencies on Cygwin #7175
+ [radarhere]
+
+- Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219
+ [abey79, radarhere]
+
+- Added in_place argument to ImageOps.exif_transpose() #7092
+ [radarhere]
+
+- Fixed calling putpalette() on L and LA images before load() #7187
+ [radarhere]
+
+- Fixed saving TIFF multiframe images with LONG8 tag types #7078
+ [radarhere]
+
+- Fixed combining single duration across duplicate APNG frames #7146
+ [radarhere]
+
+- Remove temporary file when error is raised #7148
+ [radarhere]
+
+- Do not use temporary file when grabbing clipboard on Linux #7200
+ [radarhere]
+
+- If the clipboard fails to open on Windows, wait and try again #7141
+ [radarhere]
+
+- Fixed saving multiple 1 mode frames to GIF #7181
+ [radarhere]
+
+- Replaced absolute PIL import with relative import #7173
+ [radarhere]
+
+- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192
+ [radarhere]
+
+- Improved wl-paste mimetype handling in ImageGrab #7094
+ [rrcgat, radarhere]
+
+- Added _repr_jpeg_() for IPython display_jpeg #7135
+ [n3011, radarhere, nulano]
+
+- Use "/sbin/ldconfig" if ldconfig is not found #7068
+ [radarhere]
+
+- Prefer screenshots using XCB over gnome-screenshot #7143
+ [nulano, radarhere]
+
+- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151
+ [radarhere]
+
+- Support reading signed 8-bit TIFF images #7111
+ [radarhere]
+
+- Added width argument to ImageDraw regular_polygon #7132
+ [radarhere]
+
+- Support I mode for ImageFilter.BuiltinFilter #7108
+ [radarhere]
+
+- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112
+ [radarhere]
+
+- Added unpacker from I;16B to I;16 #7125
+ [radarhere]
+
+- Support float font sizes #7107
+ [radarhere]
+
+- Use later value for duplicate xref entries in PdfParser #7102
+ [radarhere]
+
+- Load before getting size in __getstate__ #7105
+ [bigcat88, radarhere]
+
+- Fixed type handling for include and lib directories #7069
+ [adisbladis, radarhere]
+
+- Remove deprecations for Pillow 10.0.0 #7059, #7080
+ [hugovk, radarhere]
+
+- Drop support for soon-EOL Python 3.7 #7058
+ [hugovk, radarhere]
+
+9.5.0 (2023-04-01)
+------------------
+
+- Added ImageSourceData to TAGS_V2 #7053
+ [radarhere]
+
+- Clear PPM half token after use #7052
+ [radarhere]
+
+- Removed absolute path to ldconfig #7044
+ [radarhere]
+
+- Support custom comments and PLT markers when saving JPEG2000 images #6903
+ [joshware, radarhere, hugovk]
+
+- Load before getting size in __array_interface__ #7034
+ [radarhere]
+
+- Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 #7010
+ [radarhere]
+
+- Consider transparency when applying APNG blend mask #7018
+ [radarhere]
+
+- Round duration when saving animated WebP images #6996
+ [radarhere]
+
+- Added reading of JPEG2000 comments #6909
+ [radarhere]
+
+- Decrement reference count #7003
+ [radarhere, nulano]
+
+- Allow libtiff_support_custom_tags to be missing #7020
+ [radarhere]
+
+- Improved I;16N support #6834
+ [radarhere]
+
+- Added QOI reading #6852
+ [radarhere, hugovk]
+
+- Added saving RGBA images as PDFs #6925
+ [radarhere]
+
+- Do not raise an error if os.environ does not contain PATH #6935
+ [radarhere, hugovk]
+
+- Close OleFileIO instance when closing or exiting FPX or MIC #7005
+ [radarhere]
+
+- Added __int__ to IFDRational for Python >= 3.11 #6998
+ [radarhere]
+
+- Added memoryview support to Dib.frombytes() #6988
+ [radarhere, nulano]
+
+- Close file pointer copy in the libtiff encoder if still open #6986
+ [fcarron, radarhere]
+
+- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978
+ [radarhere]
+
+- Added "corners" argument to ImageDraw rounded_rectangle() #6954
+ [radarhere]
+
+- Added memoryview support to frombytes() #6974
+ [radarhere]
+
+- 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)
+------------------
+
+- Fixed null pointer dereference crash with malformed font #6846
+ [wiredfool, radarhere]
+
+- Return from ImagingFill early if image has a zero dimension #6842
+ [radarhere]
+
+- Reversed deprecations for Image constants, except for duplicate Resampling attributes #6830
+ [radarhere]
+
+- Improve exception traceback readability #6836
+ [hugovk, radarhere]
+
+- Do not attempt to read IFD1 if absent #6840
+ [radarhere]
+
+- Fixed writing int as ASCII tag #6800
+ [radarhere]
+
+- If available, use wl-paste or xclip for grabclipboard() on Linux #6783
+ [radarhere]
+
+- Added signed option when saving JPEG2000 images #6709
+ [radarhere]
+
+- Patch OpenJPEG to include ARM64 fix #6718
+ [radarhere]
+
+- Added support for I;16 modes in putdata() #6825
+ [radarhere]
+
+- Added conversion from RGBa to RGB #6708
+ [radarhere]
+
+- Added DDS support for uncompressed L and LA images #6820
+ [radarhere, REDxEYE]
+
+- Added LightSource tag values to ExifTags #6749
+ [radarhere]
+
+- Fixed PyAccess after changing ICO size #6821
+ [radarhere]
+
+- Do not use EXIF from info when saving PNG images #6819
+ [radarhere]
+
+- Fixed saving EXIF data to MPO #6817
+ [radarhere]
+
+- Added Exif hide_offsets() #6762
+ [radarhere]
+
+- Only compare to previous frame when checking for duplicate GIF frames while saving #6787
+ [radarhere]
+
+- Always initialize all plugins in registered_extensions() #6811
+ [radarhere]
+
+- Ignore non-opaque WebP background when saving as GIF #6792
+ [radarhere]
+
+- Only set tile in ImageFile __setstate__ #6793
+ [radarhere]
+
+- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767
+ [radarhere]
+
+- Added IFD enum to ExifTags #6748
+ [radarhere]
+
+- Fixed bug combining GIF frame durations #6779
+ [radarhere]
+
+- Support saving JPEG comments #6774
+ [smason, radarhere]
+
+- Added getxmp() to WebPImagePlugin #6758
+ [radarhere]
+
+- Added "exact" option when saving WebP #6747
+ [ashafaei, radarhere]
+
+- Use fractional coordinates when drawing text #6722
+ [radarhere]
+
+- Fixed writing int as BYTE tag #6740
+ [radarhere]
+
+- Added MP Format Version when saving MPO #6735
+ [radarhere]
+
+- Added Interop to ExifTags #6724
+ [radarhere]
+
+- CVE-2007-4559 patch when building on Windows #6704
+ [TrellixVulnTeam, nulano, radarhere]
+
+- Fix compiler warning: accessing 64 bytes in a region of size 48 #6714
+ [wiredfool]
+
+- Use verbose flag for pip install #6713
+ [wiredfool, radarhere]
+
9.3.0 (2022-10-29)
------------------
diff --git a/LICENSE b/LICENSE
index 40aabc3239f..cf65e86d734 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-2022 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/MANIFEST.in b/MANIFEST.in
index 08f6dfc0877..606e7e074aa 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,7 +1,6 @@
include *.c
include *.h
include *.in
-include *.lock
include *.md
include *.py
include *.rst
@@ -10,13 +9,13 @@ include *.txt
include *.yaml
include LICENSE
include Makefile
-include Pipfile
include tox.ini
graft Tests
graft src
graft depends
graft winbuild
graft docs
+graft _custom_build
# build/src control detritus
exclude .appveyor.yml
diff --git a/Makefile b/Makefile
index 8f2862948a8..57d756b47e3 100644
--- a/Makefile
+++ b/Makefile
@@ -16,10 +16,16 @@ coverage:
python3 -m coverage report
.PHONY: doc
-doc:
+.PHONY: html
+doc html:
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs html
+.PHONY: htmlview
+htmlview:
+ python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
+ $(MAKE) -C docs htmlview
+
.PHONY: doccheck
doccheck:
$(MAKE) doc
@@ -38,8 +44,8 @@ help:
@echo " coverage run coverage test (in progress)"
@echo " doc make HTML docs"
@echo " docserve run an HTTP server on the docs directory"
- @echo " html to make standalone HTML files"
- @echo " inplace make inplace extension"
+ @echo " html make HTML docs"
+ @echo " htmlview open the index page built by the html target in your browser"
@echo " install make and install"
@echo " install-coverage make and install with C coverage"
@echo " lint run the lint checks"
@@ -47,18 +53,14 @@ help:
@echo " release-test run code and package tests before release"
@echo " test run tests on installed Pillow"
-.PHONY: inplace
-inplace: clean
- python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" .
-
.PHONY: install
install:
- python3 -m pip install .
+ python3 -m pip -v install .
python3 selftest.py
.PHONY: install-coverage
install-coverage:
- CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" .
+ CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install .
python3 selftest.py
.PHONY: debug
@@ -67,10 +69,11 @@ debug:
# for our stuff, kills optimization, and redirects to dev null so we
# see any build failures.
make clean > /dev/null
- CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null
+ CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null
.PHONY: release-test
release-test:
+ python3 Tests/check_release_notes.py
python3 -m pip install -e .[tests]
python3 selftest.py
python3 -m pytest Tests
@@ -116,5 +119,5 @@ lint:
lint-fix:
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
- python3 -m black --target-version py37 .
+ python3 -m black --target-version py38 .
python3 -m isort .
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 1e611a63ce7..00000000000
--- a/Pipfile
+++ /dev/null
@@ -1,22 +0,0 @@
-[[source]]
-url = "https://pypi.org/simple"
-verify_ssl = true
-name = "pypi"
-
-[packages]
-black = "*"
-check-manifest = "*"
-coverage = "*"
-defusedxml = "*"
-packaging = "*"
-markdown2 = "*"
-olefile = "*"
-pyroma = "*"
-pytest = "*"
-pytest-cov = "*"
-pytest-timeout = "*"
-
-[dev-packages]
-
-[requires]
-python_version = "3.9"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 600b19050f5..00000000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,324 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d"
- },
- "pipfile-spec": 6,
- "requires": {
- "python_version": "3.9"
- },
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {
- "attrs": {
- "hashes": [
- "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
- "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==21.2.0"
- },
- "black": {
- "hashes": [
- "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3",
- "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"
- ],
- "index": "pypi",
- "version": "==21.12b0"
- },
- "build": {
- "hashes": [
- "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f",
- "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==0.7.0"
- },
- "certifi": {
- "hashes": [
- "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
- "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
- ],
- "version": "==2021.10.8"
- },
- "charset-normalizer": {
- "hashes": [
- "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721",
- "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"
- ],
- "markers": "python_version >= '3'",
- "version": "==2.0.9"
- },
- "check-manifest": {
- "hashes": [
- "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95",
- "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce"
- ],
- "index": "pypi",
- "version": "==0.47"
- },
- "click": {
- "hashes": [
- "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3",
- "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==8.0.3"
- },
- "coverage": {
- "hashes": [
- "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0",
- "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd",
- "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884",
- "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48",
- "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76",
- "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0",
- "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64",
- "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685",
- "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47",
- "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d",
- "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840",
- "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f",
- "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971",
- "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c",
- "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a",
- "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de",
- "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17",
- "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4",
- "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521",
- "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57",
- "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b",
- "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282",
- "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644",
- "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475",
- "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d",
- "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da",
- "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953",
- "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2",
- "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e",
- "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c",
- "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc",
- "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64",
- "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74",
- "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617",
- "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3",
- "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d",
- "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa",
- "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739",
- "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8",
- "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8",
- "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781",
- "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58",
- "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9",
- "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c",
- "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd",
- "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e",
- "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"
- ],
- "index": "pypi",
- "version": "==6.2"
- },
- "defusedxml": {
- "hashes": [
- "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
- "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
- ],
- "index": "pypi",
- "version": "==0.7.1"
- },
- "docutils": {
- "hashes": [
- "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
- "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==0.18.1"
- },
- "idna": {
- "hashes": [
- "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
- "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
- ],
- "markers": "python_version >= '3'",
- "version": "==3.3"
- },
- "iniconfig": {
- "hashes": [
- "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
- "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
- ],
- "version": "==1.1.1"
- },
- "markdown2": {
- "hashes": [
- "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0",
- "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e"
- ],
- "index": "pypi",
- "version": "==2.4.2"
- },
- "mypy-extensions": {
- "hashes": [
- "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
- "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
- ],
- "version": "==0.4.3"
- },
- "olefile": {
- "hashes": [
- "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964"
- ],
- "index": "pypi",
- "version": "==0.46"
- },
- "packaging": {
- "hashes": [
- "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
- "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
- ],
- "index": "pypi",
- "version": "==21.3"
- },
- "pathspec": {
- "hashes": [
- "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
- "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
- ],
- "version": "==0.9.0"
- },
- "pep517": {
- "hashes": [
- "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0",
- "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161"
- ],
- "version": "==0.12.0"
- },
- "platformdirs": {
- "hashes": [
- "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2",
- "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==2.4.0"
- },
- "pluggy": {
- "hashes": [
- "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
- "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==1.0.0"
- },
- "py": {
- "hashes": [
- "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
- "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==1.11.0"
- },
- "pygments": {
- "hashes": [
- "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
- "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==2.10.0"
- },
- "pyparsing": {
- "hashes": [
- "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
- "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==3.0.6"
- },
- "pyroma": {
- "hashes": [
- "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65",
- "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a"
- ],
- "index": "pypi",
- "version": "==3.2"
- },
- "pytest": {
- "hashes": [
- "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
- "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
- ],
- "index": "pypi",
- "version": "==6.2.5"
- },
- "pytest-cov": {
- "hashes": [
- "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6",
- "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"
- ],
- "index": "pypi",
- "version": "==3.0.0"
- },
- "pytest-timeout": {
- "hashes": [
- "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112",
- "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717"
- ],
- "index": "pypi",
- "version": "==2.0.2"
- },
- "requests": {
- "hashes": [
- "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
- "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
- "version": "==2.26.0"
- },
- "setuptools": {
- "hashes": [
- "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c",
- "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==60.0.0"
- },
- "toml": {
- "hashes": [
- "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
- "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
- ],
- "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
- "version": "==0.10.2"
- },
- "tomli": {
- "hashes": [
- "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f",
- "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==1.2.3"
- },
- "typing-extensions": {
- "hashes": [
- "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
- "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==4.0.1"
- },
- "urllib3": {
- "hashes": [
- "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
- "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
- "version": "==1.26.7"
- }
- },
- "develop": {}
-}
diff --git a/README.md b/README.md
index e7c0ebc5aa5..af1ca57c25b 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).
@@ -54,9 +54,9 @@ As of 2019, Pillow development is
-
+
@@ -88,6 +88,10 @@ As of 2019, Pillow development is
+
diff --git a/RELEASING.md b/RELEASING.md
index b0506748470..604bb1b8c38 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/bench_cffi_access.py b/Tests/bench_cffi_access.py
index 87cad699d3b..69ebef9b458 100644
--- a/Tests/bench_cffi_access.py
+++ b/Tests/bench_cffi_access.py
@@ -27,25 +27,19 @@ def timer(func, label, *args):
for x in range(iterations):
func(*args)
if time.time() - starttime > 10:
- print(
- "{}: breaking at {} iterations, {:.6f} per iteration".format(
- label, x + 1, (time.time() - starttime) / (x + 1.0)
- )
- )
break
- if x == iterations - 1:
- endtime = time.time()
- print(
- "{}: {:.4f} s {:.6f} per iteration".format(
- label, endtime - starttime, (endtime - starttime) / (x + 1.0)
- )
+ endtime = time.time()
+ print(
+ "{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format(
+ label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0)
)
+ )
def test_direct():
im = hopper()
im.load()
- # im = Image.new( "RGB", (2000, 2000), (1, 3, 2))
+ # im = Image.new("RGB", (2000, 2000), (1, 3, 2))
caccess = im.im.pixel_access(False)
access = PyAccess.new(im, False)
diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py
index 08a55d349d5..c600c45ed1f 100644
--- a/Tests/check_fli_overflow.py
+++ b/Tests/check_fli_overflow.py
@@ -4,7 +4,6 @@
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_jpeg_leaks.py b/Tests/check_jpeg_leaks.py
index ab8d7771992..940c0b00d5b 100644
--- a/Tests/check_jpeg_leaks.py
+++ b/Tests/check_jpeg_leaks.py
@@ -75,43 +75,42 @@
"""
-def test_qtables_leak():
+standard_l_qtable = (
+ # fmt: off
+ 16, 11, 10, 16, 24, 40, 51, 61,
+ 12, 12, 14, 19, 26, 58, 60, 55,
+ 14, 13, 16, 24, 40, 57, 69, 56,
+ 14, 17, 22, 29, 51, 87, 80, 62,
+ 18, 22, 37, 56, 68, 109, 103, 77,
+ 24, 35, 55, 64, 81, 104, 113, 92,
+ 49, 64, 78, 87, 103, 121, 120, 101,
+ 72, 92, 95, 98, 112, 100, 103, 99,
+ # fmt: on
+)
+
+standard_chrominance_qtable = (
+ # fmt: off
+ 17, 18, 24, 47, 99, 99, 99, 99,
+ 18, 21, 26, 66, 99, 99, 99, 99,
+ 24, 26, 56, 99, 99, 99, 99, 99,
+ 47, 66, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99,
+ # fmt: on
+)
+
+
+@pytest.mark.parametrize(
+ "qtables",
+ (
+ (standard_l_qtable, standard_chrominance_qtable),
+ [standard_l_qtable, standard_chrominance_qtable],
+ ),
+)
+def test_qtables_leak(qtables):
im = hopper("RGB")
-
- standard_l_qtable = [
- int(s)
- for s in """
- 16 11 10 16 24 40 51 61
- 12 12 14 19 26 58 60 55
- 14 13 16 24 40 57 69 56
- 14 17 22 29 51 87 80 62
- 18 22 37 56 68 109 103 77
- 24 35 55 64 81 104 113 92
- 49 64 78 87 103 121 120 101
- 72 92 95 98 112 100 103 99
- """.split(
- None
- )
- ]
-
- standard_chrominance_qtable = [
- int(s)
- for s in """
- 17 18 24 47 99 99 99 99
- 18 21 26 66 99 99 99 99
- 24 26 56 99 99 99 99 99
- 47 66 99 99 99 99 99 99
- 99 99 99 99 99 99 99 99
- 99 99 99 99 99 99 99 99
- 99 99 99 99 99 99 99 99
- 99 99 99 99 99 99 99 99
- """.split(
- None
- )
- ]
-
- qtables = [standard_l_qtable, standard_chrominance_qtable]
-
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables)
diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py
index d8d645189e6..f4a129f50c6 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 00000000000..0a9a898d7f7
--- /dev/null
+++ b/Tests/check_release_notes.py
@@ -0,0 +1,6 @@
+import sys
+from pathlib import Path
+
+for rst in Path("docs/releasenotes").glob("[1-9]*.rst"):
+ if "TODO" in open(rst).read():
+ sys.exit(f"Error: remove TODO from {rst}")
diff --git a/Tests/fonts/fuzz_font-5203009437302784 b/Tests/fonts/fuzz_font-5203009437302784
new file mode 100644
index 00000000000..0465e48c204
--- /dev/null
+++ b/Tests/fonts/fuzz_font-5203009437302784
@@ -0,0 +1,10 @@
+STARTFONT
+FONT
+SIZE 10
+FONTBOUNDINGBOX
+CHARS
+STARTCHAR
+ENCODING
+BBX 2 5
+ENDCHAR
+ENDFONT
diff --git a/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf b/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf
new file mode 100644
index 00000000000..fe200842e41
Binary files /dev/null and b/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf differ
diff --git a/Tests/helper.py b/Tests/helper.py
index 0d1d03ac8c1..69246bfcf45 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -20,7 +20,7 @@
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 00000000000..043cba6af8b
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 00000000000..cef0a16de1f
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 00000000000..4bdf91760e1
Binary files /dev/null and b/Tests/images/comment.jp2 differ
diff --git a/Tests/images/duplicate_frame.gif b/Tests/images/duplicate_frame.gif
new file mode 100644
index 00000000000..ef0c894a540
Binary files /dev/null and b/Tests/images/duplicate_frame.gif differ
diff --git a/Tests/images/duplicate_xref_entry.pdf b/Tests/images/duplicate_xref_entry.pdf
new file mode 100644
index 00000000000..f57a57d61c6
Binary files /dev/null and b/Tests/images/duplicate_xref_entry.pdf differ
diff --git a/Tests/images/flower_thumbnail.png b/Tests/images/flower_thumbnail.png
new file mode 100644
index 00000000000..4a362535f25
Binary files /dev/null and b/Tests/images/flower_thumbnail.png differ
diff --git a/Tests/images/hopper.qoi b/Tests/images/hopper.qoi
new file mode 100644
index 00000000000..6b255aba13b
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 00000000000..f4dab388fe5
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 00000000000..c417c915f54
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 11a1be6faeb..5e3cf22b4ad 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 d71e175b8fd..dd2f641f1ea 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 00000000000..3e79e21aedb
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 00000000000..7fa09a3c03a
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 00000000000..d825ad26317
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 00000000000..c19da698e38
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 00000000000..f3e95d48749
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 00000000000..274d27984dc
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 00000000000..c5f40bfdbdd
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 00000000000..01bfd1750a4
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 00000000000..efd27be4f9d
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 00000000000..d3acd01abe0
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 00000000000..55ddbc033fb
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 00000000000..c000b26e967
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 00000000000..7056b4fd934
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 00000000000..5eca030b9a0
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 00000000000..7f1f0034400
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 00000000000..2e815f4ada2
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png differ
diff --git a/Tests/images/imagedraw_rounded_rectangle_x_odd.png b/Tests/images/imagedraw_rounded_rectangle_x_odd.png
new file mode 100644
index 00000000000..f23f1945e1f
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_x_odd.png differ
diff --git a/Tests/images/imagedraw_rounded_rectangle_y_odd.png b/Tests/images/imagedraw_rounded_rectangle_y_odd.png
new file mode 100644
index 00000000000..96441bc7289
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_y_odd.png differ
diff --git a/Tests/images/imagedraw_triangle_width.png b/Tests/images/imagedraw_triangle_width.png
new file mode 100644
index 00000000000..3d35326e73b
Binary files /dev/null and b/Tests/images/imagedraw_triangle_width.png differ
diff --git a/Tests/images/orientation_rectangle.jpg b/Tests/images/orientation_rectangle.jpg
new file mode 100644
index 00000000000..85cfbd0a813
Binary files /dev/null and b/Tests/images/orientation_rectangle.jpg differ
diff --git a/Tests/images/pil123rgba.qoi b/Tests/images/pil123rgba.qoi
new file mode 100644
index 00000000000..1e46036c7c6
Binary files /dev/null and b/Tests/images/pil123rgba.qoi differ
diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png
index cf002b12cd0..7e98b8eac8d 100644
Binary files a/Tests/images/test_anchor_multiline_mm_right.png and b/Tests/images/test_anchor_multiline_mm_right.png differ
diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png
index 7b1e9c4e42f..6a15130248a 100644
Binary files a/Tests/images/test_combine_multiline_lm_center.png and b/Tests/images/test_combine_multiline_lm_center.png differ
diff --git a/Tests/images/test_combine_multiline_lm_left.png b/Tests/images/test_combine_multiline_lm_left.png
index a26996c2dbe..8eb254fdf26 100644
Binary files a/Tests/images/test_combine_multiline_lm_left.png and b/Tests/images/test_combine_multiline_lm_left.png differ
diff --git a/Tests/images/test_combine_multiline_lm_right.png b/Tests/images/test_combine_multiline_lm_right.png
index 7caf5cb742a..cb640a7409f 100644
Binary files a/Tests/images/test_combine_multiline_lm_right.png and b/Tests/images/test_combine_multiline_lm_right.png differ
diff --git a/Tests/images/test_combine_multiline_mm_center.png b/Tests/images/test_combine_multiline_mm_center.png
index a859e9570c8..d1146b8b856 100644
Binary files a/Tests/images/test_combine_multiline_mm_center.png and b/Tests/images/test_combine_multiline_mm_center.png differ
diff --git a/Tests/images/test_combine_multiline_mm_left.png b/Tests/images/test_combine_multiline_mm_left.png
index aadb5191f0e..f539a8e62e6 100644
Binary files a/Tests/images/test_combine_multiline_mm_left.png and b/Tests/images/test_combine_multiline_mm_left.png differ
diff --git a/Tests/images/test_combine_multiline_mm_right.png b/Tests/images/test_combine_multiline_mm_right.png
index 8238d4ec8ca..02634163e1c 100644
Binary files a/Tests/images/test_combine_multiline_mm_right.png and b/Tests/images/test_combine_multiline_mm_right.png differ
diff --git a/Tests/images/test_combine_multiline_rm_center.png b/Tests/images/test_combine_multiline_rm_center.png
index 7568dd63a33..4cce8f6a00e 100644
Binary files a/Tests/images/test_combine_multiline_rm_center.png and b/Tests/images/test_combine_multiline_rm_center.png differ
diff --git a/Tests/images/test_combine_multiline_rm_left.png b/Tests/images/test_combine_multiline_rm_left.png
index b8c3b5b143d..93d8162b3bf 100644
Binary files a/Tests/images/test_combine_multiline_rm_left.png and b/Tests/images/test_combine_multiline_rm_left.png differ
diff --git a/Tests/images/test_combine_multiline_rm_right.png b/Tests/images/test_combine_multiline_rm_right.png
index 14c478a72d0..6c4634560ed 100644
Binary files a/Tests/images/test_combine_multiline_rm_right.png and b/Tests/images/test_combine_multiline_rm_right.png differ
diff --git a/Tests/images/text_float_coord.png b/Tests/images/text_float_coord.png
index 49468698cd4..d2270826a5b 100644
Binary files a/Tests/images/text_float_coord.png and b/Tests/images/text_float_coord.png differ
diff --git a/Tests/images/text_float_coord_1_alt.png b/Tests/images/text_float_coord_1_alt.png
index 50bdac3d8f3..2287071ffab 100644
Binary files a/Tests/images/text_float_coord_1_alt.png and b/Tests/images/text_float_coord_1_alt.png differ
diff --git a/Tests/images/uncompressed_l.dds b/Tests/images/uncompressed_l.dds
new file mode 100644
index 00000000000..b82282587ec
Binary files /dev/null and b/Tests/images/uncompressed_l.dds differ
diff --git a/Tests/images/uncompressed_l.png b/Tests/images/uncompressed_l.png
new file mode 100644
index 00000000000..9d22a26a446
Binary files /dev/null and b/Tests/images/uncompressed_l.png differ
diff --git a/Tests/images/uncompressed_la.dds b/Tests/images/uncompressed_la.dds
new file mode 100644
index 00000000000..30bf93576fd
Binary files /dev/null and b/Tests/images/uncompressed_la.dds differ
diff --git a/Tests/images/uncompressed_la.png b/Tests/images/uncompressed_la.png
new file mode 100644
index 00000000000..0d4ea602fc3
Binary files /dev/null and b/Tests/images/uncompressed_la.png differ
diff --git a/Tests/images/zero_width.gif b/Tests/images/zero_width.gif
new file mode 100644
index 00000000000..da6823b60bb
Binary files /dev/null and b/Tests/images/zero_width.gif differ
diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh
index 09cc7bc1696..7e9098f530b 100755
--- a/Tests/oss-fuzz/build.sh
+++ b/Tests/oss-fuzz/build.sh
@@ -19,29 +19,17 @@ python3 setup.py build --build-base=/tmp/build install
# Build fuzzers in $OUT.
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
- fuzzer_basename=$(basename -s .py $fuzzer)
- fuzzer_package=${fuzzer_basename}.pkg
- pyinstaller \
+ compile_python_fuzzer $fuzzer \
--add-binary /usr/local/lib/libjpeg.so.62.3.0:. \
--add-binary /usr/local/lib/libfreetype.so.6:. \
--add-binary /usr/local/lib/liblcms2.so.2:. \
--add-binary /usr/local/lib/libopenjp2.so.7:. \
--add-binary /usr/local/lib/libpng16.so.16:. \
- --add-binary /usr/local/lib/libtiff.so.5:. \
+ --add-binary /usr/local/lib/libtiff.so.6:. \
--add-binary /usr/local/lib/libwebp.so.7:. \
--add-binary /usr/local/lib/libwebpdemux.so.2:. \
--add-binary /usr/local/lib/libwebpmux.so.3:. \
- --add-binary /usr/local/lib/libxcb.so.1:. \
- --distpath $OUT --onefile --name $fuzzer_package $fuzzer
-
- # Create execution wrapper.
- echo "#!/bin/sh
-# LLVMFuzzerTestOneInput for fuzzer detection.
-this_dir=\$(dirname \"\$0\")
-LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \
-ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \
-\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename
- chmod u+x $OUT/$fuzzer_basename
+ --add-binary /usr/local/lib/libxcb.so.1:.
done
find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@
diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py
index 629e9ac00d4..dc111c38b36 100644
--- a/Tests/oss-fuzz/test_fuzzers.py
+++ b/Tests/oss-fuzz/test_fuzzers.py
@@ -57,6 +57,6 @@ def test_fuzz_fonts(path):
with open(path, "rb") as f:
try:
fuzzers.fuzz_font(f.read())
- except (Image.DecompressionBombError, Image.DecompressionBombWarning):
+ except (Image.DecompressionBombError, Image.DecompressionBombWarning, OSError):
pass
assert True
diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py
index b17aad2ea50..002a44a4f4d 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:
@@ -35,6 +34,7 @@ def test_questionable():
"pal8os2v2.bmp",
"rgb24prof.bmp",
"pal1p1.bmp",
+ "pal4rletrns.bmp",
"pal8offs.bmp",
"rgb24lprof.bmp",
"rgb32fakealpha.bmp",
diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py
index 385192a3cdc..9021a9fb36d 100644
--- a/Tests/test_core_resources.py
+++ b/Tests/test_core_resources.py
@@ -177,13 +177,14 @@ def test_units(self):
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 63071b78c9c..87681a0b557 100644
--- a/Tests/test_decompression_bomb.py
+++ b/Tests/test_decompression_bomb.py
@@ -36,12 +36,10 @@ def test_warning(self):
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
- def open():
+ with pytest.warns(Image.DecompressionBombWarning):
with Image.open(TEST_FILE):
pass
- pytest.warns(Image.DecompressionBombWarning, open)
-
def test_exception(self):
# Set limit to trigger exception on the test file
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
@@ -66,6 +64,15 @@ def test_exception_gif_extents(self):
with pytest.raises(Image.DecompressionBombError):
im.seek(1)
+ def test_exception_gif_zero_width(self):
+ # Set limit to trigger exception on the test file
+ Image.MAX_IMAGE_PIXELS = 4 * 64 * 128
+ assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128
+
+ with pytest.raises(Image.DecompressionBombError):
+ with Image.open("Tests/images/zero_width.gif"):
+ pass
+
def test_exception_bmp(self):
with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/bmp/b/reallybig.bmp"):
@@ -87,7 +94,8 @@ def test_enlarge_crop(self):
# same decompression bomb warnings on them.
with hopper() as src:
box = (0, 0, src.width * 2, src.height * 2)
- pytest.warns(Image.DecompressionBombWarning, src.crop, box)
+ with pytest.warns(Image.DecompressionBombWarning):
+ src.crop(box)
def test_crop_decompression_checks(self):
im = Image.new("RGB", (100, 100))
@@ -95,7 +103,8 @@ def test_crop_decompression_checks(self):
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 30ed4a8081d..f175b90af16 100644
--- a/Tests/test_deprecate.py
+++ b/Tests/test_deprecate.py
@@ -7,9 +7,9 @@
"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 2528ff3f7d4..00000000000
--- 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 51637c78645..8cb9a814ea5 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 open():
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 open_frames_zero_default():
im.seek(im.n_frames - 1)
im.load()
- def open():
+ with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
assert not im.is_animated
im.load()
- pytest.warns(UserWarning, open)
-
@pytest.mark.parametrize(
"test_file",
@@ -376,6 +374,20 @@ def test_apng_save(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
+def test_apng_save_alpha(tmp_path):
+ test_file = str(tmp_path / "temp.png")
+
+ im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
+ im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127))
+ im.save(test_file, save_all=True, append_images=[im2])
+
+ with Image.open(test_file) as reloaded:
+ assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255)
+
+ reloaded.seek(1)
+ assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)
+
+
def test_apng_save_split_fdat(tmp_path):
# test to make sure we do not generate sequence errors when writing
# frames with image data spanning multiple fdAT chunks (in this case
@@ -449,6 +461,17 @@ def test_apng_save_duration_loop(tmp_path):
assert im.info.get("duration") == 750
+def test_apng_save_duplicate_duration(tmp_path):
+ test_file = str(tmp_path / "temp.png")
+ frame = Image.new("RGB", (1, 1))
+
+ # Test a single duration is correctly combined across duplicate frames
+ frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
+ with Image.open(test_file) as im:
+ assert im.n_frames == 1
+ assert im.info.get("duration") == 1500
+
+
def test_apng_save_disposal(tmp_path):
test_file = str(tmp_path / "temp.png")
size = (128, 64)
@@ -657,13 +680,3 @@ def test_different_modes_in_later_frames(mode, tmp_path):
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
with Image.open(test_file) as reloaded:
assert reloaded.mode == mode
-
-
-def test_constants_deprecation():
- for enum, prefix in {
- PngImagePlugin.Disposal: "APNG_DISPOSE_",
- PngImagePlugin.Blend: "APNG_BLEND_",
- }.items():
- for name in enum.__members__:
- with pytest.warns(DeprecationWarning):
- assert getattr(PngImagePlugin, prefix + name) == enum[name]
diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py
index ba2781820e0..8b1355b6280 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 5f6d523558a..9e79937e9f5 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 e330404d64e..a7714c92cc8 100644
--- a/Tests/test_file_bufrstub.py
+++ b/Tests/test_file_bufrstub.py
@@ -10,7 +10,6 @@
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 open(self, im):
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 0f09c4b9915..22686af3485 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 open():
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_dds.py b/Tests/test_file_dds.py
index 4b9f8949ef5..cac4108a8f0 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -22,6 +22,8 @@
TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds"
TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds"
TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds"
+TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
+TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
@@ -194,26 +196,24 @@ def test_unimplemented_dxgi_format():
pass
-def test_uncompressed_rgb():
- """Check uncompressed RGB images can be opened"""
-
- # convert -format dds -define dds:compression=none hopper.jpg hopper.dds
- with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im:
- assert im.format == "DDS"
- assert im.mode == "RGB"
- assert im.size == (128, 128)
-
- assert_image_equal_tofile(im, "Tests/images/hopper.png")
+@pytest.mark.parametrize(
+ ("mode", "size", "test_file"),
+ [
+ ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
+ ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
+ ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
+ ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
+ ],
+)
+def test_uncompressed(mode, size, test_file):
+ """Check uncompressed images can be opened"""
- # Test image with alpha
- with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im:
+ with Image.open(test_file) as im:
assert im.format == "DDS"
- assert im.mode == "RGBA"
- assert im.size == (800, 600)
+ assert im.mode == mode
+ assert im.size == size
- assert_image_equal_tofile(
- im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png")
- )
+ assert_image_equal_tofile(im, test_file.replace(".dds", ".png"))
def test__accept_true():
@@ -305,6 +305,8 @@ def test_save_unsupported_mode(tmp_path):
@pytest.mark.parametrize(
("mode", "test_file"),
[
+ ("L", "Tests/images/linear_gradient.png"),
+ ("LA", "Tests/images/uncompressed_la.png"),
("RGB", "Tests/images/hopper.png"),
("RGBA", "Tests/images/pil123rgba.png"),
],
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index 015dda992c6..26adfff8786 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -28,34 +28,65 @@
# 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.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
+@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(t, ending):
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_file_psfile(test_string, ending):
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_file_psfile(test_string, ending):
_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 447888acd8d..68b3eb567fd 100644
--- a/Tests/test_file_fits.py
+++ b/Tests/test_file_fits.py
@@ -2,7 +2,7 @@
import pytest
-from PIL import FitsImagePlugin, FitsStubImagePlugin, Image
+from PIL import FitsImagePlugin, Image
from .helper import assert_image_equal, hopper
@@ -12,7 +12,6 @@
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 b8b999d70d0..f96afdc95be 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -36,7 +36,8 @@ def open():
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 fa22e90f660..9a1784d31a7 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 cae20fa46eb..ac6253db056 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 926f5c1eea8..f4a17264f4a 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -36,7 +36,8 @@ def open():
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_bilevel(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
-
- assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
-
- # 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)
+ # Check for correctness after conversion back to 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
+
+ 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
@@ -251,6 +252,19 @@ def test_roundtrip_save_all(tmp_path):
assert reread.n_frames == 5
+def test_roundtrip_save_all_1(tmp_path):
+ out = str(tmp_path / "temp.gif")
+ im = Image.new("1", (1, 1))
+ im2 = Image.new("1", (1, 1), 1)
+ im.save(out, save_all=True, append_images=[im2])
+
+ with Image.open(out) as reloaded:
+ assert reloaded.getpixel((0, 0)) == 0
+
+ reloaded.seek(1)
+ assert reloaded.getpixel((0, 0)) == 255
+
+
@pytest.mark.parametrize(
"path, mode",
(
@@ -281,13 +295,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 +317,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 +332,6 @@ def roundtrip(im, *args, **kwargs):
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 +582,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
@@ -677,6 +686,24 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0)
+def test_dispose2_background_frame(tmp_path):
+ out = str(tmp_path / "temp.gif")
+
+ im_list = [Image.new("RGBA", (1, 20))]
+
+ different_frame = Image.new("RGBA", (1, 20))
+ different_frame.putpixel((0, 10), (255, 0, 0, 255))
+ im_list.append(different_frame)
+
+ # Frame that matches the background
+ im_list.append(Image.new("RGBA", (1, 20)))
+
+ im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
+
+ with Image.open(out) as im:
+ assert im.n_frames == 3
+
+
def test_transparency_in_second_frame(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im:
@@ -752,7 +779,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:
@@ -765,7 +791,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:
@@ -791,6 +816,22 @@ def test_roundtrip_info_duration(tmp_path):
] == duration_list
+def test_roundtrip_info_duration_combined(tmp_path):
+ out = str(tmp_path / "temp.gif")
+ with Image.open("Tests/images/duplicate_frame.gif") as im:
+ assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
+ 1000,
+ 1000,
+ 1000,
+ ]
+ im.save(out, save_all=True)
+
+ with Image.open(out) as reloaded:
+ assert [
+ frame.info["duration"] for frame in ImageSequence.Iterator(reloaded)
+ ] == [1000, 2000]
+
+
def test_identical_frames(tmp_path):
duration_list = [1000, 1500, 2000, 4000]
@@ -807,7 +848,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
@@ -859,14 +899,23 @@ def test_background(tmp_path):
im.info["background"] = 1
im.save(out)
with Image.open(out) as reread:
-
assert reread.info["background"] == im.info["background"]
+
+def test_webp_background(tmp_path):
+ out = str(tmp_path / "temp.gif")
+
+ # Test opaque WebP background
if features.check("webp") and features.check("webp_anim"):
with Image.open("Tests/images/hopper.webp") as im:
- assert isinstance(im.info["background"], tuple)
+ assert im.info["background"] == (255, 255, 255, 255)
im.save(out)
+ # Test non-opaque WebP background
+ im = Image.new("L", (100, 100), "#000")
+ im.info["background"] = (0, 0, 0, 0)
+ im.save(out)
+
def test_comment(tmp_path):
with Image.open(TEST_GIF) as im:
@@ -1052,7 +1101,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
@@ -1080,6 +1130,18 @@ def test_bbox(tmp_path):
assert reread.n_frames == 2
+def test_bbox_alpha(tmp_path):
+ out = str(tmp_path / "temp.gif")
+
+ im = Image.new("RGBA", (1, 2), (255, 0, 0, 255))
+ im.putpixel((0, 1), (255, 0, 0, 0))
+ im2 = Image.new("RGBA", (1, 2), (255, 0, 0, 0))
+ im.save(out, save_all=True, append_images=[im2])
+
+ with Image.open(out) as reread:
+ assert reread.n_frames == 2
+
+
def test_palette_save_L(tmp_path):
# Generate an L mode image with a separate palette
diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py
index fd427746e96..dd1c5e7d281 100644
--- a/Tests/test_file_gribstub.py
+++ b/Tests/test_file_gribstub.py
@@ -10,7 +10,6 @@
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 open(self, im):
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 20b4b9619af..7ca10fac5dd 100644
--- a/Tests/test_file_hdf5stub.py
+++ b/Tests/test_file_hdf5stub.py
@@ -8,7 +8,6 @@
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 open(self, im):
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 55632909c6b..42275424d91 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 3fcd5c61f0d..4e6dbe6ede4 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -71,6 +71,19 @@ def test_save_to_bytes():
)
+def test_getpixel(tmp_path):
+ temp_file = str(tmp_path / "temp.ico")
+
+ im = hopper()
+ im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
+
+ with Image.open(temp_file) as reloaded:
+ reloaded.load()
+ reloaded.size = (32, 32)
+
+ assert reloaded.getpixel((0, 0)) == (18, 20, 62)
+
+
def test_no_duplicates(tmp_path):
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
@@ -162,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)
@@ -200,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 5cf93713be4..fd00f260e78 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -32,7 +32,8 @@ def open():
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 2d0e6977a70..2d99528d37c 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -11,7 +11,6 @@
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 fa96e425b8c..0247527f5b2 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -57,7 +57,6 @@ def gen_random_image(self, size, mode="RGB"):
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"))
@@ -85,8 +84,35 @@ def test_app(self):
)
assert len(im.applist) == 2
+ assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
+ assert im.app["COM"] == im.info["comment"]
+
+ def test_comment_write(self):
+ with Image.open(TEST_FILE) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
+ # Test that existing comment is saved by default
+ out = BytesIO()
+ im.save(out, format="JPEG")
+ with Image.open(out) as reloaded:
+ assert im.info["comment"] == reloaded.info["comment"]
+
+ # Ensure that a blank comment causes any existing comment to be removed
+ for comment in ("", b"", None):
+ out = BytesIO()
+ im.save(out, format="JPEG", comment=comment)
+ with Image.open(out) as reloaded:
+ assert "comment" not in reloaded.info
+
+ # Test that a comment argument overrides the default comment
+ for comment in ("Test comment text", b"Text comment text"):
+ out = BytesIO()
+ im.save(out, format="JPEG", comment=comment)
+ with Image.open(out) as reloaded:
+ if not isinstance(comment, bytes):
+ comment = comment.encode()
+ assert reloaded.info["comment"] == comment
+
def test_cmyk(self):
# Test CMYK handling. Thanks to Tim and Charlie for test data,
# Michael for getting me to look one more time.
@@ -244,7 +270,10 @@ def test_large_exif(self, tmp_path):
# 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:
@@ -341,7 +370,6 @@ def test_exif_rollback(self):
def test_exif_gps_typeerror(self):
with Image.open("Tests/images/exif_gps_typeerror.jpg") as im:
-
# Should not raise a TypeError
im._getexif()
@@ -415,6 +443,13 @@ def test_exif(self):
info = im._getexif()
assert info[305] == "Adobe Photoshop CS Macintosh"
+ def test_get_child_images(self):
+ with Image.open("Tests/images/flower.jpg") as im:
+ ims = im.get_child_images()
+
+ assert len(ims) == 1
+ 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:
assert im._getmp() is None
@@ -601,12 +636,6 @@ def test_save_low_quality_baseline_qtables(self):
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:
@@ -648,7 +677,6 @@ def test_bad_mpo_header(self):
# 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"
@@ -670,7 +698,6 @@ def test_save_tiff_with_dpi(self, tmp_path):
# 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"])
@@ -697,7 +724,6 @@ def test_dpi_tuple_from_exif(self):
# 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)
@@ -706,7 +732,6 @@ def test_dpi_int_from_exif(self):
# 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)
@@ -715,7 +740,6 @@ def test_dpi_from_dpcm_exif(self):
# 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)
@@ -724,7 +748,6 @@ def test_dpi_exif_zero_division(self):
# 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)
@@ -733,7 +756,6 @@ def test_dpi_exif_string(self):
# Arrange
# 0x011A tag in this exif contains string '300300\x02'
with Image.open("Tests/images/broken_exif_dpi.jpg") as im:
-
# Act / Assert
# This should return the default
assert im.info.get("dpi") == (72, 72)
@@ -743,7 +765,6 @@ def test_no_dpi_in_exif(self):
# 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
@@ -753,7 +774,6 @@ def test_invalid_exif(self):
# 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)
@@ -776,7 +796,6 @@ def test_exif_x_resolution(self, tmp_path):
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)
@@ -786,7 +805,6 @@ def test_ifd_offset_exif(self):
# 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"
@@ -904,6 +922,19 @@ def closure(mode, *args):
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
+ def test_repr_jpeg(self):
+ im = hopper()
+
+ with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
+ assert repr_jpeg.format == "JPEG"
+ assert_image_similar(im, repr_jpeg, 17)
+
+ def test_repr_jpeg_error(self):
+ im = hopper("F")
+
+ with pytest.raises(ValueError):
+ im._repr_jpeg_()
+
@pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg")
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index cd142e67fc7..b6e8215f729 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -4,13 +4,21 @@
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"
@@ -252,11 +260,24 @@ def test_mct():
assert_image_similar(im, jp2, 1.0e-3)
+def test_sgnd(tmp_path):
+ outfile = str(tmp_path / "temp.jp2")
+
+ im = Image.new("L", (1, 1))
+ im.save(outfile)
+ with Image.open(outfile) as reloaded:
+ assert reloaded.getpixel((0, 0)) == 0
+
+ im = Image.new("L", (1, 1))
+ im.save(outfile, signed=True)
+ with Image.open(outfile) as reloaded_signed:
+ assert reloaded_signed.getpixel((0, 0)) == 128
+
+
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()
@@ -340,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",
[
@@ -357,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 1109cd15e99..ac78b086965 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -645,7 +645,6 @@ def test_save_bytesio(self):
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 @@ def test_save_ycbcr(self, tmp_path):
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 @@ def check_write(libtiff):
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 @@ def test_open_missing_samplesperpixel(self):
) 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 @@ def test_save_zero(self, compression, tmp_path):
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 464d138e2af..2588d3a0574 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 d94bdaa96c9..2e921e46701 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -42,7 +42,8 @@ def open():
im = Image.open(test_files[0])
im.load()
- pytest.warns(ResourceWarning, open)
+ with pytest.warns(ResourceWarning):
+ open()
def test_closed_file():
@@ -80,7 +81,10 @@ def test_app(test_file):
@pytest.mark.parametrize("test_file", test_files)
def test_exif(test_file):
- with Image.open(test_file) as im:
+ with Image.open(test_file) as im_original:
+ im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
+
+ for im in (im_original, im_reloaded):
info = im._getexif()
assert info[272] == "Nintendo 3DS"
assert info[296] == 2
@@ -165,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"]
@@ -177,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)
@@ -268,6 +270,7 @@ def test_save_all():
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
+ assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1)
assert_image_similar(im2, im_reloaded, 1)
diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py
index 50d7c590b7a..497052b05e9 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 9667b6a4aad..967f5c35ed3 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -8,7 +8,7 @@
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 im_generator(ims):
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)
@@ -286,6 +317,7 @@ def test_pdf_append_to_bytesio():
@pytest.mark.timeout(1)
+@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
def test_redos(newline):
malicious = b" trailer<<>>" + newline * 3456
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index 37235fe6f02..b460761d838 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -78,9 +78,8 @@ def get_chunks(self, filename):
return chunks
def test_sanity(self, tmp_path):
-
# internal version number
- assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib"))
+ assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
test_file = str(tmp_path / "temp.png")
@@ -156,7 +155,6 @@ def test_bad_ztxt(self):
assert im.info == {"spam": "egg"}
def test_bad_itxt(self):
-
im = load(HEAD + chunk(b"iTXt") + TAIL)
assert im.info == {}
@@ -201,7 +199,6 @@ def test_bad_itxt(self):
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 @@ def test_trns_null(self):
# 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 @@ def test_roundtrip_private_chunk(self):
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",
@@ -706,10 +702,18 @@ def test_exif(self):
assert exif[274] == 3
def test_exif_save(self, tmp_path):
+ # Test exif is not saved from info
+ test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/exif.png") as im:
- test_file = str(tmp_path / "temp.png")
im.save(test_file)
+ with Image.open(test_file) as reloaded:
+ assert reloaded._getexif() is None
+
+ # Test passing in exif
+ with Image.open("Tests/images/exif.png") as im:
+ im.save(test_file, exif=im.getexif())
+
with Image.open(test_file) as reloaded:
exif = reloaded._getexif()
assert exif[274] == 1
@@ -720,7 +724,7 @@ def test_exif_save(self, tmp_path):
def test_exif_from_jpg(self, tmp_path):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
test_file = str(tmp_path / "temp.png")
- im.save(test_file)
+ im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded:
exif = reloaded._getexif()
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index fbcbea6c691..292642ca9f8 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 4f934375c7c..e405834b5ca 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -27,7 +27,8 @@ def open():
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 00000000000..f33eada6110
--- /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 0e3b705a295..09f1ef8e4a6 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -25,7 +25,8 @@ def open():
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 05c78c3161b..edb3206038b 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 5daab47fca3..b27fa25f3e0 100644
--- a/Tests/test_file_tar.py
+++ b/Tests/test_file_tar.py
@@ -10,27 +10,28 @@
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 7d8b5139aa9..1a5730f49d9 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 4f3c8e39010..f13436ce868 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -25,7 +25,6 @@
class TestFileTiff:
def test_sanity(self, tmp_path):
-
filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename)
@@ -62,7 +61,8 @@ def open():
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 @@ def test_context_manager(self):
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]
@@ -114,39 +96,16 @@ def test_mac_tiff(self):
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
- def test_bigtiff(self):
+ def test_bigtiff(self, tmp_path):
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
- @pytest.mark.parametrize(
- "file_name,mode,size,tile",
- [
- (
- "tiff_wrong_bits_per_sample.tiff",
- "RGBA",
- (52, 53),
- [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
- ),
- (
- "tiff_wrong_bits_per_sample_2.tiff",
- "RGB",
- (16, 16),
- [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
- ),
- (
- "tiff_wrong_bits_per_sample_3.tiff",
- "RGBA",
- (512, 256),
- [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
- ),
- ],
- )
- def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
- with Image.open("Tests/images/" + file_name) as im:
- assert im.mode == mode
- assert im.size == size
- assert im.tile == tile
- im.load()
+ with Image.open("Tests/images/hopper_bigtiff.tif") as im:
+ # multistrip support not yet implemented
+ del im.tag_v2[273]
+
+ outfile = str(tmp_path / "temp.tif")
+ im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_set_legacy_api(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
@@ -157,7 +116,6 @@ def test_set_legacy_api(self):
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 +129,6 @@ def test_xyres_tiff(self):
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 +143,6 @@ def test_xyres_fallback_tiff(self):
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 +191,8 @@ def test_invalid_file(self):
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 +205,12 @@ def test_save_unsupported_mode(self, tmp_path):
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 +344,6 @@ def test_frame_order(self):
def test___str__(self):
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
-
# Act
ret = str(im.ifd)
@@ -392,7 +354,6 @@ def test_dict(self):
# Arrange
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
-
# v2 interface
v2_tags = {
256: 55,
@@ -630,7 +591,6 @@ def test_with_underscores(self, tmp_path):
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 d38c1c523ea..b7d100e7a05 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,
@@ -185,20 +182,54 @@ def test_iptc(tmp_path):
im.save(out)
-def test_writing_bytes_to_ascii(tmp_path):
- im = hopper()
+@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
+def test_writing_other_types_to_ascii(value, expected, tmp_path):
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271]
assert tag.type == TiffTags.ASCII
- info[271] = b"test"
+ info[271] = value
+
+ im = hopper()
+ out = str(tmp_path / "temp.tiff")
+ im.save(out, tiffinfo=info)
+
+ with Image.open(out) as reloaded:
+ assert reloaded.tag_v2[271] == expected
+
+
+@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] = value
+
+ out = str(tmp_path / "temp.tiff")
+ im.save(out, tiffinfo=info)
+
+ with Image.open(out) as reloaded:
+ 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[271] == "test"
+ assert reloaded.tag_v2[33723] == b"1"
def test_undefined_zero(tmp_path):
@@ -221,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):
@@ -387,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("= 0.5, the transparent area will be filled with black
+ # (or something more conducive to compression)
+ assert_image_equal(reloaded.convert("RGB"), image)
+
+
def test_write_unsupported_mode_PA(tmp_path):
"""
Saving a palette-based file with transparency to WebP format
diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py
index c621df0d99c..2fd5e548406 100644
--- a/Tests/test_file_webp_animated.py
+++ b/Tests/test_file_webp_animated.py
@@ -134,6 +134,18 @@ def test_timestamp_and_duration(tmp_path):
ts += durations[frame]
+def test_float_duration(tmp_path):
+ temp_file = str(tmp_path / "temp.webp")
+ with Image.open("Tests/images/iss634.apng") as im:
+ assert im.info["duration"] == 70.0
+
+ im.save(temp_file, save_all=True)
+
+ with Image.open(temp_file) as reloaded:
+ reloaded.load()
+ assert reloaded.info["duration"] == 70
+
+
def test_seeking(tmp_path):
"""
Create an animated WebP file, and then try seeking through frames in reverse-order,
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index f77a245c035..037479f9fbb 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -11,12 +11,15 @@
skip_unless_feature("webp_mux"),
]
+try:
+ from defusedxml import ElementTree
+except ImportError:
+ ElementTree = None
-def test_read_exif_metadata():
+def test_read_exif_metadata():
file_path = "Tests/images/flower.webp"
with Image.open(file_path) as image:
-
assert image.format == "WEBP"
exif_data = image.info.get("exif", None)
assert exif_data
@@ -59,10 +62,8 @@ def test_write_exif_metadata():
def test_read_icc_profile():
-
file_path = "Tests/images/flower2.webp"
with Image.open(file_path) as image:
-
assert image.format == "WEBP"
assert image.info.get("icc_profile", None)
@@ -110,6 +111,22 @@ def test_read_no_exif():
assert not webp_image._getexif()
+def test_getxmp():
+ with Image.open("Tests/images/flower.webp") as im:
+ assert "xmp" not in im.info
+ assert im.getxmp() == {}
+
+ with Image.open("Tests/images/flower2.webp") as im:
+ if ElementTree is None:
+ with pytest.warns(UserWarning):
+ assert im.getxmp() == {}
+ else:
+ assert (
+ im.getxmp()["xmpmeta"]["xmptk"]
+ == "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
+ )
+
+
@skip_unless_feature("webp_anim")
def test_write_animated_metadata(tmp_path):
iccp_data = b""
diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py
index 439cb15bca9..7c8b54fd173 100644
--- a/Tests/test_file_wmf.py
+++ b/Tests/test_file_wmf.py
@@ -6,7 +6,6 @@
def test_load_raw():
-
# Test basic EMF open and rendering
with Image.open("Tests/images/drawing.emf") as im:
if hasattr(Image.core, "drawwmf"):
diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py
index 9c54c675560..d2c05b78a82 100644
--- a/Tests/test_file_xbm.py
+++ b/Tests/test_file_xbm.py
@@ -44,7 +44,6 @@ def test_open():
# Act
with Image.open(filename) as im:
-
# Assert
assert im.mode == "1"
assert im.size == (128, 128)
@@ -57,7 +56,6 @@ def test_open_filename_with_underscore():
# Act
with Image.open(filename) as im:
-
# Assert
assert im.mode == "1"
assert im.size == (128, 128)
diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py
index ae53d2b6357..9efe7ec1438 100644
--- a/Tests/test_file_xvthumb.py
+++ b/Tests/test_file_xvthumb.py
@@ -10,7 +10,6 @@
def test_open():
# Act
with Image.open(TEST_FILE) as im:
-
# Assert
assert im.format == "XVThumb"
diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py
new file mode 100644
index 00000000000..27663f396ea
--- /dev/null
+++ b/Tests/test_font_crash.py
@@ -0,0 +1,22 @@
+import pytest
+
+from PIL import Image, ImageDraw, ImageFont
+
+from .helper import skip_unless_feature
+
+
+class TestFontCrash:
+ def _fuzz_font(self, font):
+ # from fuzzers.fuzz_font
+ font.getbbox("ABC")
+ font.getmask("test text")
+ with Image.new(mode="RGBA", size=(200, 200)) as im:
+ draw = ImageDraw.Draw(im)
+ draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2)
+ draw.text((10, 10), "Test Text", font=font, fill="#000")
+
+ @skip_unless_feature("freetype2")
+ def test_segfault(self):
+ with pytest.raises(OSError):
+ font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
+ self._fuzz_font(font)
diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py
index c217378fb74..815ef1d9254 100644
--- a/Tests/test_font_pcf.py
+++ b/Tests/test_font_pcf.py
@@ -82,9 +82,6 @@ def test_textsize(request, tmp_path):
assert dy == 20
assert dx in (0, 10)
assert font.getlength(chr(i)) == dx
- with pytest.warns(DeprecationWarning) as log:
- assert font.getsize(chr(i)) == (dx, dy)
- assert len(log) == 1
for i in range(len(message)):
msg = message[: i + 1]
assert font.getlength(msg) == len(msg) * 10
diff --git a/Tests/test_image.py b/Tests/test_image.py
index e579034904d..85f9f7d0231 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -7,7 +7,14 @@
import pytest
-from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features
+from PIL import (
+ ExifTags,
+ Image,
+ ImageDraw,
+ ImagePalette,
+ UnidentifiedImageError,
+ features,
+)
from .helper import (
assert_image_equal,
@@ -41,6 +48,9 @@ class TestImage:
"RGBX",
"RGBA",
"RGBa",
+ "BGR;15",
+ "BGR;16",
+ "BGR;24",
"CMYK",
"YCbCr",
"LAB",
@@ -50,9 +60,7 @@ class TestImage:
def test_image_modes_success(self, mode):
Image.new(mode, (1, 1))
- @pytest.mark.parametrize(
- "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32")
- )
+ @pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode):
with pytest.raises(ValueError) as e:
Image.new(mode, (1, 1))
@@ -62,7 +70,6 @@ def test_exception_inheritance(self):
assert issubclass(UnidentifiedImageError, OSError)
def test_sanity(self):
-
im = Image.new("L", (100, 100))
assert repr(im)[:45] == ">> 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 178cfcef359..a12ce329fd1 100644
--- a/Tests/test_image_tobitmap.py
+++ b/Tests/test_image_tobitmap.py
@@ -4,7 +4,6 @@
def test_sanity():
-
with pytest.raises(ValueError):
hopper().tobitmap()
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index a78349801fc..64a5c94596f 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -42,12 +42,12 @@ def test_palette(self):
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 @@ def test_quad(self):
# 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 @@ def test_mesh(self):
# 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 @@ def _test_nearest(self, op, mode):
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 b839a7b140a..d0fea385433 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 3d8dbe6bbf5..8efe063c11d 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_imagecolor.py b/Tests/test_imagecolor.py
index dcc44e6e342..2fae6151cfd 100644
--- a/Tests/test_imagecolor.py
+++ b/Tests/test_imagecolor.py
@@ -193,6 +193,10 @@ def test_rounding_errors():
Image.new("LA", (1, 1), "white")
+def test_color_hsv():
+ assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV")
+
+
def test_color_too_long():
# Arrange
color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)"
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 76b7c65cc37..7497fdc66a8 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -27,15 +27,21 @@
Y0 = int(H / 4)
Y1 = int(X0 * 3)
-# Two kinds of bounding box
-BBOX1 = [(X0, Y0), (X1, Y1)]
-BBOX2 = [X0, Y0, X1, Y1]
-
-# Two kinds of coordinate sequences
-POINTS1 = [(10, 10), (20, 40), (30, 30)]
-POINTS2 = [10, 10, 20, 40, 30, 30]
+# Bounding boxes
+BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1])
+
+# Coordinate sequences
+POINTS = (
+ ((10, 10), (20, 40), (30, 30)),
+ [(10, 10), (20, 40), (30, 30)],
+ (10, 10, 20, 40, 30, 30),
+ [10, 10, 20, 40, 30, 30],
+)
-KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)]
+KITE_POINTS = (
+ ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)),
+ [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)],
+)
def test_sanity():
@@ -52,7 +58,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))
@@ -64,7 +69,7 @@ def test_mode_mismatch():
ImageDraw.ImageDraw(im, mode="L")
-@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
def test_arc(bbox, start, end):
# Arrange
@@ -78,7 +83,8 @@ def test_arc(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1)
-def test_arc_end_le_start():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_arc_end_le_start(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -86,13 +92,14 @@ def test_arc_end_le_start():
end = 0
# Act
- draw.arc(BBOX1, start=start, end=end)
+ draw.arc(bbox, start=start, end=end)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png")
-def test_arc_no_loops():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_arc_no_loops(bbox):
# No need to go in loops
# Arrange
im = Image.new("RGB", (W, H))
@@ -101,57 +108,61 @@ def test_arc_no_loops():
end = 370
# Act
- draw.arc(BBOX1, start=start, end=end)
+ draw.arc(bbox, start=start, end=end)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1)
-def test_arc_width():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_arc_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.arc(BBOX1, 10, 260, width=5)
+ draw.arc(bbox, 10, 260, width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1)
-def test_arc_width_pieslice_large():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_arc_width_pieslice_large(bbox):
# Tests an arc with a large enough width that it is a pieslice
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.arc(BBOX1, 10, 260, fill="yellow", width=100)
+ draw.arc(bbox, 10, 260, fill="yellow", width=100)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1)
-def test_arc_width_fill():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_arc_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.arc(BBOX1, 10, 260, fill="yellow", width=5)
+ draw.arc(bbox, 10, 260, fill="yellow", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1)
-def test_arc_width_non_whole_angle():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_arc_width_non_whole_angle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png"
# Act
- draw.arc(BBOX1, 10, 259.5, width=5)
+ draw.arc(bbox, 10, 259.5, width=5)
# Assert
assert_image_similar_tofile(im, expected, 1)
@@ -185,7 +196,7 @@ def test_bitmap():
@pytest.mark.parametrize("mode", ("RGB", "L"))
-@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("bbox", BBOX)
def test_chord(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
@@ -199,37 +210,40 @@ def test_chord(mode, bbox):
assert_image_similar_tofile(im, expected, 1)
-def test_chord_width():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_chord_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.chord(BBOX1, 10, 260, outline="yellow", width=5)
+ draw.chord(bbox, 10, 260, outline="yellow", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1)
-def test_chord_width_fill():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_chord_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5)
+ draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1)
-def test_chord_zero_width():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_chord_zero_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0)
+ draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=0)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png")
@@ -248,7 +262,7 @@ def test_chord_too_fat():
@pytest.mark.parametrize("mode", ("RGB", "L"))
-@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
@@ -262,13 +276,14 @@ def test_ellipse(mode, bbox):
assert_image_similar_tofile(im, expected, 1)
-def test_ellipse_translucent():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_ellipse_translucent(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
# Act
- draw.ellipse(BBOX1, fill=(0, 255, 0, 127))
+ draw.ellipse(bbox, fill=(0, 255, 0, 127))
# Assert
expected = "Tests/images/imagedraw_ellipse_translucent.png"
@@ -298,13 +313,14 @@ def test_ellipse_symmetric():
assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT))
-def test_ellipse_width():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_ellipse_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.ellipse(BBOX1, outline="blue", width=5)
+ draw.ellipse(bbox, outline="blue", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1)
@@ -322,25 +338,27 @@ def test_ellipse_width_large():
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1)
-def test_ellipse_width_fill():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_ellipse_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.ellipse(BBOX1, fill="green", outline="blue", width=5)
+ draw.ellipse(bbox, fill="green", outline="blue", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1)
-def test_ellipse_zero_width():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_ellipse_zero_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.ellipse(BBOX1, fill="green", outline="blue", width=0)
+ draw.ellipse(bbox, fill="green", outline="blue", width=0)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png")
@@ -356,7 +374,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:
@@ -381,7 +405,7 @@ def test_ellipse_various_sizes_filled():
)
-@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+@pytest.mark.parametrize("points", POINTS)
def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
@@ -453,7 +477,7 @@ def test_transform():
assert_image_equal(im, expected)
-@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
def test_pieslice(bbox, start, end):
# Arrange
@@ -467,38 +491,41 @@ def test_pieslice(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1)
-def test_pieslice_width():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_pieslice_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.pieslice(BBOX1, 10, 260, outline="blue", width=5)
+ draw.pieslice(bbox, 10, 260, outline="blue", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1)
-def test_pieslice_width_fill():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_pieslice_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_pieslice_width_fill.png"
# Act
- draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5)
+ draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=5)
# Assert
assert_image_similar_tofile(im, expected, 1)
-def test_pieslice_zero_width():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_pieslice_zero_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=0)
+ draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=0)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png")
@@ -546,7 +573,7 @@ def test_pieslice_no_spikes():
assert_image_equal(im, im_pre_erase)
-@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+@pytest.mark.parametrize("points", POINTS)
def test_point(points):
# Arrange
im = Image.new("RGB", (W, H))
@@ -559,7 +586,7 @@ def test_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
-@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+@pytest.mark.parametrize("points", POINTS)
def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
@@ -573,7 +600,8 @@ def test_polygon(points):
@pytest.mark.parametrize("mode", ("RGB", "L"))
-def test_polygon_kite(mode):
+@pytest.mark.parametrize("kite_points", KITE_POINTS)
+def test_polygon_kite(mode, kite_points):
# Test drawing lines of different gradients (dx>dy, dy>dx) and
# vertical (dx==0) and horizontal (dy==0) lines
# Arrange
@@ -582,7 +610,7 @@ def test_polygon_kite(mode):
expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png"
# Act
- draw.polygon(KITE_POINTS, fill="blue", outline="yellow")
+ draw.polygon(kite_points, fill="blue", outline="yellow")
# Assert
assert_image_equal_tofile(im, expected)
@@ -629,7 +657,7 @@ def test_polygon_translucent():
assert_image_equal_tofile(im, expected)
-@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
@@ -656,63 +684,68 @@ def test_big_rectangle():
assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1)
-def test_rectangle_width():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_rectangle_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_rectangle_width.png"
# Act
- draw.rectangle(BBOX1, outline="green", width=5)
+ draw.rectangle(bbox, outline="green", width=5)
# Assert
assert_image_equal_tofile(im, expected)
-def test_rectangle_width_fill():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_rectangle_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_rectangle_width_fill.png"
# Act
- draw.rectangle(BBOX1, fill="blue", outline="green", width=5)
+ draw.rectangle(bbox, fill="blue", outline="green", width=5)
# Assert
assert_image_equal_tofile(im, expected)
-def test_rectangle_zero_width():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_rectangle_zero_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.rectangle(BBOX1, fill="blue", outline="green", width=0)
+ draw.rectangle(bbox, fill="blue", outline="green", width=0)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png")
-def test_rectangle_I16():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_rectangle_I16(bbox):
# Arrange
im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.rectangle(BBOX1, fill="black", outline="green")
+ draw.rectangle(bbox, fill="black", outline="green")
# Assert
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
-def test_rectangle_translucent_outline():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_rectangle_translucent_outline(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
# Act
- draw.rectangle(BBOX1, fill="black", outline=(0, 255, 0, 127), width=5)
+ draw.rectangle(bbox, fill="black", outline=(0, 255, 0, 127), width=5)
# Assert
assert_image_equal_tofile(
@@ -736,6 +769,36 @@ def test_rounded_rectangle(xy):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png")
+@pytest.mark.parametrize("top_left", (True, False))
+@pytest.mark.parametrize("top_right", (True, False))
+@pytest.mark.parametrize("bottom_right", (True, False))
+@pytest.mark.parametrize("bottom_left", (True, False))
+def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_left):
+ corners = (top_left, top_right, bottom_right, bottom_left)
+
+ # Arrange
+ im = Image.new("RGB", (200, 200))
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ draw.rounded_rectangle(
+ (10, 20, 190, 180), 30, fill="red", outline="green", width=5, corners=corners
+ )
+
+ # Assert
+ suffix = "".join(
+ (
+ ("y" if top_left else "n"),
+ ("y" if top_right else "n"),
+ ("y" if bottom_right else "n"),
+ ("y" if bottom_left else "n"),
+ )
+ )
+ assert_image_equal_tofile(
+ im, "Tests/images/imagedraw_rounded_rectangle_corners_" + suffix + ".png"
+ )
+
+
@pytest.mark.parametrize(
"xy, radius, type",
[
@@ -759,13 +822,14 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type):
)
-def test_rounded_rectangle_zero_radius():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_rounded_rectangle_zero_radius(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
- draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5)
+ draw.rounded_rectangle(bbox, 0, fill="blue", outline="green", width=5)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png")
@@ -775,7 +839,9 @@ def test_rounded_rectangle_zero_radius():
"xy, suffix",
[
((20, 10, 80, 90), "x"),
+ ((20, 10, 81, 90), "x_odd"),
((10, 20, 90, 80), "y"),
+ ((10, 20, 90, 81), "y_odd"),
((20, 20, 80, 80), "both"),
],
)
@@ -795,14 +861,15 @@ def test_rounded_rectangle_translucent(xy, suffix):
)
-def test_floodfill():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_floodfill(bbox):
red = ImageColor.getrgb("red")
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
- draw.rectangle(BBOX2, outline="yellow", fill="green")
+ draw.rectangle(bbox, outline="yellow", fill="green")
centre_point = (int(W / 2), int(H / 2))
# Act
@@ -827,13 +894,14 @@ def test_floodfill():
assert_image_equal(im, Image.new("RGB", (1, 1), red))
-def test_floodfill_border():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_floodfill_border(bbox):
# floodfill() is experimental
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
- draw.rectangle(BBOX2, outline="yellow", fill="green")
+ draw.rectangle(bbox, outline="yellow", fill="green")
centre_point = (int(W / 2), int(H / 2))
# Act
@@ -848,13 +916,14 @@ def test_floodfill_border():
assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png")
-def test_floodfill_thresh():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_floodfill_thresh(bbox):
# floodfill() is experimental
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
- draw.rectangle(BBOX2, outline="darkgreen", fill="green")
+ draw.rectangle(bbox, outline="darkgreen", fill="green")
centre_point = (int(W / 2), int(H / 2))
# Act
@@ -903,9 +972,6 @@ def test_square():
img, draw = create_base_image_draw((10, 10))
draw.rectangle((2, 2, 7, 7), BLACK)
assert_image_equal_tofile(img, expected, "square as normal rectangle failed")
- img, draw = create_base_image_draw((10, 10))
- draw.rectangle((7, 7, 2, 2), BLACK)
- assert_image_equal_tofile(img, expected, "square as inverted rectangle failed")
def test_triangle_right():
@@ -1192,21 +1258,6 @@ def test_textbbox_stroke():
assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50)
-def test_textsize_deprecation():
- im = Image.new("RGB", (W, H))
- draw = ImageDraw.Draw(im)
-
- with pytest.warns(DeprecationWarning) as log:
- draw.textsize("Hello")
- assert len(log) == 1
- with pytest.warns(DeprecationWarning) as log:
- draw.textsize("Hello\nWorld")
- assert len(log) == 1
- with pytest.warns(DeprecationWarning) as log:
- draw.multiline_textsize("Hello\nWorld")
- assert len(log) == 1
-
-
@skip_unless_feature("freetype2")
def test_stroke():
for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items():
@@ -1238,6 +1289,27 @@ def test_stroke_descender():
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76)
+@skip_unless_feature("freetype2")
+def test_split_word():
+ # Arrange
+ im = Image.new("RGB", (230, 55))
+ expected = im.copy()
+ expected_draw = ImageDraw.Draw(expected)
+ font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 48)
+ expected_draw.text((0, 0), "paradise", font=font)
+
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ draw.text((0, 0), "par", font=font)
+
+ length = draw.textlength("par", font=font)
+ draw.text((length, 0), "adise", font=font)
+
+ # Assert
+ assert_image_equal(im, expected)
+
+
@skip_unless_feature("freetype2")
def test_stroke_multiline():
# Arrange
@@ -1271,7 +1343,8 @@ def test_setting_default_font():
assert isinstance(draw.getfont(), ImageFont.ImageFont)
-def test_same_color_outline():
+@pytest.mark.parametrize("bbox", BBOX)
+def test_same_color_outline(bbox):
# Prepare shape
x0, y0 = 5, 5
x1, y1 = 5, 50
@@ -1287,12 +1360,12 @@ def test_same_color_outline():
for mode in ["RGB", "L"]:
for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]:
for operation, args in {
- "chord": [BBOX1, 0, 180],
- "ellipse": [BBOX1],
+ "chord": [bbox, 0, 180],
+ "ellipse": [bbox],
"shape": [s],
- "pieslice": [BBOX1, -90, 45],
+ "pieslice": [bbox, -90, 45],
"polygon": [[(18, 30), (85, 30), (60, 72)]],
- "rectangle": [BBOX1],
+ "rectangle": [bbox],
}.items():
# Arrange
im = Image.new(mode, (W, H))
@@ -1309,20 +1382,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)
@@ -1449,3 +1522,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 6fc829f1a54..a2c2fa1f010 100644
--- a/Tests/test_imagedraw2.py
+++ b/Tests/test_imagedraw2.py
@@ -2,7 +2,7 @@
import pytest
-from PIL import Image, ImageDraw, ImageDraw2
+from PIL import Image, ImageDraw, ImageDraw2, features
from .helper import (
assert_image_equal,
@@ -27,15 +27,16 @@
Y0 = int(H / 4)
Y1 = int(X0 * 3)
-# Two kinds of bounding box
-BBOX1 = [(X0, Y0), (X1, Y1)]
-BBOX2 = [X0, Y0, X1, Y1]
+# Bounding boxes
+BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1])
-# Two kinds of coordinate sequences
-POINTS1 = [(10, 10), (20, 40), (30, 30)]
-POINTS2 = [10, 10, 20, 40, 30, 30]
-
-KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)]
+# Coordinate sequences
+POINTS = (
+ ((10, 10), (20, 40), (30, 30)),
+ [(10, 10), (20, 40), (30, 30)],
+ (10, 10, 20, 40, 30, 30),
+ [10, 10, 20, 40, 30, 30],
+)
FONT_PATH = "Tests/fonts/FreeMono.ttf"
@@ -52,7 +53,7 @@ def test_sanity():
draw.line(list(range(10)), pen)
-@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(bbox):
# Arrange
im = Image.new("RGB", (W, H))
@@ -80,7 +81,7 @@ def test_ellipse_edge():
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1)
-@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+@pytest.mark.parametrize("points", POINTS)
def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
@@ -94,7 +95,8 @@ def test_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
-def test_line_pen_as_brush():
+@pytest.mark.parametrize("points", POINTS)
+def test_line_pen_as_brush(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -103,13 +105,13 @@ def test_line_pen_as_brush():
# Act
# Pass in the pen as the brush parameter
- draw.line(POINTS1, pen, brush)
+ draw.line(points, pen, brush)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
-@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+@pytest.mark.parametrize("points", POINTS)
def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
@@ -124,7 +126,7 @@ def test_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
-@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
@@ -171,19 +173,18 @@ def test_text():
@skip_unless_feature("freetype2")
-def test_textsize():
+def test_textbbox():
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
font = ImageDraw2.Font("white", FONT_PATH)
# Act
- with pytest.warns(DeprecationWarning) as log:
- size = draw.textsize("ImageDraw2", font)
- assert len(log) == 1
+ bbox = draw.textbbox((0, 0), "ImageDraw2", font)
# Assert
- assert size[1] == 12
+ right = 72 if features.check_feature("raqm") else 70
+ assert bbox == (0, 2, right, 12)
@skip_unless_feature("freetype2")
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index fc0fbfb9bbc..412bc10d9a8 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -30,7 +30,6 @@
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 306a2f1bff6..02622e72138 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -191,6 +191,16 @@ def test_getlength(
assert length == length_raqm
+def test_float_size():
+ lengths = []
+ for size in (48, 48.5, 49):
+ f = ImageFont.truetype(
+ "Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine
+ )
+ lengths.append(f.getlength("text"))
+ assert lengths[0] != lengths[1] != lengths[2]
+
+
def test_render_multiline(font):
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -251,27 +261,6 @@ def test_draw_align(font):
draw.text((100, 40), line, (0, 0, 0), font=font, align="left")
-def test_multiline_size(font):
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
-
- with pytest.warns(DeprecationWarning) as log:
- # Test that textsize() correctly connects to multiline_textsize()
- assert draw.textsize(TEST_TEXT, font=font) == draw.multiline_textsize(
- TEST_TEXT, font=font
- )
-
- # Test that multiline_textsize corresponds to ImageFont.textsize()
- # for single line text
- assert font.getsize("A") == draw.multiline_textsize("A", font=font)
-
- # Test that textsize() can pass on additional arguments
- # to multiline_textsize()
- draw.textsize(TEST_TEXT, font=font, spacing=4)
- draw.textsize(TEST_TEXT, font, 4)
- assert len(log) == 6
-
-
def test_multiline_bbox(font):
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -298,12 +287,6 @@ def test_multiline_width(font):
draw.textbbox((0, 0), "longest line", font=font)[2]
== draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2]
)
- with pytest.warns(DeprecationWarning) as log:
- assert (
- draw.textsize("longest line", font=font)[0]
- == draw.multiline_textsize("longest line\nline", font=font)[0]
- )
- assert len(log) == 2
def test_multiline_spacing(font):
@@ -326,32 +309,27 @@ def test_rotated_transposed_font(font, orientation):
# Original font
draw.font = font
- with pytest.warns(DeprecationWarning) as log:
- box_size_a = draw.textsize(word)
- assert box_size_a == font.getsize(word)
- assert len(log) == 2
bbox_a = draw.textbbox((10, 10), word)
# Rotated font
draw.font = transposed_font
- with pytest.warns(DeprecationWarning) as log:
- box_size_b = draw.textsize(word)
- assert box_size_b == transposed_font.getsize(word)
- assert len(log) == 2
bbox_b = draw.textbbox((20, 20), word)
- # Check (w,h) of box a is (h,w) of box b
- assert box_size_a[0] == box_size_b[1]
- assert box_size_a[1] == box_size_b[0]
+ # Check (w, h) of box a is (h, w) of box b
+ assert (
+ bbox_a[2] - bbox_a[0],
+ bbox_a[3] - bbox_a[1],
+ ) == (
+ bbox_b[3] - bbox_b[1],
+ bbox_b[2] - bbox_b[0],
+ )
- # Check bbox b is (20, 20, 20 + h, 20 + w)
- assert bbox_b[0] == 20
- assert bbox_b[1] == 20
- assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1]
- assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0]
+ # Check top left co-ordinates are correct
+ assert bbox_b[:2] == (20, 20)
# text length is undefined for vertical text
- pytest.raises(ValueError, draw.textlength, word)
+ with pytest.raises(ValueError):
+ draw.textlength(word)
@pytest.mark.parametrize(
@@ -372,28 +350,25 @@ def test_unrotated_transposed_font(font, orientation):
# Original font
draw.font = font
- with pytest.warns(DeprecationWarning) as log:
- box_size_a = draw.textsize(word)
- assert len(log) == 1
bbox_a = draw.textbbox((10, 10), word)
length_a = draw.textlength(word)
# Rotated font
draw.font = transposed_font
- with pytest.warns(DeprecationWarning) as log:
- box_size_b = draw.textsize(word)
- assert len(log) == 1
bbox_b = draw.textbbox((20, 20), word)
length_b = draw.textlength(word)
# Check boxes a and b are same size
- assert box_size_a == box_size_b
+ assert (
+ bbox_a[2] - bbox_a[0],
+ bbox_a[3] - bbox_a[1],
+ ) == (
+ bbox_b[2] - bbox_b[0],
+ bbox_b[3] - bbox_b[1],
+ )
- # Check bbox b is (20, 20, 20 + w, 20 + h)
- assert bbox_b[0] == 20
- assert bbox_b[1] == 20
- assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0]
- assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1]
+ # Check top left co-ordinates are correct
+ assert bbox_b[:2] == (20, 20)
assert length_a == length_b
@@ -446,19 +421,6 @@ def test_free_type_font_get_metrics(font):
assert (ascent, descent) == (16, 4)
-def test_free_type_font_get_offset(font):
- # Arrange
- text = "offset this"
-
- # Act
- with pytest.warns(DeprecationWarning) as log:
- offset = font.getoffset(text)
-
- # Assert
- assert len(log) == 1
- assert offset == (0, 3)
-
-
def test_free_type_font_get_mask(font):
# Arrange
text = "mask this"
@@ -501,6 +463,11 @@ def test_default_font():
assert_image_equal_tofile(im, "Tests/images/default_font.png")
+@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
+def test_getbbox(font, mode):
+ assert (0, 4, 12, 16) == font.getbbox("A", mode)
+
+
def test_getbbox_empty(font):
# issue #2614, should not crash.
assert (0, 0, 0, 0) == font.getbbox("")
@@ -617,19 +584,6 @@ def test_imagefont_getters(font):
assert font.getlength("M") == 12
assert font.getlength("y") == 12
assert font.getlength("a") == 12
- with pytest.warns(DeprecationWarning) as log:
- assert font.getsize("A") == (12, 16)
- assert font.getsize("AB") == (24, 16)
- assert font.getsize("M") == (12, 16)
- assert font.getsize("y") == (12, 20)
- assert font.getsize("a") == (12, 16)
- assert font.getsize_multiline("A") == (12, 16)
- assert font.getsize_multiline("AB") == (24, 16)
- assert font.getsize_multiline("a") == (12, 16)
- assert font.getsize_multiline("ABC\n") == (36, 36)
- assert font.getsize_multiline("ABC\nA") == (36, 36)
- assert font.getsize_multiline("ABC\nAaaa") == (48, 36)
- assert len(log) == 11
@pytest.mark.parametrize("stroke_width", (0, 2))
@@ -640,16 +594,6 @@ def test_getsize_stroke(font, stroke_width):
12 + stroke_width,
16 + stroke_width,
)
- with pytest.warns(DeprecationWarning) as log:
- assert font.getsize("A", stroke_width=stroke_width) == (
- 12 + stroke_width * 2,
- 16 + stroke_width * 2,
- )
- assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == (
- 48 + stroke_width * 2,
- 36 + stroke_width * 4,
- )
- assert len(log) == 2
def test_complex_font_settings():
@@ -780,11 +724,8 @@ def test_textbbox_non_freetypefont():
im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im)
default_font = ImageFont.load_default()
- with pytest.warns(DeprecationWarning) as log:
- width, height = d.textsize("test", font=default_font)
- assert len(log) == 1
- assert d.textlength("test", font=default_font) == width
- assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height)
+ assert d.textlength("test", font=default_font) == 24
+ assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
@pytest.mark.parametrize(
@@ -872,25 +813,23 @@ def test_anchor_invalid(font):
d.font = font
for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]:
- pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor))
- pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor))
- pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor))
- pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor))
- pytest.raises(
- ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
- )
- pytest.raises(
- ValueError,
- lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
- )
+ with pytest.raises(ValueError):
+ font.getmask2("hello", anchor=anchor)
+ with pytest.raises(ValueError):
+ font.getbbox("hello", anchor=anchor)
+ with pytest.raises(ValueError):
+ d.text((0, 0), "hello", anchor=anchor)
+ with pytest.raises(ValueError):
+ d.textbbox((0, 0), "hello", anchor=anchor)
+ with pytest.raises(ValueError):
+ d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
+ with pytest.raises(ValueError):
+ d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor)
for anchor in ["lt", "lb"]:
- pytest.raises(
- ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
- )
- pytest.raises(
- ValueError,
- lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
- )
+ with pytest.raises(ValueError):
+ d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
+ with pytest.raises(ValueError):
+ d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor)
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
@@ -1084,14 +1023,6 @@ def test_woff2(layout_engine):
assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5)
-def test_fill_deprecation(font):
- with pytest.warns(DeprecationWarning):
- font.getmask2("Hello world", fill=Image.core.fill)
- with pytest.warns(DeprecationWarning):
- with pytest.raises(TypeError):
- font.getmask2("Hello world", fill=None)
-
-
def test_render_mono_size():
# issue 4177
@@ -1107,10 +1038,30 @@ def test_render_mono_size():
assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
+def test_too_many_characters(font):
+ with pytest.raises(ValueError):
+ font.getlength("A" * 1_000_001)
+ with pytest.raises(ValueError):
+ font.getbbox("A" * 1_000_001)
+ with pytest.raises(ValueError):
+ font.getmask2("A" * 1_000_001)
+
+ transposed_font = ImageFont.TransposedFont(font)
+ with pytest.raises(ValueError):
+ transposed_font.getlength("A" * 1_000_001)
+
+ default_font = ImageFont.load_default()
+ with pytest.raises(ValueError):
+ default_font.getlength("A" * 1_000_001)
+ with pytest.raises(ValueError):
+ default_font.getbbox("A" * 1_000_001)
+
+
@pytest.mark.parametrize(
"test_file",
[
"Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf",
+ "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
],
)
def test_oom(test_file):
@@ -1131,12 +1082,3 @@ def test_raqm_missing_warning(monkeypatch):
"Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout."
)
-
-
-def test_constants_deprecation():
- for enum, prefix in {
- ImageFont.Layout: "LAYOUT_",
- }.items():
- for name in enum.__members__:
- with pytest.warns(DeprecationWarning):
- assert getattr(ImageFont, prefix + name) == enum[name]
diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py
index cf039e86e61..6099b04e44b 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 fa2291582d4..f8059eca443 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -1,4 +1,5 @@
import os
+import shutil
import subprocess
import sys
@@ -33,7 +34,9 @@ def test_grab_x11(self):
@pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB")
def test_grab_no_xcb(self):
- if sys.platform not in ("win32", "darwin"):
+ if sys.platform not in ("win32", "darwin") and not shutil.which(
+ "gnome-screenshot"
+ ):
with pytest.raises(OSError) as e:
ImageGrab.grab()
assert str(e.value).startswith("Pillow was built without XCB support")
@@ -61,9 +64,13 @@ def test_grabclipboard(self):
)
p.communicate()
else:
- with pytest.raises(NotImplementedError) as e:
- ImageGrab.grabclipboard()
- assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only"
+ if not shutil.which("wl-paste") and not shutil.which("xclip"):
+ with pytest.raises(
+ NotImplementedError,
+ match="wl-paste or xclip is required for"
+ r" ImageGrab.grabclipboard\(\) on Linux",
+ ):
+ ImageGrab.grabclipboard()
return
ImageGrab.grabclipboard()
@@ -91,3 +98,18 @@ def test_grabclipboard_png(self):
im = ImageGrab.grabclipboard()
assert_image_equal_tofile(im, "Tests/images/hopper.png")
+
+ @pytest.mark.skipif(
+ (
+ sys.platform != "linux"
+ or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy"))
+ ),
+ reason="Linux with wl-clipboard only",
+ )
+ @pytest.mark.parametrize("ext", ("gif", "png", "ico"))
+ def test_grabclipboard_wl_clipboard(self, ext):
+ image_path = "Tests/images/hopper." + ext
+ with open(image_path, "rb") as fp:
+ subprocess.call(["wl-copy"], stdin=fp)
+ im = ImageGrab.grabclipboard()
+ assert_image_equal_tofile(im, image_path)
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index c9b2fd865b8..b05785be0ec 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -21,7 +21,6 @@ def getmesh(self, im):
def test_sanity():
-
ImageOps.autocontrast(hopper("L"))
ImageOps.autocontrast(hopper("RGB"))
@@ -405,6 +404,18 @@ def check(orientation_im):
assert 0x0112 not in transposed_im.getexif()
+def test_exif_transpose_in_place():
+ with Image.open("Tests/images/orientation_rectangle.jpg") as im:
+ assert im.size == (2, 1)
+ assert im.getexif()[0x0112] == 8
+ expected = im.rotate(90, expand=True)
+
+ ImageOps.exif_transpose(im, in_place=True)
+ assert im.size == (1, 2)
+ assert 0x0112 not in im.getexif()
+ assert_image_equal(im, expected)
+
+
def test_autocontrast_cutoff():
# Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:
@@ -419,7 +430,6 @@ def 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 +449,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 5bda2811717..baa698bb4f4 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -6,14 +6,9 @@
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_imagepath.py b/Tests/test_imagepath.py
index de3920cf5eb..c112cfd87aa 100644
--- a/Tests/test_imagepath.py
+++ b/Tests/test_imagepath.py
@@ -8,7 +8,6 @@
def test_path():
-
p = ImagePath.Path(list(range(10)))
# sequence interface
@@ -29,7 +28,7 @@ def test_path():
(6.0, 7.0),
(8.0, 9.0),
]
- assert p.tolist(1) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
+ assert p.tolist(True) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
assert p.getbbox() == (0.0, 1.0, 8.0, 9.0)
@@ -39,51 +38,65 @@ def test_path():
p.transform((1, 0, 1, 0, 1, 1))
assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)]
- # alternative constructors
- p = ImagePath.Path([0, 1])
- assert list(p) == [(0.0, 1.0)]
- p = ImagePath.Path([0.0, 1.0])
- assert list(p) == [(0.0, 1.0)]
- p = ImagePath.Path([0, 1])
- assert list(p) == [(0.0, 1.0)]
- p = ImagePath.Path([(0, 1)])
- assert list(p) == [(0.0, 1.0)]
- p = ImagePath.Path(p)
- assert list(p) == [(0.0, 1.0)]
- p = ImagePath.Path(p.tolist(0))
- assert list(p) == [(0.0, 1.0)]
- p = ImagePath.Path(p.tolist(1))
- assert list(p) == [(0.0, 1.0)]
- p = ImagePath.Path(array.array("f", [0, 1]))
- assert list(p) == [(0.0, 1.0)]
- arr = array.array("f", [0, 1])
- if hasattr(arr, "tobytes"):
- p = ImagePath.Path(arr.tobytes())
- else:
- p = ImagePath.Path(arr.tostring())
- assert list(p) == [(0.0, 1.0)]
+@pytest.mark.parametrize(
+ "coords",
+ (
+ (0, 1),
+ [0, 1],
+ (0.0, 1.0),
+ [0.0, 1.0],
+ ((0, 1),),
+ [(0, 1)],
+ ((0.0, 1.0),),
+ [(0.0, 1.0)],
+ array.array("f", [0, 1]),
+ array.array("f", [0, 1]).tobytes(),
+ ImagePath.Path((0, 1)),
+ ),
+)
+def test_path_constructors(coords):
+ # Arrange / Act
+ p = ImagePath.Path(coords)
+ # Assert
+ assert list(p) == [(0.0, 1.0)]
-def test_invalid_coords():
- # Arrange
- coords = ["a", "b"]
- # Act / Assert
+@pytest.mark.parametrize(
+ "coords",
+ (
+ ("a", "b"),
+ ([0, 1],),
+ [[0, 1]],
+ ([0.0, 1.0],),
+ [[0.0, 1.0]],
+ ),
+)
+def test_invalid_path_constructors(coords):
+ # Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
+ # Assert
assert str(e.value) == "incorrect coordinate type"
-def test_path_odd_number_of_coordinates():
- # Arrange
- coords = [0]
-
- # Act / Assert
+@pytest.mark.parametrize(
+ "coords",
+ (
+ (0,),
+ [0],
+ (0, 1, 2),
+ [0, 1, 2],
+ ),
+)
+def test_path_odd_number_of_coordinates(coords):
+ # Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
+ # Assert
assert str(e.value) == "wrong number of coordinates"
diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py
index 2f2b0791853..2c73a209465 100644
--- a/Tests/test_imageqt.py
+++ b/Tests/test_imageqt.py
@@ -2,12 +2,9 @@
import pytest
-from .helper import assert_image_similar, hopper
-
-with warnings.catch_warnings() as w:
- warnings.simplefilter("ignore", category=DeprecationWarning)
- from PIL import ImageQt
+from PIL import ImageQt
+from .helper import assert_image_similar, hopper
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 6af7e760204..62f52833227 100644
--- a/Tests/test_imagesequence.py
+++ b/Tests/test_imagesequence.py
@@ -6,7 +6,6 @@
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 3e147a9efec..e54372b60de 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 5717fe15036..b3b5db13ff2 100644
--- a/Tests/test_imagestat.py
+++ b/Tests/test_imagestat.py
@@ -6,7 +6,6 @@
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 995d0ee1f38..a0c9574ba94 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 9d64d17a30f..5e489284f2f 100644
--- a/Tests/test_imagewin.py
+++ b/Tests/test_imagewin.py
@@ -100,8 +100,11 @@ def test_dib_frombytes_tobytes_roundtrip(self):
# Act
# Make one the same as the using tobytes()/frombytes()
test_buffer = dib1.tobytes()
- dib2.frombytes(test_buffer)
-
- # Assert
- # Confirm they're the same
- assert dib1.tobytes() == dib2.tobytes()
+ 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()
diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py
index 37ed3659d0a..f6818be46dc 100644
--- a/Tests/test_lib_image.py
+++ b/Tests/test_lib_image.py
@@ -4,7 +4,6 @@
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 979806cae99..f7812f62bd8 100644
--- a/Tests/test_lib_pack.py
+++ b/Tests/test_lib_pack.py
@@ -207,6 +207,9 @@ def test_I(self):
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 @@ def test_F_float(self):
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 @@ def test_I16(self):
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 efcdab9ec43..1786dba3847 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 tobytes(mode):
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 185e477ecc5..147f94a71fd 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -4,7 +4,7 @@
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")
@@ -34,7 +34,7 @@ def to_image(dtype, bands=1, boolean=0):
# Check supported 1-bit integer formats
assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE)
- assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE)
+ assert_image(to_image(numpy.bool_, 1, 1), "1", TEST_IMAGE_SIZE)
# Check supported 8-bit integer formats
assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE)
@@ -193,7 +193,7 @@ def test_putdata():
"dtype",
(
bool,
- numpy.bool8,
+ numpy.bool_,
numpy.int8,
numpy.int16,
numpy.int32,
@@ -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 ea9b33dfc91..105a838d9da 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"<" + name.encode() + b" (" + date + b")>>", 0)[
- 0
- ]
+ b = b"<" + name.encode() + b" (" + date + 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 23eb9e39f41..2f6d0588853 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 1fc8161467b..5d2e41212f2 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 @@
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 @@ def __init__(self):
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 c1983031a14..95c13ba757b 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 12f475df036..6e3fcec90e1 100644
--- a/Tests/test_tiff_ifdrational.py
+++ b/Tests/test_tiff_ifdrational.py
@@ -7,7 +7,6 @@
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 34197c14f80..5bd9bacdb35 100644
--- a/Tests/test_webp_leaks.py
+++ b/Tests/test_webp_leaks.py
@@ -9,7 +9,6 @@
@skip_unless_feature("webp")
class TestWebPLeaks(PillowLeakTestCase):
-
mem_limit = 3 * 1024 # kb
iterations = 100
diff --git a/_custom_build/backend.py b/_custom_build/backend.py
new file mode 100755
index 00000000000..9b3265a949f
--- /dev/null
+++ b/_custom_build/backend.py
@@ -0,0 +1,56 @@
+import sys
+
+from setuptools.build_meta import * # noqa: F401, F403
+from setuptools.build_meta import build_wheel
+
+backend_class = build_wheel.__self__.__class__
+
+
+class _CustomBuildMetaBackend(backend_class):
+ def run_setup(self, setup_script="setup.py"):
+ if self.config_settings:
+
+ def config_has(key, value):
+ settings = self.config_settings.get(key)
+ if settings:
+ if not isinstance(settings, list):
+ settings = [settings]
+ return value in settings
+
+ flags = []
+ for dependency in (
+ "zlib",
+ "jpeg",
+ "tiff",
+ "freetype",
+ "raqm",
+ "lcms",
+ "webp",
+ "webpmux",
+ "jpeg2000",
+ "imagequant",
+ "xcb",
+ ):
+ if config_has(dependency, "enable"):
+ flags.append("--enable-" + dependency)
+ elif config_has(dependency, "disable"):
+ flags.append("--disable-" + dependency)
+ for dependency in ("raqm", "fribidi"):
+ if config_has(dependency, "vendor"):
+ flags.append("--vendor-" + dependency)
+ if self.config_settings.get("platform-guessing") == "disable":
+ flags.append("--disable-platform-guessing")
+ if self.config_settings.get("debug") == "true":
+ flags.append("--debug")
+ if flags:
+ sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:]
+ return super().run_setup(setup_script)
+
+ def build_wheel(
+ self, wheel_directory, config_settings=None, metadata_directory=None
+ ):
+ self.config_settings = config_settings
+ return super().build_wheel(wheel_directory, config_settings, metadata_directory)
+
+
+build_wheel = _CustomBuildMetaBackend().build_wheel
diff --git a/codecov.yml b/codecov.yml
index f3afccc1caf..1ea7974ebbe 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,9 +1,9 @@
-# Documentation: https://docs.codecov.io/docs/codecov-yaml
+# Documentation: https://docs.codecov.com/docs/codecov-yaml
codecov:
# Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]"
# https://github.com/codecov/support/issues/363
- # https://docs.codecov.io/docs/comparing-commits
+ # https://docs.codecov.com/docs/comparing-commits
allow_coverage_offsets: true
comment: false
@@ -12,7 +12,7 @@ coverage:
status:
project:
default:
- threshold: 0.01%
+ threshold: 0.1%
# Matches 'omit:' in .coveragerc
ignore:
diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh
index d9608e7827a..a318bfafd9f 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 02da12d61a4..1ef6f4e9783 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
-
- 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
+archive=test-images-main
+
+./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 64dd024bd7f..fd6000ee12b 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 99250365065..24c1f9c3029 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 05867b7d448..6f867ab3788 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.2
./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 25f03b34312..bc44ba388a6 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-2022 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/Guardfile b/docs/Guardfile
index b689b079aea..6cbf07b0637 100755
--- a/docs/Guardfile
+++ b/docs/Guardfile
@@ -2,7 +2,7 @@
from livereload.compiler import shell
from livereload.task import Task
-Task.add('*.rst', shell('make html'))
-Task.add('*/*.rst', shell('make html'))
-Task.add('Makefile', shell('make html'))
-Task.add('conf.py', shell('make html'))
+Task.add("*.rst", shell("make html"))
+Task.add("*/*.rst", shell("make html"))
+Task.add("Makefile", shell("make html"))
+Task.add("conf.py", shell("make html"))
diff --git a/docs/Makefile b/docs/Makefile
index 458299aac7b..3b4deb9bf9d 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -15,11 +15,13 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
-
+.PHONY: 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 " 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"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@@ -39,42 +41,49 @@ help:
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
+.PHONY: clean
clean:
-rm -rf $(BUILDDIR)/*
install-sphinx:
- $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile
+ $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-removed-in sphinxext-opengraph
+.PHONY: html
html:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+.PHONY: dirhtml
dirhtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+.PHONY: singlehtml
singlehtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+.PHONY: pickle
pickle:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
+.PHONY: json
json:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
+.PHONY: htmlhelp
htmlhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@@ -82,6 +91,7 @@ htmlhelp:
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
+.PHONY: qthelp
qthelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@@ -92,6 +102,7 @@ qthelp:
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc"
+.PHONY: devhelp
devhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@@ -102,12 +113,14 @@ devhelp:
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork"
@echo "# devhelp"
+.PHONY: epub
epub:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+.PHONY: latex
latex:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@@ -116,6 +129,7 @@ latex:
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
+.PHONY: latexpdf
latexpdf:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@@ -123,18 +137,21 @@ latexpdf:
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+.PHONY: text
text:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
+.PHONY: man
man:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+.PHONY: texinfo
texinfo:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@@ -143,6 +160,7 @@ texinfo:
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
+.PHONY: info
info:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@@ -150,18 +168,21 @@ info:
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+.PHONY: gettext
gettext:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+.PHONY: changes
changes:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
+.PHONY: linkcheck
linkcheck:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@@ -169,14 +190,21 @@ linkcheck:
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
+.PHONY: doctest
doctest:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/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
+.PHONY: serve
serve:
cd $(BUILDDIR)/html; $(PYTHON) -m http.server
diff --git a/docs/conf.py b/docs/conf.py
index bc67d936893..a2c825292f7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -27,12 +27,13 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- "sphinx_copybutton",
- "sphinx_issues",
- "sphinx_removed_in",
"sphinx.ext.autodoc",
+ "sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
+ "sphinx_copybutton",
+ "sphinx_inline_tabs",
+ "sphinx_removed_in",
"sphinxext.opengraph",
]
@@ -51,8 +52,10 @@
# General information about the project.
project = "Pillow (PIL Fork)"
-copyright = "1995-2011 Fredrik Lundh, 2010-2022 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
@@ -242,7 +245,7 @@
master_doc,
"PillowPILFork.tex",
"Pillow (PIL Fork) Documentation",
- "Alex Clark",
+ "Jeffrey A. Clark (Alex)",
"manual",
)
]
@@ -292,7 +295,7 @@
"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",
)
]
@@ -314,8 +317,28 @@ def setup(app):
app.add_css_file("css/dark.css")
-# GitHub repo for sphinx-issues
-issues_github_path = "python-pillow/Pillow"
+linkcheck_allowed_redirects = {
+ r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501
+ r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501
+ r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501
+ r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501
+ r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
+ r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501
+ r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501
+ r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501
+}
+
+# sphinx.ext.extlinks
+# This config is a dictionary of external sites,
+# mapping unique short aliases to a base URL and a prefix.
+# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
+_repo = "https://github.com/python-pillow/Pillow/"
+extlinks = {
+ "cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"),
+ "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
+ "issue": (_repo + "issues/%s", "#%s"),
+ "pr": (_repo + "pull/%s", "#%s"),
+}
# sphinxext.opengraph
ogp_image = (
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index dec652df88f..ce956cadeff 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -12,22 +12,50 @@ Deprecated features
Below are features which are considered deprecated. Where appropriate,
a ``DeprecationWarning`` is issued.
+PSFile
+~~~~~~
+
+.. deprecated:: 9.5.0
+
+The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will
+be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
+be used internally, so there is no replacement. If you need this functionality
+though, it is a very short class that can easily be recreated in your own code.
+
+PyAccess and Image.USE_CFFI_ACCESS
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 10.0.0
+
+Since Pillow's C API is now faster than PyAccess on PyPy,
+:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow
+11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead.
+
+``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
+similarly deprecated.
+
+Removed features
+----------------
+
+Deprecated features are only removed in major releases after an appropriate
+period of deprecation has passed.
+
Tk/Tcl 8.4
~~~~~~~~~~
.. deprecated:: 8.2.0
+.. versionremoved:: 10.0.0
-Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01),
-when Tk/Tcl 8.5 will be the minimum supported.
+Support for Tk/Tcl 8.4 was removed in Pillow 10.0.0 (2023-07-01).
Categories
~~~~~~~~~~
.. deprecated:: 8.2.0
+.. versionremoved:: 10.0.0
-``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01),
-along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and
-``Image.CONTAINER`` attributes.
+``im.category`` was removed along with the related ``Image.NORMAL``,
+``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes.
To determine if an image has multiple frames or not,
``getattr(im, "is_animated", False)`` can be used instead.
@@ -36,78 +64,53 @@ 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 removed.
+Instead, ``enum.IntEnum`` classes have been added.
+
+.. note::
-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.
+ Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that
+ was reversed in Pillow 9.4.0 and those constants will now remain available.
+ See :ref:`restored-image-constants`
===================================================== ============================================================
-Deprecated Use instead
+Removed Use instead
===================================================== ============================================================
-``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
-``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
-``Image.ORDERED`` ``Image.Dither.ORDERED``
-``Image.RASTERIZE`` ``Image.Dither.RASTERIZE``
-``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG``
-``Image.WEB`` ``Image.Palette.WEB``
-``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE``
-``Image.AFFINE`` ``Image.Transform.AFFINE``
-``Image.EXTENT`` ``Image.Transform.EXTENT``
-``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE``
-``Image.QUAD`` ``Image.Transform.QUAD``
-``Image.MESH`` ``Image.Transform.MESH``
-``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT``
-``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM``
-``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90``
-``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180``
-``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270``
-``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
-``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
-``Image.BOX`` ``Image.Resampling.BOX``
-``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
-``Image.LINEAR`` ``Image.Resampling.BILINEAR``
-``Image.HAMMING`` ``Image.Resampling.HAMMING``
-``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
-``Image.CUBIC`` ``Image.Resampling.BICUBIC``
-``Image.LANCZOS`` ``Image.Resampling.LANCZOS``
-``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS``
-``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT``
-``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE``
-``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE``
-``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT``
+``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``
@@ -137,71 +140,31 @@ FitsStubImagePlugin
~~~~~~~~~~~~~~~~~~~
.. deprecated:: 9.1.0
+.. versionremoved:: 10.0.0
-The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in
-Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
-:mod:`~PIL.FitsImagePlugin` instead.
-
-FreeTypeFont.getmask2 fill parameter
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. deprecated:: 9.2.0
-
-The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been
-deprecated and will be removed in Pillow 10 (2023-07-01).
-
-PhotoImage.paste box parameter
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. deprecated:: 9.2.0
-
-The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
-
-PyQt5 and PySide2
-~~~~~~~~~~~~~~~~~
-
-.. deprecated:: 9.2.0
-
-`Qt 5 reached end-of-life `_ on 2020-12-08 for
-open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
-
-Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
-in Pillow 10 (2023-07-01). Upgrade to
-`PyQt6 `_ or
-`PySide6 `_ instead.
-
-Image.coerce_e
-~~~~~~~~~~~~~~
-
-.. deprecated:: 9.2.0
-
-This undocumented method has been deprecated and will be removed in Pillow 10
-(2023-07-01).
-
-.. _Font size and offset methods:
+The stub image plugin ``FitsStubImagePlugin`` has been removed.
+FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead.
Font size and offset methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 9.2.0
+.. versionremoved:: 10.0.0
-Several functions for computing the size and offset of rendered text
-have been deprecated and will be removed in Pillow 10 (2023-07-01):
+Several functions for computing the size and offset of rendered text have been removed:
-=========================================================================== =============================================================================================================
-Deprecated Use instead
-=========================================================================== =============================================================================================================
-:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength`
-:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox`
-:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength`
-:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength`
-:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox`
-:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength`
-=========================================================================== =============================================================================================================
+=============================================================== =============================================================================================================
+Removed Use instead
+=============================================================== =============================================================================================================
+``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength`
+``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox`
+``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength`
+``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength`
+``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox`
+``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength`
+=============================================================== =============================================================================================================
-Previous code:
-
-.. code-block:: python
+Previous code::
from PIL import Image, ImageDraw, ImageFont
@@ -216,9 +179,7 @@ Previous code:
width, height = font.getsize_multiline("Hello\nworld")
width, height = draw.multiline_textsize("Hello\nworld")
-Use instead:
-
-.. code-block:: python
+Use instead::
from PIL import Image, ImageDraw, ImageFont
@@ -233,11 +194,43 @@ Use instead:
left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld")
width, height = right - left, bottom - top
-Removed features
-----------------
+FreeTypeFont.getmask2 fill parameter
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Deprecated features are only removed in major releases after an appropriate
-period of deprecation has passed.
+.. deprecated:: 9.2.0
+.. versionremoved:: 10.0.0
+
+The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been
+removed.
+
+PhotoImage.paste box parameter
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 9.2.0
+.. versionremoved:: 10.0.0
+
+The ``box`` parameter was unused and has been removed.
+
+PyQt5 and PySide2
+~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 9.2.0
+.. versionremoved:: 10.0.0
+
+`Qt 5 reached end-of-life `_ on 2020-12-08 for
+open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
+
+Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to
+`PyQt6 `_ or
+`PySide6 `_ instead.
+
+Image.coerce_e
+~~~~~~~~~~~~~~
+
+.. deprecated:: 9.2.0
+.. versionremoved:: 10.0.0
+
+This undocumented method has been removed.
PILLOW_VERSION constant
~~~~~~~~~~~~~~~~~~~~~~~
@@ -287,7 +280,7 @@ FreeType 2.7
Support for FreeType 2.7 has been removed.
We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe
-vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`).
+vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
.. _FreeType: https://freetype.org/
@@ -358,16 +351,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem
Use a context manager or call ``Image.close()`` instead to close the file in a
deterministic way.
-Previous method:
-
-.. code-block:: python
+Previous method::
im = Image.open("hopper.png")
im.save("out.jpg")
-Use instead:
-
-.. code-block:: python
+Use instead::
with Image.open("hopper.png") as im:
im.save("out.jpg")
diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py
index ec3938b367f..26451533eee 100644
--- a/docs/example/DdsImagePlugin.py
+++ b/docs/example/DdsImagePlugin.py
@@ -211,13 +211,16 @@ class DdsImageFile(ImageFile.ImageFile):
def _open(self):
if not _accept(self.fp.read(4)):
- raise SyntaxError("not a DDS file")
+ msg = "not a DDS file"
+ raise SyntaxError(msg)
(header_size,) = struct.unpack("`_. 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 `_.
@@ -57,9 +57,9 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more
+
Overview
========
diff --git a/docs/installation.rst b/docs/installation.rst
index eb69d580567..5c4872b832c 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -23,6 +23,11 @@ Pillow supports these Python versions.
:file: older-versions.csv
:header-rows: 1
+.. _Linux Installation:
+.. _macOS Installation:
+.. _Windows Installation:
+.. _FreeBSD Installation:
+
Basic Installation
------------------
@@ -38,74 +43,84 @@ Install Pillow with :command:`pip`::
python3 -m pip install --upgrade Pillow
-Windows Installation
-^^^^^^^^^^^^^^^^^^^^
+.. tab:: Linux
-We provide Pillow binaries for Windows compiled for the matrix of
-supported Pythons in both 32 and 64-bit versions in the wheel format.
-These binaries include support for all optional libraries except
-libimagequant and libxcb. Raqm support requires
-FriBiDi to be installed separately::
+ We provide binaries for Linux for each of the supported Python
+ versions in the manylinux wheel format. These include support for all
+ optional libraries except libimagequant. Raqm support requires
+ FriBiDi to be installed separately::
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --upgrade Pillow
-To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_.
+ Most major Linux distributions, including Fedora, Ubuntu and ArchLinux
+ also include Pillow in packages that previously contained PIL e.g.
+ ``python-imaging``. Debian splits it into two packages, ``python3-pil``
+ and ``python3-pil.imagetk``.
+.. tab:: macOS
-macOS Installation
-^^^^^^^^^^^^^^^^^^
+ We provide binaries for macOS for each of the supported Python
+ versions in the wheel format. These include support for all optional
+ libraries except libimagequant. Raqm support requires
+ FriBiDi to be installed separately::
-We provide binaries for macOS for each of the supported Python
-versions in the wheel format. These include support for all optional
-libraries except libimagequant. Raqm support requires
-FriBiDi to be installed separately::
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --upgrade Pillow
- 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::
-Linux Installation
-^^^^^^^^^^^^^^^^^^
+ 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
-We provide binaries for Linux for each of the supported Python
-versions in the manylinux wheel format. These include support for all
-optional libraries except libimagequant. Raqm support requires
-FriBiDi to be installed separately::
+ Then, with the names of the downloaded wheels, use Python to combine them::
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
+ 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')
-Most major Linux distributions, including Fedora, Ubuntu and ArchLinux
-also include Pillow in packages that previously contained PIL e.g.
-``python-imaging``. Debian splits it into two packages, ``python3-pil``
-and ``python3-pil.imagetk``.
+.. tab:: Windows
-FreeBSD Installation
-^^^^^^^^^^^^^^^^^^^^
+ We provide Pillow binaries for Windows compiled for the matrix of
+ supported Pythons in both 32 and 64-bit versions in the wheel format.
+ These binaries include support for all optional libraries except
+ libimagequant and libxcb. Raqm support requires
+ FriBiDi to be installed separately::
-Pillow can be installed on FreeBSD via the official Ports or Packages systems:
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --upgrade Pillow
-**Ports**::
+ To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_.
- cd /usr/ports/graphics/py-pillow && make install clean
+.. tab:: FreeBSD
-**Packages**::
+ Pillow can be installed on FreeBSD via the official Ports or Packages systems:
- pkg install py38-pillow
+ **Ports**::
-.. note::
+ cd /usr/ports/graphics/py-pillow && make install clean
- The `Pillow FreeBSD port
- `_ and packages
- are tested by the ports team with all supported FreeBSD versions.
+ **Packages**::
+ pkg install py38-pillow
-Building From Source
---------------------
+ .. note::
+
+ The `Pillow FreeBSD port
+ `_ and packages
+ are tested by the ports team with all supported FreeBSD versions.
-Download and extract the `compressed archive from PyPI`_.
-.. _compressed archive from PyPI: https://pypi.org/project/Pillow/
+.. _Building on Linux:
+.. _Building on macOS:
+.. _Building on Windows:
+.. _Building on Windows using MSYS2/MinGW:
+.. _Building on FreeBSD:
+.. _Building on Android:
+
+Building From Source
+--------------------
.. _external-libraries:
@@ -140,14 +155,14 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality
- * Pillow has been tested with libtiff versions **3.x** and **4.0-4.4**
+ * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5.1**
* **libfreetype** provides type related services
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
- above uses liblcms2. Tested with **1.19** and **2.7-2.13.1**.
+ above uses liblcms2. Tested with **1.19** and **2.7-2.15**.
* **libwebp** provides the WebP format.
@@ -166,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.
@@ -183,229 +198,227 @@ 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.
* **libxcb** provides X11 screengrab support.
-Once you have installed the prerequisites, run::
+.. tab:: Linux
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow --no-binary :all:
+ If you didn't build Python from source, make sure you have Python's
+ development libraries installed.
-If the prerequisites are installed in the standard library locations
-for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
-additional configuration should be required. If they are installed in
-a non-standard location, you may need to configure setuptools to use
-those locations by editing :file:`setup.py` or
-:file:`setup.cfg`, or by adding environment variables on the command
-line::
+ In Debian or Ubuntu::
- CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
+ sudo apt-get install python3-dev python3-setuptools
-If Pillow has been previously built without the required
-prerequisites, it may be necessary to manually clear the pip cache or
-build without cache using the ``--no-cache-dir`` option to force a
-build with newly installed external libraries.
+ In Fedora, the command is::
+ sudo dnf install python3-devel redhat-rpm-config
-Build Options
-^^^^^^^^^^^^^
+ In Alpine, the command is::
-* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
- multiprocessing to build the extension. Setting ``MAX_CONCURRENCY``
- sets the number of CPUs to use, or can disable parallel building by
- using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
- available, as many as are present.
+ sudo apk add python3-dev py3-setuptools
-* Build flags: ``--disable-zlib``, ``--disable-jpeg``,
- ``--disable-tiff``, ``--disable-freetype``, ``--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.
+ .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
-* Build flags: ``--enable-zlib``, ``--enable-jpeg``,
- ``--enable-tiff``, ``--enable-freetype``, ``--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.
+ Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
-* Build flags: ``--vendor-raqm --vendor-fribidi``
- These flags are used to compile a modified version of libraqm and
- a shim that dynamically loads libfribidi at runtime. These are
- used to compile the standard Pillow wheels. Compiling libraqm requires
- a C99-compliant compiler.
+ sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
+ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
+ libharfbuzz-dev libfribidi-dev libxcb1-dev
-* Build flag: ``--disable-platform-guessing``. Skips all of the
- platform dependent guessing of include and library directories for
- automated build systems that configure the proper paths in the
- environment variables (e.g. Buildroot).
+ To install libraqm, ``sudo apt-get install meson`` and then see
+ ``depends/install_raqm.sh``.
-* Build flag: ``--debug``. Adds a debugging flag to the include and
- library search process to dump all paths searched for and found to
- stdout.
+ Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
+ sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
+ freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \
+ harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel
-Sample usage::
+ Note that the package manager may be yum or DNF, depending on the
+ exact distribution.
- python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]"
+ Prerequisites are installed for **Alpine** with::
+ sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \
+ libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \
+ libxcb-dev libpng-dev
-Building on macOS
-^^^^^^^^^^^^^^^^^
+ See also the ``Dockerfile``\s in the Test Infrastructure repo
+ (https://github.com/python-pillow/docker-images) for a known working
+ install process for other tested distros.
-The Xcode command line tools are required to compile portions of
-Pillow. The tools are installed by running ``xcode-select --install``
-from the command line. The command line tools are required even if you
-have the full Xcode package installed. It may be necessary to run
-``sudo xcodebuild -license`` to accept the license prior to using the
-tools.
+.. tab:: macOS
-The easiest way to install external libraries is via `Homebrew
-`_. After you install Homebrew, run::
+ The Xcode command line tools are required to compile portions of
+ Pillow. The tools are installed by running ``xcode-select --install``
+ from the command line. The command line tools are required even if you
+ have the full Xcode package installed. It may be necessary to run
+ ``sudo xcodebuild -license`` to accept the license prior to using the
+ tools.
- brew install libjpeg libtiff little-cms2 openjpeg webp
+ The easiest way to install external libraries is via `Homebrew
+ `_. After you install Homebrew, run::
-To install libraqm on macOS use Homebrew to install its dependencies::
+ brew install libjpeg libtiff little-cms2 openjpeg webp
- brew install freetype harfbuzz fribidi
+ To install libraqm on macOS use Homebrew to install its dependencies::
-Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
+ brew install freetype harfbuzz fribidi
-Now install Pillow with::
+ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow --no-binary :all:
+.. tab:: Windows
-or from within the uncompressed source directory::
-
- python3 -m pip install .
+ We recommend you use prebuilt wheels from PyPI.
+ If you wish to compile Pillow manually, you can use the build scripts
+ in the ``winbuild`` directory used for CI testing and development.
+ These scripts require Visual Studio 2017 or newer and NASM.
-Building on Windows
-^^^^^^^^^^^^^^^^^^^
+ The scripts also install Pillow from the local copy of the source code, so the
+ `Installing`_ instructions will not be necessary afterwards.
-We recommend you use prebuilt wheels from PyPI.
-If you wish to compile Pillow manually, you can use the build scripts
-in the ``winbuild`` directory used for CI testing and development.
-These scripts require Visual Studio 2017 or newer and NASM.
+.. tab:: Windows using MSYS2/MinGW
-Building on Windows using MSYS2/MinGW
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
+ **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
-To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
-**MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
+ The following instructions target the 64-bit build, for 32-bit
+ replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
-The following instructions target the 64-bit build, for 32-bit
-replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
+ Make sure you have Python and GCC installed::
-Make sure you have Python and GCC installed::
+ pacman -S \
+ mingw-w64-x86_64-gcc \
+ mingw-w64-x86_64-python3 \
+ mingw-w64-x86_64-python3-pip \
+ mingw-w64-x86_64-python3-setuptools
- pacman -S \
- mingw-w64-x86_64-gcc \
- mingw-w64-x86_64-python3 \
- mingw-w64-x86_64-python3-pip \
- mingw-w64-x86_64-python3-setuptools
+ Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
-Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
+ pacman -S \
+ mingw-w64-x86_64-libjpeg-turbo \
+ mingw-w64-x86_64-zlib \
+ mingw-w64-x86_64-libtiff \
+ mingw-w64-x86_64-freetype \
+ mingw-w64-x86_64-lcms2 \
+ mingw-w64-x86_64-libwebp \
+ mingw-w64-x86_64-openjpeg2 \
+ mingw-w64-x86_64-libimagequant \
+ mingw-w64-x86_64-libraqm
- pacman -S \
- mingw-w64-x86_64-libjpeg-turbo \
- mingw-w64-x86_64-zlib \
- mingw-w64-x86_64-libtiff \
- mingw-w64-x86_64-freetype \
- mingw-w64-x86_64-lcms2 \
- mingw-w64-x86_64-libwebp \
- mingw-w64-x86_64-openjpeg2 \
- mingw-w64-x86_64-libimagequant \
- mingw-w64-x86_64-libraqm
+ https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with
+ MSYS2. To workaround this, before installing Pillow you must run::
-Now install Pillow with::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow --no-binary :all:
+ export SETUPTOOLS_USE_DISTUTILS=stdlib
+.. tab:: FreeBSD
-Building on FreeBSD
-^^^^^^^^^^^^^^^^^^^
+ .. Note:: Only FreeBSD 10 and 11 tested
-.. Note:: Only FreeBSD 10 and 11 tested
+ Make sure you have Python's development libraries installed::
-Make sure you have Python's development libraries installed::
+ sudo pkg install python3
- sudo pkg install python3
+ Prerequisites are installed on **FreeBSD 10 or 11** with::
-Prerequisites are installed on **FreeBSD 10 or 11** with::
+ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
- sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
+ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
-Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
+.. tab:: Android
+ Basic Android support has been added for compilation within the Termux
+ environment. The dependencies can be installed by::
-Building on Linux
-^^^^^^^^^^^^^^^^^
+ pkg install -y python ndk-sysroot clang make \
+ libjpeg-turbo
-If you didn't build Python from source, make sure you have Python's
-development libraries installed.
+ This has been tested within the Termux app on ChromeOS, on x86.
-In Debian or Ubuntu::
+Installing
+^^^^^^^^^^
- sudo apt-get install python3-dev python3-setuptools
+Once you have installed the prerequisites, to install Pillow from the source
+code on PyPI, run::
-In Fedora, the command is::
-
- sudo dnf install python3-devel redhat-rpm-config
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --upgrade Pillow --no-binary :all:
-In Alpine, the command is::
+If the prerequisites are installed in the standard library locations
+for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
+additional configuration should be required. If they are installed in
+a non-standard location, you may need to configure setuptools to use
+those locations by editing :file:`setup.py` or
+:file:`setup.cfg`, or by adding environment variables on the command
+line::
- sudo apk add python3-dev py3-setuptools
+ CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
-.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
+If Pillow has been previously built without the required
+prerequisites, it may be necessary to manually clear the pip cache or
+build without cache using the ``--no-cache-dir`` option to force a
+build with newly installed external libraries.
-Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
+If you would like to install from a local copy of the source code instead, you
+can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow``
+or download and extract the `compressed archive from PyPI`_.
- sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
- libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
- libharfbuzz-dev libfribidi-dev libxcb1-dev
+After navigating to the Pillow directory, run::
-To install libraqm, ``sudo apt-get install meson`` and then see
-``depends/install_raqm.sh``.
+ python3 -m pip install --upgrade pip
+ python3 -m pip install .
-Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
+.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files
- sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
- freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \
- harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel
+Build Options
+"""""""""""""
-Note that the package manager may be yum or DNF, depending on the
-exact distribution.
+* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
+ multiprocessing to build the extension. Setting ``MAX_CONCURRENCY``
+ sets the number of CPUs to use, or can disable parallel building by
+ using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
+ available, as many as are present.
-Prerequisites are installed for **Alpine** with::
+* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
+ ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
+ ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``,
+ ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``.
+ Disable building the corresponding feature even if the development
+ libraries are present on the building machine.
- sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \
- libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \
- libxcb-dev libpng-dev
+* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``,
+ ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``,
+ ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``,
+ ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``.
+ Require that the corresponding feature is built. The build will raise
+ an exception if the libraries are not found. Webpmux (WebP metadata)
+ relies on WebP support. Tcl and Tk also must be used together.
-See also the ``Dockerfile``\s in the Test Infrastructure repo
-(https://github.com/python-pillow/docker-images) for a known working
-install process for other tested distros.
+* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``.
+ These flags are used to compile a modified version of libraqm and
+ a shim that dynamically loads libfribidi at runtime. These are
+ used to compile the standard Pillow wheels. Compiling libraqm requires
+ a C99-compliant compiler.
-Building on Android
-^^^^^^^^^^^^^^^^^^^
+* Build flag: ``-C platform-guessing=disable``. Skips all of the
+ platform dependent guessing of include and library directories for
+ automated build systems that configure the proper paths in the
+ environment variables (e.g. Buildroot).
-Basic Android support has been added for compilation within the Termux
-environment. The dependencies can be installed by::
+* Build flag: ``-C debug=true``. Adds a debugging flag to the include and
+ library search process to dump all paths searched for and found to
+ stdout.
- pkg install -y python ndk-sysroot clang make \
- libjpeg-turbo
-This has been tested within the Termux app on ChromeOS, on x86.
+Sample usage::
+ python3 -m pip install --upgrade Pillow -C [feature]=enable
Platform Support
----------------
@@ -426,7 +439,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 |
+----------------------------------+----------------------------+---------------------+
@@ -436,35 +451,37 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Debian 10 Buster | 3.7 | x86 |
+| Debian 11 Bullseye | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Debian 11 Bullseye | 3.9 | x86 |
+| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Fedora 35 | 3.10 | x86-64 |
+| Fedora 37 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Fedora 36 | 3.10 | 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 | |
-+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |
+| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| | 3.12, PyPy3 | |
+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
-| | PyPy3 | |
+| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, |
-| | | s390x, x86-64 |
+| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| | 3.12, PyPy3 | |
+| +----------------------------+---------------------+
+| | 3.10 | arm64v8, ppc64le, |
+| | | s390x |
+----------------------------------+----------------------------+---------------------+
-| Windows Server 2016 | 3.7 | x86-64 |
+| Windows Server 2016 | 3.8 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 |
-| | PyPy3 | |
+| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| | 3.12, PyPy3 | |
+| +----------------------------+---------------------+
+| | 3.11 | x86 |
| +----------------------------+---------------------+
-| | 3.9 (MinGW) | x86, x86-64 |
+| | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+
-| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 |
+| | 3.8, 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+
@@ -482,11 +499,13 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+
-| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.2.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 | 9.2.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 c943319addc..0ed5ee1a57e 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 ed2369259d4..d53947ff5c9 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/ExifTags.rst b/docs/reference/ExifTags.rst
index ff57885240e..464ab77ea35 100644
--- a/docs/reference/ExifTags.rst
+++ b/docs/reference/ExifTags.rst
@@ -4,8 +4,50 @@
:py:mod:`~PIL.ExifTags` Module
==============================
-The :py:mod:`~PIL.ExifTags` module exposes two dictionaries which
-provide constants and clear-text names for various well-known EXIF tags.
+The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes
+which provide constants and clear-text names for various well-known EXIF tags.
+
+.. py:data:: Base
+
+ >>> from PIL.ExifTags import Base
+ >>> Base.ImageDescription.value
+ 270
+ >>> Base(270).name
+ 'ImageDescription'
+
+.. py:data:: GPS
+
+ >>> from PIL.ExifTags import GPS
+ >>> GPS.GPSDestLatitude.value
+ 20
+ >>> GPS(20).name
+ 'GPSDestLatitude'
+
+.. py:data:: Interop
+
+ >>> from PIL.ExifTags import Interop
+ >>> Interop.RelatedImageFileFormat.value
+ 4096
+ >>> Interop(4096).name
+ 'RelatedImageFileFormat'
+
+.. py:data:: IFD
+
+ >>> from PIL.ExifTags import IFD
+ >>> IFD.Exif.value
+ 34665
+ >>> IFD(34665).name
+ 'Exif
+
+.. py:data:: LightSource
+
+ >>> from PIL.ExifTags import LightSource
+ >>> LightSource.Unknown.value
+ 0
+ >>> LightSource(0).name
+ 'Unknown'
+
+Two of these values are also exposed as dictionaries.
.. py:data:: TAGS
:type: dict
@@ -26,22 +68,3 @@ provide constants and clear-text names for various well-known EXIF tags.
>>> from PIL.ExifTags import GPSTAGS
>>> GPSTAGS[20]
'GPSDestLatitude'
-
-
-These values are also exposed as ``enum.IntEnum`` classes.
-
-.. py:data:: Base
-
- >>> from PIL.ExifTags import Base
- >>> Base.ImageDescription.value
- 270
- >>> Base(270).name
- 'ImageDescription'
-
-.. py:data:: GPS
-
- >>> from PIL.ExifTags import GPS
- >>> GPS.GPSDestLatitude.value
- 20
- >>> GPS(20).name
- 'GPSDestLatitude'
diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst
index 7f6f666c33c..41d3b8fcec0 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 9aa26916a90..31f63695ef5 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -16,7 +16,7 @@ For a more advanced drawing library for PIL, see the `aggdraw module`_.
Example: Draw a gray cross over an image
----------------------------------------
-.. code-block:: python
+::
import sys
from PIL import Image, ImageDraw
@@ -78,7 +78,7 @@ libraries, and may not available in all PIL builds.
Example: Draw Partial Opacity Text
----------------------------------
-.. code-block:: python
+::
from PIL import Image, ImageDraw, ImageFont
@@ -105,7 +105,7 @@ Example: Draw Partial Opacity Text
Example: Draw Multiline Text
----------------------------
-.. code-block:: python
+::
from PIL import Image, ImageDraw, ImageFont
@@ -243,6 +243,7 @@ Methods
.. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None)
Draws a line between the coordinates in the ``xy`` list.
+ The coordinate pixels are included in the drawn line.
:param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or
numeric values like ``[x, y, x, y, ...]``.
@@ -287,7 +288,7 @@ Methods
The polygon outline consists of straight lines between the given
coordinates, plus a straight line between the last and the first
- coordinate.
+ coordinate. The coordinate pixels are included in the drawn polygon.
:param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or
numeric values like ``[x, y, x, y, ...]``.
@@ -296,7 +297,7 @@ Methods
:param width: The line width, in pixels.
-.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None)
+.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1)
Draws a regular polygon inscribed in ``bounding_circle``,
with ``n_sides``, and rotation of ``rotation`` degrees.
@@ -311,6 +312,7 @@ Methods
(e.g. ``rotation=90``, applies a 90 degree rotation).
:param fill: Color to use for the fill.
:param outline: Color to use for the outline.
+ :param width: The line width, in pixels.
.. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1)
@@ -318,25 +320,28 @@ Methods
Draws a rectangle.
:param xy: Two points to define the bounding box. Sequence of either
- ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box
- is inclusive of both endpoints.
- :param outline: Color to use for the outline.
+ ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and
+ ``y1 >= y0``. The bounding box is inclusive of both endpoints.
:param fill: Color to use for the fill.
+ :param outline: Color to use for the outline.
:param width: The line width, in pixels.
.. versionadded:: 5.3.0
-.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1)
+.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1, corners=None)
Draws a rounded rectangle.
:param xy: Two points to define the bounding box. Sequence of either
- ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box
- is inclusive of both endpoints.
+ ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and
+ ``y1 >= y0``. The bounding box is inclusive of both endpoints.
:param radius: Radius of the corners.
- :param outline: Color to use for the outline.
:param fill: Color to use for the fill.
+ :param outline: Color to use for the outline.
:param width: The line width, in pixels.
+ :param corners: A tuple of whether to round each corner,
+ ``(top_left, top_right, bottom_right, bottom_left)``.
+ Keyword-only argument.
.. versionadded:: 8.2.0
@@ -472,116 +477,6 @@ Methods
.. versionadded:: 8.0.0
-.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
-
- .. deprecated:: 9.2.0
-
- See :ref:`deprecations ` for more information.
-
- Use :py:meth:`textlength()` to measure the offset of following text with
- 1/64 pixel precision.
- Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor.
-
- Return the size of the given string, in pixels.
-
- .. note:: For historical reasons this function measures text height from
- the ascender line instead of the top, see :ref:`text-anchors`.
- If you wish to measure text height from the top, it is recommended
- to use :meth:`textbbox` with ``anchor='lt'`` instead.
-
- :param text: Text to be measured. If it contains any newline characters,
- the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`.
- :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
- :param spacing: If the text is passed on to
- :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`,
- the number of pixels between lines.
- :param direction: Direction of the text. It can be ``"rtl"`` (right to
- left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
- Requires libraqm.
-
- .. versionadded:: 4.2.0
- :param features: A list of OpenType font features to be used during text
- layout. This is usually used to turn on optional
- font features that are not enabled by default,
- for example ``"dlig"`` or ``"ss01"``, but can be also
- used to turn off default font features, for
- example ``"-liga"`` to disable ligatures or ``"-kern"``
- to disable kerning. To get all supported
- features, see `OpenType docs`_.
- Requires libraqm.
-
- .. versionadded:: 4.2.0
- :param language: Language of the text. Different languages may use
- different glyph shapes or ligatures. This parameter tells
- the font which language the text is in, and to apply the
- correct substitutions as appropriate, if available.
- It should be a `BCP 47 language code`_.
- Requires libraqm.
-
- .. versionadded:: 6.0.0
-
- :param stroke_width: The width of the text stroke.
-
- .. versionadded:: 6.2.0
-
- :return: (width, height)
-
-.. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
-
- .. deprecated:: 9.2.0
-
- See :ref:`deprecations ` for more information.
-
- Use :py:meth:`.multiline_textbbox` instead.
-
- Return the size of the given string, in pixels.
-
- Use :py:meth:`textlength()` to measure the offset of following text with
- 1/64 pixel precision.
- Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor.
-
- .. note:: For historical reasons this function measures text height as the
- distance between the top ascender line and bottom descender line,
- not the top and bottom of the text, see :ref:`text-anchors`.
- If you wish to measure text height from the top to the bottom of text,
- it is recommended to use :meth:`multiline_textbbox` instead.
-
- :param text: Text to be measured.
- :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
- :param spacing: The number of pixels between lines.
- :param direction: Direction of the text. It can be ``"rtl"`` (right to
- left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
- Requires libraqm.
-
- .. versionadded:: 4.2.0
-
- :param features: A list of OpenType font features to be used during text
- layout. This is usually used to turn on optional
- font features that are not enabled by default,
- for example ``"dlig"`` or ``"ss01"``, but can be also
- used to turn off default font features, for
- example ``"-liga"`` to disable ligatures or ``"-kern"``
- to disable kerning. To get all supported
- features, see `OpenType docs`_.
- Requires libraqm.
-
- .. versionadded:: 4.2.0
-
- :param language: Language of the text. Different languages may use
- different glyph shapes or ligatures. This parameter tells
- the font which language the text is in, and to apply the
- correct substitutions as appropriate, if available.
- It should be a `BCP 47 language code`_.
- Requires libraqm.
-
- .. versionadded:: 6.0.0
-
- :param stroke_width: The width of the text stroke.
-
- .. versionadded:: 6.2.0
-
- :return: (width, height)
-
.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False)
Returns length (in pixels with 1/64 precision) of given text when rendered
@@ -597,18 +492,14 @@ Methods
string due to kerning. If you need to adjust for kerning, include the following
character and subtract its length.
- For example, instead of
-
- .. code-block:: python
+ For example, instead of ::
hello = draw.textlength("Hello", font)
world = draw.textlength("World", font)
hello_world = hello + world # not adjusted for kerning
assert hello_world == draw.textlength("HelloWorld", font) # may fail
- use
-
- .. code-block:: python
+ use ::
hello = draw.textlength("HelloW", font) - draw.textlength(
"W", font
@@ -617,9 +508,7 @@ Methods
hello_world = hello + world # adjusted for kerning
assert hello_world == draw.textlength("HelloWorld", font) # True
- or disable kerning with (requires libraqm)
-
- .. code-block:: python
+ or disable kerning with (requires libraqm) ::
hello = draw.textlength("Hello", font, features=["-kern"])
world = draw.textlength("World", font, features=["-kern"])
diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst
index 29ceee314cc..457f0d4df1b 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 3cf59c610c8..047990f1c2a 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 c85da4fb57a..044aede6264 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 516fa63a783..2abfa0cc997 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -18,10 +18,19 @@ OpenType fonts (as well as other font formats supported by the FreeType
library). For earlier versions, TrueType support is only available as part of
the imToolkit package.
+.. warning::
+ To protect against potential DOS attacks when using arbitrary strings as
+ text input, Pillow will raise a ``ValueError`` if the number of characters
+ is over a certain limit, :py:data:`MAX_STRING_LENGTH`.
+
+ This threshold can be changed by setting
+ :py:data:`MAX_STRING_LENGTH`. It can be disabled by setting
+ ``ImageFont.MAX_STRING_LENGTH = None``.
+
Example
-------
-.. code-block:: python
+::
from PIL import ImageFont, ImageDraw
@@ -73,3 +82,12 @@ Constants
Requires Raqm, you can check support using
:py:func:`PIL.features.check_feature` with ``feature="raqm"``.
+
+Constants
+---------
+
+.. data:: MAX_STRING_LENGTH
+
+ Set to 1,000,000, to protect against potential DOS attacks. Pillow will
+ raise a ``ValueError`` if the number of characters is over this limit. The
+ check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst
index 3086ba8c311..0b94032d5f8 100644
--- a/docs/reference/ImageGrab.rst
+++ b/docs/reference/ImageGrab.rst
@@ -15,8 +15,9 @@ or the clipboard to a PIL image memory.
returned as an "RGBA" on macOS, or an "RGB" image otherwise.
If the bounding box is omitted, the entire screen is copied.
- On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it
- is installed. To capture the default X11 display instead, pass ``xdisplay=""``.
+ On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return
+ a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is
+ installed. To disable this behaviour, pass ``xdisplay=""`` instead.
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux)
@@ -39,9 +40,11 @@ or the clipboard to a PIL image memory.
.. py:function:: grabclipboard()
- Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported.
+ Take a snapshot of the clipboard image, if any.
- .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS)
+ On Linux, ``wl-paste`` or ``xclip`` is required.
+
+ .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux)
:return: On Windows, an image, a list of filenames,
or None if the clipboard does not contain image data or filenames.
@@ -49,3 +52,5 @@ or the clipboard to a PIL image memory.
On Mac, an image,
or None if the clipboard does not contain image data.
+
+ On Linux, an image.
diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst
index 63f88fddd21..118d988d6d4 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 b9bdfc50772..500096ef7dc 100644
--- a/docs/reference/ImagePath.rst
+++ b/docs/reference/ImagePath.rst
@@ -48,7 +48,7 @@ vector data. Path objects can be passed to the methods on the
Maps the path through a function.
-.. py:method:: PIL.ImagePath.Path.tolist(flat=0)
+.. py:method:: PIL.ImagePath.Path.tolist(flat=False)
Converts the path to a Python list [(x, y), …].
@@ -60,9 +60,7 @@ vector data. Path objects can be passed to the methods on the
.. py:method:: PIL.ImagePath.Path.transform(matrix)
Transforms the path in place, using an affine transform. The matrix is a
- 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:
-
- .. code-block:: python
+ 6-tuple (a, b, c, d, e, f), and each point is mapped as follows::
xOut = xIn * a + yIn * b + c
yOut = xIn * d + yIn * e + f
diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst
index 15d052d1c4e..7e67a44d364 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 f2e7d9edd28..a27b2fb4efc 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 2ee3cadb70b..4151be4a746 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 b234b7b4efb..04d6f5dcd58 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 f9eb9b52418..ed58ca3a591 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 dc4c2bf94d0..5e85869058c 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 `_
-::
-
- 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``
+Then ``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 6bfd50588ab..f31941c9abb 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 00000000000..4cd6293229a
--- /dev/null
+++ b/docs/releasenotes/10.0.0.rst
@@ -0,0 +1,209 @@
+10.0.0
+------
+
+Backwards Incompatible Changes
+==============================
+
+Categories
+^^^^^^^^^^
+
+``im.category`` has been removed, along with the related ``Image.NORMAL``,
+``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes.
+
+To determine if an image has multiple frames or not,
+``getattr(im, "is_animated", False)`` can be used instead.
+
+Tk/Tcl 8.4
+^^^^^^^^^^
+
+Support for Tk/Tcl 8.4 has been removed.
+
+JpegImagePlugin.convert_dict_qtables
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer
+performed any operations on the data given to it, and has been removed.
+
+ImagePalette size parameter
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by
+default, and the ``size`` parameter could be used to override that. Pillow 8.3.0
+removed the default required length, also removing the need for the ``size`` parameter.
+
+ImageShow.Viewer.show_file file argument
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been
+removed and replaced by ``path``.
+
+In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
+
+Constants
+^^^^^^^^^
+
+A number of constants have been removed.
+Instead, ``enum.IntEnum`` classes have been added.
+
+===================================================== ============================================================
+Removed Use instead
+===================================================== ============================================================
+``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR``
+``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC``
+``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS``
+``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL``
+``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC``
+``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION``
+``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``
+``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT``
+``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT``
+``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF``
+``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC``
+``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM``
+``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG``
+``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED``
+``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT``
+``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA``
+``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1``
+``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3``
+``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5``
+``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1``
+``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED``
+``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE``
+``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND``
+``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS``
+``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE``
+``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER``
+===================================================== ============================================================
+
+FitsStubImagePlugin
+^^^^^^^^^^^^^^^^^^^
+
+The stub image plugin ``FitsStubImagePlugin`` has been removed.
+FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead.
+
+Font size and offset methods
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Several functions for computing the size and offset of rendered text have been removed:
+
+=============================================================== =============================================================================================================
+Removed Use instead
+=============================================================== =============================================================================================================
+``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength`
+``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox`
+``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength`
+``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength`
+``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox`
+``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength`
+=============================================================== =============================================================================================================
+
+FreeTypeFont.getmask2 fill parameter
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been
+removed.
+
+PhotoImage.paste box parameter
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``box`` parameter was unused and has been removed.
+
+PyQt5 and PySide2
+^^^^^^^^^^^^^^^^^
+
+`Qt 5 reached end-of-life `_ on 2020-12-08 for
+open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
+
+Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to
+`PyQt6 `_ or
+`PySide6 `_ instead.
+
+Image.coerce_e
+^^^^^^^^^^^^^^
+
+This undocumented method has been removed.
+
+Deprecations
+============
+
+PyAccess and Image.USE_CFFI_ACCESS
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Since Pillow's C API is now faster than PyAccess on PyPy,
+:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow
+11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead.
+
+``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
+similarly deprecated.
+
+API Changes
+===========
+
+Added line width parameter to ImageDraw regular_polygon
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+An optional line ``width`` parameter has been added to
+``ImageDraw.Draw.regular_polygon``.
+
+API Additions
+=============
+
+Added ``alpha_only`` argument to ``getbbox()``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:meth:`~PIL.Image.Image.getbbox` now accepts a keyword argument of
+``alpha_only``. This is an optional flag, defaulting to ``True``. If ``True``
+and the image has an alpha channel, trim transparent pixels. Otherwise, trim
+pixels when all channels are zero.
+
+Security
+========
+
+Limit size even if one dimension is zero
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When performing decompression bomb checks, Pillow did not reject images with
+excessive width and zero height, or zero width and excessive height. That has
+now been fixed.
+
+This effectively dates to the PIL fork, since problem images would still have
+been processed before Pillow started checking for decompression bombs.
+
+Added ImageFont.MAX_STRING_LENGTH
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To protect against potential DOS attacks when using arbitrary strings as text
+input, Pillow will now raise a ``ValueError`` if the number of characters
+passed into ImageFont methods is over a certain limit,
+:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.
+
+This threshold can be changed by setting
+:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting
+``ImageFont.MAX_STRING_LENGTH = None``.
+
+Other Changes
+=============
+
+32-bit wheels
+^^^^^^^^^^^^^
+
+32-bit wheels are no longer provided.
+
+Support display_jpeg() in IPython
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In addition to ``display()`` and ``display_png``, ``display_jpeg()`` can now
+also be used to display images in IPython::
+
+ from PIL import Image
+ from IPython.display import display_jpeg
+
+ im = Image.new("RGB", (100, 100), (255, 0, 0))
+ display_jpeg(im)
+
+Support reading signed 8-bit TIFF images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+TIFF images with signed integer data, 8 bits per sample and a photometric
+interpretaton of BlackIsZero can now be read.
diff --git a/docs/releasenotes/10.0.1.rst b/docs/releasenotes/10.0.1.rst
new file mode 100644
index 00000000000..6ac30e7fce1
--- /dev/null
+++ b/docs/releasenotes/10.0.1.rst
@@ -0,0 +1,14 @@
+10.0.1
+------
+
+Security
+========
+
+This release addresses :cve:`2023-4863`, by providing an updated install script and
+updated wheels to include libwebp 1.3.2, preventing a potential heap buffer overflow
+in WebP.
+
+Updated tests to pass with latest zlib version
+==============================================
+
+The release of zlib 1.3 caused one of the tests in the Pillow test suite to fail.
diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst
index dda814c1f7d..0b3eeeb49d5 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 38118ea39c4..5d60e116cc1 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 b5f7cfe9963..04325ad868f 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 eb4304843e1..76e13b06172 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 20a009cc177..7daac1b1902 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 79d4b88aac4..47692a3de6a 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 80002b0ce71..f2e2352897a 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 0024a537d12..6e231464e93 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 3584a5d72e9..f7a1cea65da 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 8ed1d9d85cc..69726e628b7 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 4081c49ca5c..18d0a33f1cd 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 50d132f3337..de50a3f1dad 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 c902ccf71fb..452077f1a64 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 0bfead14470..e74880f6f40 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 6b5c759fc0a..3333d63a1e8 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 9becf91465e..e61471e726f 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 a19da361aaa..73e77ad3ef6 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 c1feee088b6..acb92dc4151 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 48ce6fef70c..19690ca59b5 100644
--- a/docs/releasenotes/9.1.0.rst
+++ b/docs/releasenotes/9.1.0.rst
@@ -53,6 +53,11 @@ Constants
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.
+.. note::
+
+ Some of these deprecations were restored in Pillow 9.4.0. See
+ :ref:`restored-image-constants`
+
===================================================== ============================================================
Deprecated Use instead
===================================================== ============================================================
@@ -177,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 f8b155f3d6a..bab70f8f984 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 6dbfa2702eb..b875edf8e5c 100644
--- a/docs/releasenotes/9.2.0.rst
+++ b/docs/releasenotes/9.2.0.rst
@@ -15,7 +15,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde
Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
in Pillow 10 (2023-07-01). Upgrade to
`PyQt6 `_ or
-`PySide6 `_ instead.
+`PySide6 `_ instead.
FreeTypeFont.getmask2 fill parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -48,20 +48,18 @@ Font size and offset methods
Several functions for computing the size and offset of rendered text
have been deprecated and will be removed in Pillow 10 (2023-07-01):
-=========================================================================== =============================================================================================================
-Deprecated Use instead
-=========================================================================== =============================================================================================================
-:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength`
-:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox`
-:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength`
-:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength`
-:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox`
-:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength`
-=========================================================================== =============================================================================================================
+=============================================================== =============================================================================================================
+Deprecated Use instead
+=============================================================== =============================================================================================================
+``FreeTypeFont.getsize()`` and ``FreeTypeFont.getoffset()`` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength`
+``FreeTypeFont.getsize_multiline()`` :py:meth:`.ImageDraw.multiline_textbbox`
+``ImageFont.getsize()`` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength`
+``TransposedFont.getsize()`` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength`
+``ImageDraw.textsize()`` and ``ImageDraw.multiline_textsize()`` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox`
+``ImageDraw2.Draw.textsize()`` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength`
+=============================================================== =============================================================================================================
-Previous code:
-
-.. code-block:: python
+Previous code::
from PIL import Image, ImageDraw, ImageFont
@@ -76,9 +74,7 @@ Previous code:
width, height = font.getsize_multiline("Hello\nworld")
width, height = draw.multiline_textsize("Hello\nworld")
-Use instead:
-
-.. code-block:: python
+Use instead::
from PIL import Image, ImageDraw, ImageFont
diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst
new file mode 100644
index 00000000000..0af5bc8ca11
--- /dev/null
+++ b/docs/releasenotes/9.4.0.rst
@@ -0,0 +1,135 @@
+9.4.0
+-----
+
+API Additions
+=============
+
+Added start position for getmask and getmask2
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Text may render differently when starting at fractional coordinates, so
+:py:meth:`.FreeTypeFont.getmask` and :py:meth:`.FreeTypeFont.getmask2` now
+support a ``start`` argument. This tuple of horizontal and vertical offset
+will be used internally by :py:meth:`.ImageDraw.text` to more accurately place
+text at the ``xy`` coordinates.
+
+Added the ``exact`` encoding option for WebP
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``exact`` encoding option for WebP is now supported. The WebP encoder
+removes the hidden RGB values for better compression by default in libwebp 0.5
+or later. By setting this option to ``True``, the encoder will keep the hidden
+RGB values.
+
+Added ``signed`` option when saving JPEG2000
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If the ``signed`` keyword argument is present and true when saving JPEG2000
+images, then tell the encoder to save the image as signed.
+
+Added IFD, Interop and LightSource ExifTags enums
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:data:`~PIL.ExifTags.IFD` has been added, allowing enums to be used with
+:py:meth:`~PIL.Image.Exif.get_ifd`::
+
+ from PIL import Image, ExifTags
+ im = Image.open("Tests/images/flower.jpg")
+ print(im.getexif().get_ifd(ExifTags.IFD.Exif))
+
+``IFD1`` can also be used with :py:meth:`~PIL.Image.Exif.get_ifd`, but it should
+not be used in other contexts, as the enum value is only internally meaningful.
+
+:py:data:`~PIL.ExifTags.Interop` has been added for tags within the Interop IFD::
+
+ from PIL import Image, ExifTags
+ im = Image.open("Tests/images/flower.jpg")
+ interop_ifd = im.getexif().get_ifd(ExifTags.IFD.Interop)
+ print(interop_ifd.get(ExifTags.Interop.InteropIndex)) # R98
+
+:py:data:`~PIL.ExifTags.LightSource` has been added for values within the LightSource
+tag::
+
+ from PIL import Image, ExifTags
+ im = Image.open("Tests/images/iptc.jpg")
+ exif_ifd = im.getexif().get_ifd(ExifTags.IFD.Exif)
+ print(ExifTags.LightSource(exif_ifd[0x9208])) # LightSource.Unknown
+
+getxmp()
+^^^^^^^^
+
+`XMP data `_ can now be
+decoded for WEBP images through ``getxmp()``.
+
+Writing JPEG comments
+^^^^^^^^^^^^^^^^^^^^^
+
+When saving a JPEG image, a comment can now be written from
+:py:attr:`~PIL.Image.Image.info`, or by using an argument when saving::
+
+ im.save(out, comment="Test comment")
+
+Security
+========
+
+Fix memory DOS in ImageFont
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A corrupt or specially crafted TTF font could have font metrics that lead to
+unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not
+check the image size before allocating memory for it. This dates to the PIL
+fork. Pillow 8.2.0 added a check for large sizes, but did not consider the
+case where one dimension is zero.
+
+Null pointer dereference crash in ImageFont
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a
+crash. An error is now raised instead. This has been present since
+Pillow 8.0.0.
+
+Other Changes
+=============
+
+Added support for DDS L and LA images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added to read and write L and LA DDS images in the uncompressed
+format, known as "luminance" textures.
+
+.. _restored-image-constants:
+
+Constants
+^^^^^^^^^
+
+In Pillow 9.1.0, the following constants were deprecated. That has been reversed and
+these constants will now remain available.
+
+- ``Image.NONE``
+- ``Image.NEAREST``
+- ``Image.ORDERED``
+- ``Image.RASTERIZE``
+- ``Image.FLOYDSTEINBERG``
+- ``Image.WEB``
+- ``Image.ADAPTIVE``
+- ``Image.AFFINE``
+- ``Image.EXTENT``
+- ``Image.PERSPECTIVE``
+- ``Image.QUAD``
+- ``Image.MESH``
+- ``Image.FLIP_LEFT_RIGHT``
+- ``Image.FLIP_TOP_BOTTOM``
+- ``Image.ROTATE_90``
+- ``Image.ROTATE_180``
+- ``Image.ROTATE_270``
+- ``Image.TRANSPOSE``
+- ``Image.TRANSVERSE``
+- ``Image.BOX``
+- ``Image.BILINEAR``
+- ``Image.HAMMING``
+- ``Image.BICUBIC``
+- ``Image.LANCZOS``
+- ``Image.MEDIANCUT``
+- ``Image.MAXCOVERAGE``
+- ``Image.FASTOCTREE``
+- ``Image.LIBIMAGEQUANT``
diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst
new file mode 100644
index 00000000000..b1e982fccff
--- /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 8c436be3bd8..1dee0715372 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,10 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 10.0.1
+ 10.0.0
+ 9.5.0
+ 9.4.0
9.3.0
9.2.0
9.1.1
diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst
index f7271ae2bf8..440d04b1cc4 100644
--- a/docs/releasenotes/template.rst
+++ b/docs/releasenotes/template.rst
@@ -1,5 +1,5 @@
-x.y.z
------
+xx.y.z
+------
Backwards Incompatible Changes
==============================
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000000..93a43360891
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,9 @@
+[build-system]
+build-backend = "backend"
+requires = [
+ "setuptools>=67.8",
+ "wheel",
+]
+backend-path = [
+ "_custom_build",
+]
diff --git a/setup.cfg b/setup.cfg
index 44feb25ff7f..06e95d7cc2c 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
@@ -46,7 +46,7 @@ docs =
olefile
sphinx>=2.4
sphinx-copybutton
- sphinx-issues>=3.0.1
+ sphinx-inline-tabs
sphinx-removed-in
sphinxext-opengraph
tests =
diff --git a/setup.py b/setup.py
index 7a1fabe23f6..024634ad8f9 100755
--- a/setup.py
+++ b/setup.py
@@ -10,6 +10,7 @@
import os
import re
+import shutil
import struct
import subprocess
import sys
@@ -136,7 +137,6 @@ class RequiredDependencyException(Exception):
PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version
-PLATFORM_PYPY = hasattr(sys, "pypy_version_info")
def _dbg(s, tp=None):
@@ -150,6 +150,7 @@ def _dbg(s, tp=None):
def _find_library_dirs_ldconfig():
# Based on ctypes.util from Python 2
+ ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig"
if sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
if struct.calcsize("l") == 4:
machine = os.uname()[4] + "-32"
@@ -166,14 +167,14 @@ def _find_library_dirs_ldconfig():
# Assuming GLIBC's ldconfig (with option -p)
# Alpine Linux uses musl that can't print cache
- args = ["/sbin/ldconfig", "-p"]
+ args = [ldconfig, "-p"]
expr = rf".*\({abi_type}.*\) => (.*)"
env = dict(os.environ)
env["LC_ALL"] = "C"
env["LANG"] = "C"
elif sys.platform.startswith("freebsd"):
- args = ["/sbin/ldconfig", "-r"]
+ args = [ldconfig, "-r"]
expr = r".* => (.*)"
env = {}
@@ -242,7 +243,9 @@ def _find_include_dir(self, dirname, include):
return subdir
-def _cmd_exists(cmd):
+def _cmd_exists(cmd: str) -> bool:
+ if "PATH" not in os.environ:
+ return False
return any(
os.access(os.path.join(path, cmd), os.X_OK)
for path in os.environ["PATH"].split(os.pathsep)
@@ -263,18 +266,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
@@ -362,15 +363,15 @@ def finalize_options(self):
self.feature.required.discard(x)
_dbg("Disabling %s", x)
if getattr(self, f"enable_{x}"):
- raise ValueError(
- f"Conflicting options: --enable-{x} and --disable-{x}"
- )
+ msg = f"Conflicting options: --enable-{x} and --disable-{x}"
+ raise ValueError(msg)
if x == "freetype":
_dbg("--disable-freetype implies --disable-raqm")
if getattr(self, "enable_raqm"):
- raise ValueError(
+ msg = (
"Conflicting options: --enable-raqm and --disable-freetype"
)
+ raise ValueError(msg)
setattr(self, "disable_raqm", True)
if getattr(self, f"enable_{x}"):
_dbg("Requiring %s", x)
@@ -381,13 +382,11 @@ def finalize_options(self):
for x in ("raqm", "fribidi"):
if getattr(self, f"vendor_{x}"):
if getattr(self, "disable_raqm"):
- raise ValueError(
- f"Conflicting options: --vendor-{x} and --disable-raqm"
- )
+ msg = f"Conflicting options: --vendor-{x} and --disable-raqm"
+ raise ValueError(msg)
if x == "fribidi" and not getattr(self, "vendor_raqm"):
- raise ValueError(
- f"Conflicting options: --vendor-{x} and not --vendor-raqm"
- )
+ msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm"
+ raise ValueError(msg)
_dbg("Using vendored version of %s", x)
self.feature.vendor.add(x)
@@ -432,7 +431,6 @@ def get_macos_sdk_path(self):
return sdk_path
def build_extensions(self):
-
library_dirs = []
include_dirs = []
@@ -475,8 +473,16 @@ def build_extensions(self):
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"):
@@ -508,6 +514,7 @@ def build_extensions(self):
elif sys.platform == "cygwin":
# pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory
+ self.compiler.shared_lib_extension = ".dll.a"
_add_directory(
library_dirs,
os.path.join(
@@ -569,9 +576,7 @@ def build_extensions(self):
):
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
@@ -682,10 +687,6 @@ def build_extensions(self):
# 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)
@@ -846,14 +847,7 @@ def build_extensions(self):
if struct.unpack("h", b"\0\1")[0] == 1:
defs.append(("WORDS_BIGENDIAN", None))
- if (
- sys.platform == "win32"
- and sys.version_info < (3, 9)
- and not (PLATFORM_PYPY or PLATFORM_MINGW)
- ):
- defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""'))
- else:
- defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"'))
+ defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"'))
self._update_extension("PIL._imaging", libs, defs)
@@ -915,7 +909,6 @@ def build_extensions(self):
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 102b72e1d5a..075d462907a 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()]
-
- bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y)
+ # 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()]
+
+ # 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
@@ -86,7 +97,8 @@ def __init__(self, fp):
s = fp.readline()
if s[:13] != b"STARTFONT 2.1":
- raise SyntaxError("not a valid BDF file")
+ msg = "not a valid BDF file"
+ raise SyntaxError(msg)
props = {}
comments = []
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 53399773716..0ca60ff2471 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -35,7 +35,6 @@
from io import BytesIO
from . import Image, ImageFile
-from ._deprecate import deprecate
class Format(IntEnum):
@@ -54,20 +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]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
-
-
def unpack_565(i):
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
@@ -278,7 +263,8 @@ def _open(self):
if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
else:
- raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}")
+ msg = f"Bad BLP magic {repr(self.magic)}"
+ raise BLPFormatError(msg)
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
@@ -292,7 +278,8 @@ def decode(self, buffer):
self._read_blp_header()
self._load()
except struct.error as e:
- raise OSError("Truncated BLP file") from e
+ msg = "Truncated BLP file"
+ raise OSError(msg) from e
return -1, 0
def _read_blp_header(self):
@@ -354,13 +341,11 @@ def _load(self):
data = self._read_bgra(palette)
self.set_as_raw(bytes(data))
else:
- raise BLPFormatError(
- f"Unsupported BLP encoding {repr(self._blp_encoding)}"
- )
+ msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
+ raise BLPFormatError(msg)
else:
- raise BLPFormatError(
- f"Unsupported BLP compression {repr(self._blp_encoding)}"
- )
+ msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
+ raise BLPFormatError(msg)
def _decode_jpeg_stream(self):
from .JpegImagePlugin import JpegImageFile
@@ -373,6 +358,9 @@ def _decode_jpeg_stream(self):
data = BytesIO(data)
image = JpegImageFile(data)
Image._decompression_bomb_check(image.size)
+ if image.mode == "CMYK":
+ decoder_name, extents, offset, args = image.tile[0]
+ image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
r, g, b = image.convert("RGB").split()
image = Image.merge("RGB", (b, g, r))
self.set_as_raw(image.tobytes())
@@ -412,16 +400,15 @@ def _load(self):
for d in decode_dxt5(self._safe_read(linesize)):
data += d
else:
- raise BLPFormatError(
- f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
- )
+ msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
+ raise BLPFormatError(msg)
else:
- raise BLPFormatError(f"Unknown BLP encoding {repr(self._blp_encoding)}")
+ msg = f"Unknown BLP encoding {repr(self._blp_encoding)}"
+ raise BLPFormatError(msg)
else:
- raise BLPFormatError(
- f"Unknown BLP compression {repr(self._blp_compression)}"
- )
+ msg = f"Unknown BLP compression {repr(self._blp_compression)}"
+ raise BLPFormatError(msg)
self.set_as_raw(bytes(data))
@@ -457,7 +444,8 @@ def encode(self, bufsize):
def _save(im, fp, filename, save_all=False):
if im.mode != "P":
- raise ValueError("Unsupported BLP image mode")
+ msg = "Unsupported BLP image mode"
+ raise ValueError(msg)
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
fp.write(magic)
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index bdf51aa5cf1..5bda0a5b05d 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -146,7 +146,8 @@ def _bitmap(self, header=0, offset=0):
file_info["a_mask"],
)
else:
- raise OSError(f"Unsupported BMP header type ({file_info['header_size']})")
+ msg = f"Unsupported BMP header type ({file_info['header_size']})"
+ raise OSError(msg)
# ------------------ Special case : header is reported 40, which
# ---------------------- is shorter than real size for bpp >= 16
@@ -164,7 +165,8 @@ def _bitmap(self, header=0, offset=0):
# ---------------------- Check bit depth for unusual unsupported values
self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
if self.mode is None:
- raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
+ msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
+ raise OSError(msg)
# ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw"
@@ -205,23 +207,26 @@ def _bitmap(self, header=0, offset=0):
):
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
else:
- raise OSError("Unsupported BMP bitfields layout")
+ msg = "Unsupported BMP bitfields layout"
+ raise OSError(msg)
else:
- raise OSError("Unsupported BMP bitfields layout")
+ msg = "Unsupported BMP bitfields layout"
+ raise OSError(msg)
elif file_info["compression"] == self.RAW:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self.mode = "BGRA", "RGBA"
elif file_info["compression"] in (self.RLE8, self.RLE4):
decoder_name = "bmp_rle"
else:
- raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
+ msg = f"Unsupported BMP compression ({file_info['compression']})"
+ raise OSError(msg)
# --------------- 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):
- raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})")
+ msg = f"Unsupported BMP Palette size ({file_info['colors']})"
+ raise OSError(msg)
else:
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
@@ -271,7 +276,8 @@ def _open(self):
head_data = self.fp.read(14)
# choke if the file does not have the required magic bytes
if not _accept(head_data):
- raise SyntaxError("Not a BMP file")
+ msg = "Not a BMP file"
+ raise SyntaxError(msg)
# read the start position of the BMP image data (u32)
offset = i32(head_data, 10)
# load bitmap information (offset=raster info)
@@ -353,7 +359,6 @@ def decode(self, buffer):
# Image plugin for the DIB format (BMP alias)
# =============================================================================
class DibImageFile(BmpImageFile):
-
format = "DIB"
format_description = "Windows Bitmap"
@@ -383,7 +388,8 @@ def _save(im, fp, filename, bitmap_header=True):
try:
rawmode, bits, colors = SAVE[im.mode]
except KeyError as e:
- raise OSError(f"cannot write mode {im.mode} as BMP") from e
+ msg = f"cannot write mode {im.mode} as BMP"
+ raise OSError(msg) from e
info = im.encoderinfo
@@ -411,7 +417,8 @@ def _save(im, fp, filename, bitmap_header=True):
offset = 14 + header + colors * 4
file_size = offset + image
if file_size > 2**32 - 1:
- raise ValueError("File size is too large for the BMP format")
+ msg = "File size is too large for the BMP format"
+ raise ValueError(msg)
fp.write(
b"BM" # file type (magic)
+ o32(file_size) # file size
diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py
index 9510f733e1a..0425bbd750e 100644
--- a/src/PIL/BufrStubImagePlugin.py
+++ b/src/PIL/BufrStubImagePlugin.py
@@ -33,16 +33,15 @@ 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)):
- raise SyntaxError("Not a BUFR file")
+ msg = "Not a BUFR file"
+ raise SyntaxError(msg)
self.fp.seek(offset)
@@ -60,7 +59,8 @@ def _load(self):
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
- raise OSError("BUFR save handler not installed")
+ msg = "BUFR save handler not installed"
+ raise OSError(msg)
_handler.save(im, fp, filename)
diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py
index 42af5cafcef..94efff34156 100644
--- a/src/PIL/CurImagePlugin.py
+++ b/src/PIL/CurImagePlugin.py
@@ -32,18 +32,17 @@ def _accept(prefix):
class CurImageFile(BmpImagePlugin.BmpImageFile):
-
format = "CUR"
format_description = "Windows Cursor"
def _open(self):
-
offset = self.fp.tell()
# check magic
s = self.fp.read(6)
if not _accept(s):
- raise SyntaxError("not a CUR file")
+ msg = "not a CUR file"
+ raise SyntaxError(msg)
# pick the largest cursor in the file
m = b""
@@ -54,7 +53,8 @@ def _open(self):
elif s[0] > m[0] and s[1] > m[1]:
m = s
if not m:
- raise TypeError("No cursors were found")
+ msg = "No cursors were found"
+ raise TypeError(msg)
# load as bitmap
self._bitmap(i32(m, 12) + offset)
diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py
index aeed1e7c7ba..cde9d42f09f 100644
--- a/src/PIL/DcxImagePlugin.py
+++ b/src/PIL/DcxImagePlugin.py
@@ -37,17 +37,16 @@ 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):
- raise SyntaxError("not a DCX file")
+ msg = "not a DCX file"
+ raise SyntaxError(msg)
# Component directory
self._offset = []
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index eea6e31534c..a946daeaa6b 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -114,13 +114,16 @@ class DdsImageFile(ImageFile.ImageFile):
def _open(self):
if not _accept(self.fp.read(4)):
- raise SyntaxError("not a DDS file")
+ msg = "not a DDS file"
+ raise SyntaxError(msg)
(header_size,) = struct.unpack(" 255:
- raise SyntaxError("not an EPS file")
+ self._size = None
+
+ byte_arr = bytearray(255)
+ bytes_mv = memoryview(byte_arr)
+ bytes_read = 0
+ reading_comments = True
+
+ 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 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)
except re.error as e:
- raise SyntaxError("not an EPS file") from e
+ msg = "not an EPS file"
+ raise SyntaxError(msg) from e
if m:
k, v = m.group(1, 2)
@@ -251,16 +309,12 @@ def _open(self):
]
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] == "%":
@@ -268,65 +322,68 @@ def _open(self):
# tools mistakenly put in the Comments section
pass
else:
- raise OSError("bad EPS header")
-
- s_raw = fp.readline()
- s = s_raw.strip("\r\n")
-
- if s and s[:1] != "%":
- break
-
- #
- # Scan for an "ImageData" descriptor
-
- while s[:1] == "%":
-
- if len(s) > 255:
- raise SyntaxError("not an EPS file")
-
- if s[:11] == "%ImageData:":
- # Encoded bitmapped image.
- x, y, bi, mo = s[11:].split(None, 7)[:4]
-
- if int(bi) == 1:
+ 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
+
+ # 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 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:
- raise OSError("cannot determine EPS bounding box")
+ check_required_header_comments()
- def _find_offset(self, fp):
+ if not self._size:
+ msg = "cannot determine EPS bounding box"
+ raise OSError(msg)
- s = fp.read(160)
+ def _find_offset(self, fp):
+ s = fp.read(4)
- 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:
- raise SyntaxError("not an EPS file")
+ msg = "not an EPS file"
+ raise SyntaxError(msg)
return length, offset
@@ -345,18 +402,15 @@ def load_seek(self, *args, **kwargs):
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")
@@ -365,10 +419,10 @@ def _save(im, fp, filename, eps=1):
elif im.mode == "CMYK":
operator = (8, 4, b"false 4 colorimage")
else:
- raise ValueError("image mode is not supported")
+ msg = "image mode is not supported"
+ 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")
@@ -380,7 +434,6 @@ def _save(im, fp, filename, eps=1):
fp.write(b"%%ImageData: %d %d " % im.size)
fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
- #
# image header
fp.write(b"gsave\n")
fp.write(b"10 dict begin\n")
@@ -401,7 +454,6 @@ def _save(im, fp, filename, eps=1):
fp.flush()
-#
# --------------------------------------------------------------------
diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py
index f3a73bf1a52..2347c6d4c27 100644
--- a/src/PIL/ExifTags.py
+++ b/src/PIL/ExifTags.py
@@ -338,3 +338,43 @@ class GPS(IntEnum):
"""Maps EXIF GPS tags to tag names."""
GPSTAGS = {i.value: i.name for i in GPS}
+
+
+class Interop(IntEnum):
+ InteropIndex = 1
+ InteropVersion = 2
+ RelatedImageFileFormat = 4096
+ RelatedImageWidth = 4097
+ RleatedImageHeight = 4098
+
+
+class IFD(IntEnum):
+ Exif = 34665
+ GPSInfo = 34853
+ Makernote = 37500
+ Interop = 40965
+ IFD1 = -1
+
+
+class LightSource(IntEnum):
+ Unknown = 0
+ Daylight = 1
+ Fluorescent = 2
+ Tungsten = 3
+ Flash = 4
+ Fine = 9
+ Cloudy = 10
+ Shade = 11
+ DaylightFluorescent = 12
+ DayWhiteFluorescent = 13
+ CoolWhiteFluorescent = 14
+ WhiteFluorescent = 15
+ StandardLightA = 17
+ StandardLightB = 18
+ StandardLightC = 19
+ D55 = 20
+ D65 = 21
+ D75 = 22
+ D50 = 23
+ ISO = 24
+ Other = 255
diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index c16300efa89..1359aeb1282 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"
@@ -28,20 +27,23 @@ def _open(self):
while True:
header = self.fp.read(80)
if not header:
- raise OSError("Truncated FITS file")
+ msg = "Truncated FITS file"
+ raise OSError(msg)
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"):
- raise SyntaxError("Not a FITS file")
+ msg = "Not a FITS file"
+ raise SyntaxError(msg)
headers[keyword] = value
naxis = int(headers[b"NAXIS"])
if naxis == 0:
- raise ValueError("No image data")
+ msg = "No image data"
+ raise ValueError(msg)
elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"])
else:
diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py
deleted file mode 100644
index 440240a9958..00000000000
--- a/src/PIL/FitsStubImagePlugin.py
+++ /dev/null
@@ -1,76 +0,0 @@
-#
-# The Python Imaging Library
-# $Id$
-#
-# FITS stub adapter
-#
-# Copyright (c) 1998-2003 by Fredrik Lundh
-#
-# See the README file for information on usage and redistribution.
-#
-
-from . import FitsImagePlugin, Image, ImageFile
-from ._deprecate import deprecate
-
-_handler = None
-
-
-def register_handler(handler):
- """
- Install application-specific FITS image handler.
-
- :param handler: Handler object.
- """
- global _handler
- _handler = handler
-
- deprecate(
- "FitsStubImagePlugin",
- 10,
- action="FITS images can now be read without "
- "a handler through FitsImagePlugin instead",
- )
-
- # Override FitsImagePlugin with this handler
- # for backwards compatibility
- try:
- Image.ID.remove(FITSStubImageFile.format)
- except ValueError:
- pass
-
- Image.register_open(
- FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept
- )
-
-
-class FITSStubImageFile(ImageFile.StubImageFile):
-
- format = FitsImagePlugin.FitsImageFile.format
- format_description = FitsImagePlugin.FitsImageFile.format_description
-
- def _open(self):
- offset = self.fp.tell()
-
- im = FitsImagePlugin.FitsImageFile(self.fp)
- self._size = im.size
- self.mode = im.mode
- self.tile = []
-
- self.fp.seek(offset)
-
- loader = self._load()
- if loader:
- loader.open(self)
-
- def _load(self):
- return _handler
-
-
-def _save(im, fp, filename):
- raise OSError("FITS save handler not installed")
-
-
-# --------------------------------------------------------------------
-# Registry
-
-Image.register_save(FITSStubImageFile.format, _save)
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index 908bed9f427..f4e89a03e02 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -40,17 +40,16 @@ 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"):
- raise SyntaxError("not an FLI/FLC file")
+ msg = "not an FLI/FLC file"
+ raise SyntaxError(msg)
# frames
self.n_frames = i16(s, 6)
@@ -141,7 +140,8 @@ def _seek(self, frame):
self.load()
if frame != self.__frame + 1:
- raise ValueError(f"cannot seek to frame {frame}")
+ msg = f"cannot seek to frame {frame}"
+ raise ValueError(msg)
self.__frame = frame
# move to next frame
diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py
index c5fc80b37ba..5ec0a6632e3 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 a55376d0e08..2450c67e9a6 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"
@@ -60,10 +59,12 @@ def _open(self):
try:
self.ole = olefile.OleFileIO(self.fp)
except OSError as e:
- raise SyntaxError("not an FPX file; invalid OLE file") from e
+ msg = "not an FPX file; invalid OLE file"
+ raise SyntaxError(msg) from e
if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
- raise SyntaxError("not an FPX file; bad root CLSID")
+ msg = "not an FPX file; bad root CLSID"
+ raise SyntaxError(msg)
self._open_index(1)
@@ -99,7 +100,8 @@ def _open_index(self, index=1):
colors = []
bands = i32(s, 4)
if bands > 4:
- raise OSError("Invalid number of bands")
+ msg = "Invalid number of bands"
+ raise OSError(msg)
for i in range(bands):
# note: for now, we ignore the "uncalibrated" flag
colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
@@ -141,7 +143,8 @@ def _open_subimage(self, index=1, subimage=0):
length = i32(s, 32)
if size != self.size:
- raise OSError("subimage mismatch")
+ msg = "subimage mismatch"
+ raise OSError(msg)
# get tile descriptors
fp.seek(28 + offset)
@@ -153,7 +156,6 @@ def _open_subimage(self, index=1, subimage=0):
self.tile = []
for i in range(0, len(s), length):
-
x1 = min(xsize, x + xtile)
y1 = min(ysize, y + ytile)
@@ -170,7 +172,6 @@ def _open_subimage(self, index=1, subimage=0):
)
elif compression == 1:
-
# FIXME: the fill decoder is not implemented
self.tile.append(
(
@@ -182,7 +183,6 @@ def _open_subimage(self, index=1, subimage=0):
)
elif compression == 2:
-
internal_color_conversion = s[14]
jpeg_tables = s[15]
rawmode = self.rawmode
@@ -217,7 +217,8 @@ def _open_subimage(self, index=1, subimage=0):
self.tile_prefix = self.jpeg[jpeg_tables]
else:
- raise OSError("unknown/invalid compression")
+ msg = "unknown/invalid compression"
+ raise OSError(msg)
x = x + xtile
if x >= xsize:
@@ -229,12 +230,19 @@ def _open_subimage(self, index=1, subimage=0):
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 1b714eb4f65..c46b2f28ba6 100644
--- a/src/PIL/FtexImagePlugin.py
+++ b/src/PIL/FtexImagePlugin.py
@@ -56,7 +56,6 @@
from io import BytesIO
from . import Image, ImageFile
-from ._deprecate import deprecate
MAGIC = b"FTEX"
@@ -66,23 +65,14 @@ 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]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
-
-
class FtexImageFile(ImageFile.ImageFile):
format = "FTEX"
format_description = "Texture File Format (IW2:EOC)"
def _open(self):
if not _accept(self.fp.read(4)):
- raise SyntaxError("not an FTEX file")
+ msg = "not an FTEX file"
+ raise SyntaxError(msg)
struct.unpack(" 100:
- raise SyntaxError("bad palette file")
+ msg = "bad palette file"
+ raise SyntaxError(msg)
v = tuple(map(int, s.split()[:3]))
if len(v) != 3:
- raise ValueError("bad palette entry")
+ msg = "bad palette entry"
+ raise ValueError(msg)
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
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 4575f8237dc..8a799f19caa 100644
--- a/src/PIL/GribStubImagePlugin.py
+++ b/src/PIL/GribStubImagePlugin.py
@@ -33,16 +33,15 @@ 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)):
- raise SyntaxError("Not a GRIB file")
+ msg = "Not a GRIB file"
+ raise SyntaxError(msg)
self.fp.seek(offset)
@@ -60,7 +59,8 @@ def _load(self):
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
- raise OSError("GRIB save handler not installed")
+ msg = "GRIB save handler not installed"
+ raise OSError(msg)
_handler.save(im, fp, filename)
diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py
index df11cf2a6db..bba05ed65a7 100644
--- a/src/PIL/Hdf5StubImagePlugin.py
+++ b/src/PIL/Hdf5StubImagePlugin.py
@@ -33,16 +33,15 @@ 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)):
- raise SyntaxError("Not an HDF file")
+ msg = "Not an HDF file"
+ raise SyntaxError(msg)
self.fp.seek(offset)
@@ -60,7 +59,8 @@ def _load(self):
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
- raise OSError("HDF5 save handler not installed")
+ msg = "HDF5 save handler not installed"
+ raise OSError(msg)
_handler.save(im, fp, filename)
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index fa192f053f9..27cb89f735e 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -22,11 +22,11 @@
import struct
import sys
-from PIL import Image, ImageFile, PngImagePlugin, features
+from . import Image, ImageFile, PngImagePlugin, features
enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k:
- from PIL import Jpeg2KImagePlugin
+ from . import Jpeg2KImagePlugin
MAGIC = b"icns"
HEADERSIZE = 8
@@ -42,7 +42,8 @@ def read_32t(fobj, start_length, size):
fobj.seek(start)
sig = fobj.read(4)
if sig != b"\x00\x00\x00\x00":
- raise SyntaxError("Unknown signature, expecting 0x00000000")
+ msg = "Unknown signature, expecting 0x00000000"
+ raise SyntaxError(msg)
return read_32(fobj, (start + 4, length - 4), size)
@@ -82,7 +83,8 @@ def read_32(fobj, start_length, size):
if bytesleft <= 0:
break
if bytesleft != 0:
- raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]")
+ msg = f"Error reading channel [{repr(bytesleft)} left]"
+ raise SyntaxError(msg)
band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1)
im.im.putband(band.im, band_ix)
return {"RGB": im}
@@ -113,10 +115,11 @@ def read_png_or_jpeg2000(fobj, start_length, size):
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
):
if not enable_jpeg2k:
- raise ValueError(
+ msg = (
"Unsupported icon subimage format (rebuild PIL "
"with JPEG 2000 support to fix this)"
)
+ raise ValueError(msg)
# j2k, jpc or j2c
fobj.seek(start)
jp2kstream = fobj.read(length)
@@ -127,11 +130,11 @@ def read_png_or_jpeg2000(fobj, start_length, size):
im = im.convert("RGBA")
return {"RGBA": im}
else:
- raise ValueError("Unsupported icon subimage format")
+ msg = "Unsupported icon subimage format"
+ raise ValueError(msg)
class IcnsFile:
-
SIZES = {
(512, 512, 2): [(b"ic10", read_png_or_jpeg2000)],
(512, 512, 1): [(b"ic09", read_png_or_jpeg2000)],
@@ -168,12 +171,14 @@ def __init__(self, fobj):
self.fobj = fobj
sig, filesize = nextheader(fobj)
if not _accept(sig):
- raise SyntaxError("not an icns file")
+ msg = "not an icns file"
+ raise SyntaxError(msg)
i = HEADERSIZE
while i < filesize:
sig, blocksize = nextheader(fobj)
if blocksize <= 0:
- raise SyntaxError("invalid block header")
+ msg = "invalid block header"
+ raise SyntaxError(msg)
i += HEADERSIZE
blocksize -= HEADERSIZE
dct[sig] = (i, blocksize)
@@ -183,7 +188,7 @@ def __init__(self, fobj):
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
@@ -192,7 +197,8 @@ def itersizes(self):
def bestsize(self):
sizes = self.itersizes()
if not sizes:
- raise SyntaxError("No 32bit icon resources found")
+ msg = "No 32bit icon resources found"
+ raise SyntaxError(msg)
return max(sizes)
def dataforsize(self, size):
@@ -275,7 +281,8 @@ def size(self, value):
if value in simple_sizes:
info_size = self.info["sizes"][simple_sizes.index(value)]
if info_size not in self.info["sizes"]:
- raise ValueError("This is not one of the allowed sizes of this image")
+ msg = "This is not one of the allowed sizes of this image"
+ raise ValueError(msg)
self._size = value
def load(self):
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index 17b9855a0a5..a188f8fdcea 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -127,7 +127,8 @@ def __init__(self, buf):
# check magic
s = buf.read(6)
if not _accept(s):
- raise SyntaxError("not an ICO file")
+ msg = "not an ICO file"
+ raise SyntaxError(msg)
self.buf = buf
self.entry = []
@@ -184,7 +185,7 @@ def sizes(self):
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
@@ -316,7 +317,8 @@ def size(self):
@size.setter
def size(self, value):
if value not in self.info["sizes"]:
- raise ValueError("This is not one of the allowed sizes of this image")
+ msg = "This is not one of the allowed sizes of this image"
+ raise ValueError(msg)
self._size = value
def load(self):
@@ -327,6 +329,7 @@ def load(self):
# if tile is PNG, it won't really be loaded yet
im.load()
self.im = im.im
+ self.pyaccess = None
self.mode = im.mode
if im.size != self.size:
warnings.warn("Image was not the expected size")
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 31b0ff46901..746743f658c 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -115,18 +115,17 @@ 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.
if b"\n" not in self.fp.read(100):
- raise SyntaxError("not an IM file")
+ msg = "not an IM file"
+ raise SyntaxError(msg)
self.fp.seek(0)
n = 0
@@ -139,7 +138,6 @@ def _open(self):
self.rawmode = "L"
while True:
-
s = self.fp.read(1)
# Some versions of IFUNC uses \n\r instead of \r\n...
@@ -153,7 +151,8 @@ def _open(self):
s = s + self.fp.readline()
if len(s) > 100:
- raise SyntaxError("not an IM file")
+ msg = "not an IM file"
+ raise SyntaxError(msg)
if s[-2:] == b"\r\n":
s = s[:-2]
@@ -163,10 +162,10 @@ def _open(self):
try:
m = split.match(s)
except re.error as e:
- raise SyntaxError("not an IM file") from e
+ msg = "not an IM file"
+ raise SyntaxError(msg) from e
if m:
-
k, v = m.group(1, 2)
# Don't know if this is the correct encoding,
@@ -197,13 +196,12 @@ def _open(self):
n += 1
else:
-
- raise SyntaxError(
- "Syntax error in IM header: " + s.decode("ascii", "replace")
- )
+ msg = "Syntax error in IM header: " + s.decode("ascii", "replace")
+ raise SyntaxError(msg)
if not n:
- raise SyntaxError("Not an IM file")
+ msg = "Not an IM file"
+ raise SyntaxError(msg)
# Basic attributes
self._size = self.info[SIZE]
@@ -213,7 +211,8 @@ def _open(self):
while s and s[:1] != b"\x1A":
s = self.fp.read(1)
if not s:
- raise SyntaxError("File truncated")
+ msg = "File truncated"
+ raise SyntaxError(msg)
if LUT in self.info:
# convert lookup table to palette or lut attribute
@@ -248,7 +247,6 @@ def _open(self):
self._fp = self.fp # FIXME: hack
if self.rawmode[:2] == "F;":
-
# ifunc95 formats
try:
# use bit decoder (if necessary)
@@ -328,11 +326,11 @@ def tell(self):
def _save(im, fp, filename):
-
try:
image_type, rawmode = SAVE[im.mode]
except KeyError as e:
- raise ValueError(f"Cannot save {im.mode} images as IM") from e
+ msg = f"Cannot save {im.mode} images as IM"
+ raise ValueError(msg) from e
frames = im.encoderinfo.get("frames", 1)
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 7faf0c2481b..a519a28af36 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -47,35 +47,17 @@
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
# Use __version__ instead.
-from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
+from . import (
+ ExifTags,
+ ImageMode,
+ TiffTags,
+ UnidentifiedImageError,
+ __version__,
+ _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]
- elif name in ("NEAREST", "NONE"):
- deprecate(name, 10, "Resampling.NEAREST or Dither.NONE")
- return 0
- old_resampling = {
- "LINEAR": "BILINEAR",
- "CUBIC": "BICUBIC",
- "ANTIALIAS": "LANCZOS",
- }
- if name in old_resampling:
- deprecate(name, 10, f"Resampling.{old_resampling[name]}")
- return Resampling[old_resampling[name]]
- for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize):
- if name in enum.__members__:
- deprecate(name, 10, f"{enum.__name__}.{name}")
- return enum[name]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
-
-
logger = logging.getLogger(__name__)
@@ -100,11 +82,12 @@ class DecompressionBombError(Exception):
from . import _imaging as core
if __version__ != getattr(core, "PILLOW_VERSION", None):
- raise ImportError(
+ msg = (
"The _imaging extension was built for another version of Pillow or PIL:\n"
f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n"
f"Pillow version: {__version__}"
)
+ raise ImportError(msg)
except ImportError as v:
core = DeferredError(ImportError("The _imaging C module is not installed."))
@@ -124,8 +107,7 @@ class DecompressionBombError(Exception):
raise
-# works everywhere, win for pypy, not cpython
-USE_CFFI_ACCESS = hasattr(sys, "pypy_version_info")
+USE_CFFI_ACCESS = False
try:
import cffi
except ImportError:
@@ -149,6 +131,7 @@ def isImageType(t):
#
# Constants
+
# transpose
class Transpose(IntEnum):
FLIP_LEFT_RIGHT = 0
@@ -209,6 +192,12 @@ class Quantize(IntEnum):
LIBIMAGEQUANT = 3
+module = sys.modules[__name__]
+for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize):
+ for item in enum:
+ setattr(module, item.name, item.value)
+
+
if hasattr(core, "DEFAULT_STRATEGY"):
DEFAULT_STRATEGY = core.DEFAULT_STRATEGY
FILTERED = core.FILTERED
@@ -381,7 +370,6 @@ def init():
def _getdecoder(mode, decoder_name, args, extra=()):
-
# tweak arguments
if args is None:
args = ()
@@ -399,12 +387,12 @@ def _getdecoder(mode, decoder_name, args, extra=()):
# get decoder
decoder = getattr(core, decoder_name + "_decoder")
except AttributeError as e:
- raise OSError(f"decoder {decoder_name} not available") from e
+ msg = f"decoder {decoder_name} not available"
+ raise OSError(msg) from e
return decoder(mode, *args + extra)
def _getencoder(mode, encoder_name, args, extra=()):
-
# tweak arguments
if args is None:
args = ()
@@ -422,7 +410,8 @@ def _getencoder(mode, encoder_name, args, extra=()):
# get encoder
encoder = getattr(core, encoder_name + "_encoder")
except AttributeError as e:
- raise OSError(f"encoder {encoder_name} not available") from e
+ msg = f"encoder {encoder_name} not available"
+ raise OSError(msg) from e
return encoder(mode, *args + extra)
@@ -430,26 +419,18 @@ def _getencoder(mode, encoder_name, args, extra=()):
# Simple expression analyzer
-def coerce_e(value):
- deprecate("coerce_e", 10)
- return value if isinstance(value, _E) else _E(1, value)
-
-
-# _E(scale, offset) represents the affine transformation scale * x + offset.
-# The "data" field is named for compatibility with the old implementation,
-# and should be renamed once coerce_e is removed.
class _E:
- def __init__(self, scale, data):
+ def __init__(self, scale, offset):
self.scale = scale
- self.data = data
+ self.offset = offset
def __neg__(self):
- return _E(-self.scale, -self.data)
+ return _E(-self.scale, -self.offset)
def __add__(self, other):
if isinstance(other, _E):
- return _E(self.scale + other.scale, self.data + other.data)
- return _E(self.scale, self.data + other)
+ return _E(self.scale + other.scale, self.offset + other.offset)
+ return _E(self.scale, self.offset + other)
__radd__ = __add__
@@ -462,19 +443,19 @@ def __rsub__(self, other):
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)
# --------------------------------------------------------------------
@@ -505,17 +486,10 @@ def __init__(self):
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]
@@ -628,7 +602,6 @@ def __eq__(self, other):
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()
)
@@ -659,26 +632,38 @@ def _repr_pretty_(self, p, cycle):
)
)
- def _repr_png_(self):
- """iPython display hook support
+ def _repr_image(self, image_format, **kwargs):
+ """Helper function for iPython display hook.
- :returns: png version of the image as bytes
+ :param image_format: Image format.
+ :returns: image as bytes, saved into the given format.
"""
b = io.BytesIO()
try:
- self.save(b, "PNG")
+ self.save(b, image_format, **kwargs)
except Exception as e:
- raise ValueError("Could not save to PNG for display") from e
+ msg = f"Could not save to {image_format} for display"
+ raise ValueError(msg) from e
return b.getvalue()
+ def _repr_png_(self):
+ """iPython display hook support for PNG format.
+
+ :returns: PNG version of the image as bytes
+ """
+ return self._repr_image("PNG", compress_level=1)
+
+ def _repr_jpeg_(self):
+ """iPython display hook support for JPEG format.
+
+ :returns: JPEG version of the image as bytes
+ """
+ return self._repr_image("JPEG")
+
@property
def __array_interface__(self):
# numpy array interface support
- new = {}
- shape, typestr = _conv_type_shape(self)
- new["shape"] = shape
- new["typestr"] = typestr
- new["version"] = 3
+ new = {"version": 3}
try:
if self.mode == "1":
# Binary images need to be extended from bits to bytes
@@ -697,14 +682,15 @@ def __array_interface__(self):
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)
- self.tile = []
info, mode, size, palette, data = state
self.info = info
self.mode = mode
@@ -754,16 +740,17 @@ def tobytes(self, encoder_name="raw", *args):
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:
- raise RuntimeError(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"):
"""
@@ -778,7 +765,8 @@ def tobitmap(self, name="image"):
self.load()
if self.mode != "1":
- raise ValueError("not a bitmap")
+ msg = "not a bitmap"
+ raise ValueError(msg)
data = self.tobytes("xbm")
return b"".join(
[
@@ -812,9 +800,11 @@ def frombytes(self, data, decoder_name="raw", *args):
s = d.decode(data)
if s[0] >= 0:
- raise ValueError("not enough image data")
+ msg = "not enough image data"
+ raise ValueError(msg)
if s[1] != 0:
- raise ValueError("cannot decode image data")
+ msg = "cannot decode image data"
+ raise ValueError(msg)
def load(self):
"""
@@ -935,7 +925,8 @@ def convert(
if matrix:
# matrix conversion
if mode not in ("L", "RGB"):
- raise ValueError("illegal conversion")
+ msg = "illegal conversion"
+ raise ValueError(msg)
im = self.im.convert_matrix(mode, matrix)
new = self._new(im)
if has_transparency and self.im.bands == 3:
@@ -1020,7 +1011,8 @@ def convert_transparency(m, v):
elif isinstance(t, int):
self.im.putpalettealpha(t, 0)
else:
- raise ValueError("Transparency for P mode should be bytes or int")
+ msg = "Transparency for P mode should be bytes or int"
+ raise ValueError(msg)
if mode == "P" and palette == Palette.ADAPTIVE:
im = self.im.quantize(colors)
@@ -1070,7 +1062,8 @@ def convert_transparency(m, v):
im = self.im.convert(modebase)
im = im.convert(mode, dither)
except KeyError as e:
- raise ValueError("illegal conversion") from e
+ msg = "illegal conversion"
+ raise ValueError(msg) from e
new_im = self._new(im)
if mode == "P" and palette != Palette.ADAPTIVE:
@@ -1129,7 +1122,6 @@ def quantize(
Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
(default).
:returns: A new image
-
"""
self.load()
@@ -1145,20 +1137,21 @@ def quantize(
Quantize.LIBIMAGEQUANT,
):
# Caller specified an invalid mode.
- raise ValueError(
+ msg = (
"Fast Octree (method == 2) and libimagequant (method == 3) "
"are the only valid methods for quantizing RGBA images"
)
+ raise ValueError(msg)
if palette:
# use palette from reference image
palette.load()
if palette.mode != "P":
- raise ValueError("bad mode for palette image")
+ msg = "bad mode for palette image"
+ raise ValueError(msg)
if self.mode != "RGB" and self.mode != "L":
- raise ValueError(
- "only RGB or L mode images can be quantized to a palette"
- )
+ msg = "only RGB or L mode images can be quantized to a palette"
+ raise ValueError(msg)
im = self.im.convert("P", dither, palette.im)
new_im = self._new(im)
new_im.palette = palette.palette.copy()
@@ -1204,9 +1197,11 @@ def crop(self, box=None):
return self.copy()
if box[2] < box[0]:
- raise ValueError("Coordinate 'right' is less than 'left'")
+ msg = "Coordinate 'right' is less than 'left'"
+ raise ValueError(msg)
elif box[3] < box[1]:
- raise ValueError("Coordinate 'lower' is less than 'upper'")
+ msg = "Coordinate 'lower' is less than 'upper'"
+ raise ValueError(msg)
self.load()
return self._new(self._crop(self.im, box))
@@ -1249,7 +1244,8 @@ def draft(self, mode, size):
currently implemented only for JPEG and MPO images.
:param mode: The requested mode.
- :param size: The requested size.
+ :param size: The requested size in pixels, as a 2-tuple:
+ (width, height).
"""
pass
@@ -1257,7 +1253,7 @@ def _expand(self, xmargin, ymargin=None):
if ymargin is None:
ymargin = xmargin
self.load()
- return self._new(self.im.expand(xmargin, ymargin, 0))
+ return self._new(self.im.expand(xmargin, ymargin))
def filter(self, filter):
"""
@@ -1274,9 +1270,8 @@ def filter(self, filter):
if isinstance(filter, Callable):
filter = filter()
if not hasattr(filter, "filter"):
- raise TypeError(
- "filter argument should be ImageFilter.Filter instance or class"
- )
+ msg = "filter argument should be ImageFilter.Filter instance or class"
+ raise TypeError(msg)
multiband = isinstance(filter, ImageFilter.MultibandFilter)
if self.im.bands == 1 or multiband:
@@ -1297,11 +1292,15 @@ def getbands(self):
"""
return ImageMode.getmode(self.mode).bands
- def getbbox(self):
+ def getbbox(self, *, alpha_only=True):
"""
Calculates the bounding box of the non-zero regions in the
image.
+ :param alpha_only: Optional flag, defaulting to ``True``.
+ If ``True`` and the image has an alpha channel, trim transparent pixels.
+ Otherwise, trim pixels when all channels are zero.
+ Keyword-only argument.
:returns: The bounding box is returned as a 4-tuple defining the
left, upper, right, and lower pixel coordinate. See
:ref:`coordinate-system`. If the image is completely empty, this
@@ -1310,7 +1309,7 @@ def getbbox(self):
"""
self.load()
- return self.im.getbbox()
+ return self.im.getbbox(alpha_only)
def getcolors(self, maxcolors=256):
"""
@@ -1411,6 +1410,11 @@ def get_value(element):
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
@@ -1432,12 +1436,12 @@ def getexif(self):
self._exif.load(exif_info)
# XMP tags
- if 0x0112 not in self._exif:
+ if ExifTags.Base.Orientation not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match:
- self._exif[0x0112] = int(match[2])
+ self._exif[ExifTags.Base.Orientation] = int(match[2])
return self._exif
@@ -1447,6 +1451,49 @@ def _reload_exif(self):
self._exif._loaded = False
self.getexif()
+ def get_child_images(self):
+ child_images = []
+ exif = self.getexif()
+ ifds = []
+ if ExifTags.Base.SubIFDs in exif:
+ subifd_offsets = exif[ExifTags.Base.SubIFDs]
+ if subifd_offsets:
+ if not isinstance(subifd_offsets, tuple):
+ subifd_offsets = (subifd_offsets,)
+ for subifd_offset in subifd_offsets:
+ ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
+ ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
+ if ifd1 and ifd1.get(513):
+ ifds.append((ifd1, exif._info.next))
+
+ offset = None
+ for ifd, ifd_offset in ifds:
+ current_offset = self.fp.tell()
+ if offset is None:
+ offset = current_offset
+
+ fp = self.fp
+ thumbnail_offset = ifd.get(513)
+ if thumbnail_offset is not None:
+ try:
+ thumbnail_offset += self._exif_offset
+ except AttributeError:
+ pass
+ self.fp.seek(thumbnail_offset)
+ data = self.fp.read(ifd.get(514))
+ fp = io.BytesIO(data)
+
+ with open(fp) as im:
+ if thumbnail_offset is None:
+ im._frame_pos = [ifd_offset]
+ im._seek(0)
+ im.load()
+ child_images.append(im)
+
+ if offset is not None:
+ self.fp.seek(offset)
+ return child_images
+
def getim(self):
"""
Returns a capsule that points to the internal image memory.
@@ -1482,7 +1529,8 @@ def getpalette(self, rawmode="RGB"):
def apply_transparency(self):
"""
If a P mode image has a "transparency" key in the info dictionary,
- remove the key and apply the transparency to the palette instead.
+ remove the key and instead apply the transparency to the palette.
+ Otherwise, the image is unchanged.
"""
if self.mode != "P" or "transparency" not in self.info:
return
@@ -1641,7 +1689,8 @@ def paste(self, im, box=None, mask=None):
size = mask.size
else:
# FIXME: use self.size here?
- raise ValueError("cannot determine region size; use 4-item box")
+ msg = "cannot determine region size; use 4-item box"
+ raise ValueError(msg)
box += (box[0] + size[0], box[1] + size[1])
if isinstance(im, str):
@@ -1680,15 +1729,20 @@ def alpha_composite(self, im, dest=(0, 0), source=(0, 0)):
"""
if not isinstance(source, (list, tuple)):
- raise ValueError("Source must be a tuple")
+ msg = "Source must be a tuple"
+ raise ValueError(msg)
if not isinstance(dest, (list, tuple)):
- raise ValueError("Destination must be a tuple")
- if not len(source) in (2, 4):
- raise ValueError("Source must be a 2 or 4-tuple")
+ msg = "Destination must be a tuple"
+ raise ValueError(msg)
+ if len(source) not in (2, 4):
+ msg = "Source must be a 2 or 4-tuple"
+ raise ValueError(msg)
if not len(dest) == 2:
- raise ValueError("Destination must be a 2-tuple")
+ msg = "Destination must be a 2-tuple"
+ raise ValueError(msg)
if min(source) < 0:
- raise ValueError("Source must be non-negative")
+ msg = "Source must be non-negative"
+ raise ValueError(msg)
if len(source) == 2:
source = source + im.size
@@ -1753,7 +1807,8 @@ def point(self, data):
if self.mode == "F":
# FIXME: _imaging returns a confusing error message for this case
- raise ValueError("point operation not supported for this mode")
+ msg = "point operation not supported for this mode"
+ raise ValueError(msg)
if mode != "F":
lut = [round(i) for i in lut]
@@ -1787,7 +1842,8 @@ def putalpha(self, alpha):
self.pyaccess = None
self.mode = self.im.mode
except KeyError as e:
- raise ValueError("illegal image mode") from e
+ msg = "illegal image mode"
+ raise ValueError(msg) from e
if self.mode in ("LA", "PA"):
band = 1
@@ -1797,7 +1853,8 @@ def putalpha(self, alpha):
if isImageType(alpha):
# alpha layer
if alpha.mode not in ("1", "L"):
- raise ValueError("illegal image mode")
+ msg = "illegal image mode"
+ raise ValueError(msg)
alpha.load()
if alpha.mode == "1":
alpha = alpha.convert("L")
@@ -1853,7 +1910,8 @@ def putpalette(self, data, rawmode="RGB"):
from . import ImagePalette
if self.mode not in ("L", "LA", "P", "PA"):
- raise ValueError("illegal image mode")
+ msg = "illegal image mode"
+ raise ValueError(msg)
if isinstance(data, ImagePalette.ImagePalette):
palette = ImagePalette.raw(data.rawmode, data.palette)
else:
@@ -1922,7 +1980,8 @@ def remap_palette(self, dest_map, source_palette=None):
from . import ImagePalette
if self.mode not in ("L", "P"):
- raise ValueError("illegal image mode")
+ msg = "illegal image mode"
+ raise ValueError(msg)
bands = 3
palette_mode = "RGB"
@@ -2054,7 +2113,7 @@ def resize(self, size, resample=None, box=None, reducing_gap=None):
Resampling.BOX,
Resampling.HAMMING,
):
- message = f"Unknown resampling filter ({resample})."
+ msg = f"Unknown resampling filter ({resample})."
filters = [
f"{filter[1]} ({filter[0]})"
@@ -2067,12 +2126,12 @@ def resize(self, size, resample=None, box=None, reducing_gap=None):
(Resampling.HAMMING, "Image.Resampling.HAMMING"),
)
]
- raise ValueError(
- message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
- )
+ msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
+ raise ValueError(msg)
if reducing_gap is not None and reducing_gap < 1.0:
- raise ValueError("reducing_gap must be 1.0 or greater")
+ msg = "reducing_gap must be 1.0 or greater"
+ raise ValueError(msg)
size = tuple(size)
@@ -2330,7 +2389,8 @@ def save(self, fp, format=None, **params):
try:
format = EXTENSION[ext]
except KeyError as e:
- raise ValueError(f"unknown file extension: {ext}") from e
+ msg = f"unknown file extension: {ext}"
+ raise ValueError(msg) from e
if format.upper() not in SAVE:
init()
@@ -2394,8 +2454,8 @@ def show(self, title=None):
The image is first saved to a temporary file. By default, it will be in
PNG format.
- On Unix, the image is then opened using the **display**, **eog** or
- **xv** utility, depending on which one can be found.
+ On Unix, the image is then opened using the **xdg-open**, **display**,
+ **gm**, **eog** or **xv** utility, depending on which one can be found.
On macOS, the image is opened with the native Preview application.
@@ -2444,7 +2504,8 @@ def getchannel(self, channel):
try:
channel = self.getbands().index(channel)
except ValueError as e:
- raise ValueError(f'The image has no channel "{channel}"') from e
+ msg = f'The image has no channel "{channel}"'
+ raise ValueError(msg) from e
return self._new(self.im.getband(channel))
@@ -2473,7 +2534,8 @@ def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0):
apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original
image.
- :param size: Requested size.
+ :param size: The requested size in pixels, as a 2-tuple:
+ (width, height).
:param resample: Optional resampling filter. This can be one
of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
:py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
@@ -2560,7 +2622,8 @@ def transform(
given size, and the same mode as the original, and copies data
to the new image using the given transform.
- :param size: The output size.
+ :param size: The output size in pixels, as a 2-tuple:
+ (width, height).
:param method: The transformation method. This is one of
:py:data:`Transform.EXTENT` (cut out a rectangular subregion),
:py:data:`Transform.AFFINE` (affine transform),
@@ -2615,7 +2678,8 @@ def getdata(self):
method, data = method.getdata()
if data is None:
- raise ValueError("missing method data")
+ msg = "missing method data"
+ raise ValueError(msg)
im = new(self.mode, size, fillcolor)
if self.mode == "P" and self.palette:
@@ -2676,7 +2740,8 @@ def __transformer(
)
else:
- raise ValueError("unknown transformation method")
+ msg = "unknown transformation method"
+ raise ValueError(msg)
if resample not in (
Resampling.NEAREST,
@@ -2684,13 +2749,13 @@ def __transformer(
Resampling.BICUBIC,
):
if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS):
- message = {
+ msg = {
Resampling.BOX: "Image.Resampling.BOX",
Resampling.HAMMING: "Image.Resampling.HAMMING",
Resampling.LANCZOS: "Image.Resampling.LANCZOS",
}[resample] + f" ({resample}) cannot be used."
else:
- message = f"Unknown resampling filter ({resample})."
+ msg = f"Unknown resampling filter ({resample})."
filters = [
f"{filter[1]} ({filter[0]})"
@@ -2700,9 +2765,8 @@ def __transformer(
(Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
)
]
- raise ValueError(
- message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
- )
+ msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
+ raise ValueError(msg)
image.load()
@@ -2741,7 +2805,8 @@ def toqimage(self):
from . import ImageQt
if not ImageQt.qt_is_installed:
- raise ImportError("Qt bindings are not installed")
+ msg = "Qt bindings are not installed"
+ raise ImportError(msg)
return ImageQt.toqimage(self)
def toqpixmap(self):
@@ -2749,7 +2814,8 @@ def toqpixmap(self):
from . import ImageQt
if not ImageQt.qt_is_installed:
- raise ImportError("Qt bindings are not installed")
+ msg = "Qt bindings are not installed"
+ raise ImportError(msg)
return ImageQt.toqpixmap(self)
@@ -2797,11 +2863,14 @@ def _check_size(size):
"""
if not isinstance(size, (list, tuple)):
- raise ValueError("Size must be a tuple")
+ msg = "Size must be a tuple"
+ raise ValueError(msg)
if len(size) != 2:
- raise ValueError("Size must be a tuple of length 2")
+ msg = "Size must be a tuple of length 2"
+ raise ValueError(msg)
if size[0] < 0 or size[1] < 0:
- raise ValueError("Width and height must be >= 0")
+ msg = "Width and height must be >= 0"
+ raise ValueError(msg)
return True
@@ -2816,7 +2885,7 @@ def new(mode, size, color=0):
:param color: What color to use for the image. Default is black.
If given, this should be a single integer or floating point value
for single-band modes, and a tuple for multi-band modes (one value
- per band). When creating RGB images, you can also use color
+ per band). When creating RGB or HSV images, you can also use color
strings as supported by the ImageColor module. If the color is
None, the image is not initialised.
:returns: An :py:class:`~PIL.Image.Image` object.
@@ -2943,21 +3012,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
@@ -2987,11 +3064,13 @@ def fromarray(obj, mode=None):
try:
typekey = (1, 1) + shape[2:], arr["typestr"]
except KeyError as e:
- raise TypeError("Cannot handle this data type") from e
+ msg = "Cannot handle this data type"
+ raise TypeError(msg) from e
try:
mode, rawmode = _fromarray_typemap[typekey]
except KeyError as e:
- raise TypeError("Cannot handle this data type: %s, %s" % typekey) from e
+ msg = "Cannot handle this data type: %s, %s" % typekey
+ raise TypeError(msg) from e
else:
rawmode = mode
if mode in ["1", "L", "I", "P", "F"]:
@@ -3001,7 +3080,8 @@ def fromarray(obj, mode=None):
else:
ndmax = 4
if ndim > ndmax:
- raise ValueError(f"Too many dimensions: {ndim} > {ndmax}.")
+ msg = f"Too many dimensions: {ndim} > {ndmax}."
+ raise ValueError(msg)
size = 1 if ndim == 1 else shape[1], shape[0]
if strides is not None:
@@ -3018,7 +3098,8 @@ def fromqimage(im):
from . import ImageQt
if not ImageQt.qt_is_installed:
- raise ImportError("Qt bindings are not installed")
+ msg = "Qt bindings are not installed"
+ raise ImportError(msg)
return ImageQt.fromqimage(im)
@@ -3027,7 +3108,8 @@ def fromqpixmap(im):
from . import ImageQt
if not ImageQt.qt_is_installed:
- raise ImportError("Qt bindings are not installed")
+ msg = "Qt bindings are not installed"
+ raise ImportError(msg)
return ImageQt.fromqpixmap(im)
@@ -3062,13 +3144,14 @@ def _decompression_bomb_check(size):
if MAX_IMAGE_PIXELS is None:
return
- pixels = size[0] * size[1]
+ pixels = max(1, size[0]) * max(1, size[1])
if pixels > 2 * MAX_IMAGE_PIXELS:
- raise DecompressionBombError(
+ msg = (
f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} "
"pixels, could be decompression bomb DOS attack."
)
+ raise DecompressionBombError(msg)
if pixels > MAX_IMAGE_PIXELS:
warnings.warn(
@@ -3091,7 +3174,8 @@ def open(fp, mode="r", formats=None):
:param fp: A filename (string), pathlib.Path object or a file object.
The file object must implement ``file.read``,
``file.seek``, and ``file.tell`` methods,
- and be opened in binary mode.
+ and be opened in binary mode. The file object will also seek to zero
+ before reading.
:param mode: The mode. If given, this argument must be "r".
:param formats: A list or tuple of formats to attempt to load the file in.
This can be used to restrict the set of formats checked.
@@ -3108,17 +3192,20 @@ def open(fp, mode="r", formats=None):
"""
if mode != "r":
- raise ValueError(f"bad mode {repr(mode)}")
+ msg = f"bad mode {repr(mode)}"
+ raise ValueError(msg)
elif isinstance(fp, io.StringIO):
- raise ValueError(
+ msg = (
"StringIO cannot be used to open an image. "
"Binary data must be used instead."
)
+ raise ValueError(msg)
if formats is None:
formats = ID
elif not isinstance(formats, (list, tuple)):
- raise TypeError("formats must be a list or tuple")
+ msg = "formats must be a list or tuple"
+ raise TypeError(msg)
exclusive_fp = False
filename = ""
@@ -3171,9 +3258,15 @@ def _open_core(fp, filename, prefix, formats):
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
@@ -3183,9 +3276,8 @@ def _open_core(fp, filename, prefix, formats):
fp.close()
for message in accept_warnings:
warnings.warn(message)
- raise UnidentifiedImageError(
- "cannot identify image file %r" % (filename if filename else fp)
- )
+ msg = "cannot identify image file %r" % (filename if filename else fp)
+ raise UnidentifiedImageError(msg)
#
@@ -3276,12 +3368,15 @@ def merge(mode, bands):
"""
if getmodebands(mode) != len(bands) or "*" in mode:
- raise ValueError("wrong number of bands")
+ msg = "wrong number of bands"
+ raise ValueError(msg)
for band in bands[1:]:
if band.mode != getmodetype(mode):
- raise ValueError("mode mismatch")
+ msg = "mode mismatch"
+ raise ValueError(msg)
if band.size != bands[0].size:
- raise ValueError("size mismatch")
+ msg = "size mismatch"
+ raise ValueError(msg)
for band in bands:
band.load()
return bands[0]._new(core.merge(mode, *[b.im for b in bands]))
@@ -3302,7 +3397,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
@@ -3368,8 +3464,7 @@ def registered_extensions():
Returns a dictionary containing all file extensions belonging
to registered plugins
"""
- if not EXTENSION:
- init()
+ init()
return EXTENSION
@@ -3498,11 +3593,45 @@ def _apply_env_variables(env=None):
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
def __init__(self):
self._data = {}
+ self._hidden_data = {}
self._ifds = {}
self._info = None
self._loaded_exif = None
@@ -3556,6 +3685,7 @@ def load(self, data):
return
self._loaded_exif = data
self._data.clear()
+ self._hidden_data.clear()
self._ifds.clear()
if data and data.startswith(b"Exif\x00\x00"):
data = data[6:]
@@ -3576,6 +3706,7 @@ def load(self, data):
def load_from_fp(self, fp, offset=None):
self._loaded_exif = None
self._data.clear()
+ self._hidden_data.clear()
self._ifds.clear()
# process dictionary
@@ -3598,14 +3729,16 @@ def _get_merged_dict(self):
merged_dict = dict(self)
# get EXIF extension
- if 0x8769 in self:
- ifd = self._get_ifd_dict(self[0x8769])
+ if ExifTags.IFD.Exif in self:
+ ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif])
if ifd:
merged_dict.update(ifd)
# GPS
- if 0x8825 in self:
- merged_dict[0x8825] = self._get_ifd_dict(self[0x8825])
+ if ExifTags.IFD.GPSInfo in self:
+ merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict(
+ self[ExifTags.IFD.GPSInfo]
+ )
return merged_dict
@@ -3615,31 +3748,35 @@ def tobytes(self, offset=8):
head = self._get_head()
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, value in self.items():
- if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict):
+ if tag in [
+ ExifTags.IFD.Exif,
+ ExifTags.IFD.GPSInfo,
+ ] and not isinstance(value, dict):
value = self.get_ifd(tag)
if (
- tag == 0x8769
- and 0xA005 in value
- and not isinstance(value[0xA005], dict)
+ tag == ExifTags.IFD.Exif
+ and ExifTags.IFD.Interop in value
+ and not isinstance(value[ExifTags.IFD.Interop], dict)
):
value = value.copy()
- value[0xA005] = self.get_ifd(0xA005)
+ value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop)
ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset)
def get_ifd(self, tag):
if tag not in self._ifds:
- if tag in [0x8769, 0x8825]:
- # exif, gpsinfo
- if tag in self:
- self._ifds[tag] = self._get_ifd_dict(self[tag])
- elif tag in [0xA005, 0x927C]:
- # interop, makernote
- if 0x8769 not in self._ifds:
- self.get_ifd(0x8769)
- tag_data = self._ifds[0x8769][tag]
- if tag == 0x927C:
- # makernote
+ if tag == ExifTags.IFD.IFD1:
+ if self._info is not None and self._info.next != 0:
+ self._ifds[tag] = self._get_ifd_dict(self._info.next)
+ elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
+ offset = self._hidden_data.get(tag, self.get(tag))
+ if offset is not None:
+ self._ifds[tag] = self._get_ifd_dict(offset)
+ elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
+ if ExifTags.IFD.Exif not in self._ifds:
+ self.get_ifd(ExifTags.IFD.Exif)
+ tag_data = self._ifds[ExifTags.IFD.Exif][tag]
+ if tag == ExifTags.IFD.Makernote:
from .TiffImagePlugin import ImageFileDirectory_v2
if tag_data[:8] == b"FUJIFILM":
@@ -3715,14 +3852,27 @@ def get_ifd(self, tag):
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
self._ifds[tag] = makernote
else:
- # interop
+ # Interop
self._ifds[tag] = self._get_ifd_dict(tag_data)
- return self._ifds.get(tag, {})
+ ifd = self._ifds.get(tag, {})
+ if tag == ExifTags.IFD.Exif and self._hidden_data:
+ ifd = {
+ k: v
+ for (k, v) in ifd.items()
+ if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote)
+ }
+ return ifd
+
+ def hide_offsets(self):
+ for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo):
+ if tag in self:
+ self._hidden_data[tag] = self[tag]
+ del self[tag]
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 fec4694b290..70120031797 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 605252d5d4c..3a337f9f209 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -18,12 +18,10 @@
import sys
from enum import IntEnum
-from PIL import Image
-
-from ._deprecate import deprecate
+from . import Image
try:
- from PIL import _imagingcms
+ from . import _imagingcms
except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing
# anything in core.
@@ -117,16 +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]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
-
-
#
# flags
@@ -191,17 +179,14 @@ def __init__(self, profile):
elif isinstance(profile, _imagingcms.CmsProfile):
self._set(profile)
else:
- raise TypeError("Invalid type for Profile")
+ msg = "Invalid type for Profile"
+ raise TypeError(msg)
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):
"""
@@ -269,7 +254,8 @@ def apply(self, im, imOut=None):
def apply_in_place(self, im):
im.load()
if im.mode != self.output_mode:
- raise ValueError("mode mismatch") # wrong output mode
+ msg = "mode mismatch"
+ raise ValueError(msg) # wrong output mode
self.transform.apply(im.im.id, im.im.id)
im.info["icc_profile"] = self.output_profile.tobytes()
return im
@@ -285,7 +271,7 @@ def get_display_profile(handle=None):
if sys.platform != "win32":
return None
- from PIL import ImageWin
+ from . import ImageWin
if isinstance(handle, ImageWin.HDC):
profile = core.get_display_profile_win32(handle, 1)
@@ -374,10 +360,12 @@ def profileToProfile(
outputMode = im.mode
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
- raise PyCMSError("renderingIntent must be an integer between 0 and 3")
+ msg = "renderingIntent must be an integer between 0 and 3"
+ raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- raise PyCMSError(f"flags must be an integer between 0 and {_MAX_FLAG}")
+ msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
+ raise PyCMSError(msg)
try:
if not isinstance(inputProfile, ImageCmsProfile):
@@ -489,10 +477,12 @@ def buildTransform(
"""
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
- raise PyCMSError("renderingIntent must be an integer between 0 and 3")
+ msg = "renderingIntent must be an integer between 0 and 3"
+ raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
+ msg = "flags must be an integer between 0 and %s" + _MAX_FLAG
+ raise PyCMSError(msg)
try:
if not isinstance(inputProfile, ImageCmsProfile):
@@ -591,10 +581,12 @@ def buildProofTransform(
"""
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
- raise PyCMSError("renderingIntent must be an integer between 0 and 3")
+ msg = "renderingIntent must be an integer between 0 and 3"
+ raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
+ msg = "flags must be an integer between 0 and %s" + _MAX_FLAG
+ raise PyCMSError(msg)
try:
if not isinstance(inputProfile, ImageCmsProfile):
@@ -705,17 +697,17 @@ def createProfile(colorSpace, colorTemp=-1):
"""
if colorSpace not in ["LAB", "XYZ", "sRGB"]:
- raise PyCMSError(
+ msg = (
f"Color space not supported for on-the-fly profile creation ({colorSpace})"
)
+ raise PyCMSError(msg)
if colorSpace == "LAB":
try:
colorTemp = float(colorTemp)
except (TypeError, ValueError) as e:
- raise PyCMSError(
- f'Color temperature must be numeric, "{colorTemp}" not valid'
- ) from e
+ msg = f'Color temperature must be numeric, "{colorTemp}" not valid'
+ raise PyCMSError(msg) from e
try:
return core.createProfile(colorSpace, colorTemp)
diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py
index 9cbce4143f3..befc1fd1d88 100644
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -33,7 +33,8 @@ def getrgb(color):
:return: ``(red, green, blue[, alpha])``
"""
if len(color) > 100:
- raise ValueError("color specifier is too long")
+ msg = "color specifier is too long"
+ raise ValueError(msg)
color = color.lower()
rgb = colormap.get(color, None)
@@ -115,14 +116,17 @@ def getrgb(color):
m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
if m:
return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
- raise ValueError(f"unknown color specifier: {repr(color)}")
+ msg = f"unknown color specifier: {repr(color)}"
+ raise ValueError(msg)
def getcolor(color, mode):
"""
- Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
- greyscale value if ``mode`` is not color or a palette image. If the string
- cannot be parsed, this function raises a :py:exc:`ValueError` exception.
+ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
+ ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
+ not color or a palette image, converts the RGB value to a greyscale value.
+ If the string cannot be parsed, this function raises a :py:exc:`ValueError`
+ exception.
.. versionadded:: 1.1.4
@@ -135,7 +139,13 @@ def getcolor(color, mode):
if len(color) == 4:
color, alpha = color[:3], color[3]
- if Image.getmodebase(mode) == "L":
+ if mode == "HSV":
+ from colorsys import rgb_to_hsv
+
+ r, g, b = color
+ h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255)
+ return int(h * 255), int(s * 255), int(v * 255)
+ elif Image.getmodebase(mode) == "L":
r, g, b = color
# ITU-R Recommendation 601-2 for nonlinear RGB
# scaled to 24 bits to match the convert's implementation.
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index ff94f0ce3d6..7d1790faa93 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.
@@ -69,7 +67,8 @@ def __init__(self, im, mode=None):
if mode == "RGBA" and im.mode == "RGB":
blend = 1
else:
- raise ValueError("mode mismatch")
+ msg = "mode mismatch"
+ raise ValueError(msg)
if mode == "P":
self.palette = im.palette
else:
@@ -280,11 +279,11 @@ def polygon(self, xy, fill=None, outline=None, width=1):
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."""
@@ -294,29 +293,43 @@ def rectangle(self, xy, fill=None, outline=None, width=1):
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)
-
- if d == 0:
- # If the corners have no curve, that is a rectangle
+ full_x, full_y = False, False
+ if all(corners):
+ full_x = d >= x1 - x0 - 1
+ if full_x:
+ # The two left and two right corners are joined
+ d = x1 - x0
+ full_y = d >= y1 - y0 - 1
+ if full_y:
+ # The two top and two bottom corners are joined
+ d = y1 - y0
+ if full_x and full_y:
+ # If all corners are joined, that is a circle
+ return self.ellipse(xy, fill, outline, width)
+
+ if d == 0 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
@@ -337,12 +350,17 @@ def draw_corners(pieslice):
)
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)))
@@ -357,28 +375,52 @@ def draw_corners(pieslice):
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
@@ -389,17 +431,11 @@ def _multiline_split(self, text):
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,
@@ -419,6 +455,7 @@ def text(
*args,
**kwargs,
):
+ """Draw text."""
if self._multiline_check(text):
return self.multiline_text(
xy,
@@ -437,7 +474,8 @@ def text(
)
if embedded_color and self.mode not in ("RGB", "RGBA"):
- raise ValueError("Embedded color supported only in RGB and RGBA modes")
+ msg = "Embedded color supported only in RGB and RGBA modes"
+ raise ValueError(msg)
if font is None:
font = self.getfont()
@@ -452,7 +490,11 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
mode = self.fontmode
if stroke_width == 0 and embedded_color:
mode = "RGBA"
- coord = xy
+ coord = []
+ start = []
+ for i in range(2):
+ coord.append(int(xy[i]))
+ start.append(math.modf(xy[i])[0])
try:
mask, offset = font.getmask2(
text,
@@ -463,6 +505,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
stroke_width=stroke_width,
anchor=anchor,
ink=ink,
+ start=start,
*args,
**kwargs,
)
@@ -478,6 +521,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
stroke_width,
anchor,
ink,
+ start=start,
*args,
**kwargs,
)
@@ -490,7 +534,7 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
# extract mask and set text alpha
color, mask = mask, mask.getband(3)
color.fillband(3, (ink >> 24) & 0xFF)
- x, y = (int(c) for c in coord)
+ x, y = coord
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
else:
self.draw.draw_bitmap(coord, mask, ink)
@@ -528,14 +572,17 @@ def multiline_text(
embedded_color=False,
):
if direction == "ttb":
- raise ValueError("ttb direction is unsupported for multiline text")
+ msg = "ttb direction is unsupported for multiline text"
+ raise ValueError(msg)
if anchor is None:
anchor = "la"
elif len(anchor) != 2:
- raise ValueError("anchor must be a 2 character string")
+ msg = "anchor must be a 2 character string"
+ raise ValueError(msg)
elif anchor[1] in "tb":
- raise ValueError("anchor not supported for multiline text")
+ msg = "anchor not supported for multiline text"
+ raise ValueError(msg)
widths = []
max_width = 0
@@ -572,7 +619,8 @@ def multiline_text(
elif align == "right":
left += width_difference
else:
- raise ValueError('align must be "left", "center" or "right"')
+ msg = 'align must be "left", "center" or "right"'
+ raise ValueError(msg)
self.text(
(left, top),
@@ -589,72 +637,6 @@ def multiline_text(
)
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,
@@ -666,29 +648,16 @@ def textlength(
):
"""Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text):
- raise ValueError("can't measure length of multiline text")
+ msg = "can't measure length of multiline text"
+ raise ValueError(msg)
if embedded_color and self.mode not in ("RGB", "RGBA"):
- raise ValueError("Embedded color supported only in RGB and RGBA modes")
+ msg = "Embedded color supported only in RGB and RGBA modes"
+ raise ValueError(msg)
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,
@@ -706,7 +675,8 @@ def textbbox(
):
"""Get the bounding box of a given string, in pixels."""
if embedded_color and self.mode not in ("RGB", "RGBA"):
- raise ValueError("Embedded color supported only in RGB and RGBA modes")
+ msg = "Embedded color supported only in RGB and RGBA modes"
+ raise ValueError(msg)
if self._multiline_check(text):
return self.multiline_textbbox(
@@ -746,14 +716,17 @@ def multiline_textbbox(
embedded_color=False,
):
if direction == "ttb":
- raise ValueError("ttb direction is unsupported for multiline text")
+ msg = "ttb direction is unsupported for multiline text"
+ raise ValueError(msg)
if anchor is None:
anchor = "la"
elif len(anchor) != 2:
- raise ValueError("anchor must be a 2 character string")
+ msg = "anchor must be a 2 character string"
+ raise ValueError(msg)
elif anchor[1] in "tb":
- raise ValueError("anchor not supported for multiline text")
+ msg = "anchor not supported for multiline text"
+ raise ValueError(msg)
widths = []
max_width = 0
@@ -797,7 +770,8 @@ def multiline_textbbox(
elif align == "right":
left += width_difference
else:
- raise ValueError('align must be "left", "center" or "right"')
+ msg = 'align must be "left", "center" or "right"'
+ raise ValueError(msg)
bbox_line = self.textbbox(
(left, top),
@@ -909,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
@@ -973,38 +947,44 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
# 1. Error Handling
# 1.1 Check `n_sides` has an appropriate value
if not isinstance(n_sides, int):
- raise TypeError("n_sides should be an int")
+ msg = "n_sides should be an int"
+ raise TypeError(msg)
if n_sides < 3:
- raise ValueError("n_sides should be an int > 2")
+ msg = "n_sides should be an int > 2"
+ raise ValueError(msg)
# 1.2 Check `bounding_circle` has an appropriate value
if not isinstance(bounding_circle, (list, tuple)):
- raise TypeError("bounding_circle should be a tuple")
+ msg = "bounding_circle should be a tuple"
+ raise TypeError(msg)
if len(bounding_circle) == 3:
*centroid, polygon_radius = bounding_circle
elif len(bounding_circle) == 2:
centroid, polygon_radius = bounding_circle
else:
- raise ValueError(
+ msg = (
"bounding_circle should contain 2D coordinates "
"and a radius (e.g. (x, y, r) or ((x, y), r) )"
)
+ raise ValueError(msg)
if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)):
- raise ValueError("bounding_circle should only contain numeric data")
+ msg = "bounding_circle should only contain numeric data"
+ raise ValueError(msg)
if not len(centroid) == 2:
- raise ValueError(
- "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
- )
+ msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
+ raise ValueError(msg)
if polygon_radius <= 0:
- raise ValueError("bounding_circle radius should be > 0")
+ msg = "bounding_circle radius should be > 0"
+ raise ValueError(msg)
# 1.3 Check `rotation` has an appropriate value
if not isinstance(rotation, (int, float)):
- raise ValueError("rotation should be an int or float")
+ msg = "rotation should be an int or float"
+ raise ValueError(msg)
# 2. Define Helper Functions
def _apply_rotation(point, degrees, centroid):
diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py
index 2667b77dd43..7ce0224a67c 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 @@ def text(self, xy, text, font):
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 f281b9e14c4..8e4f7dfb2c8 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -63,12 +63,13 @@
def raise_oserror(error):
try:
- message = Image.core.getcodecstatus(error)
+ msg = Image.core.getcodecstatus(error)
except AttributeError:
- message = ERRORS.get(error)
- if not message:
- message = f"decoder error {error}"
- raise OSError(message + " when reading image file")
+ msg = ERRORS.get(error)
+ if not msg:
+ msg = f"decoder error {error}"
+ msg += " when reading image file"
+ raise OSError(msg)
def _tilesort(t):
@@ -124,7 +125,8 @@ def __init__(self, fp=None, filename=None):
raise SyntaxError(v) from v
if not self.mode or self.size[0] <= 0 or self.size[1] <= 0:
- raise SyntaxError("not identified by this driver")
+ msg = "not identified by this driver"
+ raise SyntaxError(msg)
except BaseException:
# close the file only if we have opened it this constructor
if self._exclusive_fp:
@@ -137,6 +139,10 @@ def get_format_mimetype(self):
if self.format is not None:
return Image.MIME.get(self.format.upper())
+ def __setstate__(self, state):
+ self.tile = []
+ super().__setstate__(state)
+
def verify(self):
"""Check file integrity"""
@@ -150,7 +156,8 @@ def load(self):
"""Load image data based on tile list"""
if self.tile is None:
- raise OSError("cannot load this image")
+ msg = "cannot load this image"
+ raise OSError(msg)
pixel = Image.Image.load(self)
if not self.tile:
@@ -245,16 +252,18 @@ def load(self):
if LOAD_TRUNCATED_IMAGES:
break
else:
- raise OSError("image file is truncated") from e
+ msg = "image file is truncated"
+ raise OSError(msg) from e
if not s: # truncated jpeg
if LOAD_TRUNCATED_IMAGES:
break
else:
- raise OSError(
+ msg = (
"image file is truncated "
f"({len(b)} bytes not processed)"
)
+ raise OSError(msg)
b = b + s
n, err_code = decoder.decode(b)
@@ -310,7 +319,8 @@ def _seek_check(self, frame):
and frame >= self.n_frames + self._min_frame
)
):
- raise EOFError("attempt to seek outside sequence")
+ msg = "attempt to seek outside sequence"
+ raise EOFError(msg)
return self.tell() != frame
@@ -324,12 +334,14 @@ class StubImageFile(ImageFile):
"""
def _open(self):
- raise NotImplementedError("StubImageFile subclass must implement _open")
+ msg = "StubImageFile subclass must implement _open"
+ raise NotImplementedError(msg)
def load(self):
loader = self._load()
if loader is None:
- raise OSError(f"cannot find loader for this {self.format} file")
+ msg = f"cannot find loader for this {self.format} file"
+ raise OSError(msg)
image = loader.load(self)
assert image is not None
# become the other object (!)
@@ -339,7 +351,8 @@ def load(self):
def _load(self):
"""(Hook) Find actual image loader."""
- raise NotImplementedError("StubImageFile subclass must implement _load")
+ msg = "StubImageFile subclass must implement _load"
+ raise NotImplementedError(msg)
class Parser:
@@ -382,7 +395,6 @@ def feed(self, data):
# parse what we have
if self.decoder:
-
if self.offset > 0:
# skip header
skip = min(len(self.data), self.offset)
@@ -407,14 +419,12 @@ def feed(self, data):
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:
@@ -464,9 +474,11 @@ def close(self):
self.feed(b"")
self.data = self.decoder = None
if not self.finished:
- raise OSError("image was incomplete")
+ msg = "image was incomplete"
+ raise OSError(msg)
if not self.image:
- raise OSError("cannot parse this image")
+ msg = "cannot parse this image"
+ raise OSError(msg)
if self.data:
# incremental parsing not possible; reopen the file
# not that we have all data
@@ -518,20 +530,21 @@ 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:
- raise OSError(f"encoder error {s} when writing image file") from exc
+ 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()
@@ -554,7 +567,8 @@ def _safe_read(fp, size):
if size <= SAFEBLOCK:
data = fp.read(size)
if len(data) < size:
- raise OSError("Truncated File Read")
+ msg = "Truncated File Read"
+ raise OSError(msg)
return data
data = []
remaining_size = size
@@ -565,7 +579,8 @@ def _safe_read(fp, size):
data.append(block)
remaining_size -= len(block)
if sum(len(d) for d in data) < size:
- raise OSError("Truncated File Read")
+ msg = "Truncated File Read"
+ raise OSError(msg)
return b"".join(data)
@@ -641,13 +656,15 @@ def setimage(self, im, extents=None):
self.state.ysize = y1 - y0
if self.state.xsize <= 0 or self.state.ysize <= 0:
- raise ValueError("Size cannot be negative")
+ msg = "Size cannot be negative"
+ raise ValueError(msg)
if (
self.state.xsize + self.state.xoff > self.im.size[0]
or self.state.ysize + self.state.yoff > self.im.size[1]
):
- raise ValueError("Tile cannot extend outside image")
+ msg = "Tile cannot extend outside image"
+ raise ValueError(msg)
class PyDecoder(PyCodec):
@@ -692,9 +709,11 @@ def set_as_raw(self, data, rawmode=None):
s = d.decode(data)
if s[0] >= 0:
- raise ValueError("not enough image data")
+ msg = "not enough image data"
+ raise ValueError(msg)
if s[1] != 0:
- raise ValueError("cannot decode image data")
+ msg = "cannot decode image data"
+ raise ValueError(msg)
class PyEncoder(PyCodec):
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index e10c6fdf14d..33bc7cc2e30 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -28,13 +28,14 @@ class MultibandFilter(Filter):
class BuiltinFilter(MultibandFilter):
def filter(self, image):
if image.mode == "P":
- raise ValueError("cannot filter palette images")
+ msg = "cannot filter palette images"
+ raise ValueError(msg)
return image.filter(*self.filterargs)
class Kernel(BuiltinFilter):
"""
- Create a convolution kernel. The current version only
+ Create a convolution kernel. The current version only
supports 3x3 and 5x5 integer and floating point kernels.
In the current version, kernels can only be applied to
@@ -42,9 +43,10 @@ class Kernel(BuiltinFilter):
:param size: Kernel size, given as (width, height). In the current
version, this must be (3,3) or (5,5).
- :param kernel: A sequence containing kernel weights.
+ :param kernel: A sequence containing kernel weights. The kernel will
+ be flipped vertically before being applied to the image.
:param scale: Scale factor. If given, the result for each pixel is
- divided by this value. The default is the sum of the
+ divided by this value. The default is the sum of the
kernel weights.
:param offset: Offset. If given, this value is added to the result,
after it has been divided by the scale factor.
@@ -57,7 +59,8 @@ def __init__(self, size, kernel, scale=None, offset=0):
# default scale is sum of kernel
scale = functools.reduce(lambda a, b: a + b, kernel)
if size[0] * size[1] != len(kernel):
- raise ValueError("not enough coefficients in kernel")
+ msg = "not enough coefficients in kernel"
+ raise ValueError(msg)
self.filterargs = size, scale, offset, kernel
@@ -80,7 +83,8 @@ def __init__(self, size, rank):
def filter(self, image):
if image.mode == "P":
- raise ValueError("cannot filter palette images")
+ msg = "cannot filter palette images"
+ raise ValueError(msg)
image = image.expand(self.size // 2, self.size // 2)
return image.rankfilter(self.size, self.rank)
@@ -180,6 +184,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):
@@ -355,7 +362,8 @@ class Color3DLUT(MultibandFilter):
def __init__(self, size, table, channels=3, target_mode=None, **kwargs):
if channels not in (3, 4):
- raise ValueError("Only 3 or 4 output channels are supported")
+ msg = "Only 3 or 4 output channels are supported"
+ raise ValueError(msg)
self.size = size = self._check_size(size)
self.channels = channels
self.mode = target_mode
@@ -395,19 +403,21 @@ def __init__(self, size, table, channels=3, target_mode=None, **kwargs):
table, raw_table = [], table
for pixel in raw_table:
if len(pixel) != channels:
- raise ValueError(
+ msg = (
"The elements of the table should "
- "have a length of {}.".format(channels)
+ f"have a length of {channels}."
)
+ raise ValueError(msg)
table.extend(pixel)
if wrong_size or len(table) != items * channels:
- raise ValueError(
+ msg = (
"The table should have either channels * size**3 float items "
"or size**3 items of channels-sized tuples with floats. "
f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. "
f"Actual length: {len(table)}"
)
+ raise ValueError(msg)
self.table = table
@staticmethod
@@ -415,15 +425,15 @@ def _check_size(size):
try:
_, _, _ = size
except ValueError as e:
- raise ValueError(
- "Size should be either an integer or a tuple of three integers."
- ) from e
+ msg = "Size should be either an integer or a tuple of three integers."
+ raise ValueError(msg) from e
except TypeError:
size = (size, size, size)
size = [int(x) for x in size]
for size_1d in size:
if not 2 <= size_1d <= 65:
- raise ValueError("Size should be in [2, 65] range.")
+ msg = "Size should be in [2, 65] range."
+ raise ValueError(msg)
return size
@classmethod
@@ -441,7 +451,8 @@ def generate(cls, size, callback, channels=3, target_mode=None):
"""
size_1d, size_2d, size_3d = cls._check_size(size)
if channels not in (3, 4):
- raise ValueError("Only 3 or 4 output channels are supported")
+ msg = "Only 3 or 4 output channels are supported"
+ raise ValueError(msg)
table = [0] * (size_1d * size_2d * size_3d * channels)
idx_out = 0
@@ -481,7 +492,8 @@ def transform(self, callback, with_normals=False, channels=None, target_mode=Non
lookup table.
"""
if channels not in (None, 3, 4):
- raise ValueError("Only 3 or 4 output channels are supported")
+ msg = "Only 3 or 4 output channels are supported"
+ raise ValueError(msg)
ch_in = self.channels
ch_out = channels or ch_in
size_1d, size_2d, size_3d = self.size
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 457e906c872..05828a72fdf 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -33,7 +33,6 @@
from io import BytesIO
from . import Image
-from ._deprecate import deprecate
from ._util import is_directory, is_path
@@ -42,29 +41,21 @@ class Layout(IntEnum):
RAQM = 1
-def __getattr__(name):
- for enum, prefix in {Layout: "LAYOUT_"}.items():
- if name.startswith(prefix):
- name = name[len(prefix) :]
- if name in enum.__members__:
- deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
- return enum[name]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
-
-
-class _ImagingFtNotInstalled:
- # module placeholder
- def __getattr__(self, id):
- raise ImportError("The _imagingft C module is not installed")
+MAX_STRING_LENGTH = 1_000_000
try:
from . import _imagingft as core
-except ImportError:
- core = _ImagingFtNotInstalled()
+except ImportError as ex:
+ from ._util import DeferredError
+
+ core = DeferredError(ex)
-_UNSPECIFIED = object()
+def _string_length_check(text):
+ if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
+ msg = "too many characters in string"
+ raise ValueError(msg)
# FIXME: add support for pilfont2 format (see FontFile.py)
@@ -87,7 +78,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"):
@@ -104,7 +94,8 @@ def _load_pilfont(self, filename):
else:
if image:
image.close()
- raise OSError("cannot find glyph data file")
+ msg = "cannot find glyph data file"
+ raise OSError(msg)
self.file = fullname
@@ -112,10 +103,10 @@ def _load_pilfont(self, filename):
image.close()
def _load_pilfont_data(self, file, image):
-
# read PILfont header
if file.readline() != b"PILfont\n":
- raise SyntaxError("Not a PILfont file")
+ msg = "Not a PILfont file"
+ raise SyntaxError(msg)
file.readline().split(b";")
self.info = [] # FIXME: should be a dictionary
while True:
@@ -129,29 +120,13 @@ def _load_pilfont_data(self, file, image):
# check image
if image.mode not in ("1", "L"):
- raise TypeError("invalid font image mode")
+ msg = "invalid font image mode"
+ raise TypeError(msg)
image.load()
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.
@@ -186,6 +161,7 @@ def getbbox(self, text, *args, **kwargs):
:return: ``(left, top, right, bottom)`` bounding box
"""
+ _string_length_check(text)
width, height = self.font.getsize(text)
return 0, 0, width, height
@@ -196,6 +172,7 @@ def getlength(self, text, *args, **kwargs):
.. versionadded:: 9.2.0
"""
+ _string_length_check(text)
width, height = self.font.getsize(text)
return width
@@ -259,10 +236,6 @@ def __setstate__(self, state):
path, size, index, encoding, layout_engine = state
self.__init__(path, size, index, encoding, layout_engine)
- def _multiline_split(self, text):
- split_character = "\n" if isinstance(text, str) else b"\n"
- return text.split(split_character)
-
def getname(self):
"""
:return: A tuple of the font family (e.g. Helvetica) and the font style
@@ -293,27 +266,21 @@ def getlength(self, text, mode="", direction=None, features=None, language=None)
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"])
@@ -353,6 +320,7 @@ def getlength(self, text, mode="", direction=None, features=None, language=None)
:return: Width for horizontal, height for vertical text.
"""
+ _string_length_check(text)
return self.font.getlength(text, mode, direction, features, language) / 64
def getbbox(
@@ -412,6 +380,7 @@ def getbbox(
:return: ``(left, top, right, bottom)`` bounding box
"""
+ _string_length_check(text)
size, offset = self.font.getsize(
text, mode, direction, features, language, anchor
)
@@ -419,165 +388,6 @@ def getbbox(
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,
@@ -588,6 +398,7 @@ def getmask(
stroke_width=0,
anchor=None,
ink=0,
+ start=None,
):
"""
Create a bitmap for the text.
@@ -647,6 +458,11 @@ def getmask(
.. versionadded:: 8.0.0
+ :param start: Tuple of horizontal and vertical offset, as text may render
+ differently when starting at fractional coordinates.
+
+ .. versionadded:: 9.4.0
+
:return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module.
"""
@@ -659,19 +475,20 @@ def getmask(
stroke_width=stroke_width,
anchor=anchor,
ink=ink,
+ start=start,
)[0]
def getmask2(
self,
text,
mode="",
- fill=_UNSPECIFIED,
direction=None,
features=None,
language=None,
stroke_width=0,
anchor=None,
ink=0,
+ start=None,
*args,
**kwargs,
):
@@ -690,12 +507,6 @@ def getmask2(
.. 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.
@@ -739,24 +550,41 @@ def getmask2(
.. versionadded:: 8.0.0
+ :param start: Tuple of horizontal and vertical offset, as text may render
+ differently when starting at fractional coordinates.
+
+ .. versionadded:: 9.4.0
+
:return: A tuple of an internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking
"""
- if fill is _UNSPECIFIED:
- fill = Image.core.fill
- else:
- deprecate("fill", 10)
- size, offset = self.font.getsize(
- text, mode, direction, features, language, anchor
+ _string_length_check(text)
+ if start is None:
+ start = (0, 0)
+ im = None
+
+ def fill(mode, size):
+ nonlocal im
+
+ im = Image.core.fill(mode, size)
+ return im
+
+ size, offset = self.font.render(
+ text,
+ fill,
+ mode,
+ direction,
+ features,
+ language,
+ stroke_width,
+ anchor,
+ ink,
+ start[0],
+ start[1],
+ Image.MAX_IMAGE_PIXELS,
)
- size = size[0] + stroke_width * 2, size[1] + stroke_width * 2
- offset = offset[0] - stroke_width, offset[1] - stroke_width
Image._decompression_bomb_check(size)
- im = fill("RGBA" if mode == "RGBA" else "L", size, 0)
- self.font.render(
- text, im.id, mode, direction, features, language, stroke_width, ink
- )
return im, offset
def font_variant(
@@ -792,7 +620,8 @@ def get_variation_names(self):
try:
names = self.font.getvarnames()
except AttributeError as e:
- raise NotImplementedError("FreeType 2.9.1 or greater is required") from e
+ msg = "FreeType 2.9.1 or greater is required"
+ raise NotImplementedError(msg) from e
return [name.replace(b"\x00", b"") for name in names]
def set_variation_by_name(self, name):
@@ -822,7 +651,8 @@ def get_variation_axes(self):
try:
axes = self.font.getvaraxes()
except AttributeError as e:
- raise NotImplementedError("FreeType 2.9.1 or greater is required") from e
+ msg = "FreeType 2.9.1 or greater is required"
+ raise NotImplementedError(msg) from e
for axis in axes:
axis["name"] = axis["name"].replace(b"\x00", b"")
return axes
@@ -835,7 +665,8 @@ def set_variation_by_axes(self, axes):
try:
self.font.setvaraxes(axes)
except AttributeError as e:
- raise NotImplementedError("FreeType 2.9.1 or greater is required") from e
+ msg = "FreeType 2.9.1 or greater is required"
+ raise NotImplementedError(msg) from e
class TransposedFont:
@@ -855,22 +686,6 @@ def __init__(self, font, orientation=None):
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:
@@ -889,9 +704,9 @@ def getbbox(self, text, *args, **kwargs):
def getlength(self, text, *args, **kwargs):
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
- raise ValueError(
- "text length is undefined for text rotated by 90 or 270 degrees"
- )
+ msg = "text length is undefined for text rotated by 90 or 270 degrees"
+ raise ValueError(msg)
+ _string_length_check(text)
return self.font.getlength(text, *args, **kwargs)
@@ -988,7 +803,7 @@ def freetype(font):
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
@@ -1036,7 +851,8 @@ def load_path(filename):
return load(os.path.join(directory, filename))
except OSError:
pass
- raise OSError("cannot find font file")
+ msg = "cannot find font file"
+ raise OSError(msg)
def load_default():
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index 38074cb1b0d..927033c6073 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -15,6 +15,7 @@
# See the README file for information on usage and redistribution.
#
+import io
import os
import shutil
import subprocess
@@ -61,7 +62,17 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
left, top, right, bottom = bbox
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im
- elif shutil.which("gnome-screenshot"):
+ try:
+ if not Image.core.HAVE_XCB:
+ msg = "Pillow was built without XCB support"
+ raise OSError(msg)
+ size, data = Image.core.grabscreen_x11(xdisplay)
+ except OSError:
+ if (
+ xdisplay is None
+ and sys.platform not in ("darwin", "win32")
+ and shutil.which("gnome-screenshot")
+ ):
fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
subprocess.call(["gnome-screenshot", "-f", filepath])
@@ -73,26 +84,25 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
im.close()
return im_cropped
return im
- # use xdisplay=None for default display on non-win32/macOS systems
- if not Image.core.HAVE_XCB:
- raise OSError("Pillow was built without XCB support")
- size, data = Image.core.grabscreen_x11(xdisplay)
- im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
- if bbox:
- im = im.crop(bbox)
- return im
+ else:
+ raise
+ else:
+ im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
+ if bbox:
+ im = im.crop(bbox)
+ return im
def grabclipboard():
if sys.platform == "darwin":
- fh, filepath = tempfile.mkstemp(".jpg")
+ fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
commands = [
'set theFile to (open for access POSIX file "'
+ filepath
+ '" with write permission)',
"try",
- " write (the clipboard as JPEG picture) to theFile",
+ " write (the clipboard as «class PNGf») to theFile",
"end try",
"close access theFile",
]
@@ -119,8 +129,6 @@ def grabclipboard():
files = data[o:].decode("mbcs").split("\0")
return files[: files.index("")]
if isinstance(data, bytes):
- import io
-
data = io.BytesIO(data)
if fmt == "png":
from . import PngImagePlugin
@@ -132,4 +140,30 @@ def grabclipboard():
return BmpImagePlugin.DibImageFile(data)
return None
else:
- raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only")
+ if shutil.which("wl-paste"):
+ output = subprocess.check_output(["wl-paste", "-l"]).decode()
+ mimetypes = output.splitlines()
+ if "image/png" in mimetypes:
+ mimetype = "image/png"
+ elif mimetypes:
+ mimetype = mimetypes[0]
+ else:
+ mimetype = None
+
+ args = ["wl-paste"]
+ if mimetype:
+ args.extend(["-t", mimetype])
+ elif shutil.which("xclip"):
+ args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
+ else:
+ msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
+ raise NotImplementedError(msg)
+ p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ err = p.stderr
+ if err:
+ msg = f"{args[0]} error: {err.strip().decode()}"
+ raise ChildProcessError(msg)
+ data = io.BytesIO(p.stdout)
+ im = Image.open(data)
+ im.load()
+ return im
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index 09d9898d750..ac7d36b698c 100644
--- a/src/PIL/ImageMath.py
+++ b/src/PIL/ImageMath.py
@@ -39,7 +39,8 @@ def __fixup(self, im1):
elif im1.im.mode in ("I", "F"):
return im1.im
else:
- raise ValueError(f"unsupported mode: {im1.im.mode}")
+ msg = f"unsupported mode: {im1.im.mode}"
+ raise ValueError(msg)
else:
# argument was a constant
if _isconstant(im1) and self.im.mode in ("1", "L", "I"):
@@ -56,7 +57,8 @@ def apply(self, op, im1, im2=None, mode=None):
try:
op = getattr(_imagingmath, op + "_" + im1.mode)
except AttributeError as e:
- raise TypeError(f"bad operand type for '{op}'") from e
+ msg = f"bad operand type for '{op}'"
+ raise TypeError(msg) from e
_imagingmath.unop(op, out.im.id, im1.im.id)
else:
# binary operation
@@ -80,7 +82,8 @@ def apply(self, op, im1, im2=None, mode=None):
try:
op = getattr(_imagingmath, op + "_" + im1.mode)
except AttributeError as e:
- raise TypeError(f"bad operand type for '{op}'") from e
+ msg = f"bad operand type for '{op}'"
+ raise TypeError(msg) from e
_imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id)
return _Operand(out)
@@ -249,7 +252,8 @@ def scan(code):
for name in code.co_names:
if name not in args and name != "abs":
- raise ValueError(f"'{name}' not allowed")
+ msg = f"'{name}' not allowed"
+ raise ValueError(msg)
scan(compiled_code)
out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args)
diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py
index 0973536c934..a0b33514296 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/ImageMorph.py b/src/PIL/ImageMorph.py
index 1e22c36a8ae..6fccc315b3d 100644
--- a/src/PIL/ImageMorph.py
+++ b/src/PIL/ImageMorph.py
@@ -81,7 +81,8 @@ def __init__(self, patterns=None, op_name=None):
],
}
if op_name not in known_patterns:
- raise Exception("Unknown pattern " + op_name + "!")
+ msg = "Unknown pattern " + op_name + "!"
+ raise Exception(msg)
self.patterns = known_patterns[op_name]
@@ -145,7 +146,8 @@ def build_lut(self):
for p in self.patterns:
m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
if not m:
- raise Exception('Syntax error in pattern "' + p + '"')
+ msg = 'Syntax error in pattern "' + p + '"'
+ raise Exception(msg)
options = m.group(1)
pattern = m.group(2)
result = int(m.group(3))
@@ -193,10 +195,12 @@ def apply(self, image):
Returns a tuple of the number of changed pixels and the
morphed image"""
if self.lut is None:
- raise Exception("No operator loaded")
+ msg = "No operator loaded"
+ raise Exception(msg)
if image.mode != "L":
- raise ValueError("Image mode must be L")
+ msg = "Image mode must be L"
+ raise ValueError(msg)
outimage = Image.new(image.mode, image.size, None)
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage
@@ -208,10 +212,12 @@ def match(self, image):
Returns a list of tuples of (x,y) coordinates
of all matching pixels. See :ref:`coordinate-system`."""
if self.lut is None:
- raise Exception("No operator loaded")
+ msg = "No operator loaded"
+ raise Exception(msg)
if image.mode != "L":
- raise ValueError("Image mode must be L")
+ msg = "Image mode must be L"
+ raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id)
def get_on_pixels(self, image):
@@ -221,7 +227,8 @@ def get_on_pixels(self, image):
of all matching pixels. See :ref:`coordinate-system`."""
if image.mode != "L":
- raise ValueError("Image mode must be L")
+ msg = "Image mode must be L"
+ raise ValueError(msg)
return _imagingmorph.get_on_pixels(image.im.id)
def load_lut(self, filename):
@@ -231,12 +238,14 @@ def load_lut(self, filename):
if len(self.lut) != LUT_SIZE:
self.lut = None
- raise Exception("Wrong size operator file!")
+ msg = "Wrong size operator file!"
+ raise Exception(msg)
def save_lut(self, filename):
"""Save an operator to an mrl file"""
if self.lut is None:
- raise Exception("No operator loaded")
+ msg = "No operator loaded"
+ raise Exception(msg)
with open(filename, "wb") as f:
f.write(self.lut)
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 443c540b61a..17702778c13 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -21,7 +21,7 @@
import operator
import re
-from . import Image, ImagePalette
+from . import ExifTags, Image, ImagePalette
#
# helpers
@@ -49,13 +49,15 @@ def _color(color, mode):
def _lut(image, lut):
if image.mode == "P":
# FIXME: apply to lookup table, not image data
- raise NotImplementedError("mode P support coming soon")
+ msg = "mode P support coming soon"
+ raise NotImplementedError(msg)
elif image.mode in ("L", "RGB"):
if image.mode == "RGB" and len(lut) == 256:
lut = lut + lut + lut
return image.point(lut)
else:
- raise OSError("not supported for this image mode")
+ msg = "not supported for this image mode"
+ raise OSError(msg)
#
@@ -203,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:
@@ -213,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)
@@ -246,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.
"""
@@ -274,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.
@@ -326,13 +328,15 @@ 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:
return image.copy()
elif factor <= 0:
- raise ValueError("the factor must be greater than 0")
+ msg = "the factor must be greater than 0"
+ raise ValueError(msg)
else:
size = (round(factor * image.width), round(factor * image.height))
return image.resize(size, resample)
@@ -422,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).
@@ -571,19 +576,20 @@ def solarize(image, threshold=128):
return _lut(image, lut)
-def exif_transpose(image):
+def exif_transpose(image, *, in_place=False):
"""
- If an image has an EXIF Orientation tag, other than 1, return a new image
- that is transposed accordingly. The new image will have the orientation
- data removed.
-
- Otherwise, return a copy of the image.
+ If an image has an EXIF Orientation tag, other than 1, transpose the image
+ accordingly, and remove the orientation data.
:param image: The image to transpose.
- :return: An image.
- """
- exif = image.getexif()
- orientation = exif.get(0x0112)
+ :param in_place: Boolean. Keyword-only argument.
+ If ``True``, the original image is modified in-place, and ``None`` is returned.
+ If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
+ with the transposition applied. If there is no transposition, a copy of the
+ image will be returned.
+ """
+ image_exif = image.getexif()
+ orientation = image_exif.get(ExifTags.Base.Orientation)
method = {
2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.Transpose.ROTATE_180,
@@ -595,22 +601,28 @@ def exif_transpose(image):
}.get(orientation)
if method is not None:
transposed_image = image.transpose(method)
- transposed_exif = transposed_image.getexif()
- if 0x0112 in transposed_exif:
- del transposed_exif[0x0112]
- if "exif" in transposed_image.info:
- transposed_image.info["exif"] = transposed_exif.tobytes()
- elif "Raw profile type exif" in transposed_image.info:
- transposed_image.info[
- "Raw profile type exif"
- ] = transposed_exif.tobytes().hex()
- elif "XML:com.adobe.xmp" in transposed_image.info:
+ if in_place:
+ image.im = transposed_image.im
+ image.pyaccess = None
+ image._size = transposed_image._size
+ exif_image = image if in_place else transposed_image
+
+ exif = exif_image.getexif()
+ if ExifTags.Base.Orientation in exif:
+ del exif[ExifTags.Base.Orientation]
+ if "exif" in exif_image.info:
+ exif_image.info["exif"] = exif.tobytes()
+ elif "Raw profile type exif" in exif_image.info:
+ exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
+ elif "XML:com.adobe.xmp" in exif_image.info:
for pattern in (
r'tiff:Orientation="([0-9])"',
r"([0-9])",
):
- transposed_image.info["XML:com.adobe.xmp"] = re.sub(
- pattern, "", transposed_image.info["XML:com.adobe.xmp"]
+ exif_image.info["XML:com.adobe.xmp"] = re.sub(
+ pattern, "", exif_image.info["XML:com.adobe.xmp"]
)
- return transposed_image
- return image.copy()
+ if not in_place:
+ return transposed_image
+ elif not in_place:
+ return image.copy()
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index fe76c86f40e..f0c09470863 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,15 +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):
- raise ValueError("wrong palette size")
@property
def palette(self):
@@ -97,7 +92,8 @@ def tobytes(self):
.. warning:: This method is experimental.
"""
if self.rawmode:
- raise ValueError("palette contains raw palette data")
+ msg = "palette contains raw palette data"
+ raise ValueError(msg)
if isinstance(self.palette, bytes):
return self.palette
arr = array.array("B", self.palette)
@@ -112,14 +108,14 @@ def getcolor(self, color, image=None):
.. warning:: This method is experimental.
"""
if self.rawmode:
- raise ValueError("palette contains raw palette data")
+ msg = "palette contains raw palette data"
+ raise ValueError(msg)
if isinstance(color, tuple):
if self.mode == "RGB":
if len(color) == 4:
if color[3] != 255:
- raise ValueError(
- "cannot add non-opaque RGBA color to RGB palette"
- )
+ msg = "cannot add non-opaque RGBA color to RGB palette"
+ raise ValueError(msg)
color = color[:3]
elif self.mode == "RGBA":
if len(color) == 3:
@@ -147,7 +143,8 @@ def getcolor(self, color, image=None):
index = i
break
if index >= 256:
- raise ValueError("cannot allocate more than 256 colors") from e
+ msg = "cannot allocate more than 256 colors"
+ raise ValueError(msg) from e
self.colors[color] = index
if index * 3 < len(self.palette):
self._palette = (
@@ -160,7 +157,8 @@ def getcolor(self, color, image=None):
self.dirty = 1
return index
else:
- raise ValueError(f"unknown color specifier: {repr(color)}")
+ msg = f"unknown color specifier: {repr(color)}"
+ raise ValueError(msg)
def save(self, fp):
"""Save palette to text file.
@@ -168,7 +166,8 @@ def save(self, fp):
.. warning:: This method is experimental.
"""
if self.rawmode:
- raise ValueError("palette contains raw palette data")
+ msg = "palette contains raw palette data"
+ raise ValueError(msg)
if isinstance(fp, str):
fp = open(fp, "w")
fp.write("# Palette\n")
@@ -243,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,
@@ -263,6 +260,7 @@ def load(filename):
# traceback.print_exc()
pass
else:
- raise OSError("cannot load palette")
+ msg = "cannot load palette"
+ raise OSError(msg)
return lut # data, rawmode
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index a34678c7852..9b7245454df 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -20,14 +20,11 @@
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 @@
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
@@ -179,7 +166,8 @@ def _toqclass_helper(im):
else:
if exclusive_fp:
im.close()
- raise ValueError(f"unsupported image mode {repr(im.mode)}")
+ msg = f"unsupported image mode {repr(im.mode)}"
+ raise ValueError(msg)
size = im.size
__data = data or align8to32(im.tobytes(), size[0], im.mode)
diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py
index 9df910a4330..c4bb6334acf 100644
--- a/src/PIL/ImageSequence.py
+++ b/src/PIL/ImageSequence.py
@@ -30,7 +30,8 @@ class Iterator:
def __init__(self, im):
if not hasattr(im, "seek"):
- raise AttributeError("im must have seek method")
+ msg = "im must have seek method"
+ raise AttributeError(msg)
self.im = im
self.position = getattr(self.im, "_min_frame", 0)
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index 76f42a3072d..8b1c3f8bb63 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -17,9 +17,7 @@
import sys
from shlex import quote
-from PIL import Image
-
-from ._deprecate import deprecate
+from . import Image
_viewers = []
@@ -111,21 +109,11 @@ def show_image(self, image, **options):
"""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:
- raise TypeError("Missing required argument: 'path'")
- os.system(self.get_command(path, **options))
+ os.system(self.get_command(path, **options)) # nosec
return 1
@@ -163,20 +151,10 @@ def get_command(self, file, **options):
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:
- raise TypeError("Missing required argument: 'path'")
subprocess.call(["open", "-a", "Preview.app", path])
executable = sys.executable or shutil.which("python3")
if executable:
@@ -213,20 +191,10 @@ def get_command_ex(self, file, **options):
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:
- raise TypeError("Missing required argument: 'path'")
subprocess.Popen(["xdg-open", path])
return 1
@@ -243,19 +211,10 @@ def get_command_ex(self, file, title=None, **options):
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:
- raise TypeError("Missing required argument: 'path'")
args = ["display"]
title = options.get("title")
if title:
@@ -274,19 +233,10 @@ def get_command_ex(self, file, **options):
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:
- raise TypeError("Missing required argument: 'path'")
subprocess.Popen(["gm", "display", path])
return 1
@@ -299,19 +249,10 @@ def get_command_ex(self, file, **options):
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:
- raise TypeError("Missing required argument: 'path'")
subprocess.Popen(["eog", "-n", path])
return 1
@@ -330,19 +271,10 @@ def get_command_ex(self, file, title=None, **options):
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:
- raise TypeError("Missing required argument: 'path'")
args = ["xv"]
title = options.get("title")
if title:
@@ -383,7 +315,6 @@ def show_image(self, image, **options):
if __name__ == "__main__":
-
if len(sys.argv) < 2:
print("Syntax: python3 ImageShow.py imagefile [title]")
sys.exit()
diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py
index 1baef7db499..b7ebddf066a 100644
--- a/src/PIL/ImageStat.py
+++ b/src/PIL/ImageStat.py
@@ -36,7 +36,8 @@ def __init__(self, image_or_list, mask=None):
except AttributeError:
self.h = image_or_list # assume it to be a histogram list
if not isinstance(self.h, list):
- raise TypeError("first argument must be image or list")
+ msg = "first argument must be image or list"
+ raise TypeError(msg)
self.bands = list(range(len(self.h) // 256))
def __getattr__(self, id):
diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py
index 949cf1fbf9d..bf98eb2c8c2 100644
--- a/src/PIL/ImageTk.py
+++ b/src/PIL/ImageTk.py
@@ -29,7 +29,6 @@
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 @@ def height(self):
"""
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 @@ def paste(self, im, box=None):
: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)
@@ -284,7 +275,8 @@ def __init__(self, master, im):
super().__init__(master, image=self.image, bg="black", bd=0)
if not tkinter._default_root:
- raise OSError("tkinter not initialized")
+ msg = "tkinter not initialized"
+ raise OSError(msg)
top = tkinter.Toplevel()
if title:
top.title(title)
diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py
index dc707801274..ac267457b06 100644
--- a/src/PIL/ImtImagePlugin.py
+++ b/src/PIL/ImtImagePlugin.py
@@ -30,23 +30,21 @@
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.
buffer = self.fp.read(100)
if b"\n" not in buffer:
- raise SyntaxError("not an IM file")
+ msg = "not an IM file"
+ raise SyntaxError(msg)
xsize = ysize = 0
while True:
-
if buffer:
s = buffer[:1]
buffer = buffer[1:]
@@ -56,7 +54,6 @@ def _open(self):
break
if s == b"\x0C":
-
# image data begins
self.tile = [
(
@@ -70,7 +67,6 @@ def _open(self):
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 0bbe50668d8..4c47b55c1a5 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"
@@ -66,12 +65,14 @@ def field(self):
# syntax
if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9:
- raise SyntaxError("invalid IPTC/NAA file")
+ msg = "invalid IPTC/NAA file"
+ raise SyntaxError(msg)
# field size
size = s[3]
if size > 132:
- raise OSError("illegal field length in IPTC/NAA file")
+ msg = "illegal field length in IPTC/NAA file"
+ raise OSError(msg)
elif size == 128:
size = 0
elif size > 128:
@@ -82,7 +83,6 @@ def field(self):
return tag, size
def _open(self):
-
# load descriptive fields
while True:
offset = self.fp.tell()
@@ -122,7 +122,8 @@ def _open(self):
try:
compression = COMPRESSION[self.getint((3, 120))]
except KeyError as e:
- raise OSError("Unknown IPTC image compression") from e
+ msg = "Unknown IPTC image compression"
+ raise OSError(msg) from e
# tile
if tag == (8, 10):
@@ -131,7 +132,6 @@ def _open(self):
]
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 c67d8d6bf60..9309768bacf 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -17,7 +17,7 @@
import os
import struct
-from . import Image, ImageFile
+from . import Image, ImageFile, _binary
class BoxReader:
@@ -44,13 +44,13 @@ def _can_read(self, num_bytes):
def _read_bytes(self, num_bytes):
if not self._can_read(num_bytes):
- raise SyntaxError("Not enough data in header")
+ msg = "Not enough data in header"
+ raise SyntaxError(msg)
data = self.fp.read(num_bytes)
if len(data) < num_bytes:
- raise OSError(
- f"Expected to read {num_bytes} bytes but only got {len(data)}."
- )
+ msg = f"Expected to read {num_bytes} bytes but only got {len(data)}."
+ raise OSError(msg)
if self.remaining_in_box > 0:
self.remaining_in_box -= num_bytes
@@ -87,7 +87,8 @@ def next_box_type(self):
hlen = 8
if lbox < hlen or not self._can_read(lbox - hlen):
- raise SyntaxError("Invalid header length")
+ msg = "Invalid header length"
+ raise SyntaxError(msg)
self.remaining_in_box = lbox - hlen
return tbox
@@ -98,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
@@ -189,7 +190,8 @@ def _parse_jp2_header(fp):
break
if size is None or mode is None:
- raise SyntaxError("Malformed JP2 header")
+ msg = "Malformed JP2 header"
+ raise SyntaxError(msg)
return size, mode, mimetype, dpi
@@ -216,11 +218,15 @@ def _open(self):
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:
- raise SyntaxError("not a JPEG 2000 file")
+ msg = "not a JPEG 2000 file"
+ raise SyntaxError(msg)
if self.size is None or self.mode is None:
- raise SyntaxError("unable to determine size/mode")
+ msg = "unable to determine size/mode"
+ raise SyntaxError(msg)
self._reduce = 0
self.layers = 0
@@ -250,6 +256,28 @@ def _open(self):
)
]
+ 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
@@ -312,7 +340,8 @@ def _save(im, fp, filename):
]
)
):
- raise ValueError("quality_layers must be a sequence of numbers")
+ msg = "quality_layers must be a sequence of numbers"
+ raise ValueError(msg)
num_resolutions = info.get("num_resolutions", 0)
cblk_size = info.get("codeblock_size", None)
@@ -321,8 +350,13 @@ def _save(im, fp, filename):
progression = info.get("progression", "LRCP")
cinema_mode = info.get("cinema_mode", "no")
mct = info.get("mct", 0)
- fd = -1
+ signed = info.get("signed", False)
+ 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()
@@ -342,7 +376,10 @@ def _save(im, fp, filename):
progression,
cinema_mode,
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 a6ed223bc6f..dfc7e6e9f56 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -41,11 +41,11 @@
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 ._deprecate import deprecate
+from ._binary import o16be as o16
from .JpegPresets import presets
#
@@ -89,6 +89,7 @@ def APP(self, marker):
if "exif" not in self.info:
# extract EXIF information (incomplete)
self.info["exif"] = s # FIXME: value will change
+ self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
@@ -202,7 +203,8 @@ def SOF(self, marker):
self.bits = s[0]
if self.bits != 8:
- raise SyntaxError(f"cannot handle {self.bits}-bit layers")
+ msg = f"cannot handle {self.bits}-bit layers"
+ raise SyntaxError(msg)
self.layers = s[5]
if self.layers == 1:
@@ -212,7 +214,8 @@ def SOF(self, marker):
elif self.layers == 4:
self.mode = "CMYK"
else:
- raise SyntaxError(f"cannot handle {self.layers}-layer images")
+ msg = f"cannot handle {self.layers}-layer images"
+ raise SyntaxError(msg)
if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]:
self.info["progressive"] = self.info["progression"] = 1
@@ -251,7 +254,8 @@ def DQT(self, marker):
precision = 1 if (v // 16 == 0) else 2 # in bytes
qt_length = 1 + precision * 64
if len(s) < qt_length:
- raise SyntaxError("bad quantization table marker")
+ msg = "bad quantization table marker"
+ raise SyntaxError(msg)
data = array.array("B" if precision == 1 else "H", s[1:qt_length])
if sys.byteorder == "little" and precision > 1:
data.byteswap() # the values are always big-endian
@@ -339,16 +343,15 @@ 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):
- raise SyntaxError("not a JPEG file")
+ msg = "not a JPEG file"
+ raise SyntaxError(msg)
s = b"\xFF"
# Create attributes
@@ -364,7 +367,6 @@ def _open(self):
self.icclist = []
while True:
-
i = s[0]
if i == 0xFF:
s = s + self.fp.read(1)
@@ -392,7 +394,8 @@ def _open(self):
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
s = self.fp.read(1)
else:
- raise SyntaxError("no marker found")
+ msg = "no marker found"
+ raise SyntaxError(msg)
def load_read(self, read_bytes):
"""
@@ -411,7 +414,6 @@ def load_read(self, read_bytes):
return s
def draft(self, mode, size):
-
if len(self.tile) != 1:
return
@@ -448,7 +450,6 @@ def draft(self, mode, size):
return self.mode, box
def load_djpeg(self):
-
# ALTERNATIVE: handle JPEGs via the IJG command line utilities
f, path = tempfile.mkstemp()
@@ -456,7 +457,13 @@ def load_djpeg(self):
if os.path.exists(self.filename):
subprocess.check_call(["djpeg", "-outfile", path, self.filename])
else:
- raise ValueError("Invalid Filename")
+ try:
+ os.unlink(path)
+ except OSError:
+ pass
+
+ msg = "Invalid Filename"
+ raise ValueError(msg)
try:
with Image.open(path) as _im:
@@ -516,18 +523,22 @@ 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)
info.load(file_contents)
mp = dict(info)
except Exception as e:
- raise SyntaxError("malformed MP Index (unreadable directory)") from e
+ msg = "malformed MP Index (unreadable directory)"
+ raise SyntaxError(msg) from e
# it's an error not to have a number of images
try:
quant = mp[0xB001]
except KeyError as e:
- raise SyntaxError("malformed MP Index (no number of images)") from e
+ msg = "malformed MP Index (no number of images)"
+ raise SyntaxError(msg) from e
# get MP entries
mpentries = []
try:
@@ -549,7 +560,8 @@ def _getmp(self):
if mpentryattr["ImageDataFormat"] == 0:
mpentryattr["ImageDataFormat"] = "JPEG"
else:
- raise SyntaxError("unsupported picture format in MPO")
+ msg = "unsupported picture format in MPO"
+ raise SyntaxError(msg)
mptypemap = {
0x000000: "Undefined",
0x010001: "Large Thumbnail (VGA Equivalent)",
@@ -564,7 +576,8 @@ def _getmp(self):
mpentries.append(mpentry)
mp[0xB002] = mpentries
except KeyError as e:
- raise SyntaxError("malformed MP Index (bad MP Entry)") from e
+ msg = "malformed MP Index (bad MP Entry)"
+ raise SyntaxError(msg) from e
# Next we should try and parse the individual image unique ID list;
# we don't because I've never seen this actually used in a real MPO
# file and so can't test it.
@@ -603,11 +616,6 @@ def _getmp(self):
# 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),
@@ -624,12 +632,14 @@ def get_sampling(im):
def _save(im, fp, filename):
if im.width == 0 or im.height == 0:
- raise ValueError("cannot write empty image as JPEG")
+ msg = "cannot write empty image as JPEG"
+ raise ValueError(msg)
try:
rawmode = RAWMODE[im.mode]
except KeyError as e:
- raise OSError(f"cannot write mode {im.mode} as JPEG") from e
+ msg = f"cannot write mode {im.mode} as JPEG"
+ raise OSError(msg) from e
info = im.encoderinfo
@@ -649,7 +659,8 @@ def _save(im, fp, filename):
subsampling = preset.get("subsampling", -1)
qtables = preset.get("quantization")
elif not isinstance(quality, int):
- raise ValueError("Invalid quality setting")
+ msg = "Invalid quality setting"
+ raise ValueError(msg)
else:
if subsampling in presets:
subsampling = presets[subsampling].get("subsampling", -1)
@@ -668,7 +679,8 @@ def _save(im, fp, filename):
subsampling = 2
elif subsampling == "keep":
if im.format != "JPEG":
- raise ValueError("Cannot use 'keep' when original image is not a JPEG")
+ msg = "Cannot use 'keep' when original image is not a JPEG"
+ raise ValueError(msg)
subsampling = get_sampling(im)
def validate_qtables(qtables):
@@ -682,7 +694,8 @@ def validate_qtables(qtables):
for num in line.split("#", 1)[0].split()
]
except ValueError as e:
- raise ValueError("Invalid quantization table") from e
+ msg = "Invalid quantization table"
+ raise ValueError(msg) from e
else:
qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)]
if isinstance(qtables, (tuple, list, dict)):
@@ -693,30 +706,33 @@ def validate_qtables(qtables):
elif isinstance(qtables, tuple):
qtables = list(qtables)
if not (0 < len(qtables) < 5):
- raise ValueError("None or too many quantization tables")
+ msg = "None or too many quantization tables"
+ raise ValueError(msg)
for idx, table in enumerate(qtables):
try:
if len(table) != 64:
raise TypeError
table = array.array("H", table)
except TypeError as e:
- raise ValueError("Invalid quantization table") from e
+ msg = "Invalid quantization table"
+ raise ValueError(msg) from e
else:
qtables[idx] = list(table)
return qtables
if qtables == "keep":
if im.format != "JPEG":
- raise ValueError("Cannot use 'keep' when original image is not a JPEG")
+ msg = "Cannot use 'keep' when original image is not a JPEG"
+ raise ValueError(msg)
qtables = getattr(im, "quantization", None)
qtables = validate_qtables(qtables)
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:
@@ -724,7 +740,7 @@ def validate_qtables(qtables):
icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:]
i = 1
for marker in markers:
- size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker))
+ size = o16(2 + ICC_OVERHEAD_LEN + len(marker))
extra += (
b"\xFF\xE2"
+ size
@@ -735,6 +751,8 @@ def validate_qtables(qtables):
)
i += 1
+ comment = info.get("comment", im.info.get("comment"))
+
# "progressive" is the official name, but older documentation
# says "progression"
# FIXME: issue a warning if the wrong form is used (post-1.1.7)
@@ -745,6 +763,9 @@ def validate_qtables(qtables):
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 = (
@@ -757,6 +778,7 @@ def validate_qtables(qtables):
dpi[1],
subsampling,
qtables,
+ comment,
extra,
exif,
)
diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py
index cd047fe9d9d..17c008b9a6a 100644
--- a/src/PIL/McIdasImagePlugin.py
+++ b/src/PIL/McIdasImagePlugin.py
@@ -30,16 +30,15 @@ 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:
- raise SyntaxError("not an McIdas area file")
+ msg = "not an McIdas area file"
+ raise SyntaxError(msg)
self.area_descriptor_raw = s
self.area_descriptor = w = [0] + list(struct.unpack("!64i", s))
@@ -56,7 +55,8 @@ def _open(self):
mode = "I"
rawmode = "I;32B"
else:
- raise SyntaxError("unsupported McIdas format")
+ msg = "unsupported McIdas format"
+ raise SyntaxError(msg)
self.mode = mode
self._size = w[10], w[9]
diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py
index d4f6c90f778..801318930d5 100644
--- a/src/PIL/MicImagePlugin.py
+++ b/src/PIL/MicImagePlugin.py
@@ -34,20 +34,19 @@ 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
try:
self.ole = olefile.OleFileIO(self.fp)
except OSError as e:
- raise SyntaxError("not an MIC file; invalid OLE file") from e
+ msg = "not an MIC file; invalid OLE file"
+ raise SyntaxError(msg) from e
# find ACI subfiles with Image members (maybe not the
# best way to identify MIC files, but what the... ;-)
@@ -60,15 +59,13 @@ def _open(self):
# if we didn't find any images, this is probably not
# an MIC file.
if not self.images:
- raise SyntaxError("not an MIC file; no image entries")
+ msg = "not an MIC file; no image entries"
+ raise SyntaxError(msg)
self.frame = None
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):
@@ -77,7 +74,8 @@ def seek(self, frame):
try:
filename = self.images[frame]
except IndexError as e:
- raise EOFError("no such frame") from e
+ msg = "no such frame"
+ raise EOFError(msg) from e
self.fp = self.ole.openstream(filename)
@@ -88,6 +86,14 @@ def seek(self, frame):
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 a358dfdce62..d96d3a11c49 100644
--- a/src/PIL/MpegImagePlugin.py
+++ b/src/PIL/MpegImagePlugin.py
@@ -58,16 +58,15 @@ def read(self, bits):
class MpegImageFile(ImageFile.ImageFile):
-
format = "MPEG"
format_description = "MPEG"
def _open(self):
-
s = BitStream(self.fp)
if s.read(32) != 0x1B3:
- raise SyntaxError("not an MPEG file")
+ msg = "not an MPEG file"
+ raise SyntaxError(msg)
self.mode = "RGB"
self._size = s.read(12), s.read(12)
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index 5bfd8efc1a6..f9261c77d68 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -22,7 +22,14 @@
import os
import struct
-from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin
+from . import (
+ ExifTags,
+ Image,
+ ImageFile,
+ ImageSequence,
+ JpegImagePlugin,
+ TiffImagePlugin,
+)
from ._binary import i16be as i16
from ._binary import o32le
@@ -45,14 +52,22 @@ def _save_all(im, fp, filename):
_save(im, fp, filename)
return
+ mpf_offset = 28
offsets = []
for imSequence in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(imSequence):
if not offsets:
# APP2 marker
- im.encoderinfo["extra"] = (
- b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70
+ im_frame.encoderinfo["extra"] = (
+ b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82
)
+ exif = im_frame.encoderinfo.get("exif")
+ if isinstance(exif, Image.Exif):
+ exif = exif.tobytes()
+ im_frame.encoderinfo["exif"] = exif
+ if exif:
+ mpf_offset += 4 + len(exif)
+
JpegImagePlugin._save(im_frame, fp, filename)
offsets.append(fp.tell())
else:
@@ -60,6 +75,7 @@ def _save_all(im, fp, filename):
offsets.append(fp.tell() - offsets[-1])
ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ ifd[0xB000] = b"0100"
ifd[0xB001] = len(offsets)
mpentries = b""
@@ -71,11 +87,11 @@ def _save_all(im, fp, filename):
mptype = 0x000000 # Undefined
mpentries += struct.pack(" 100:
- raise SyntaxError("bad palette file")
+ msg = "bad palette file"
+ raise SyntaxError(msg)
v = [int(x) for x in s.split()]
try:
@@ -49,5 +48,4 @@ def __init__(self, fp):
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 700f10e3f79..a88a907917d 100644
--- a/src/PIL/PalmImagePlugin.py
+++ b/src/PIL/PalmImagePlugin.py
@@ -112,9 +112,7 @@ def build_prototype_image():
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
@@ -138,7 +136,8 @@ def _save(im, fp, filename):
bpp = im.info["bpp"]
im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval))
else:
- raise OSError(f"cannot write mode {im.mode} as Palm")
+ msg = f"cannot write mode {im.mode} as Palm"
+ raise OSError(msg)
# we ignore the palette here
im.mode = "P"
@@ -146,15 +145,14 @@ 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:
-
- raise OSError(f"cannot write mode {im.mode} as Palm")
+ msg = f"cannot write mode {im.mode} as Palm"
+ raise OSError(msg)
#
# make sure image data is available
diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py
index 38caf5c63c1..e390f3fe51d 100644
--- a/src/PIL/PcdImagePlugin.py
+++ b/src/PIL/PcdImagePlugin.py
@@ -24,18 +24,17 @@
class PcdImageFile(ImageFile.ImageFile):
-
format = "PCD"
format_description = "Kodak PhotoCD"
def _open(self):
-
# rough
self.fp.seek(2048)
s = self.fp.read(2048)
if s[:4] != b"PCD_":
- raise SyntaxError("not a PCD file")
+ msg = "not a PCD file"
+ raise SyntaxError(msg)
orientation = s[1538] & 3
self.tile_post_rotate = None
diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py
index 442ac70c49d..8db5822fe7d 100644
--- a/src/PIL/PcfFontFile.py
+++ b/src/PIL/PcfFontFile.py
@@ -58,12 +58,12 @@ class PcfFontFile(FontFile.FontFile):
name = "name"
def __init__(self, fp, charset_encoding="iso8859-1"):
-
self.charset_encoding = charset_encoding
magic = l32(fp.read(4))
if magic != PCF_MAGIC:
- raise SyntaxError("not a PCF file")
+ msg = "not a PCF file"
+ raise SyntaxError(msg)
super().__init__()
@@ -86,12 +86,24 @@ def __init__(self, fp, charset_encoding="iso8859-1"):
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
@@ -107,7 +119,6 @@ def _getformat(self, tag):
return fp, format, i16, i32
def _load_properties(self):
-
#
# font properties
@@ -135,7 +146,6 @@ def _load_properties(self):
return properties
def _load_metrics(self):
-
#
# font metrics
@@ -146,7 +156,6 @@ def _load_metrics(self):
append = metrics.append
if (format & 0xFF00) == 0x100:
-
# "compressed" metrics
for i in range(i16(fp.read(2))):
left = i8(fp.read(1)) - 128
@@ -159,7 +168,6 @@ def _load_metrics(self):
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))
@@ -175,7 +183,6 @@ def _load_metrics(self):
return metrics
def _load_bitmaps(self, metrics):
-
#
# bitmap data
@@ -186,7 +193,8 @@ def _load_bitmaps(self, metrics):
nbitmaps = i32(fp.read(4))
if nbitmaps != len(metrics):
- raise OSError("Wrong number of bitmaps")
+ msg = "Wrong number of bitmaps"
+ raise OSError(msg)
offsets = []
for i in range(nbitmaps):
@@ -211,9 +219,11 @@ def _load_bitmaps(self, metrics):
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 841c18a2200..f42c2456b4b 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -45,21 +45,21 @@ 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):
- raise SyntaxError("not a PCX file")
+ msg = "not a PCX file"
+ raise SyntaxError(msg)
# image
bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1
if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]:
- raise SyntaxError("bad PCX image size")
+ msg = "bad PCX image size"
+ raise SyntaxError(msg)
logger.debug("BBox: %s %s %s %s", *bbox)
# format
@@ -105,7 +105,8 @@ def _open(self):
rawmode = "RGB;L"
else:
- raise OSError("unknown PCX mode")
+ msg = "unknown PCX mode"
+ raise OSError(msg)
self.mode = mode
self._size = bbox[2] - bbox[0], bbox[3] - bbox[1]
@@ -140,11 +141,11 @@ def _open(self):
def _save(im, fp, filename):
-
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:
- raise ValueError(f"Cannot save {im.mode} images as PCX") from e
+ msg = f"Cannot save {im.mode} images as PCX"
+ raise ValueError(msg) from e
# bytes per plane
stride = (im.size[0] * bits + 7) // 8
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
index 404759a7fcb..c41f8aee004 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,13 +173,18 @@ 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")
procset = "ImageC" # color images
decode = [1, 0, 1, 0, 1, 0, 1, 0]
else:
- raise ValueError(f"cannot save mode {im.mode}")
+ msg = f"cannot save mode {im.mode}"
+ raise ValueError(msg)
#
# image
@@ -193,12 +203,15 @@ 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":
ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)])
else:
- raise ValueError(f"unsupported PDF filter ({filter})")
+ msg = f"unsupported PDF filter ({filter})"
+ raise ValueError(msg)
stream = op.getvalue()
if filter == "CCITTFaxDecode":
@@ -212,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,
@@ -233,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],
)
@@ -243,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 fd5cc5a61e3..dc1012f54d3 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -138,9 +138,10 @@ def __delitem__(self, key):
elif key in self.deleted_entries:
generation = self.deleted_entries[key]
else:
- raise IndexError(
+ msg = (
"object ID " + str(key) + " cannot be deleted because it doesn't exist"
)
+ raise IndexError(msg)
def __contains__(self, key):
return key in self.existing_entries or key in self.new_entries
@@ -314,9 +315,8 @@ def decode(self):
expected_length = self.dictionary.Length
return zlib.decompress(self.buf, bufsize=int(expected_length))
else:
- raise NotImplementedError(
- f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
- )
+ msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
+ raise NotImplementedError(msg)
def pdf_repr(x):
@@ -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")"
@@ -358,7 +356,8 @@ class PdfParser:
def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"):
if buf and f:
- raise RuntimeError("specify buf or f or filename, but not both buf and f")
+ msg = "specify buf or f or filename, but not both buf and f"
+ raise RuntimeError(msg)
self.filename = filename
self.buf = buf
self.f = f
@@ -816,10 +815,10 @@ def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1):
try:
stream_len = int(result[b"Length"])
except (TypeError, KeyError, ValueError) as e:
- raise PdfFormatError(
- "bad or missing Length in stream dict (%r)"
- % result.get(b"Length", None)
- ) from e
+ msg = "bad or missing Length in stream dict (%r)" % result.get(
+ b"Length", None
+ )
+ raise PdfFormatError(msg) from e
stream_data = data[m.end() : m.end() + stream_len]
m = cls.re_stream_end.match(data, m.end() + stream_len)
check_format_condition(m, "stream end not found")
@@ -873,7 +872,8 @@ def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1):
if m:
return cls.get_literal_string(data, m.end())
# return None, offset # fallback (only for debugging)
- raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32]))
+ msg = "unrecognized object: " + repr(data[offset : offset + 32])
+ raise PdfFormatError(msg)
re_lit_str_token = re.compile(
rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))"
@@ -920,7 +920,8 @@ def get_literal_string(cls, data, offset):
result.extend(b")")
nesting_depth -= 1
offset = m.end()
- raise PdfFormatError("unfinished literal string")
+ msg = "unfinished literal string"
+ raise PdfFormatError(msg)
re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline)
re_xref_subsection_start = re.compile(
@@ -956,14 +957,11 @@ def read_xref_table(self, xref_section_offset):
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 c4860b6c4f3..7eb82228a99 100644
--- a/src/PIL/PixarImagePlugin.py
+++ b/src/PIL/PixarImagePlugin.py
@@ -35,16 +35,15 @@ 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):
- raise SyntaxError("not a PIXAR file")
+ msg = "not a PIXAR file"
+ raise SyntaxError(msg)
# read rest of header
s = s + self.fp.read(508)
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 2c53be109ae..bfa8cb7ac66 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -45,7 +45,6 @@
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,21 +130,12 @@ 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]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
-
-
def _safe_zlib_decompress(s):
dobj = zlib.decompressobj()
plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
if dobj.unconsumed_tail:
- raise ValueError("Decompressed Data Too Large")
+ msg = "Decompressed Data Too Large"
+ raise ValueError(msg)
return plaintext
@@ -159,7 +149,6 @@ def _crc32(data, seed=0):
class ChunkStream:
def __init__(self, fp):
-
self.fp = fp
self.queue = []
@@ -178,7 +167,8 @@ def read(self):
if not is_cid(cid):
if not ImageFile.LOAD_TRUNCATED_IMAGES:
- raise SyntaxError(f"broken PNG file (chunk {repr(cid)})")
+ msg = f"broken PNG file (chunk {repr(cid)})"
+ raise SyntaxError(msg)
return cid, pos, length
@@ -192,7 +182,6 @@ def close(self):
self.queue = self.fp = None
def push(self, cid, pos, length):
-
self.queue.append((cid, pos, length))
def call(self, cid, pos, length):
@@ -215,13 +204,11 @@ def crc(self, cid, data):
crc1 = _crc32(data, _crc32(cid))
crc2 = i32(self.fp.read(4))
if crc1 != crc2:
- raise SyntaxError(
- f"broken PNG file (bad header checksum in {repr(cid)})"
- )
+ msg = f"broken PNG file (bad header checksum in {repr(cid)})"
+ raise SyntaxError(msg)
except struct.error as e:
- raise SyntaxError(
- f"broken PNG file (incomplete checksum in {repr(cid)})"
- ) from e
+ msg = f"broken PNG file (incomplete checksum in {repr(cid)})"
+ raise SyntaxError(msg) from e
def crc_skip(self, cid, data):
"""Read checksum"""
@@ -229,7 +216,6 @@ def crc_skip(self, cid, data):
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.
@@ -239,7 +225,8 @@ def verify(self, endchunk=b"IEND"):
try:
cid, pos, length = self.read()
except struct.error as e:
- raise OSError("truncated PNG file") from e
+ msg = "truncated PNG file"
+ raise OSError(msg) from e
if cid == endchunk:
break
@@ -376,10 +363,11 @@ def __init__(self, fp):
def check_text_memory(self, chunklen):
self.text_memory += chunklen
if self.text_memory > MAX_TEXT_MEMORY:
- raise ValueError(
+ msg = (
"Too much memory used in text chunks: "
f"{self.text_memory}>MAX_TEXT_MEMORY"
)
+ raise ValueError(msg)
def save_rewind(self):
self.rewind_state = {
@@ -394,7 +382,6 @@ def rewind(self):
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:
@@ -407,7 +394,8 @@ def chunk_iCCP(self, pos, length):
logger.debug("Compression method %s", s[i])
comp_method = s[i]
if comp_method != 0:
- raise SyntaxError(f"Unknown compression method {comp_method} in iCCP chunk")
+ msg = f"Unknown compression method {comp_method} in iCCP chunk"
+ raise SyntaxError(msg)
try:
icc_profile = _safe_zlib_decompress(s[i + 2 :])
except ValueError:
@@ -421,13 +409,13 @@ def chunk_iCCP(self, pos, length):
return s
def chunk_IHDR(self, pos, length):
-
# image header
s = ImageFile._safe_read(self.fp, length)
if length < 13:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("Truncated IHDR chunk")
+ msg = "Truncated IHDR chunk"
+ raise ValueError(msg)
self.im_size = i32(s, 0), i32(s, 4)
try:
self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]
@@ -436,11 +424,11 @@ def chunk_IHDR(self, pos, length):
if s[12]:
self.im_info["interlace"] = 1
if s[11]:
- raise SyntaxError("unknown filter category")
+ msg = "unknown filter category"
+ raise SyntaxError(msg)
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)]
@@ -453,12 +441,10 @@ def chunk_IDAT(self, pos, length):
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":
@@ -466,7 +452,6 @@ def chunk_PLTE(self, pos, length):
return s
def chunk_tRNS(self, pos, length):
-
# transparency
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
@@ -512,18 +497,19 @@ def chunk_sRGB(self, pos, length):
if length < 1:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("Truncated sRGB chunk")
+ msg = "Truncated sRGB chunk"
+ raise ValueError(msg)
self.im_info["srgb"] = s[0]
return s
def chunk_pHYs(self, pos, length):
-
# pixels per unit
s = ImageFile._safe_read(self.fp, length)
if length < 9:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("Truncated pHYs chunk")
+ msg = "Truncated pHYs chunk"
+ raise ValueError(msg)
px, py = i32(s, 0), i32(s, 4)
unit = s[8]
if unit == 1: # meter
@@ -534,7 +520,6 @@ def chunk_pHYs(self, pos, length):
return s
def chunk_tEXt(self, pos, length):
-
# text
s = ImageFile._safe_read(self.fp, length)
try:
@@ -554,7 +539,6 @@ def chunk_tEXt(self, pos, length):
return s
def chunk_zTXt(self, pos, length):
-
# compressed text
s = ImageFile._safe_read(self.fp, length)
try:
@@ -567,7 +551,8 @@ def chunk_zTXt(self, pos, length):
else:
comp_method = 0
if comp_method != 0:
- raise SyntaxError(f"Unknown compression method {comp_method} in zTXt chunk")
+ msg = f"Unknown compression method {comp_method} in zTXt chunk"
+ raise SyntaxError(msg)
try:
v = _safe_zlib_decompress(v[1:])
except ValueError:
@@ -588,7 +573,6 @@ def chunk_zTXt(self, pos, length):
return s
def chunk_iTXt(self, pos, length):
-
# international text
r = s = ImageFile._safe_read(self.fp, length)
try:
@@ -639,7 +623,8 @@ def chunk_acTL(self, pos, length):
if length < 8:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("APNG contains truncated acTL chunk")
+ msg = "APNG contains truncated acTL chunk"
+ raise ValueError(msg)
if self.im_n_frames is not None:
self.im_n_frames = None
warnings.warn("Invalid APNG, will use default PNG image if possible")
@@ -658,18 +643,21 @@ def chunk_fcTL(self, pos, length):
if length < 26:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("APNG contains truncated fcTL chunk")
+ msg = "APNG contains truncated fcTL chunk"
+ raise ValueError(msg)
seq = i32(s)
if (self._seq_num is None and seq != 0) or (
self._seq_num is not None and self._seq_num != seq - 1
):
- raise SyntaxError("APNG contains frame sequence errors")
+ msg = "APNG contains frame sequence errors"
+ raise SyntaxError(msg)
self._seq_num = seq
width, height = i32(s, 4), i32(s, 8)
px, py = i32(s, 12), i32(s, 16)
im_w, im_h = self.im_size
if px + width > im_w or py + height > im_h:
- raise SyntaxError("APNG contains invalid frames")
+ msg = "APNG contains invalid frames"
+ raise SyntaxError(msg)
self.im_info["bbox"] = (px, py, px + width, py + height)
delay_num, delay_den = i16(s, 20), i16(s, 22)
if delay_den == 0:
@@ -684,11 +672,13 @@ def chunk_fdAT(self, pos, length):
if ImageFile.LOAD_TRUNCATED_IMAGES:
s = ImageFile._safe_read(self.fp, length)
return s
- raise ValueError("APNG contains truncated fDAT chunk")
+ msg = "APNG contains truncated fDAT chunk"
+ raise ValueError(msg)
s = ImageFile._safe_read(self.fp, 4)
seq = i32(s)
if self._seq_num != seq - 1:
- raise SyntaxError("APNG contains frame sequence errors")
+ msg = "APNG contains frame sequence errors"
+ raise SyntaxError(msg)
self._seq_num = seq
return self.chunk_IDAT(pos + 4, length - 4)
@@ -706,14 +696,13 @@ def _accept(prefix):
class PngImageFile(ImageFile.ImageFile):
-
format = "PNG"
format_description = "Portable network graphics"
def _open(self):
-
if not _accept(self.fp.read(8)):
- raise SyntaxError("not a PNG file")
+ msg = "not a PNG file"
+ raise SyntaxError(msg)
self._fp = self.fp
self.__frame = 0
@@ -724,7 +713,6 @@ def _open(self):
self.png = PngStream(self.fp)
while True:
-
#
# get next chunk
@@ -797,7 +785,8 @@ def verify(self):
"""Verify PNG file"""
if self.fp is None:
- raise RuntimeError("verify must be called directly after open")
+ msg = "verify must be called directly after open"
+ raise RuntimeError(msg)
# back up to beginning of IDAT block
self.fp.seek(self.tile[0][2] - 8)
@@ -821,7 +810,8 @@ def seek(self, frame):
self._seek(f)
except EOFError as e:
self.seek(last_frame)
- raise EOFError("no more images in APNG file") from e
+ msg = "no more images in APNG file"
+ raise EOFError(msg) from e
def _seek(self, frame, rewind=False):
if frame == 0:
@@ -844,7 +834,8 @@ def _seek(self, frame, rewind=False):
self.__frame = 0
else:
if frame != self.__frame + 1:
- raise ValueError(f"cannot seek to frame {frame}")
+ msg = f"cannot seek to frame {frame}"
+ raise ValueError(msg)
# ensure previous frame was loaded
self.load()
@@ -869,11 +860,13 @@ def _seek(self, frame, rewind=False):
break
if cid == b"IEND":
- raise EOFError("No more images in APNG file")
+ msg = "No more images in APNG file"
+ raise EOFError(msg)
if cid == b"fcTL":
if frame_start:
# there must be at least one fdAT chunk between fcTL chunks
- raise SyntaxError("APNG missing frame data")
+ msg = "APNG missing frame data"
+ raise SyntaxError(msg)
frame_start = True
try:
@@ -998,9 +991,13 @@ def load_end(self):
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
@@ -1141,19 +1138,22 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
else:
base_im = previous["im"]
delta = ImageChops.subtract_modulo(
- im_frame.convert("RGB"), base_im.convert("RGB")
+ im_frame.convert("RGBA"), base_im.convert("RGBA")
)
- bbox = delta.getbbox()
+ bbox = delta.getbbox(alpha_only=False)
if (
not bbox
and prev_disposal == encoderinfo.get("disposal")
and prev_blend == encoderinfo.get("blend")
):
- if isinstance(duration, (list, tuple)):
- previous["encoderinfo"]["duration"] += encoderinfo["duration"]
+ previous["encoderinfo"]["duration"] += encoderinfo.get(
+ "duration", duration
+ )
continue
else:
bbox = None
+ if "duration" not in encoderinfo:
+ encoderinfo["duration"] = duration
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
# animation control
@@ -1178,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
im_frame = im_frame.crop(bbox)
size = im_frame.size
encoderinfo = frame_data["encoderinfo"]
- frame_duration = int(round(encoderinfo.get("duration", duration)))
+ frame_duration = int(round(encoderinfo["duration"]))
frame_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend)
# frame control
@@ -1243,7 +1243,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:
@@ -1277,7 +1276,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
try:
rawmode, mode = _OUTMODES[mode]
except KeyError as e:
- raise OSError(f"cannot write mode {mode} as PNG") from e
+ msg = f"cannot write mode {mode} as PNG"
+ raise OSError(msg) from e
#
# write minimal PNG file
@@ -1358,7 +1358,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
if "transparency" in im.encoderinfo:
# don't bother with transparency if it's an RGBA
# and it's in the info dict. It's probably just stale.
- raise OSError("cannot use transparency for this mode")
+ msg = "cannot use transparency for this mode"
+ raise OSError(msg)
else:
if im.mode == "P" and im.im.getpalettemode() == "RGBA":
alpha = im.im.getpalette("RGBA", "A")
@@ -1383,7 +1384,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunks.remove(cid)
chunk(fp, cid, data)
- exif = im.encoderinfo.get("exif", im.info.get("exif"))
+ exif = im.encoderinfo.get("exif")
if exif:
if isinstance(exif, Image.Exif):
exif = exif.tobytes(8)
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 392771d3e96..2cb1e56365d 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"
@@ -84,9 +83,11 @@ def _read_token(self):
token += c
if not token:
# Token was not even 1 byte
- raise ValueError("Reached EOF while reading header")
+ msg = "Reached EOF while reading header"
+ raise ValueError(msg)
elif len(token) > 10:
- raise ValueError(f"Token too long in file header: {token.decode()}")
+ msg = f"Token too long in file header: {token.decode()}"
+ raise ValueError(msg)
return token
def _open(self):
@@ -94,7 +95,8 @@ def _open(self):
try:
mode = MODES[magic_number]
except KeyError:
- raise SyntaxError("not a PPM file")
+ msg = "not a PPM file"
+ raise SyntaxError(msg)
if magic_number in (b"P1", b"P4"):
self.custom_mimetype = "image/x-portable-bitmap"
@@ -122,9 +124,8 @@ def _open(self):
elif ix == 2: # token is maxval
maxval = token
if not 0 < maxval < 65536:
- raise ValueError(
- "maxval must be greater than 0 and less than 65536"
- )
+ msg = "maxval must be greater than 0 and less than 65536"
+ raise ValueError(msg)
if maxval > 255 and mode == "L":
self.mode = "I"
@@ -208,7 +209,8 @@ def _decode_bitonal(self):
tokens = b"".join(block.split())
for token in tokens:
if token not in (48, 49):
- raise ValueError(f"Invalid token for this mode: {bytes([token])}")
+ msg = b"Invalid token for this mode: %s" % bytes([token])
+ raise ValueError(msg)
data = (data + tokens)[:total_bytes]
invert = bytes.maketrans(b"01", b"\xFF\x00")
return data.translate(invert)
@@ -235,24 +237,26 @@ def _decode_blocks(self, maxval):
if half_token:
block = half_token + block # stitch half_token to new block
+ half_token = False
tokens = block.split()
if block and not block[-1:].isspace(): # block might split token
half_token = tokens.pop() # save half token for later
if len(half_token) > max_len: # prevent buildup of half_token
- raise ValueError(
- f"Token too long found in data: {half_token[:max_len + 1]}"
+ msg = (
+ b"Token too long found in data: %s" % half_token[: max_len + 1]
)
+ raise ValueError(msg)
for token in tokens:
if len(token) > max_len:
- raise ValueError(
- f"Token too long found in data: {token[:max_len + 1]}"
- )
+ msg = b"Token too long found in data: %s" % token[: max_len + 1]
+ raise ValueError(msg)
value = int(token)
if value > maxval:
- raise ValueError(f"Channel value too large for this mode: {value}")
+ msg = f"Channel value too large for this mode: {value}"
+ raise ValueError(msg)
value = round(value / maxval * out_max)
data += o32(value) if self.mode == "I" else o8(value)
if len(data) == total_bytes: # finished!
@@ -312,7 +316,8 @@ def _save(im, fp, filename):
elif im.mode in ("RGB", "RGBA"):
rawmode, head = "RGB", b"P6"
else:
- raise OSError(f"cannot write mode {im.mode} as PPM")
+ msg = f"cannot write mode {im.mode} as PPM"
+ raise OSError(msg)
fp.write(head + b"\n%d %d\n" % im.size)
if head == b"P6":
fp.write(b"255\n")
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index bd10e3b95dd..5a5d60d568c 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
#
@@ -65,7 +63,8 @@ def _open(self):
s = read(26)
if not _accept(s) or i16(s, 4) != 1:
- raise SyntaxError("not a PSD file")
+ msg = "not a PSD file"
+ raise SyntaxError(msg)
psd_bits = i16(s, 22)
psd_channels = i16(s, 12)
@@ -74,7 +73,8 @@ def _open(self):
mode, channels = MODES[(psd_mode, psd_bits)]
if channels > psd_channels:
- raise OSError("not enough channels")
+ msg = "not enough channels"
+ raise OSError(msg)
if mode == "RGB" and psd_channels == 4:
mode = "RGBA"
channels = 4
@@ -152,7 +152,8 @@ def seek(self, layer):
self.fp = self._fp
return name, bbox
except IndexError as e:
- raise EOFError("no such layer") from e
+ msg = "no such layer"
+ raise EOFError(msg) from e
def tell(self):
# return layer number (0=image, 1..max=layers)
@@ -170,10 +171,10 @@ def read(size):
# sanity check
if ct_bytes < (abs(ct) * 20):
- raise SyntaxError("Layer block too short for number of layers requested")
+ msg = "Layer block too short for number of layers requested"
+ raise SyntaxError(msg)
for _ in range(abs(ct)):
-
# bounding box
y0 = i32(read(4))
x0 = i32(read(4))
@@ -234,21 +235,18 @@ def read(size):
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 9a2ec48fc60..99b46a4a66c 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -13,8 +13,7 @@
# Notes:
#
-# * Implements the pixel access object following Access.
-# * Does not implement the line functions, as they don't appear to be used
+# * Implements the pixel access object following Access.c
# * Taking only the tuple form, which is used from python.
# * Fill.c uses the integer form, but it's still going to use the old
# Access.c implementation.
@@ -23,6 +22,8 @@
import logging
import sys
+from ._deprecate import deprecate
+
try:
from cffi import FFI
@@ -48,6 +49,7 @@
class PyAccess:
def __init__(self, img, readonly=False):
+ deprecate("PyAccess", 11)
vals = dict(img.im.unsafe_ptrs)
self.readonly = readonly
self.image8 = ffi.cast("unsigned char **", vals["image8"])
@@ -80,7 +82,8 @@ def __setitem__(self, xy, color):
:param color: The pixel value.
"""
if self.readonly:
- raise ValueError("Attempt to putpixel a read only image")
+ msg = "Attempt to putpixel a read only image"
+ raise ValueError(msg)
(x, y) = xy
if x < 0:
x = self.xsize + x
@@ -128,7 +131,8 @@ def __getitem__(self, xy):
def check_xy(self, xy):
(x, y) = xy
if not (0 <= x < self.xsize and 0 <= y < self.ysize):
- raise ValueError("pixel location out of range")
+ msg = "pixel location out of range"
+ raise ValueError(msg)
return xy
@@ -319,6 +323,7 @@ def set_pixel(self, x, y, color):
"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 00000000000..ef91b90abca
--- /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 f0207bb7756..3662ffd1571 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -49,18 +49,17 @@ def _accept(prefix):
##
# 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)
if not _accept(s):
- raise ValueError("Not an SGI image file")
+ msg = "Not an SGI image file"
+ raise ValueError(msg)
# compression : verbatim or RLE
compression = s[2]
@@ -91,7 +90,8 @@ def _open(self):
pass
if rawmode == "":
- raise ValueError("Unsupported SGI image mode")
+ msg = "Unsupported SGI image mode"
+ raise ValueError(msg)
self._size = xsize, ysize
self.mode = rawmode.split(";")[0]
@@ -124,7 +124,8 @@ def _open(self):
def _save(im, fp, filename):
if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L":
- raise ValueError("Unsupported SGI image mode")
+ msg = "Unsupported SGI image mode"
+ raise ValueError(msg)
# Get the keyword arguments
info = im.encoderinfo
@@ -133,7 +134,8 @@ def _save(im, fp, filename):
bpc = info.get("bpc", 1)
if bpc not in (1, 2):
- raise ValueError("Unsupported number of bytes per pixel")
+ msg = "Unsupported number of bytes per pixel"
+ raise ValueError(msg)
# Flip the image, since the origin of SGI file is the bottom-left corner
orientation = -1
@@ -158,9 +160,8 @@ def _save(im, fp, filename):
# assert we've got the right number of bands.
if len(im.getbands()) != z:
- raise ValueError(
- f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}"
- )
+ msg = f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}"
+ raise ValueError(msg)
# Minimum Byte value
pinmin = 0
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index acafc320e64..5614957c176 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -36,7 +36,7 @@
import struct
import sys
-from PIL import Image, ImageFile
+from . import Image, ImageFile
def isInt(f):
@@ -91,7 +91,6 @@ def isSpiderImage(filename):
class SpiderImageFile(ImageFile.ImageFile):
-
format = "SPIDER"
format_description = "Spider 2D image"
_close_exclusive_fp_after_loading = False
@@ -110,14 +109,17 @@ def _open(self):
t = struct.unpack("<27f", f) # little-endian
hdrlen = isSpiderHeader(t)
if hdrlen == 0:
- raise SyntaxError("not a valid Spider file")
+ msg = "not a valid Spider file"
+ raise SyntaxError(msg)
except struct.error as e:
- raise SyntaxError("not a valid Spider file") from e
+ msg = "not a valid Spider file"
+ raise SyntaxError(msg) from e
h = (99,) + t # add 1 value : spider header index starts at 1
iform = int(h[5])
if iform != 1:
- raise SyntaxError("not a Spider 2D image")
+ msg = "not a Spider 2D image"
+ raise SyntaxError(msg)
self._size = int(h[12]), int(h[2]) # size in pixels (width, height)
self.istack = int(h[24])
@@ -140,7 +142,8 @@ def _open(self):
offset = hdrlen + self.stkoffset
self.istack = 2 # So Image knows it's still a stack
else:
- raise SyntaxError("inconsistent stack header values")
+ msg = "inconsistent stack header values"
+ raise SyntaxError(msg)
if self.bigendian:
self.rawmode = "F;32BF"
@@ -168,7 +171,8 @@ def tell(self):
def seek(self, frame):
if self.istack == 0:
- raise EOFError("attempt to seek in a non-stack file")
+ msg = "attempt to seek in a non-stack file"
+ raise EOFError(msg)
if not self._seek_check(frame):
return
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
@@ -187,7 +191,7 @@ def convert2byte(self, depth=255):
# returns a ImageTk.PhotoImage object, after rescaling to 0..255
def tkPhotoImage(self):
- from PIL import ImageTk
+ from . import ImageTk
return ImageTk.PhotoImage(self.convert2byte(), palette=256)
@@ -195,6 +199,7 @@ def tkPhotoImage(self):
# --------------------------------------------------------------------
# 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"""
@@ -260,7 +265,8 @@ def _save(im, fp, filename):
hdr = makeSpiderHeader(im)
if len(hdr) < 256:
- raise OSError("Error creating Spider header")
+ msg = "Error creating Spider header"
+ raise OSError(msg)
# write the SPIDER header
fp.writelines(hdr)
@@ -283,7 +289,6 @@ def _save_spider(im, fp, filename):
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 c03759a01e6..6712583d71c 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:
@@ -54,7 +52,8 @@ def _open(self):
# HEAD
s = self.fp.read(32)
if not _accept(s):
- raise SyntaxError("not an SUN raster file")
+ msg = "not an SUN raster file"
+ raise SyntaxError(msg)
offset = 32
@@ -83,14 +82,17 @@ def _open(self):
else:
self.mode, rawmode = "RGB", "BGRX"
else:
- raise SyntaxError("Unsupported Mode/Bit Depth")
+ msg = "Unsupported Mode/Bit Depth"
+ raise SyntaxError(msg)
if palette_length:
if palette_length > 1024:
- raise SyntaxError("Unsupported Color Palette Length")
+ msg = "Unsupported Color Palette Length"
+ raise SyntaxError(msg)
if palette_type != 1:
- raise SyntaxError("Unsupported Palette Type")
+ msg = "Unsupported Palette Type"
+ raise SyntaxError(msg)
offset = offset + palette_length
self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length))
@@ -124,7 +126,8 @@ def _open(self):
elif file_type == 2:
self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)]
else:
- raise SyntaxError("Unsupported Sun Raster file type")
+ msg = "Unsupported Sun Raster file type"
+ raise SyntaxError(msg)
#
diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py
index d108362fc9f..32928f6af30 100644
--- a/src/PIL/TarIO.py
+++ b/src/PIL/TarIO.py
@@ -32,15 +32,16 @@ def __init__(self, tarfile, file):
self.fh = open(tarfile, "rb")
while True:
-
s = self.fh.read(512)
if len(s) != 512:
- raise OSError("unexpected end of tar file")
+ msg = "unexpected end of tar file"
+ raise OSError(msg)
name = s[:100].decode("utf-8")
i = name.find("\0")
if i == 0:
- raise OSError("cannot find subfile")
+ msg = "cannot find subfile"
+ raise OSError(msg)
if i > 0:
name = name[:i]
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index cd454b755c0..67dfc3d3c8e 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -46,12 +46,10 @@
class TgaImageFile(ImageFile.ImageFile):
-
format = "TGA"
format_description = "Targa"
def _open(self):
-
# process header
s = self.fp.read(18)
@@ -73,7 +71,8 @@ def _open(self):
or self.size[1] <= 0
or depth not in (1, 8, 16, 24, 32)
):
- raise SyntaxError("not a TGA file")
+ msg = "not a TGA file"
+ raise SyntaxError(msg)
# image mode
if imagetype in (3, 11):
@@ -89,7 +88,8 @@ def _open(self):
if depth == 32:
self.mode = "RGBA"
else:
- raise SyntaxError("unknown TGA mode")
+ msg = "unknown TGA mode"
+ raise SyntaxError(msg)
# orientation
orientation = flags & 0x30
@@ -99,7 +99,8 @@ def _open(self):
elif orientation in [0, 0x10]:
orientation = -1
else:
- raise SyntaxError("unknown TGA orientation")
+ msg = "unknown TGA orientation"
+ raise SyntaxError(msg)
self.info["orientation"] = orientation
@@ -171,11 +172,11 @@ def load_end(self):
def _save(im, fp, filename):
-
try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e:
- raise OSError(f"cannot write mode {im.mode} as TGA") from e
+ msg = f"cannot write mode {im.mode} as TGA"
+ raise OSError(msg) from e
if "rle" in im.encoderinfo:
rle = im.encoderinfo["rle"]
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 1dfd5275fa1..d5148828506 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -49,7 +49,7 @@
from fractions import Fraction
from numbers import Number, Rational
-from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
+from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
@@ -170,6 +170,8 @@
(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 @@
(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 @@ def delegate(self, *args):
__ceil__ = _delegate("__ceil__")
__floor__ = _delegate("__floor__")
__round__ = _delegate("__round__")
+ # Python >= 3.11
+ if hasattr(Fraction, "__int__"):
+ __int__ = _delegate("__int__")
class ImageFileDirectory_v2(MutableMapping):
@@ -500,14 +505,16 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None):
:param prefix: Override the endianness of the file.
"""
if not _accept(ifh):
- raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)")
+ msg = f"not a TIFF file (header {repr(ifh)} not valid)"
+ raise SyntaxError(msg)
self._prefix = prefix if prefix is not None else ifh[:2]
if self._prefix == MM:
self._endian = ">"
elif self._prefix == II:
self._endian = "<"
else:
- raise SyntaxError("not a TIFF IFD")
+ msg = "not a TIFF IFD"
+ raise SyntaxError(msg)
self._bigtiff = ifh[2] == 43
self.group = group
self.tagtype = {}
@@ -524,7 +531,8 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None):
@legacy_api.setter
def legacy_api(self, value):
- raise Exception("Not allowing setting of legacy api")
+ msg = "Not allowing setting of legacy api"
+ raise Exception(msg)
def reset(self):
self._tags_v1 = {} # will remain empty if legacy_api is false
@@ -719,6 +727,10 @@ def load_byte(self, data, legacy_api=True):
@_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
@_register_loader(2, 1)
@@ -730,6 +742,8 @@ def load_string(self, data, legacy_api=True):
@_register_writer(2)
def write_string(self, value):
# remerge of https://github.com/python-pillow/Pillow/pull/1416
+ if isinstance(value, int):
+ value = str(value)
if not isinstance(value, bytes):
value = value.encode("ascii", "replace")
return value + b"\0"
@@ -755,6 +769,8 @@ def load_undefined(self, data, legacy_api=True):
@_register_writer(7)
def write_undefined(self, value):
+ if isinstance(value, int):
+ value = str(value).encode("ascii", "replace")
return value
@_register_loader(10, 8)
@@ -776,14 +792,14 @@ def write_signed_rational(self, *values):
def _ensure_read(self, fp, size):
ret = fp.read(size)
if len(ret) != size:
- raise OSError(
+ msg = (
"Corrupt EXIF data. "
f"Expecting to read {size} bytes but only got {len(ret)}. "
)
+ raise OSError(msg)
return ret
def load(self, fp):
-
self.reset()
self._offset = fp.tell()
@@ -906,7 +922,8 @@ def tobytes(self, offset=0):
if stripoffsets is not None:
tag, typ, count, value, data = entries[stripoffsets]
if data:
- raise NotImplementedError("multistrip support not yet implemented")
+ msg = "multistrip support not yet implemented"
+ raise NotImplementedError(msg)
value = self._pack("L", self._unpack("L", value)[0] + offset)
entries[stripoffsets] = tag, typ, count, value, data
@@ -927,7 +944,6 @@ def tobytes(self, offset=0):
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))
@@ -1048,7 +1064,6 @@ def __getitem__(self, tag):
class TiffImageFile(ImageFile.ImageFile):
-
format = "TIFF"
format_description = "Adobe TIFF"
_close_exclusive_fp_after_loading = False
@@ -1119,7 +1134,8 @@ def _seek(self, frame):
while len(self._frame_pos) <= frame:
if not self.__next:
- raise EOFError("no more images in TIFF file")
+ msg = "no more images in TIFF file"
+ raise EOFError(msg)
logger.debug(
f"Seeking to frame {frame}, on frame {self.__frame}, "
f"__next {self.__next}, location: {self.fp.tell()}"
@@ -1151,39 +1167,6 @@ def tell(self):
"""Return the current frame number"""
return self.__frame
- def get_child_images(self):
- if SUBIFD not in self.tag_v2:
- return []
- child_images = []
- exif = self.getexif()
- offset = None
- for im_offset in self.tag_v2[SUBIFD]:
- # reset buffered io handle in case fp
- # was passed to libtiff, invalidating the buffer
- current_offset = self._fp.tell()
- if offset is None:
- offset = current_offset
-
- fp = self._fp
- ifd = exif._get_ifd_dict(im_offset)
- jpegInterchangeFormat = ifd.get(513)
- if jpegInterchangeFormat is not None:
- fp.seek(jpegInterchangeFormat)
- jpeg_data = fp.read(ifd.get(514))
-
- fp = io.BytesIO(jpeg_data)
-
- with Image.open(fp) as im:
- if jpegInterchangeFormat is None:
- im._frame_pos = [im_offset]
- im._seek(0)
- im.load()
- child_images.append(im)
-
- if offset is not None:
- self._fp.seek(offset)
- return child_images
-
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
@@ -1202,7 +1185,7 @@ def get_photoshop_blocks(self):
:returns: Photoshop "Image Resource Blocks" in a dictionary.
"""
blocks = {}
- val = self.tag_v2.get(0x8649)
+ val = self.tag_v2.get(ExifTags.Base.ImageResources)
if val:
while val[:4] == b"8BIM":
id = i16(val[4:6])
@@ -1245,7 +1228,7 @@ def load_end(self):
# 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)
@@ -1259,7 +1242,8 @@ def _load_libtiff(self):
self.load_prepare()
if not len(self.tile) == 1:
- raise OSError("Not exactly one tile")
+ msg = "Not exactly one tile"
+ raise OSError(msg)
# (self._compression, (extents tuple),
# 0, (rawmode, self._compression, fp))
@@ -1269,9 +1253,8 @@ def _load_libtiff(self):
# To be nice on memory footprint, if there's a
# file descriptor, use that instead of reading
# into a string in python.
- # libtiff closes the file descriptor, so pass in a dup.
try:
- fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno())
+ fp = hasattr(self.fp, "fileno") and self.fp.fileno()
# flush the file descriptor, prevents error on pypy 2.4+
# should also eliminate the need for fp.tell
# in _seek
@@ -1291,7 +1274,8 @@ def _load_libtiff(self):
try:
decoder.setimage(self.im, extents)
except ValueError as e:
- raise OSError("Couldn't set the image") from e
+ msg = "Couldn't set the image"
+ raise OSError(msg) from e
close_self_fp = self._exclusive_fp and not self.is_animated
if hasattr(self.fp, "getvalue"):
@@ -1320,18 +1304,11 @@ def _load_libtiff(self):
# UNDONE -- so much for that buffer size thing.
n, err = decoder.decode(self.fp.read())
- if fp:
- try:
- os.close(fp)
- except OSError:
- pass
-
self.tile = []
self.readonly = 0
self.load_end()
- # libtiff closed the fp in a, we need to close self.fp, if possible
if close_self_fp:
self.fp.close()
self.fp = None # might be shared
@@ -1345,7 +1322,8 @@ def _setup(self):
"""Setup this image object based on current tags"""
if 0xBC01 in self.tag_v2:
- raise OSError("Windows Media Photo files not yet supported")
+ msg = "Windows Media Photo files not yet supported"
+ raise OSError(msg)
# extract relevant tags
self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]
@@ -1404,7 +1382,8 @@ def _setup(self):
logger.error(
"More samples per pixel than can be decoded: %s", samples_per_pixel
)
- raise SyntaxError("Invalid value for samples per pixel")
+ msg = "Invalid value for samples per pixel"
+ raise SyntaxError(msg)
if samples_per_pixel < bps_actual_count:
# If a file has more values in bps_tuple than expected,
@@ -1416,7 +1395,8 @@ def _setup(self):
bps_tuple = bps_tuple * samples_per_pixel
if len(bps_tuple) != samples_per_pixel:
- raise SyntaxError("unknown data organization")
+ msg = "unknown data organization"
+ raise SyntaxError(msg)
# mode: check photometric interpretation and bits per pixel
key = (
@@ -1432,7 +1412,8 @@ def _setup(self):
self.mode, rawmode = OPEN_INFO[key]
except KeyError as e:
logger.debug("- unsupported format")
- raise SyntaxError("unknown pixel mode") from e
+ msg = "unknown pixel mode"
+ raise SyntaxError(msg) from e
logger.debug(f"- raw mode: {rawmode}")
logger.debug(f"- pil mode: {self.mode}")
@@ -1548,7 +1529,8 @@ def _setup(self):
layer += 1
else:
logger.debug("- unsupported data organization")
- raise SyntaxError("unknown data organization")
+ msg = "unknown data organization"
+ raise SyntaxError(msg)
# Fix up info.
if ICCPROFILE in self.tag_v2:
@@ -1560,7 +1542,7 @@ def _setup(self):
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
- self._tile_orientation = self.tag_v2.get(0x0112)
+ self._tile_orientation = self.tag_v2.get(ExifTags.Base.Orientation)
#
@@ -1596,11 +1578,11 @@ def _setup(self):
def _save(im, fp, filename):
-
try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e:
- raise OSError(f"cannot write mode {im.mode} as TIFF") from e
+ msg = f"cannot write mode {im.mode} as TIFF"
+ raise OSError(msg) from e
ifd = ImageFileDirectory_v2(prefix=prefix)
@@ -1644,7 +1626,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)
@@ -1765,11 +1747,11 @@ def _save(im, fp, filename):
if "quality" in encoderinfo:
quality = encoderinfo["quality"]
if not isinstance(quality, int) or quality < 0 or quality > 100:
- raise ValueError("Invalid quality setting")
+ msg = "Invalid quality setting"
+ raise ValueError(msg)
if compression != "jpeg":
- raise ValueError(
- "quality setting only supported for 'jpeg' compression"
- )
+ msg = "quality setting only supported for 'jpeg' compression"
+ raise ValueError(msg)
ifd[JPEGQUALITY] = quality
logger.debug("Saving using libtiff encoder")
@@ -1819,7 +1801,7 @@ def _save(im, fp, filename):
# Custom items are supported for int, float, unicode, string and byte
# values. Other types and tuples require a tagtype.
if tag not in TiffTags.LIBTIFF_CORE:
- if not Image.core.libtiff_support_custom_tags:
+ if not getattr(Image.core, "libtiff_support_custom_tags", False):
continue
if tag in ifd.tagtype:
@@ -1860,13 +1842,19 @@ 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:
- raise OSError(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:
for tag in blocklist:
@@ -1898,6 +1886,10 @@ class AppendingTiffWriter:
8, # srational
4, # float
8, # double
+ 4, # ifd
+ 2, # unicode
+ 4, # complex
+ 8, # long8
]
# StripOffsets = 273
@@ -1941,7 +1933,8 @@ def setup(self):
elif iimm == b"MM\x00\x2a":
self.setEndian(">")
else:
- raise RuntimeError("Invalid TIFF file header")
+ msg = "Invalid TIFF file header"
+ raise RuntimeError(msg)
self.skipIFDs()
self.goToEnd()
@@ -1955,12 +1948,14 @@ def finalize(self):
iimm = self.f.read(4)
if not iimm:
- # raise RuntimeError("nothing written into new page")
+ # msg = "nothing written into new page"
+ # raise RuntimeError(msg)
# Make it easy to finish a frame without committing to a new one.
return
if iimm != self.IIMM:
- raise RuntimeError("IIMM of new page doesn't match IIMM of first page")
+ msg = "IIMM of new page doesn't match IIMM of first page"
+ raise RuntimeError(msg)
ifd_offset = self.readLong()
ifd_offset += self.offsetOfNewPage
@@ -2034,29 +2029,34 @@ def rewriteLastShortToLong(self, value):
self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")
+ msg = f"wrote only {bytes_written} bytes but wanted 4"
+ raise RuntimeError(msg)
def rewriteLastShort(self, value):
self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")
+ msg = f"wrote only {bytes_written} bytes but wanted 2"
+ raise RuntimeError(msg)
def rewriteLastLong(self, value):
self.f.seek(-4, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")
+ msg = f"wrote only {bytes_written} bytes but wanted 4"
+ raise RuntimeError(msg)
def writeShort(self, value):
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")
+ msg = f"wrote only {bytes_written} bytes but wanted 2"
+ raise RuntimeError(msg)
def writeLong(self, value):
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")
+ msg = f"wrote only {bytes_written} bytes but wanted 4"
+ raise RuntimeError(msg)
def close(self):
self.finalize()
@@ -2099,7 +2099,8 @@ def fixIFD(self):
def fixOffsets(self, count, isShort=False, isLong=False):
if not isShort and not isLong:
- raise RuntimeError("offset is neither short nor long")
+ msg = "offset is neither short nor long"
+ raise RuntimeError(msg)
for i in range(count):
offset = self.readShort() if isShort else self.readLong()
@@ -2107,7 +2108,8 @@ def fixOffsets(self, count, isShort=False, isLong=False):
if isShort and offset >= 65536:
# offset is now too large - we must convert shorts to longs
if count != 1:
- raise RuntimeError("not implemented") # XXX TODO
+ msg = "not implemented"
+ raise RuntimeError(msg) # XXX TODO
# simple case - the offset is just one and therefore it is
# local (not referenced with another offset)
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 9b527713864..30b05e4e1d4 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -195,6 +195,7 @@ def lookup(tag, group=None):
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 @@ def lookup(tag, group=None):
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 0dc695a88d4..e4f47aa04bc 100644
--- a/src/PIL/WalImageFile.py
+++ b/src/PIL/WalImageFile.py
@@ -28,7 +28,6 @@
class WalImageFile(ImageFile.ImageFile):
-
format = "WAL"
format_description = "Quake2 Texture"
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index 5eaeb10ccd5..ce8e05fcbb1 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
@@ -98,6 +97,15 @@ def _getexif(self):
return None
return self.getexif()._get_merged_dict()
+ def getxmp(self):
+ """
+ Returns a dictionary containing the XMP tags.
+ Requires defusedxml to be installed.
+
+ :returns: XMP tags in a dictionary.
+ """
+ return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {}
+
def seek(self, frame):
if not self._seek_check(frame):
return
@@ -121,7 +129,8 @@ def _get_next(self):
if ret is None:
self._reset() # Reset just to be safe
self.seek(0)
- raise EOFError("failed to decode next frame in WebP file")
+ msg = "failed to decode next frame in WebP file"
+ raise EOFError(msg)
# Compute duration
data, timestamp = ret
@@ -224,9 +233,8 @@ def _save_all(im, fp, filename):
or len(background) != 4
or not all(0 <= v < 256 for v in background)
):
- raise OSError(
- f"Background color is not an RGBA tuple clamped to (0-255): {background}"
- )
+ msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}"
+ raise OSError(msg)
# Convert to packed uint
bg_r, bg_g, bg_b, bg_a = background
@@ -277,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,
@@ -297,12 +305,13 @@ 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)
if data is None:
- raise OSError("cannot write file as WebP (encoder returned None)")
+ msg = "cannot write file as WebP (encoder returned None)"
+ raise OSError(msg)
fp.write(data)
@@ -318,6 +327,7 @@ def _save(im, fp, filename):
exif = exif[6:]
xmp = im.encoderinfo.get("xmp", "")
method = im.encoderinfo.get("method", 4)
+ exact = 1 if im.encoderinfo.get("exact") else 0
if im.mode not in _VALID_WEBP_LEGACY_MODES:
alpha = (
@@ -336,11 +346,13 @@ def _save(im, fp, filename):
im.mode,
icc_profile,
method,
+ exact,
exif,
xmp,
)
if data is None:
- raise OSError("cannot write file as WebP (encoder returned None)")
+ msg = "cannot write file as WebP (encoder returned None)"
+ raise OSError(msg)
fp.write(data)
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index 2f54cdebbea..0ecab56a824 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 @@ def _open(self):
s = self.fp.read(80)
if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00":
-
# placeable windows metafile
# get units per inch
@@ -109,7 +107,8 @@ def _open(self):
# sanity check (standard metafile header)
if s[22:26] != b"\x01\x00\t\x00":
- raise SyntaxError("Unsupported WMF file format")
+ msg = "Unsupported WMF file format"
+ raise SyntaxError(msg)
elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF":
# enhanced metafile
@@ -137,7 +136,8 @@ def _open(self):
self.info["dpi"] = xdpi, ydpi
else:
- raise SyntaxError("Unsupported file format")
+ msg = "Unsupported file format"
+ raise SyntaxError(msg)
self.mode = "RGB"
self._size = size
@@ -162,7 +162,8 @@ def load(self, dpi=None):
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
- raise OSError("WMF save handler not installed")
+ msg = "WMF save handler not installed"
+ raise OSError(msg)
_handler.save(im, fp, filename)
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index 4efedb77ea7..aa4a01f4e5a 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -41,15 +41,14 @@ 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)):
- raise SyntaxError("not an XV thumbnail file")
+ msg = "not an XV thumbnail file"
+ raise SyntaxError(msg)
# Skip to beginning of next line
self.fp.readline()
@@ -58,7 +57,8 @@ def _open(self):
while True:
s = self.fp.readline()
if not s:
- raise SyntaxError("Unexpected EOF reading XV thumbnail file")
+ msg = "Unexpected EOF reading XV thumbnail file"
+ raise SyntaxError(msg)
if s[0] != 35: # ie. when not a comment: '#'
break
diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py
index 59acabebae3..3c12564c963 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -44,16 +44,15 @@ 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:
- raise SyntaxError("not a XBM file")
+ msg = "not a XBM file"
+ raise SyntaxError(msg)
xsize = int(m.group("width"))
ysize = int(m.group("height"))
@@ -68,9 +67,9 @@ def _open(self):
def _save(im, fp, filename):
-
if im.mode != "1":
- raise OSError(f"cannot write mode {im.mode} as XBM")
+ msg = f"cannot write mode {im.mode} as XBM"
+ raise OSError(msg)
fp.write(f"#define im_width {im.size[0]}\n".encode("ascii"))
fp.write(f"#define im_height {im.size[1]}\n".encode("ascii"))
diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py
index aaed2039db4..5d5bdc3edfa 100644
--- a/src/PIL/XpmImagePlugin.py
+++ b/src/PIL/XpmImagePlugin.py
@@ -33,20 +33,20 @@ def _accept(prefix):
class XpmImageFile(ImageFile.ImageFile):
-
format = "XPM"
format_description = "X11 Pixel Map"
def _open(self):
-
if not _accept(self.fp.read(9)):
- raise SyntaxError("not an XPM file")
+ msg = "not an XPM file"
+ raise SyntaxError(msg)
# skip forward to next string
while True:
s = self.fp.readline()
if not s:
- raise SyntaxError("broken XPM file")
+ msg = "broken XPM file"
+ raise SyntaxError(msg)
m = xpm_head.match(s)
if m:
break
@@ -57,7 +57,8 @@ def _open(self):
bpp = int(m.group(4))
if pal > 256 or bpp != 1:
- raise ValueError("cannot read this XPM file")
+ msg = "cannot read this XPM file"
+ raise ValueError(msg)
#
# load palette description
@@ -65,7 +66,6 @@ def _open(self):
palette = [b"\0\0\0"] * 256
for _ in range(pal):
-
s = self.fp.readline()
if s[-2:] == b"\r\n":
s = s[:-2]
@@ -76,9 +76,7 @@ def _open(self):
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":
@@ -91,13 +89,14 @@ def _open(self):
)
else:
# unknown colour
- raise ValueError("cannot read this XPM file")
+ msg = "cannot read this XPM file"
+ raise ValueError(msg)
break
else:
-
# missing colour key
- raise ValueError("cannot read this XPM file")
+ msg = "cannot read this XPM file"
+ raise ValueError(msg)
self.mode = "P"
self.palette = ImagePalette.raw("RGB", b"".join(palette))
@@ -105,7 +104,6 @@ def _open(self):
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 e65b155b2dc..2bb8f6d7f10 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 @@
"DdsImagePlugin",
"EpsImagePlugin",
"FitsImagePlugin",
- "FitsStubImagePlugin",
"FliImagePlugin",
"FpxImagePlugin",
"FtexImagePlugin",
@@ -59,6 +58,7 @@
"PngImagePlugin",
"PpmImagePlugin",
"PsdImagePlugin",
+ "QoiImagePlugin",
"SgiImagePlugin",
"SpiderImagePlugin",
"SunImagePlugin",
@@ -75,6 +75,10 @@
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 30a8a897100..2f2a3df13e3 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -43,14 +43,17 @@ def deprecate(
if when is None:
removed = "a future version"
elif when <= int(__version__.split(".")[0]):
- raise RuntimeError(f"{deprecated} {is_} deprecated and should be removed.")
- elif when == 10:
- removed = "Pillow 10 (2023-07-01)"
+ msg = f"{deprecated} {is_} deprecated and should be removed."
+ raise RuntimeError(msg)
+ elif when == 11:
+ removed = "Pillow 11 (2024-10-15)"
else:
- raise ValueError(f"Unknown removal version, update {__name__}?")
+ msg = f"Unknown removal version: {when}. Update {__name__}?"
+ raise ValueError(msg)
if replacement and action:
- raise ValueError("Use only one of 'replacement' and 'action'")
+ msg = "Use only one of 'replacement' and 'action'"
+ raise ValueError(msg)
if replacement:
action = f". Use {replacement} instead."
diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py
index 5cd7e9b1fb2..597c21b5e38 100644
--- a/src/PIL/_tkinter_finder.py
+++ b/src/PIL/_tkinter_finder.py
@@ -4,8 +4,6 @@
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 @@
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 43896fabd19..f3455f1f1f7 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,2 +1,2 @@
# Master version for Pillow
-__version__ = "9.3.0"
+__version__ = "10.0.1"
diff --git a/src/PIL/features.py b/src/PIL/features.py
index 3838568f3a6..f14e60cf5d4 100644
--- a/src/PIL/features.py
+++ b/src/PIL/features.py
@@ -24,15 +24,19 @@ def check_module(feature):
:returns: ``True`` if available, ``False`` otherwise.
:raises ValueError: If the module is not defined in this version of Pillow.
"""
- if not (feature in modules):
- raise ValueError(f"Unknown module {feature}")
+ if feature not in modules:
+ msg = f"Unknown module {feature}"
+ raise ValueError(msg)
module, ver = modules[feature]
try:
__import__(module)
return True
- except ImportError:
+ except ModuleNotFoundError:
+ return False
+ except ImportError as ex:
+ warnings.warn(str(ex))
return False
@@ -78,7 +82,8 @@ def check_codec(feature):
:raises ValueError: If the codec is not defined in this version of Pillow.
"""
if feature not in codecs:
- raise ValueError(f"Unknown codec {feature}")
+ msg = f"Unknown codec {feature}"
+ raise ValueError(msg)
codec, lib = codecs[feature]
@@ -135,14 +140,18 @@ def check_feature(feature):
:raises ValueError: If the feature is not defined in this version of Pillow.
"""
if feature not in features:
- raise ValueError(f"Unknown feature {feature}")
+ msg = f"Unknown feature {feature}"
+ raise ValueError(msg)
module, flag, ver = features[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 9852fc9d688..68247bc472d 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 16b9a2eddb5..bd3cafe9596 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 0888188fb20..7b4174d6f75 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -251,6 +251,10 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) {
static const char *must_be_sequence = "argument must be a sequence";
static const char *must_be_two_coordinates =
"coordinate list must contain exactly 2 coordinates";
+static const char *incorrectly_ordered_x_coordinate =
+ "x1 must be greater than or equal to x0";
+static const char *incorrectly_ordered_y_coordinate =
+ "y1 must be greater than or equal to y0";
static const char *wrong_mode = "unrecognized image mode";
static const char *wrong_raw_mode = "unrecognized raw mode";
static const char *outside_image = "image index out of range";
@@ -487,7 +491,7 @@ getink(PyObject *color, Imaging im, char *ink) {
int g = 0, b = 0, a = 0;
double f = 0;
/* Windows 64 bit longs are 32 bits, and 0xFFFFFFFF (white) is a
- python long (not int) that raises an overflow error when trying
+ Python long (not int) that raises an overflow error when trying
to return it into a 32 bit C long
*/
PY_LONG_LONG r = 0;
@@ -498,7 +502,8 @@ getink(PyObject *color, Imaging im, char *ink) {
be cast to either UINT8 or INT32 */
int rIsInt = 0;
- if (PyTuple_Check(color) && PyTuple_GET_SIZE(color) == 1) {
+ int tupleSize = PyTuple_Check(color) ? PyTuple_GET_SIZE(color) : -1;
+ if (tupleSize == 1) {
color = PyTuple_GetItem(color, 0);
}
if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 ||
@@ -509,15 +514,13 @@ getink(PyObject *color, Imaging im, char *ink) {
return NULL;
}
rIsInt = 1;
- } else if (im->type == IMAGING_TYPE_UINT8) {
- if (!PyTuple_Check(color)) {
- PyErr_SetString(PyExc_TypeError, "color must be int or tuple");
- return NULL;
- }
- } else {
+ } else if (im->bands == 1) {
PyErr_SetString(
PyExc_TypeError, "color must be int or single-element tuple");
return NULL;
+ } else if (tupleSize == -1) {
+ PyErr_SetString(PyExc_TypeError, "color must be int or tuple");
+ return NULL;
}
}
@@ -527,7 +530,7 @@ getink(PyObject *color, Imaging im, char *ink) {
if (im->bands == 1) {
/* unsigned integer, single layer */
if (rIsInt != 1) {
- if (PyTuple_GET_SIZE(color) != 1) {
+ if (tupleSize != 1) {
PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple");
return NULL;
} else if (!PyArg_ParseTuple(color, "L", &r)) {
@@ -537,7 +540,6 @@ getink(PyObject *color, Imaging im, char *ink) {
ink[0] = (char)CLIP8(r);
ink[1] = ink[2] = ink[3] = 0;
} else {
- a = 255;
if (rIsInt) {
/* compatibility: ABGR */
a = (UINT8)(r >> 24);
@@ -545,7 +547,7 @@ getink(PyObject *color, Imaging im, char *ink) {
g = (UINT8)(r >> 8);
r = (UINT8)r;
} else {
- int tupleSize = PyTuple_GET_SIZE(color);
+ a = 255;
if (im->bands == 2) {
if (tupleSize != 1 && tupleSize != 2) {
PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements");
@@ -589,6 +591,41 @@ getink(PyObject *color, Imaging im, char *ink) {
ink[1] = (UINT8)(r >> 8);
ink[2] = ink[3] = 0;
return ink;
+ } else {
+ if (rIsInt) {
+ b = (UINT8)(r >> 16);
+ g = (UINT8)(r >> 8);
+ r = (UINT8)r;
+ } else if (tupleSize != 3) {
+ PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or three elements");
+ return NULL;
+ } else if (!PyArg_ParseTuple(color, "Lii", &r, &g, &b)) {
+ return NULL;
+ }
+ if (!strcmp(im->mode, "BGR;15")) {
+ UINT16 v = ((((UINT16)r) << 7) & 0x7c00) +
+ ((((UINT16)g) << 2) & 0x03e0) +
+ ((((UINT16)b) >> 3) & 0x001f);
+
+ ink[0] = (UINT8)v;
+ ink[1] = (UINT8)(v >> 8);
+ ink[2] = ink[3] = 0;
+ return ink;
+ } else if (!strcmp(im->mode, "BGR;16")) {
+ UINT16 v = ((((UINT16)r) << 8) & 0xf800) +
+ ((((UINT16)g) << 3) & 0x07e0) +
+ ((((UINT16)b) >> 3) & 0x001f);
+ ink[0] = (UINT8)v;
+ ink[1] = (UINT8)(v >> 8);
+ ink[2] = ink[3] = 0;
+ return ink;
+ } else if (!strcmp(im->mode, "BGR;24")) {
+ ink[0] = (UINT8)b;
+ ink[1] = (UINT8)g;
+ ink[2] = (UINT8)r;
+ ink[3] = 0;
+ return ink;
+ }
}
}
@@ -990,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) {
static PyObject *
_expand_image(ImagingObject *self, PyObject *args) {
int x, y;
- int mode = 0;
- if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) {
+ if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
return NULL;
}
- return PyImagingNew(ImagingExpand(self->image, x, y, mode));
+ return PyImagingNew(ImagingExpand(self->image, x, y));
}
static PyObject *
@@ -1245,6 +1281,10 @@ _histogram(ImagingObject *self, PyObject *args) {
/* Build an integer list containing the histogram */
list = PyList_New(h->bands * 256);
+ if (list == NULL) {
+ ImagingHistogramDelete(h);
+ return NULL;
+ }
for (i = 0; i < h->bands * 256; i++) {
PyObject *item;
item = PyLong_FromLong(h->histogram[i]);
@@ -1531,25 +1571,21 @@ if (PySequence_Check(op)) { \
PyErr_SetString(PyExc_TypeError, must_be_sequence);
return NULL;
}
+ int endian = strncmp(image->mode, "I;16", 4) == 0 ? (strcmp(image->mode, "I;16B") == 0 ? 2 : 1) : 0;
double value;
- if (scale == 1.0 && offset == 0.0) {
- /* Clipped data */
- for (i = x = y = 0; i < n; i++) {
- set_value_to_item(seq, i);
+ for (i = x = y = 0; i < n; i++) {
+ set_value_to_item(seq, i);
+ if (scale != 1.0 || offset != 0.0) {
+ value = value * scale + offset;
+ }
+ if (endian == 0) {
image->image8[y][x] = (UINT8)CLIP8(value);
- if (++x >= (int)image->xsize) {
- x = 0, y++;
- }
+ } else {
+ image->image8[y][x * 2 + (endian == 2 ? 1 : 0)] = CLIP8((int)value % 256);
+ image->image8[y][x * 2 + (endian == 2 ? 0 : 1)] = CLIP8((int)value >> 8);
}
-
- } else {
- /* Scaled and clipped data */
- for (i = x = y = 0; i < n; i++) {
- set_value_to_item(seq, i);
- image->image8[y][x] = CLIP8(value * scale + offset);
- if (++x >= (int)image->xsize) {
- x = 0, y++;
- }
+ if (++x >= (int)image->xsize) {
+ x = 0, y++;
}
}
PyErr_Clear(); /* Avoid weird exceptions */
@@ -1829,7 +1865,7 @@ _resize(ImagingObject *self, PyObject *args) {
box[1] - (int)box[1] == 0 && box[3] - box[1] == ysize) {
imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]);
} else if (filter == IMAGING_TRANSFORM_NEAREST) {
- double a[6];
+ double a[8];
memset(a, 0, sizeof a);
a[0] = (double)(box[2] - box[0]) / xsize;
@@ -2123,9 +2159,15 @@ _isblock(ImagingObject *self) {
}
static PyObject *
-_getbbox(ImagingObject *self) {
+_getbbox(ImagingObject *self, PyObject *args) {
int bbox[4];
- if (!ImagingGetBBox(self->image, bbox)) {
+
+ int alpha_only = 1;
+ if (!PyArg_ParseTuple(args, "|i", &alpha_only)) {
+ return NULL;
+ }
+
+ if (!ImagingGetBBox(self->image, bbox, alpha_only)) {
Py_INCREF(Py_None);
return Py_None;
}
@@ -2154,6 +2196,10 @@ _getcolors(ImagingObject *self, PyObject *args) {
Py_INCREF(out);
} else {
out = PyList_New(colors);
+ if (out == NULL) {
+ free(items);
+ return NULL;
+ }
for (i = 0; i < colors; i++) {
ImagingColorItem *v = &items[i];
PyObject *item = Py_BuildValue(
@@ -2809,6 +2855,16 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) {
free(xy);
return NULL;
}
+ if (xy[2] < xy[0]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate);
+ free(xy);
+ return NULL;
+ }
+ if (xy[3] < xy[1]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate);
+ free(xy);
+ return NULL;
+ }
n = ImagingDrawArc(
self->image->image,
@@ -2890,6 +2946,16 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) {
free(xy);
return NULL;
}
+ if (xy[2] < xy[0]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate);
+ free(xy);
+ return NULL;
+ }
+ if (xy[3] < xy[1]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate);
+ free(xy);
+ return NULL;
+ }
n = ImagingDrawChord(
self->image->image,
@@ -2936,6 +3002,16 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) {
free(xy);
return NULL;
}
+ if (xy[2] < xy[0]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate);
+ free(xy);
+ return NULL;
+ }
+ if (xy[3] < xy[1]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate);
+ free(xy);
+ return NULL;
+ }
n = ImagingDrawEllipse(
self->image->image,
@@ -3105,6 +3181,16 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) {
free(xy);
return NULL;
}
+ if (xy[2] < xy[0]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate);
+ free(xy);
+ return NULL;
+ }
+ if (xy[3] < xy[1]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate);
+ free(xy);
+ return NULL;
+ }
n = ImagingDrawPieslice(
self->image->image,
@@ -3201,6 +3287,16 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) {
free(xy);
return NULL;
}
+ if (xy[2] < xy[0]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate);
+ free(xy);
+ return NULL;
+ }
+ if (xy[3] < xy[1]) {
+ PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate);
+ free(xy);
+ return NULL;
+ }
n = ImagingDrawRectangle(
self->image->image,
@@ -3483,7 +3579,7 @@ static struct PyMethodDef methods[] = {
{"isblock", (PyCFunction)_isblock, METH_NOARGS},
- {"getbbox", (PyCFunction)_getbbox, METH_NOARGS},
+ {"getbbox", (PyCFunction)_getbbox, METH_VARARGS},
{"getcolors", (PyCFunction)_getcolors, METH_VARARGS},
{"getextrema", (PyCFunction)_getextrema, METH_NOARGS},
{"getprojection", (PyCFunction)_getprojection, METH_NOARGS},
@@ -3760,6 +3856,7 @@ static PyTypeObject PixelAccess_Type = {
static PyObject *
_get_stats(PyObject *self, PyObject *args) {
PyObject *d;
+ PyObject *v;
ImagingMemoryArena arena = &ImagingDefaultArena;
if (!PyArg_ParseTuple(args, ":get_stats")) {
@@ -3770,15 +3867,29 @@ _get_stats(PyObject *self, PyObject *args) {
if (!d) {
return NULL;
}
- PyDict_SetItemString(d, "new_count", PyLong_FromLong(arena->stats_new_count));
- PyDict_SetItemString(
- d, "allocated_blocks", PyLong_FromLong(arena->stats_allocated_blocks));
- PyDict_SetItemString(
- d, "reused_blocks", PyLong_FromLong(arena->stats_reused_blocks));
- PyDict_SetItemString(
- d, "reallocated_blocks", PyLong_FromLong(arena->stats_reallocated_blocks));
- PyDict_SetItemString(d, "freed_blocks", PyLong_FromLong(arena->stats_freed_blocks));
- PyDict_SetItemString(d, "blocks_cached", PyLong_FromLong(arena->blocks_cached));
+ v = PyLong_FromLong(arena->stats_new_count);
+ PyDict_SetItemString(d, "new_count", v ? v : Py_None);
+ Py_XDECREF(v);
+
+ v = PyLong_FromLong(arena->stats_allocated_blocks);
+ PyDict_SetItemString(d, "allocated_blocks", v ? v : Py_None);
+ Py_XDECREF(v);
+
+ v = PyLong_FromLong(arena->stats_reused_blocks);
+ PyDict_SetItemString(d, "reused_blocks", v ? v : Py_None);
+ Py_XDECREF(v);
+
+ v = PyLong_FromLong(arena->stats_reallocated_blocks);
+ PyDict_SetItemString(d, "reallocated_blocks", v ? v : Py_None);
+ Py_XDECREF(v);
+
+ v = PyLong_FromLong(arena->stats_freed_blocks);
+ PyDict_SetItemString(d, "freed_blocks", v ? v : Py_None);
+ Py_XDECREF(v);
+
+ v = PyLong_FromLong(arena->blocks_cached);
+ PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None);
+ Py_XDECREF(v);
return d;
}
@@ -3988,8 +4099,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);
@@ -4073,7 +4182,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
@@ -4150,28 +4258,33 @@ setup_module(PyObject *m) {
#ifdef HAVE_LIBJPEG
{
extern const char *ImagingJpegVersion(void);
- PyDict_SetItemString(
- d, "jpeglib_version", PyUnicode_FromString(ImagingJpegVersion()));
+ PyObject *v = PyUnicode_FromString(ImagingJpegVersion());
+ PyDict_SetItemString(d, "jpeglib_version", v ? v : Py_None);
+ Py_XDECREF(v);
}
#endif
#ifdef HAVE_OPENJPEG
{
extern const char *ImagingJpeg2KVersion(void);
- PyDict_SetItemString(
- d, "jp2klib_version", PyUnicode_FromString(ImagingJpeg2KVersion()));
+ PyObject *v = PyUnicode_FromString(ImagingJpeg2KVersion());
+ PyDict_SetItemString(d, "jp2klib_version", v ? v : Py_None);
+ Py_XDECREF(v);
}
#endif
PyObject *have_libjpegturbo;
#ifdef LIBJPEG_TURBO_VERSION
have_libjpegturbo = Py_True;
+ {
#define tostr1(a) #a
#define tostr(a) tostr1(a)
- PyDict_SetItemString(
- d, "libjpeg_turbo_version", PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)));
+ PyObject *v = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION));
+ PyDict_SetItemString(d, "libjpeg_turbo_version", v ? v : Py_None);
+ Py_XDECREF(v);
#undef tostr
#undef tostr1
+ }
#else
have_libjpegturbo = Py_False;
#endif
@@ -4183,8 +4296,9 @@ setup_module(PyObject *m) {
have_libimagequant = Py_True;
{
extern const char *ImagingImageQuantVersion(void);
- PyDict_SetItemString(
- d, "imagequant_version", PyUnicode_FromString(ImagingImageQuantVersion()));
+ PyObject *v = PyUnicode_FromString(ImagingImageQuantVersion());
+ PyDict_SetItemString(d, "imagequant_version", v ? v : Py_None);
+ Py_XDECREF(v);
}
#else
have_libimagequant = Py_False;
@@ -4201,16 +4315,18 @@ setup_module(PyObject *m) {
PyModule_AddIntConstant(m, "FIXED", Z_FIXED);
{
extern const char *ImagingZipVersion(void);
- PyDict_SetItemString(
- d, "zlib_version", PyUnicode_FromString(ImagingZipVersion()));
+ PyObject *v = PyUnicode_FromString(ImagingZipVersion());
+ PyDict_SetItemString(d, "zlib_version", v ? v : Py_None);
+ Py_XDECREF(v);
}
#endif
#ifdef HAVE_LIBTIFF
{
extern const char *ImagingTiffVersion(void);
- PyDict_SetItemString(
- d, "libtiff_version", PyUnicode_FromString(ImagingTiffVersion()));
+ PyObject *v = PyUnicode_FromString(ImagingTiffVersion());
+ PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None);
+ Py_XDECREF(v);
// Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7
PyObject *support_custom_tags;
@@ -4233,7 +4349,9 @@ setup_module(PyObject *m) {
Py_INCREF(have_xcb);
PyModule_AddObject(m, "HAVE_XCB", have_xcb);
- PyDict_SetItemString(d, "PILLOW_VERSION", PyUnicode_FromString(version));
+ PyObject *pillow_version = PyUnicode_FromString(version);
+ PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None);
+ Py_XDECREF(pillow_version);
return 0;
}
@@ -4253,6 +4371,7 @@ PyInit__imaging(void) {
m = PyModule_Create(&module_def);
if (setup_module(m) < 0) {
+ Py_DECREF(m);
return NULL;
}
diff --git a/src/_imagingcms.c b/src/_imagingcms.c
index 9b5a121d7d3..ddfe6ad6486 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 7cd6dfb1da7..62819a569bc 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -33,12 +33,6 @@
#include FT_COLOR_H
#endif
-#define KEEP_PY_UNICODE
-
-#if !defined(FT_LOAD_TARGET_MONO)
-#define FT_LOAD_TARGET_MONO FT_LOAD_MONOCHROME
-#endif
-
/* -------------------------------------------------------------------- */
/* error table */
@@ -122,7 +116,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
int error = 0;
char *filename = NULL;
- Py_ssize_t size;
+ float size;
+ FT_Size_RequestRec req;
+ FT_Long width;
Py_ssize_t index = 0;
Py_ssize_t layout_engine = 0;
unsigned char *encoding;
@@ -136,10 +132,31 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
return NULL;
}
+#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11
+ PyConfig config;
+ PyConfig_InitPythonConfig(&config);
if (!PyArg_ParseTupleAndKeywords(
args,
kw,
- "etn|nsy#n",
+ "etf|nsy#n",
+ kwlist,
+ config.filesystem_encoding,
+ &filename,
+ &size,
+ &index,
+ &encoding,
+ &font_bytes,
+ &font_bytes_size,
+ &layout_engine)) {
+ PyConfig_Clear(&config);
+ return NULL;
+ }
+ PyConfig_Clear(&config);
+#else
+ if (!PyArg_ParseTupleAndKeywords(
+ args,
+ kw,
+ "etf|nsy#n",
kwlist,
Py_FileSystemDefaultEncoding,
&filename,
@@ -151,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
&layout_engine)) {
return NULL;
}
+#endif
self = PyObject_New(FontObject, &Font_Type);
if (!self) {
@@ -171,7 +189,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
/* Don't free this before FT_Done_Face */
self->font_bytes = PyMem_Malloc(font_bytes_size);
if (!self->font_bytes) {
- error = 65; // Out of Memory in Freetype.
+ error = FT_Err_Out_Of_Memory;
}
if (!error) {
memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size);
@@ -185,7 +203,13 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
}
if (!error) {
- error = FT_Set_Pixel_Sizes(self->face, 0, size);
+ width = size * 64;
+ req.type = FT_SIZE_REQUEST_TYPE_NOMINAL;
+ req.width = width;
+ req.height = width;
+ req.horiResolution = 0;
+ req.vertResolution = 0;
+ error = FT_Request_Size(self->face, &req);
}
if (!error && encoding && strlen((char *)encoding) == 4) {
@@ -230,9 +254,7 @@ text_layout_raqm(
const char *dir,
PyObject *features,
const char *lang,
- GlyphInfo **glyph_info,
- int mask,
- int color) {
+ GlyphInfo **glyph_info) {
size_t i = 0, count = 0, start = 0;
raqm_t *rq;
raqm_glyph_t *glyphs = NULL;
@@ -420,11 +442,9 @@ text_layout_fallback(
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
-#ifdef FT_LOAD_COLOR
if (color) {
load_flags |= FT_LOAD_COLOR;
}
-#endif
for (i = 0; font_getchar(string, i, &ch); i++) {
(*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch);
error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags);
@@ -471,7 +491,7 @@ text_layout(
#ifdef HAVE_RAQM
if (have_raqm && self->layout_engine == LAYOUT_RAQM) {
count = text_layout_raqm(
- string, self, dir, features, lang, glyph_info, mask, color);
+ string, self, dir, features, lang, glyph_info);
} else
#endif
{
@@ -529,75 +549,25 @@ font_getlength(FontObject *self, PyObject *args) {
return PyLong_FromLong(length);
}
-static PyObject *
-font_getsize(FontObject *self, PyObject *args) {
+static int
+bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) {
int position; /* pen position along primary axis, in 26.6 precision */
int advanced; /* pen position along primary axis, in pixels */
int px, py; /* position of current glyph, in pixels */
int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */
int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */
- int load_flags; /* FreeType load_flags parameter */
int error;
- FT_Face face;
FT_Glyph glyph;
- FT_BBox bbox; /* glyph bounding box */
- GlyphInfo *glyph_info = NULL; /* computed text layout */
- size_t i, count; /* glyph_info index and length */
- int horizontal_dir; /* is primary axis horizontal? */
- int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
- int color = 0; /* is FT_LOAD_COLOR enabled? */
- const char *mode = NULL;
- const char *dir = NULL;
- const char *lang = NULL;
- const char *anchor = NULL;
- PyObject *features = Py_None;
- PyObject *string;
-
- /* calculate size and bearing for a given string */
-
- if (!PyArg_ParseTuple(
- args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
- return NULL;
- }
-
- horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
-
- mask = mode && strcmp(mode, "1") == 0;
- color = mode && strcmp(mode, "RGBA") == 0;
-
- if (anchor == NULL) {
- anchor = horizontal_dir ? "la" : "lt";
- }
- if (strlen(anchor) != 2) {
- goto bad_anchor;
- }
-
- count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
- if (PyErr_Occurred()) {
- return NULL;
- }
-
- load_flags = FT_LOAD_DEFAULT;
- if (mask) {
- load_flags |= FT_LOAD_TARGET_MONO;
- }
-#ifdef FT_LOAD_COLOR
- if (color) {
- load_flags |= FT_LOAD_COLOR;
- }
-#endif
-
+ FT_BBox bbox; /* glyph bounding box */
+ size_t i; /* glyph_info index */
/*
* text bounds are given by:
* - bounding boxes of individual glyphs
* - pen line, i.e. 0 to `advanced` along primary axis
* this means point (0, 0) is part of the text bounding box
*/
- face = NULL;
position = x_min = x_max = y_min = y_max = 0;
for (i = 0; i < count; i++) {
- face = self->face;
-
if (horizontal_dir) {
px = PIXEL(position + glyph_info[i].x_offset);
py = PIXEL(glyph_info[i].y_offset);
@@ -620,12 +590,14 @@ font_getsize(FontObject *self, PyObject *args) {
error = FT_Load_Glyph(face, glyph_info[i].index, load_flags);
if (error) {
- return geterror(error);
+ geterror(error);
+ return 1;
}
error = FT_Get_Glyph(face->glyph, &glyph);
if (error) {
- return geterror(error);
+ geterror(error);
+ return 1;
}
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox);
@@ -649,13 +621,15 @@ font_getsize(FontObject *self, PyObject *args) {
FT_Done_Glyph(glyph);
}
- if (glyph_info) {
- PyMem_Free(glyph_info);
- glyph_info = NULL;
+ if (anchor == NULL) {
+ anchor = horizontal_dir ? "la" : "lt";
+ }
+ if (strlen(anchor) != 2) {
+ goto bad_anchor;
}
x_anchor = y_anchor = 0;
- if (face) {
+ if (count) {
if (horizontal_dir) {
switch (anchor[0]) {
case 'l': // left
@@ -673,15 +647,15 @@ font_getsize(FontObject *self, PyObject *args) {
}
switch (anchor[1]) {
case 'a': // ascender
- y_anchor = PIXEL(self->face->size->metrics.ascender);
+ y_anchor = PIXEL(face->size->metrics.ascender);
break;
case 't': // top
y_anchor = y_max;
break;
case 'm': // middle (ascender + descender) / 2
y_anchor = PIXEL(
- (self->face->size->metrics.ascender +
- self->face->size->metrics.descender) /
+ (face->size->metrics.ascender +
+ face->size->metrics.descender) /
2);
break;
case 's': // horizontal baseline
@@ -691,7 +665,7 @@ font_getsize(FontObject *self, PyObject *args) {
y_anchor = y_min;
break;
case 'd': // descender
- y_anchor = PIXEL(self->face->size->metrics.descender);
+ y_anchor = PIXEL(face->size->metrics.descender);
break;
default:
goto bad_anchor;
@@ -731,17 +705,74 @@ font_getsize(FontObject *self, PyObject *args) {
}
}
}
-
- return Py_BuildValue(
- "(ii)(ii)",
- (x_max - x_min),
- (y_max - y_min),
- (-x_anchor + x_min),
- -(-y_anchor + y_max));
+ *width = x_max - x_min;
+ *height = y_max - y_min;
+ *x_offset = -x_anchor + x_min;
+ *y_offset = -(-y_anchor + y_max);
+ return 0;
bad_anchor:
PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor);
- return NULL;
+ return 1;
+}
+
+static PyObject *
+font_getsize(FontObject *self, PyObject *args) {
+ int width, height, x_offset, y_offset;
+ int load_flags; /* FreeType load_flags parameter */
+ int error;
+ GlyphInfo *glyph_info = NULL; /* computed text layout */
+ size_t count; /* glyph_info length */
+ int horizontal_dir; /* is primary axis horizontal? */
+ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
+ int color = 0; /* is FT_LOAD_COLOR enabled? */
+ const char *mode = NULL;
+ const char *dir = NULL;
+ const char *lang = NULL;
+ const char *anchor = NULL;
+ PyObject *features = Py_None;
+ PyObject *string;
+
+ /* calculate size and bearing for a given string */
+
+ if (!PyArg_ParseTuple(
+ args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
+ return NULL;
+ }
+
+ horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
+
+ mask = mode && strcmp(mode, "1") == 0;
+ color = mode && strcmp(mode, "RGBA") == 0;
+
+ count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
+ if (PyErr_Occurred()) {
+ return NULL;
+ }
+
+ load_flags = FT_LOAD_DEFAULT;
+ if (mask) {
+ load_flags |= FT_LOAD_TARGET_MONO;
+ }
+ if (color) {
+ load_flags |= FT_LOAD_COLOR;
+ }
+
+ error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset);
+ if (glyph_info) {
+ PyMem_Free(glyph_info);
+ glyph_info = NULL;
+ }
+ if (error) {
+ return NULL;
+ }
+
+ return Py_BuildValue(
+ "(ii)(ii)",
+ width,
+ height,
+ x_offset,
+ y_offset);
}
static PyObject *
@@ -765,6 +796,7 @@ font_render(FontObject *self, PyObject *args) {
unsigned int bitmap_y; /* glyph bitmap y index */
unsigned char *source; /* glyph bitmap source buffer */
unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */
+ PyObject *image;
Imaging im;
Py_ssize_t id;
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
@@ -775,23 +807,34 @@ font_render(FontObject *self, PyObject *args) {
const char *mode = NULL;
const char *dir = NULL;
const char *lang = NULL;
+ const char *anchor = NULL;
PyObject *features = Py_None;
PyObject *string;
+ PyObject *fill;
+ float x_start = 0;
+ float y_start = 0;
+ int width, height, x_offset, y_offset;
+ int horizontal_dir; /* is primary axis horizontal? */
+ PyObject *max_image_pixels = Py_None;
/* render string into given buffer (the buffer *must* have
the right size, or this will crash) */
if (!PyArg_ParseTuple(
args,
- "On|zzOziL:render",
+ "OO|zzOzizLffO:render",
&string,
- &id,
+ &fill,
&mode,
&dir,
&features,
&lang,
&stroke_width,
- &foreground_ink_long)) {
+ &anchor,
+ &foreground_ink_long,
+ &x_start,
+ &y_start,
+ &max_image_pixels)) {
return NULL;
}
@@ -817,14 +860,52 @@ font_render(FontObject *self, PyObject *args) {
if (PyErr_Occurred()) {
return NULL;
}
- if (count == 0) {
- Py_RETURN_NONE;
+
+ load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT;
+ if (mask) {
+ load_flags |= FT_LOAD_TARGET_MONO;
+ }
+ if (color) {
+ load_flags |= FT_LOAD_COLOR;
+ }
+
+ horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
+
+ error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset);
+ if (error) {
+ PyMem_Del(glyph_info);
+ return NULL;
+ }
+
+ width += stroke_width * 2 + ceil(x_start);
+ height += stroke_width * 2 + ceil(y_start);
+ if (max_image_pixels != Py_None) {
+ if ((long long)(width > 1 ? width : 1) * (height > 1 ? height : 1) > PyLong_AsLongLong(max_image_pixels) * 2) {
+ PyMem_Del(glyph_info);
+ return Py_BuildValue("(ii)(ii)", width, height, 0, 0);
+ }
+ }
+
+ image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height);
+ if (image == NULL) {
+ PyMem_Del(glyph_info);
+ return NULL;
+ }
+ id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id"));
+ im = (Imaging)id;
+
+ x_offset -= stroke_width;
+ y_offset -= stroke_width;
+ if (count == 0 || width == 0 || height == 0) {
+ PyMem_Del(glyph_info);
+ return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset);
}
if (stroke_width) {
error = FT_Stroker_New(library, &stroker);
if (error) {
- return geterror(error);
+ geterror(error);
+ goto glyph_error;
}
FT_Stroker_Set(
@@ -835,17 +916,6 @@ font_render(FontObject *self, PyObject *args) {
0);
}
- im = (Imaging)id;
- load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT;
- if (mask) {
- load_flags |= FT_LOAD_TARGET_MONO;
- }
-#ifdef FT_LOAD_COLOR
- if (color) {
- load_flags |= FT_LOAD_COLOR;
- }
-#endif
-
/*
* calculate x_min and y_max
* must match font_getsize or there may be clipping!
@@ -858,7 +928,8 @@ font_render(FontObject *self, PyObject *args) {
error =
FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER);
if (error) {
- return geterror(error);
+ geterror(error);
+ goto glyph_error;
}
glyph_slot = self->face->glyph;
@@ -876,8 +947,8 @@ font_render(FontObject *self, PyObject *args) {
}
/* set pen position to text origin */
- x = (-x_min + stroke_width) << 6;
- y = (-y_max + (-stroke_width)) << 6;
+ x = (-x_min + stroke_width + x_start) * 64;
+ y = (-y_max + (-stroke_width) - y_start) * 64;
if (stroker == NULL) {
load_flags |= FT_LOAD_RENDER;
@@ -889,7 +960,8 @@ font_render(FontObject *self, PyObject *args) {
error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags);
if (error) {
- return geterror(error);
+ geterror(error);
+ goto glyph_error;
}
glyph_slot = self->face->glyph;
@@ -903,7 +975,8 @@ font_render(FontObject *self, PyObject *args) {
error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1);
}
if (error) {
- return geterror(error);
+ geterror(error);
+ goto glyph_error;
}
bitmap_glyph = (FT_BitmapGlyph)glyph;
@@ -917,6 +990,12 @@ font_render(FontObject *self, PyObject *args) {
yy = -(py + glyph_slot->bitmap_top);
}
+ // Null buffer, is dereferenced in FT_Bitmap_Convert
+ if (!bitmap.buffer && bitmap.rows) {
+ PyErr_SetString(PyExc_OSError, "Bitmap missing for glyph");
+ goto glyph_error;
+ }
+
/* convert non-8bpp bitmaps */
switch (bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO:
@@ -948,15 +1027,13 @@ 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_IOError, "unsupported bitmap pixel mode");
+ PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
@@ -985,7 +1062,6 @@ font_render(FontObject *self, PyObject *args) {
} else {
target = im->image8[yy] + xx;
}
-#ifdef FT_LOAD_COLOR
if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
/* paste color glyph */
for (k = x0; k < x1; k++) {
@@ -1000,9 +1076,7 @@ font_render(FontObject *self, PyObject *args) {
target[k * 4 + 3] = source[k * 4 + 3];
}
}
- } else
-#endif
- if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
+ } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
if (color) {
unsigned char *ink = (unsigned char *)&foreground_ink;
for (k = x0; k < x1; k++) {
@@ -1023,7 +1097,7 @@ font_render(FontObject *self, PyObject *args) {
}
}
} else {
- PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode");
+ PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
}
@@ -1039,11 +1113,18 @@ font_render(FontObject *self, PyObject *args) {
if (bitmap_converted_ready) {
FT_Bitmap_Done(library, &bitmap_converted);
}
+ Py_DECREF(image);
FT_Stroker_Done(stroker);
PyMem_Del(glyph_info);
- Py_RETURN_NONE;
+ return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset);
glyph_error:
+ if (im->destroy) {
+ im->destroy(im);
+ }
+ if (im->image) {
+ free(im->image);
+ }
if (stroker != NULL) {
FT_Done_Glyph(glyph);
}
@@ -1072,11 +1153,17 @@ font_getvarnames(FontObject *self) {
num_namedstyles = master->num_namedstyles;
list_names = PyList_New(num_namedstyles);
+ if (list_names == NULL) {
+ FT_Done_MM_Var(library, master);
+ return NULL;
+ }
name_count = FT_Get_Sfnt_Name_Count(self->face);
for (i = 0; i < name_count; i++) {
error = FT_Get_Sfnt_Name(self->face, i, &name);
if (error) {
+ Py_DECREF(list_names);
+ FT_Done_MM_Var(library, master);
return geterror(error);
}
@@ -1115,25 +1202,44 @@ font_getvaraxes(FontObject *self) {
name_count = FT_Get_Sfnt_Name_Count(self->face);
list_axes = PyList_New(num_axis);
+ if (list_axes == NULL) {
+ FT_Done_MM_Var(library, master);
+ return NULL;
+ }
for (i = 0; i < num_axis; i++) {
axis = master->axis[i];
list_axis = PyDict_New();
- PyDict_SetItemString(
- list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536));
- PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536));
- PyDict_SetItemString(
- list_axis, "maximum", PyLong_FromLong(axis.maximum / 65536));
+ if (list_axis == NULL) {
+ Py_DECREF(list_axes);
+ FT_Done_MM_Var(library, master);
+ return NULL;
+ }
+ PyObject *minimum = PyLong_FromLong(axis.minimum / 65536);
+ PyDict_SetItemString(list_axis, "minimum", minimum ? minimum : Py_None);
+ Py_XDECREF(minimum);
+
+ PyObject *def = PyLong_FromLong(axis.def / 65536);
+ PyDict_SetItemString(list_axis, "default", def ? def : Py_None);
+ Py_XDECREF(def);
+
+ PyObject *maximum = PyLong_FromLong(axis.maximum / 65536);
+ PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None);
+ Py_XDECREF(maximum);
for (j = 0; j < name_count; j++) {
error = FT_Get_Sfnt_Name(self->face, j, &name);
if (error) {
+ Py_DECREF(list_axis);
+ Py_DECREF(list_axes);
+ FT_Done_MM_Var(library, master);
return geterror(error);
}
if (name.name_id == axis.strid) {
axis_name = Py_BuildValue("y#", name.string, name.string_len);
- PyDict_SetItemString(list_axis, "name", axis_name);
+ PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None);
+ Py_XDECREF(axis_name);
break;
}
}
@@ -1348,7 +1454,8 @@ setup_module(PyObject *m) {
FT_Library_Version(library, &major, &minor, &patch);
v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch);
- PyDict_SetItemString(d, "freetype2_version", v);
+ PyDict_SetItemString(d, "freetype2_version", v ? v : Py_None);
+ Py_XDECREF(v);
#ifdef HAVE_RAQM
#if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM)
@@ -1366,35 +1473,34 @@ setup_module(PyObject *m) {
PyDict_SetItemString(d, "HAVE_RAQM", v);
PyDict_SetItemString(d, "HAVE_FRIBIDI", v);
PyDict_SetItemString(d, "HAVE_HARFBUZZ", v);
+ Py_DECREF(v);
if (have_raqm) {
+ v = NULL;
#ifdef RAQM_VERSION_MAJOR
v = PyUnicode_FromString(raqm_version_string());
-#else
- v = Py_None;
#endif
- PyDict_SetItemString(d, "raqm_version", v);
+ PyDict_SetItemString(d, "raqm_version", v ? v : Py_None);
+ Py_XDECREF(v);
+ v = NULL;
#ifdef FRIBIDI_MAJOR_VERSION
{
const char *a = strchr(fribidi_version_info, ')');
const char *b = strchr(fribidi_version_info, '\n');
if (a && b && a + 2 < b) {
v = PyUnicode_FromStringAndSize(a + 2, b - (a + 2));
- } else {
- v = Py_None;
}
}
-#else
- v = Py_None;
#endif
- PyDict_SetItemString(d, "fribidi_version", v);
+ PyDict_SetItemString(d, "fribidi_version", v ? v : Py_None);
+ Py_XDECREF(v);
+ v = NULL;
#ifdef HB_VERSION_STRING
v = PyUnicode_FromString(hb_version_string());
-#else
- v = Py_None;
#endif
- PyDict_SetItemString(d, "harfbuzz_version", v);
+ PyDict_SetItemString(d, "harfbuzz_version", v ? v : Py_None);
+ Py_XDECREF(v);
}
return 0;
diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c
index c0644b61609..8815c2b7ec6 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 b9273b0b882..efa7fc1b6fa 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 fd99116cb41..fe63027fbd9 100644
--- a/src/_webp.c
+++ b/src/_webp.c
@@ -178,12 +178,11 @@ _anim_encoder_new(PyObject *self, PyObject *args) {
return NULL;
}
-PyObject *
+void
_anim_encoder_dealloc(PyObject *self) {
WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self;
WebPPictureFree(&(encp->frame));
WebPAnimEncoderDelete(encp->enc);
- Py_RETURN_NONE;
}
PyObject *
@@ -400,12 +399,11 @@ _anim_decoder_new(PyObject *self, PyObject *args) {
return NULL;
}
-PyObject *
+void
_anim_decoder_dealloc(PyObject *self) {
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
WebPDataClear(&(decp->data));
WebPAnimDecoderDelete(decp->dec);
- Py_RETURN_NONE;
}
PyObject *
@@ -576,6 +574,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
int lossless;
float quality_factor;
int method;
+ int exact;
uint8_t *rgb;
uint8_t *icc_bytes;
uint8_t *exif_bytes;
@@ -597,7 +596,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "y#iiifss#is#s#",
+ "y#iiifss#iis#s#",
(char **)&rgb,
&size,
&width,
@@ -608,6 +607,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
&icc_bytes,
&icc_size,
&method,
+ &exact,
&exif_bytes,
&exif_size,
&xmp_bytes,
@@ -633,6 +633,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
config.lossless = lossless;
config.quality = quality_factor;
config.method = method;
+#if WEBP_ENCODER_ABI_VERSION >= 0x0209
+ // the "exact" flag is only available in libwebp 0.5.0 and later
+ config.exact = exact;
+#endif
// Validate the config
if (!WebPValidateConfig(&config)) {
@@ -945,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 ||
@@ -966,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;
}
@@ -983,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 7a9b956c559..7e3fadc040f 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 0ce10e2493c..754a6ae78d3 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 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
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 */
@@ -509,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL};
if (!OpenClipboard(NULL)) {
- PyErr_SetString(PyExc_OSError, "failed to open clipboard");
- return NULL;
+ // Maybe the clipboard is temporarily in use by another process.
+ // Wait and try again
+ Sleep(500);
+
+ if (!OpenClipboard(NULL)) {
+ PyErr_SetString(PyExc_OSError, "failed to open clipboard");
+ return NULL;
+ }
}
// find best format as set by clipboard owner
diff --git a/src/encode.c b/src/encode.c
index 72c7f64d0a3..308bd2059cc 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));
@@ -1048,6 +1048,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
PyObject *qtables = NULL;
unsigned int *qarrays = NULL;
int qtablesLen = 0;
+ char *comment = NULL;
+ Py_ssize_t comment_size;
char *extra = NULL;
Py_ssize_t extra_size;
char *rawExif = NULL;
@@ -1055,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "ss|nnnnnnnnOy#y#",
+ "ss|nnnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
@@ -1067,6 +1069,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
&ydpi,
&subsampling,
&qtables,
+ &comment,
+ &comment_size,
&extra,
&extra_size,
&rawExif,
@@ -1090,13 +1094,28 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
return NULL;
}
- // Freed in JpegEncode, Case 5
+ // Freed in JpegEncode, Case 6
qarrays = get_qtables_arrays(qtables, &qtablesLen);
+ if (comment && comment_size > 0) {
+ /* malloc check ok, length is from python parsearg */
+ char *p = malloc(comment_size); // Freed in JpegEncode, Case 6
+ if (!p) {
+ return ImagingError_MemoryError();
+ }
+ memcpy(p, comment, comment_size);
+ comment = p;
+ } else {
+ comment = NULL;
+ }
+
if (extra && extra_size > 0) {
/* malloc check ok, length is from python parsearg */
- char *p = malloc(extra_size); // Freed in JpegEncode, Case 5
+ char *p = malloc(extra_size); // Freed in JpegEncode, Case 6
if (!p) {
+ if (comment) {
+ free(comment);
+ }
return ImagingError_MemoryError();
}
memcpy(p, extra, extra_size);
@@ -1107,8 +1126,11 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (rawExif && rawExifLen > 0) {
/* malloc check ok, length is from python parsearg */
- char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5
+ char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6
if (!pp) {
+ if (comment) {
+ free(comment);
+ }
if (extra) {
free(extra);
}
@@ -1134,6 +1156,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype;
((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi;
((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi;
+ ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment;
+ ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size;
((JPEGENCODERSTATE *)encoder->state.context)->extra = extra;
((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size;
((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif;
@@ -1188,11 +1212,15 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
char *cinema_mode = "no";
OPJ_CINEMA_MODE cine_mode;
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|OOOsOnOOOssbn",
+ "ss|OOOsOnOOOssbbnz#p",
&mode,
&format,
&offset,
@@ -1207,7 +1235,11 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
&progression,
&cinema_mode,
&mct,
- &fd)) {
+ &sgnd,
+ &fd,
+ &comment,
+ &comment_size,
+ &plt)) {
return NULL;
}
@@ -1289,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;
@@ -1305,6 +1357,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
context->progression = prog_order;
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 514fb292913..f00939da0b3 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];
@@ -43,23 +43,6 @@ add_item(const char *mode) {
return &access_table[i];
}
-/* fetch pointer to pixel line */
-
-static void *
-line_8(Imaging im, int x, int y) {
- return &im->image8[y][x];
-}
-
-static void *
-line_16(Imaging im, int x, int y) {
- return &im->image8[y][x + x];
-}
-
-static void *
-line_32(Imaging im, int x, int y) {
- return &im->image32[y][x];
-}
-
/* fetch individual pixel */
static void
@@ -109,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));
@@ -187,36 +176,36 @@ put_pixel_32(Imaging im, int x, int y, const void *color) {
void
ImagingAccessInit() {
-#define ADD(mode_, line_, get_pixel_, put_pixel_) \
+#define ADD(mode_, get_pixel_, put_pixel_) \
{ \
ImagingAccess access = add_item(mode_); \
- access->line = line_; \
access->get_pixel = get_pixel_; \
access->put_pixel = put_pixel_; \
}
/* populate access table */
- ADD("1", line_8, get_pixel_8, put_pixel_8);
- ADD("L", line_8, get_pixel_8, put_pixel_8);
- ADD("LA", line_32, get_pixel, put_pixel);
- ADD("La", line_32, get_pixel, put_pixel);
- ADD("I", line_32, get_pixel_32, put_pixel_32);
- ADD("I;16", line_16, get_pixel_16L, put_pixel_16L);
- ADD("I;16L", line_16, get_pixel_16L, put_pixel_16L);
- ADD("I;16B", line_16, get_pixel_16B, put_pixel_16B);
- ADD("I;32L", line_32, get_pixel_32L, put_pixel_32L);
- ADD("I;32B", line_32, get_pixel_32B, put_pixel_32B);
- ADD("F", line_32, get_pixel_32, put_pixel_32);
- ADD("P", line_8, get_pixel_8, put_pixel_8);
- ADD("PA", line_32, get_pixel, put_pixel);
- ADD("RGB", line_32, get_pixel_32, put_pixel_32);
- ADD("RGBA", line_32, get_pixel_32, put_pixel_32);
- ADD("RGBa", line_32, get_pixel_32, put_pixel_32);
- ADD("RGBX", line_32, get_pixel_32, put_pixel_32);
- ADD("CMYK", line_32, get_pixel_32, put_pixel_32);
- ADD("YCbCr", line_32, get_pixel_32, put_pixel_32);
- ADD("LAB", line_32, get_pixel_32, put_pixel_32);
- ADD("HSV", line_32, get_pixel_32, put_pixel_32);
+ ADD("1", get_pixel_8, put_pixel_8);
+ ADD("L", get_pixel_8, put_pixel_8);
+ ADD("LA", get_pixel, put_pixel);
+ ADD("La", get_pixel, put_pixel);
+ ADD("I", get_pixel_32, put_pixel_32);
+ ADD("I;16", get_pixel_16L, put_pixel_16L);
+ ADD("I;16L", get_pixel_16L, put_pixel_16L);
+ ADD("I;16B", get_pixel_16B, put_pixel_16B);
+ ADD("I;16N", get_pixel_16, put_pixel_16L);
+ ADD("I;32L", get_pixel_32L, put_pixel_32L);
+ ADD("I;32B", get_pixel_32B, put_pixel_32B);
+ ADD("F", get_pixel_32, put_pixel_32);
+ ADD("P", get_pixel_8, put_pixel_8);
+ ADD("PA", get_pixel, put_pixel);
+ ADD("RGB", get_pixel_32, put_pixel_32);
+ ADD("RGBA", get_pixel_32, put_pixel_32);
+ ADD("RGBa", get_pixel_32, put_pixel_32);
+ ADD("RGBX", get_pixel_32, put_pixel_32);
+ ADD("CMYK", get_pixel_32, put_pixel_32);
+ ADD("YCbCr", get_pixel_32, put_pixel_32);
+ ADD("LAB", get_pixel_32, put_pixel_32);
+ ADD("HSV", get_pixel_32, put_pixel_32);
}
ImagingAccess
diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c
index 2e45a33587c..5afe7cf5043 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 2b45d0cc4a8..7fe24a63939 100644
--- a/src/libImaging/Convert.c
+++ b/src/libImaging/Convert.c
@@ -479,6 +479,25 @@ rgba2rgbA(UINT8 *out, const UINT8 *in, int xsize) {
}
}
+static void
+rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) {
+ int x;
+ unsigned int alpha;
+ for (x = 0; x < xsize; x++, in += 4) {
+ alpha = in[3];
+ if (alpha == 255 || alpha == 0) {
+ *out++ = in[0];
+ *out++ = in[1];
+ *out++ = in[2];
+ } else {
+ *out++ = CLIP8((255 * in[0]) / alpha);
+ *out++ = CLIP8((255 * in[1]) / alpha);
+ *out++ = CLIP8((255 * in[2]) / alpha);
+ }
+ *out++ = 255;
+ }
+}
+
/*
* Conversion of RGB + single transparent color to RGBA,
* where any pixel that matches the color will have the
@@ -934,6 +953,7 @@ static struct {
{"RGBA", "HSV", rgb2hsv},
{"RGBa", "RGBA", rgba2rgbA},
+ {"RGBa", "RGB", rgba2rgb_},
{"RGBX", "1", rgb2bit},
{"RGBX", "L", rgb2l},
@@ -970,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 77343e583d4..82f290bd0cd 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/Fill.c b/src/libImaging/Fill.c
index f7206022843..5b6bfb89cd8 100644
--- a/src/libImaging/Fill.c
+++ b/src/libImaging/Fill.c
@@ -24,6 +24,11 @@ ImagingFill(Imaging im, const void *colour) {
int x, y;
ImagingSectionCookie cookie;
+ /* 0-width or 0-height image. No need to do anything */
+ if (!im->linesize || !im->ysize) {
+ return im;
+ }
+
if (im->type == IMAGING_TYPE_SPECIAL) {
/* use generic API */
ImagingAccess access = ImagingAccessNew(im);
diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c
index fab3b494819..4dcd368ca80 100644
--- a/src/libImaging/Filter.c
+++ b/src/libImaging/Filter.c
@@ -37,8 +37,19 @@ clip8(float in) {
return (UINT8)in;
}
+static inline INT32
+clip32(float in) {
+ if (in <= 0.0) {
+ return 0;
+ }
+ if (in >= pow(2, 31) - 1) {
+ return pow(2, 31) - 1;
+ }
+ return (INT32)in;
+}
+
Imaging
-ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) {
+ImagingExpand(Imaging imIn, int xmargin, int ymargin) {
Imaging imOut;
int x, y;
ImagingSectionCookie cookie;
@@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) {
void
ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
#define KERNEL1x3(in0, x, kernel, d) \
- (_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \
- _i2f((UINT8)in0[x + d]) * (kernel)[2])
+ (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \
+ _i2f(in0[x + d]) * (kernel)[2])
int x = 0, y = 0;
@@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
if (im->bands == 1) {
// Add one time for rounding
offset += 0.5;
- for (y = 1; y < im->ysize - 1; y++) {
- UINT8 *in_1 = (UINT8 *)im->image[y - 1];
- UINT8 *in0 = (UINT8 *)im->image[y];
- UINT8 *in1 = (UINT8 *)im->image[y + 1];
- UINT8 *out = (UINT8 *)imOut->image[y];
+ if (im->type == IMAGING_TYPE_INT32) {
+ for (y = 1; y < im->ysize - 1; y++) {
+ INT32 *in_1 = (INT32 *)im->image[y - 1];
+ INT32 *in0 = (INT32 *)im->image[y];
+ INT32 *in1 = (INT32 *)im->image[y + 1];
+ INT32 *out = (INT32 *)imOut->image[y];
+
+ out[0] = in0[0];
+ for (x = 1; x < im->xsize - 1; x++) {
+ float ss = offset;
+ ss += KERNEL1x3(in1, x, &kernel[0], 1);
+ ss += KERNEL1x3(in0, x, &kernel[3], 1);
+ ss += KERNEL1x3(in_1, x, &kernel[6], 1);
+ out[x] = 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[0] = in0[0];
+ for (x = 1; x < im->xsize - 1; x++) {
+ float ss = offset;
+ ss += KERNEL1x3(in1, x, &kernel[0], 1);
+ ss += KERNEL1x3(in0, x, &kernel[3], 1);
+ ss += KERNEL1x3(in_1, x, &kernel[6], 1);
+ out[x] = clip8(ss);
+ }
+ out[x] = in0[x];
}
- out[x] = in0[x];
}
} else {
// Add one time for rounding
@@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
void
ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
#define KERNEL1x5(in0, x, kernel, d) \
- (_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \
- _i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \
- _i2f((UINT8)in0[x + d]) * (kernel)[3] + \
- _i2f((UINT8)in0[x + d + d]) * (kernel)[4])
+ (_i2f(in0[x - d - d]) * (kernel)[0] + \
+ _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \
+ _i2f(in0[x + d]) * (kernel)[3] + \
+ _i2f(in0[x + d + d]) * (kernel)[4])
int x = 0, y = 0;
@@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
if (im->bands == 1) {
// Add one time for rounding
offset += 0.5;
- for (y = 2; y < im->ysize - 2; y++) {
- UINT8 *in_2 = (UINT8 *)im->image[y - 2];
- UINT8 *in_1 = (UINT8 *)im->image[y - 1];
- UINT8 *in0 = (UINT8 *)im->image[y];
- UINT8 *in1 = (UINT8 *)im->image[y + 1];
- UINT8 *in2 = (UINT8 *)im->image[y + 2];
- UINT8 *out = (UINT8 *)imOut->image[y];
+ if (im->type == IMAGING_TYPE_INT32) {
+ for (y = 2; y < im->ysize - 2; y++) {
+ INT32 *in_2 = (INT32 *)im->image[y - 2];
+ INT32 *in_1 = (INT32 *)im->image[y - 1];
+ INT32 *in0 = (INT32 *)im->image[y];
+ INT32 *in1 = (INT32 *)im->image[y + 1];
+ INT32 *in2 = (INT32 *)im->image[y + 2];
+ INT32 *out = (INT32 *)imOut->image[y];
- out[0] = in0[0];
- out[1] = in0[1];
- for (x = 2; x < im->xsize - 2; x++) {
- float ss = offset;
- ss += KERNEL1x5(in2, x, &kernel[0], 1);
- ss += KERNEL1x5(in1, x, &kernel[5], 1);
- ss += KERNEL1x5(in0, x, &kernel[10], 1);
- ss += KERNEL1x5(in_1, x, &kernel[15], 1);
- ss += KERNEL1x5(in_2, x, &kernel[20], 1);
- out[x] = clip8(ss);
+ out[0] = in0[0];
+ out[1] = in0[1];
+ for (x = 2; x < im->xsize - 2; x++) {
+ float ss = offset;
+ ss += KERNEL1x5(in2, x, &kernel[0], 1);
+ ss += KERNEL1x5(in1, x, &kernel[5], 1);
+ ss += KERNEL1x5(in0, x, &kernel[10], 1);
+ ss += KERNEL1x5(in_1, x, &kernel[15], 1);
+ ss += KERNEL1x5(in_2, x, &kernel[20], 1);
+ out[x] = clip32(ss);
+ }
+ out[x + 0] = in0[x + 0];
+ out[x + 1] = in0[x + 1];
+ }
+ } else {
+ for (y = 2; y < im->ysize - 2; y++) {
+ UINT8 *in_2 = (UINT8 *)im->image[y - 2];
+ UINT8 *in_1 = (UINT8 *)im->image[y - 1];
+ UINT8 *in0 = (UINT8 *)im->image[y];
+ UINT8 *in1 = (UINT8 *)im->image[y + 1];
+ UINT8 *in2 = (UINT8 *)im->image[y + 2];
+ UINT8 *out = (UINT8 *)imOut->image[y];
+
+ out[0] = in0[0];
+ out[1] = in0[1];
+ for (x = 2; x < im->xsize - 2; x++) {
+ float ss = offset;
+ ss += KERNEL1x5(in2, x, &kernel[0], 1);
+ ss += KERNEL1x5(in1, x, &kernel[5], 1);
+ ss += KERNEL1x5(in0, x, &kernel[10], 1);
+ ss += KERNEL1x5(in_1, x, &kernel[15], 1);
+ ss += KERNEL1x5(in_2, x, &kernel[20], 1);
+ out[x] = clip8(ss);
+ }
+ out[x + 0] = in0[x + 0];
+ out[x + 1] = in0[x + 1];
}
- out[x + 0] = in0[x + 0];
- out[x + 1] = in0[x + 1];
}
} else {
// Add one time for rounding
@@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o
Imaging imOut;
ImagingSectionCookie cookie;
- if (!im || im->type != IMAGING_TYPE_UINT8) {
+ if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) {
return (Imaging)ImagingError_ModeError();
}
diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c
index e73153600d0..86c687ca0a8 100644
--- a/src/libImaging/GetBBox.c
+++ b/src/libImaging/GetBBox.c
@@ -19,7 +19,7 @@
#include "Imaging.h"
int
-ImagingGetBBox(Imaging im, int bbox[4]) {
+ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
/* Get the bounding box for any non-zero data in the image.*/
int x, y;
@@ -58,10 +58,11 @@ ImagingGetBBox(Imaging im, int bbox[4]) {
INT32 mask = 0xffffffff;
if (im->bands == 3) {
((UINT8 *)&mask)[3] = 0;
- } else if (
+ } else if (alpha_only && (
strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 ||
strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 ||
- strcmp(im->mode, "PA") == 0) {
+ strcmp(im->mode, "PA") == 0
+ )) {
#ifdef WORDS_BIGENDIAN
mask = 0x000000ff;
#else
diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h
index af9996ca98c..f6e7fb6b921 100644
--- a/src/libImaging/ImPlatform.h
+++ b/src/libImaging/ImPlatform.h
@@ -25,7 +25,7 @@
#endif
#endif
-#if defined(_WIN32) || defined(__CYGWIN__)
+#if defined(_WIN32) || defined(__CYGWIN__) /* WIN */
#define WIN32_LEAN_AND_MEAN
#include
@@ -37,15 +37,33 @@
#undef WIN32
#endif
-#else
+#else /* not WIN */
/* For System that are not Windows, we'll need to define these. */
+/* We have to define them instead of using typedef because the JPEG lib also
+ defines their own types with the same names, so we need to be able to undef
+ ours before including the JPEG code. */
+
+#if __STDC_VERSION__ >= 199901L /* C99+ */
+
+#include
+
+#define INT8 int8_t
+#define UINT8 uint8_t
+#define INT16 int16_t
+#define UINT16 uint16_t
+#define INT32 int32_t
+#define UINT32 uint32_t
+
+#else /* < C99 */
+
+#define INT8 signed char
#if SIZEOF_SHORT == 2
#define INT16 short
#elif SIZEOF_INT == 2
#define INT16 int
#else
-#define INT16 short /* most things works just fine anyway... */
+#error Cannot find required 16-bit integer type
#endif
#if SIZEOF_SHORT == 4
@@ -58,19 +76,13 @@
#error Cannot find required 32-bit integer type
#endif
-#if SIZEOF_LONG == 8
-#define INT64 long
-#elif SIZEOF_LONG_LONG == 8
-#define INT64 long
-#endif
-
-#define INT8 signed char
#define UINT8 unsigned char
-
#define UINT16 unsigned INT16
#define UINT32 unsigned INT32
-#endif
+#endif /* < C99 */
+
+#endif /* not WIN */
/* assume IEEE; tweak if necessary (patches are welcome) */
#define FLOAT16 UINT16
diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h
index b65f8eadd51..01f40ee7b06 100644
--- a/src/libImaging/Imaging.h
+++ b/src/libImaging/Imaging.h
@@ -124,7 +124,6 @@ struct ImagingMemoryInstance {
struct ImagingAccessInstance {
const char *mode;
- void *(*line)(Imaging im, int x, int y);
void (*get_pixel)(Imaging im, int x, int y, void *pixel);
void (*put_pixel)(Imaging im, int x, int y, const void *pixel);
};
@@ -291,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b);
extern Imaging
ImagingCrop(Imaging im, int x0, int y0, int x1, int y1);
extern Imaging
-ImagingExpand(Imaging im, int x, int y, int mode);
+ImagingExpand(Imaging im, int x, int y);
extern Imaging
ImagingFill(Imaging im, const void *ink);
extern int
@@ -318,7 +317,7 @@ ImagingMerge(const char *mode, Imaging bands[4]);
extern int
ImagingSplit(Imaging im, Imaging bands[4]);
extern int
-ImagingGetBBox(Imaging im, int bbox[4]);
+ImagingGetBBox(Imaging im, int bbox[4], int alpha_only);
typedef struct {
int x, y;
INT32 count;
diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h
index a876d3bb6d9..1d755081871 100644
--- a/src/libImaging/Jpeg.h
+++ b/src/libImaging/Jpeg.h
@@ -92,6 +92,10 @@ typedef struct {
/* in factors of DCTSIZE2 */
int qtablesLen;
+ /* Comment */
+ char *comment;
+ size_t comment_size;
+
/* Extra data (to be injected after header) */
char *extra;
int extra_size;
diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h
index d030b0c439e..e8d92f7b6bc 100644
--- a/src/libImaging/Jpeg2K.h
+++ b/src/libImaging/Jpeg2K.h
@@ -85,6 +85,9 @@ typedef struct {
/* Set multiple component transformation */
char mct;
+ /* Signed */
+ int sgnd;
+
/* Progression order (LRCP/RLCP/RPCL/PCRL/CPRL) */
OPJ_PROG_ORDER progression;
@@ -94,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 fe5511ba5cb..de8586706e2 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,8 +340,7 @@ 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 = 0;
+ image_params[n].sgnd = context->sgnd == 0 ? 0 : 1;
}
image = opj_image_create(components, image_params, color_space);
@@ -439,6 +436,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
params.tcp_mct = context->mct;
}
+ if (context->comment) {
+ params.cp_comment = context->comment;
+ }
+
params.prog_order = context->progression;
params.cp_cinema = context->cinema_mode;
@@ -463,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
}
if (!context->num_resolutions) {
- while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) {
+ while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) {
params.numresolution -= 1;
}
}
@@ -487,11 +488,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/JpegEncode.c b/src/libImaging/JpegEncode.c
index a44debcafe8..2a24eff39ca 100644
--- a/src/libImaging/JpegEncode.c
+++ b/src/libImaging/JpegEncode.c
@@ -277,6 +277,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
case 4:
+
+ if (context->comment) {
+ jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size);
+ }
+ state->state++;
+
+ case 5:
if (1024 > context->destination.pub.free_in_buffer) {
break;
}
@@ -301,7 +308,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
state->state++;
/* fall through */
- case 5:
+ case 6:
/* Finish compression */
if (context->destination.pub.free_in_buffer < 100) {
@@ -310,6 +317,10 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
jpeg_finish_compress(&context->cinfo);
/* Clean up */
+ if (context->comment) {
+ free(context->comment);
+ context->comment = NULL;
+ }
if (context->extra) {
free(context->extra);
context->extra = NULL;
diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c
index 01760e742be..14c8f1461aa 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 dfa6d842d3f..02a4a5c7668 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;
}
@@ -1717,7 +1729,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
withAlpha = !strcmp(im->mode, "RGBA");
int transparency = 0;
- unsigned char r, g, b;
+ unsigned char r = 0, g = 0, b = 0;
for (i = y = 0; y < im->ysize; y++) {
for (x = 0; x < im->xsize; x++, i++) {
p[i].v = im->image32[y][x];
diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c
index 76750aaf7f2..128595f6547 100644
--- a/src/libImaging/Storage.c
+++ b/src/libImaging/Storage.c
@@ -37,8 +37,6 @@
#include "Imaging.h"
#include
-int ImagingNewCount = 0;
-
/* --------------------------------------------------------------------
* Standard image object.
*/
@@ -131,7 +129,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
} else if (strcmp(mode, "BGR;15") == 0) {
/* EXPERIMENTAL */
/* 15-bit reversed true colour */
- im->bands = 1;
+ im->bands = 3;
im->pixelsize = 2;
im->linesize = (xsize * 2 + 3) & -4;
im->type = IMAGING_TYPE_SPECIAL;
@@ -139,7 +137,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
} else if (strcmp(mode, "BGR;16") == 0) {
/* EXPERIMENTAL */
/* 16-bit reversed true colour */
- im->bands = 1;
+ im->bands = 3;
im->pixelsize = 2;
im->linesize = (xsize * 2 + 3) & -4;
im->type = IMAGING_TYPE_SPECIAL;
@@ -147,19 +145,11 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
} else if (strcmp(mode, "BGR;24") == 0) {
/* EXPERIMENTAL */
/* 24-bit reversed true colour */
- im->bands = 1;
+ im->bands = 3;
im->pixelsize = 3;
im->linesize = (xsize * 3 + 3) & -4;
im->type = IMAGING_TYPE_SPECIAL;
- } else if (strcmp(mode, "BGR;32") == 0) {
- /* EXPERIMENTAL */
- /* 32-bit reversed true colour */
- im->bands = 1;
- im->pixelsize = 4;
- im->linesize = (xsize * 4 + 3) & -4;
- im->type = IMAGING_TYPE_SPECIAL;
-
} else if (strcmp(mode, "RGBX") == 0) {
/* 32-bit true colour images with padding */
im->bands = im->pixelsize = 4;
diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c
index 428cd93d278..35122f18245 100644
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -720,7 +720,16 @@ ImagingLibTiffDecode(
}
decode_err:
- TIFFClose(tiff);
+ // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup
+ if (clientstate->fp) {
+ // Pillow will manage the closing of the file rather than libtiff
+ // So only call TIFFCleanup
+ TIFFCleanup(tiff);
+ } else {
+ // When tif_closeproc refers to our custom _tiffCloseProc though,
+ // that is fine, as it does not close the file
+ TIFFClose(tiff);
+ }
TRACE(("Done Decoding, Returning \n"));
// Returning -1 here to force ImageFile.load to break, rather than
// even think about looping back around.
diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c
index e426ed74fce..206403ba6e0 100644
--- a/src/libImaging/Unpack.c
+++ b/src/libImaging/Unpack.c
@@ -1149,6 +1149,16 @@ unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) {
}
}
static void
+unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) {
+ int i;
+ for (i = 0; i < pixels; i++) {
+ out[0] = in[1];
+ out[1] = in[0];
+ in += 2;
+ out += 2;
+ }
+}
+static void
unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) {
int i;
for (i = 0; i < pixels; i++) {
@@ -1542,10 +1552,12 @@ static struct {
{"P", "P;4L", 4, unpackP4L},
{"P", "P", 8, copy1},
{"P", "P;R", 8, unpackLR},
+ {"P", "L", 8, copy1},
/* palette w. alpha */
{"PA", "PA", 16, unpackLA},
{"PA", "PA;L", 16, unpackLAL},
+ {"PA", "LA", 16, unpackLA},
/* true colour */
{"RGB", "RGB", 24, ImagingUnpackRGB},
@@ -1762,7 +1774,9 @@ static struct {
{"I;16", "I;16", 16, copy2},
{"I;16B", "I;16B", 16, copy2},
{"I;16L", "I;16L", 16, copy2},
+ {"I;16N", "I;16N", 16, copy2},
+ {"I;16", "I;16B", 16, unpackI16B_I16},
{"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian.
{"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian.
{"I;16B", "I;16N", 16, unpackI16N_I16B},
diff --git a/src/path.c b/src/path.c
index 3e3431575ec..e17580fa227 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 c605a5dc67a..97e2489b779 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 ae1128485f2..e8bf32e0bbb 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 3354a4d2550..ab729cdc036 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 78b70a5615e..62d2d206459 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 13f6e1f023c..2b331e1afb0 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,13 +1077,159 @@ raqm_set_freetype_load_flags_range (raqm_t *rq,
if (!rq->text_len)
return true;
- if (rq->text_utf8)
+ 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++)
{
- start = _raqm_u8_to_u32_index (rq, start);
- end = _raqm_u8_to_u32_index (rq, end);
+ 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 _raqm_set_freetype_load_flags (rq, flags, start, end);
+ 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);
}
/**
@@ -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 @@ _raqm_itemize (raqm_t *rq)
}
/* 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 @@ raqm_index_to_position (raqm_t *rq,
* @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 bdb5a50d884..6fd6089c70d 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 21b5d4b506d..a79089f5177 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,32 +1,31 @@
-# Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests
-# in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it,
-# "python3 -m pip install tox" and then run "tox" from this directory.
-
[tox]
-envlist =
+requires =
+ tox>=4.2
+env_list =
lint
- py{37,38,39,310,311,py3}
-minversion = 1.9
+ py{py3, 311, 310, 39, 38}
[testenv]
+deps =
+ cffi
+ numpy
extras =
tests
commands =
make clean
- {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" .
+ {envpython} -m pip install .
{envpython} selftest.py
{envpython} -m pytest -W always {posargs}
-deps =
- cffi
- numpy
+allowlist_externals =
+ make
[testenv:lint]
-commands =
- pre-commit run --all-files --show-diff-on-failure
- check-manifest
+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
-skip_install = true
-passenv = PRE_COMMIT_COLOR
diff --git a/winbuild/README.md b/winbuild/README.md
index d8538fbf392..7e81abcb0e5 100644
--- a/winbuild/README.md
+++ b/winbuild/README.md
@@ -10,7 +10,7 @@ For more extensive info, see the [Windows build instructions](build.rst).
* Requires Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script.
-* Requires CMake 3.12 or newer (available as Visual Studio component).
+* Requires CMake 3.15 or newer (available as Visual Studio component).
* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor).
* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
@@ -18,12 +18,12 @@ The following is a simplified version of the script used on AppVeyor:
```
set PYTHON=C:\Python38\bin
cd /D C:\Pillow\winbuild
-C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends
+%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends
build\build_dep_all.cmd
-build\build_pillow.cmd install
cd ..
+%PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor .
path C:\Pillow\winbuild\build\bin;%PATH%
%PYTHON%\python.exe selftest.py
%PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
-build\build_pillow.cmd bdist_wheel
+%PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor .
```
diff --git a/winbuild/build.rst b/winbuild/build.rst
index 71666977127..a8e4ebaa6cc 100644
--- a/winbuild/build.rst
+++ b/winbuild/build.rst
@@ -21,10 +21,13 @@ Download and install:
`_
(MSVC C++ build tools, and any Windows SDK version required)
-* `CMake 3.12 or newer `_
+* `CMake 3.15 or newer `_
(also available as Visual Studio component C++ CMake tools for Windows)
-* x86/x64: `NASM `_
+* `Ninja `_
+ (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component)
+
+* x86/x64: `Netwide Assembler (NASM) `_
Any version of Visual Studio 2017 or newer should be supported,
including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019.
@@ -35,53 +38,59 @@ Visual Studio is found automatically with ``vswhere.exe``.
Build configuration
-------------------
-The following environment variables, if set, will override the default
-behaviour of ``build_prepare.py``:
-
-* ``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.
-
-``build_prepare.py`` also supports the following command line parameters:
-
-* ``-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``.
+Run ``build_prepare.py`` to configure the build::
+
+ usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD]
+ [--depends PILLOW_DEPS]
+ [--architecture {x86,x64,ARM64}] [--nmake]
+ [--no-imagequant] [--no-fribidi]
+
+ Download and generate build scripts for Pillow dependencies.
+
+ options:
+ -h, --help show this help message and exit
+ -v, --verbose print generated scripts
+ -d PILLOW_BUILD, --dir PILLOW_BUILD, --build-dir PILLOW_BUILD
+ build directory (default: 'winbuild\build')
+ --depends PILLOW_DEPS
+ directory used to store cached dependencies (default:
+ 'winbuild\depends')
+ --architecture {x86,x64,ARM64}
+ build architecture (default: same as host Python)
+ --nmake build dependencies using NMake instead of Ninja
+ --no-imagequant skip GPL-licensed optional dependency libimagequant
+ --no-fribidi, --no-raqm
+ skip LGPL-licensed optional dependency FriBiDi
+
+ Arguments can also be supplied using the environment variables PILLOW_BUILD,
+ PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information.
+
+**Warning:** The build directory is wiped when ``build_prepare.py`` is run.
Dependencies
------------
Dependencies will be automatically downloaded by ``build_prepare.py``.
By default, downloaded dependencies are stored in ``winbuild\depends``;
-set the ``PILLOW_DEPS`` environment variable to override this location.
+use the ``--depends`` argument or ``PILLOW_DEPS`` environment variable
+to override this location.
To build all dependencies, run ``winbuild\build\build_dep_all.cmd``,
-or run the individual scripts to build each dependency separately.
+or run the individual scripts in order to build each dependency separately.
Building Pillow
---------------
-Once the dependencies are built, run
-``winbuild\build\build_pillow.cmd install`` to build and install
-Pillow for the selected version of Python.
-``winbuild\build\build_pillow.cmd bdist_wheel`` will build wheels
-instead of installing Pillow.
+Once the dependencies are built, make sure the required environment variables
+are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip::
+
+ winbuild\build\build_env.cmd
+ python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor .
-You can also use ``winbuild\build\build_pillow.cmd --inplace develop`` to build
-and install Pillow in develop mode (instead of ``python3 -m pip install --editable``).
+To build a wheel instead, run::
+
+ winbuild\build\build_env.cmd
+ python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor .
Testing Pillow
--------------
@@ -96,17 +105,16 @@ directory.
Example
-------
-The following is a simplified version of the script used on AppVeyor:
-
-.. code-block::
+The following is a simplified version of the script used on AppVeyor::
set PYTHON=C:\Python38\bin
cd /D C:\Pillow\winbuild
- C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends
+ %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends
build\build_dep_all.cmd
- build\build_pillow.cmd install
+ build\build_env.cmd
cd ..
+ %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor .
path C:\Pillow\winbuild\build\bin;%PATH%
%PYTHON%\python.exe selftest.py
%PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- build\build_pillow.cmd bdist_wheel
+ %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor .
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index b4b15cc1ef0..a88ec7a095a 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -1,10 +1,10 @@
+import argparse
import os
import platform
import re
import shutil
import struct
import subprocess
-import sys
def cmd_cd(path):
@@ -39,7 +39,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 +55,28 @@ def cmd_nmake(makefile=None, target="", params=None):
)
-def cmd_cmake(params=None, file="."):
- if params is None:
- params = ""
- elif isinstance(params, list) 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(
@@ -98,39 +102,27 @@ def cmd_msbuild(
"ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"},
}
-header = [
- cmd_set("INCLUDE", "{inc_dir}"),
- cmd_set("INCLIB", "{lib_dir}"),
- cmd_set("LIB", "{lib_dir}"),
- cmd_append("PATH", "{bin_dir}"),
-]
-
# dependencies, listed in order of compilation
deps = {
"libjpeg": {
"url": SF_PROJECTS
- + "/libjpeg-turbo/files/2.1.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"],
@@ -138,9 +130,9 @@ def cmd_msbuild(
"bins": ["cjpeg.exe", "djpeg.exe"],
},
"zlib": {
- "url": "https://zlib.net/zlib1213.zip",
- "filename": "zlib1213.zip",
- "dir": "zlib-1.2.13",
+ "url": "https://zlib.net/zlib13.zip",
+ "filename": "zlib13.zip",
+ "dir": "zlib-1.3",
"license": "README",
"license_pattern": "Copyright notice:\n\n(.+)$",
"build": [
@@ -152,34 +144,22 @@ def cmd_msbuild(
"libs": [r"*.lib"],
},
"xz": {
- "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.7.tar.gz/download",
- "filename": "xz-5.2.7.tar.gz",
- "dir": "xz-5.2.7",
+ "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download",
+ "filename": "xz-5.4.3.tar.gz",
+ "dir": "xz-5.4.3",
"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.2.tar.gz",
+ "filename": "libwebp-1.3.2.tar.gz",
+ "dir": "libwebp-1.3.2",
"license": "COPYING",
"build": [
cmd_rmdir(r"output\release-static"), # clean
@@ -200,15 +180,11 @@ def cmd_msbuild(
"libs": [r"output\release-static\{architecture}\lib\*.lib"],
},
"libtiff": {
- "url": "https://download.osgeo.org/libtiff/tiff-4.4.0.tar.gz",
- "filename": "tiff-4.4.0.tar.gz",
- "dir": "tiff-4.4.0",
- "license": "COPYRIGHT",
+ "url": "https://download.osgeo.org/libtiff/tiff-4.5.1.tar.gz",
+ "filename": "tiff-4.5.1.tar.gz",
+ "dir": "tiff-4.5.1",
+ "license": "LICENSE.md",
"patch": {
- r"cmake\LZMACodec.cmake": {
- # fix typo
- "${{LZMA_FOUND}}": "${{LIBLZMA_FOUND}}",
- },
r"libtiff\tif_lzma.c": {
# link against liblzma.lib
"#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501
@@ -217,26 +193,31 @@ def cmd_msbuild(
# link against webp.lib
"#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501
},
+ r"test\CMakeLists.txt": {
+ "add_executable(test_write_read_tags ../placeholder.h)": "",
+ "target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501
+ "target_link_libraries(test_write_read_tags PRIVATE tiff)": "",
+ "list(APPEND simple_tests test_write_read_tags)": "",
+ },
},
"build": [
- cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"),
- cmd_nmake(target="clean"),
- cmd_nmake(target="tiff"),
+ *cmds_cmake(
+ "tiff",
+ "-DBUILD_SHARED_LIBS:BOOL=OFF",
+ '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"',
+ )
],
"headers": [r"libtiff\tiff*.h"],
"libs": [r"libtiff\*.lib"],
# "bins": [r"libtiff\*.dll"],
},
"libpng": {
- "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.38/lpng1638.zip/download",
- "filename": "lpng1638.zip",
- "dir": "lpng1638",
+ "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download",
+ "filename": "lpng1639.zip",
+ "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"],
@@ -248,18 +229,15 @@ def cmd_msbuild(
"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.1.tar.gz", # noqa: E501
+ "filename": "freetype-2.13.1.tar.gz",
+ "dir": "freetype-2.13.1",
"license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"],
"patch": {
r"builds\windows\vc2010\freetype.vcxproj": {
@@ -293,9 +271,9 @@ def cmd_msbuild(
# "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"],
},
"lcms2": {
- "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.13.1.tar.gz/download",
- "filename": "lcms2-2.13.1.tar.gz",
- "dir": "lcms2-2.13.1",
+ "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": {
@@ -323,10 +301,15 @@ def cmd_msbuild(
"filename": "openjpeg-2.5.0.tar.gz",
"dir": "openjpeg-2.5.0",
"license": "LICENSE",
+ "patch": {
+ r"src\lib\openjp2\ht_dec.c": {
+ "#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501
+ }
+ },
"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"),
],
@@ -345,39 +328,36 @@ def cmd_msbuild(
}
},
"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/5.3.1.zip",
- "filename": "harfbuzz-5.3.1.zip",
- "dir": "harfbuzz-5.3.1",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/7.3.0.zip",
+ "filename": "harfbuzz-7.3.0.zip",
+ "dir": "harfbuzz-7.3.0",
"license": "COPYING",
"build": [
- cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
- cmd_nmake(target="clean"),
- cmd_nmake(target="harfbuzz"),
+ *cmds_cmake(
+ "harfbuzz",
+ "-DHB_HAVE_FREETYPE:BOOL=TRUE",
+ '-DCMAKE_CXX_FLAGS="-nologo -d2FH4-"',
+ ),
],
"headers": [r"src\*.h"],
"libs": [r"*.lib"],
},
"fribidi": {
- "url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip",
- "filename": "fribidi-1.0.12.zip",
- "dir": "fribidi-1.0.12",
+ "url": "https://github.com/fribidi/fribidi/archive/v1.0.13.zip",
+ "filename": "fribidi-1.0.13.zip",
+ "dir": "fribidi-1.0.13",
"license": "COPYING",
"build": [
- cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"),
+ cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"),
cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
- cmd_cmake(),
- cmd_nmake(target="clean"),
- cmd_nmake(target="fribidi"),
+ *cmds_cmake("fribidi"),
],
"bins": [r"*.dll"],
},
@@ -419,23 +399,12 @@ def find_msvs():
print("Visual Studio seems to be missing C compiler")
return None
- vs = {
- "header": [],
- # nmake selected by vcvarsall
- "nmake": "nmake.exe",
- "vs_dir": vspath,
- }
-
# vs2017
msbuild = os.path.join(vspath, "MSBuild", "15.0", "Bin", "MSBuild.exe")
- if os.path.isfile(msbuild):
- vs["msbuild"] = f'"{msbuild}"'
- else:
+ if not os.path.isfile(msbuild):
# vs2019
msbuild = os.path.join(vspath, "MSBuild", "Current", "Bin", "MSBuild.exe")
- if os.path.isfile(msbuild):
- vs["msbuild"] = f'"{msbuild}"'
- else:
+ if not os.path.isfile(msbuild):
print("Visual Studio MSBuild not found")
return None
@@ -443,9 +412,13 @@ def find_msvs():
if not os.path.isfile(vcvarsall):
print("Visual Studio vcvarsall not found")
return None
- vs["header"].append(f'call "{vcvarsall}" {{vcvars_arch}}')
- return vs
+ return {
+ "vs_dir": vspath,
+ "msbuild": f'"{msbuild}"',
+ "vcvarsall": f'"{vcvarsall}"',
+ "nmake": "nmake.exe", # nmake selected by vcvarsall
+ }
def extract_dep(url, filename):
@@ -453,7 +426,7 @@ def extract_dep(url, filename):
import urllib.request
import zipfile
- file = os.path.join(depends_dir, filename)
+ file = os.path.join(args.depends_dir, filename)
if not os.path.exists(file):
ex = None
for i in range(3):
@@ -469,23 +442,37 @@ def extract_dep(url, filename):
raise RuntimeError(ex)
print("Extracting " + filename)
+ sources_dir_abs = os.path.abspath(sources_dir)
if filename.endswith(".zip"):
with zipfile.ZipFile(file) as zf:
+ for member in zf.namelist():
+ member_abspath = os.path.abspath(os.path.join(sources_dir, member))
+ member_prefix = os.path.commonpath([sources_dir_abs, member_abspath])
+ if sources_dir_abs != member_prefix:
+ msg = "Attempted Path Traversal in Zip File"
+ raise RuntimeError(msg)
zf.extractall(sources_dir)
elif filename.endswith(".tar.gz") or filename.endswith(".tgz"):
with tarfile.open(file, "r:gz") as tgz:
+ for member in tgz.getnames():
+ member_abspath = os.path.abspath(os.path.join(sources_dir, member))
+ member_prefix = os.path.commonpath([sources_dir_abs, member_abspath])
+ if sources_dir_abs != member_prefix:
+ msg = "Attempted Path Traversal in Tar File"
+ raise RuntimeError(msg)
tgz.extractall(sources_dir)
else:
- raise RuntimeError("Unknown archive type: " + filename)
+ msg = "Unknown archive type: " + filename
+ raise RuntimeError(msg)
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)
@@ -501,6 +488,22 @@ def get_footer(dep):
return lines
+def build_env():
+ lines = [
+ "if defined DISTUTILS_USE_SDK goto end",
+ cmd_set("INCLUDE", "{inc_dir}"),
+ cmd_set("INCLIB", "{lib_dir}"),
+ cmd_set("LIB", "{lib_dir}"),
+ cmd_append("PATH", "{bin_dir}"),
+ "call {vcvarsall} {vcvars_arch}",
+ cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow
+ cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT
+ ":end",
+ "@echo on",
+ ]
+ write_script("build_env.cmd", lines)
+
+
def build_dep(name):
dep = deps[name]
dir = dep["dir"]
@@ -538,11 +541,11 @@ def build_dep(name):
banner = f"Building {name} ({dir})"
lines = [
+ r'call "{build_dir}\build_env.cmd"',
"@echo " + ("=" * 70),
f"@echo ==== {banner:<60} ====",
"@echo " + ("=" * 70),
- "cd /D %s" % os.path.join(sources_dir, dir),
- *prefs["header"],
+ cmd_cd(os.path.join(sources_dir, dir)),
*dep.get("build", []),
*get_footer(dep),
]
@@ -552,117 +555,132 @@ def build_dep(name):
def build_dep_all():
- lines = ["@echo on"]
+ lines = [r'call "{build_dir}\build_env.cmd"']
for dep_name in deps:
+ print()
if dep_name in disabled:
+ print(f"Skipping disabled dependency {dep_name}")
continue
script = build_dep(dep_name)
lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"')
lines.append("if errorlevel 1 echo Build failed! && exit /B 1")
+ print()
lines.append("@echo All Pillow dependencies built successfully!")
write_script("build_dep_all.cmd", lines)
-def build_pillow():
- lines = [
- "@echo ---- Building Pillow (build_ext %*) ----",
- cmd_cd("{pillow_dir}"),
- *prefs["header"],
- cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow
- cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT
- r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501
- ]
-
- write_script("build_pillow.cmd", lines)
-
-
if __name__ == "__main__":
- # winbuild directory
winbuild_dir = os.path.dirname(os.path.realpath(__file__))
-
- 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"),
+ pillow_dir = os.path.realpath(os.path.join(winbuild_dir, ".."))
+
+ parser = argparse.ArgumentParser(
+ prog="winbuild\\build_prepare.py",
+ description="Download and generate build scripts for Pillow dependencies.",
+ epilog="""Arguments can also be supplied using the environment variables
+ PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\\build.rst
+ for more information.""",
)
- build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build"))
- sources_dir = ""
- for arg in sys.argv[1:]:
- if arg == "-v":
- verbose = True
- elif arg == "--no-imagequant":
- disabled += ["libimagequant"]
- elif arg == "--no-raqm" or arg == "--no-fribidi":
- disabled += ["fribidi"]
- elif arg.startswith("--depends="):
- depends_dir = arg[10:]
- elif arg.startswith("--python="):
- python_dir = arg[9:]
- elif arg.startswith("--executable="):
- python_exe = arg[13:]
- elif arg.startswith("--architecture="):
- architecture = arg[15:]
- elif arg.startswith("--dir="):
- build_dir = arg[6:]
- elif arg == "--srcdir":
- sources_dir = os.path.sep + "src"
- else:
- raise ValueError("Unknown parameter: " + arg)
-
- # dependency cache directory
- os.makedirs(depends_dir, exist_ok=True)
- print("Caching dependencies in:", depends_dir)
-
- if python_dir is None:
- python_dir = os.path.dirname(os.path.realpath(sys.executable))
- python_exe = os.path.basename(sys.executable)
- print("Target Python:", os.path.join(python_dir, python_exe))
+ parser.add_argument(
+ "-v", "--verbose", action="store_true", help="print generated scripts"
+ )
+ parser.add_argument(
+ "-d",
+ "--dir",
+ "--build-dir",
+ dest="build_dir",
+ metavar="PILLOW_BUILD",
+ default=os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")),
+ help="build directory (default: 'winbuild\\build')",
+ )
+ parser.add_argument(
+ "--depends",
+ dest="depends_dir",
+ metavar="PILLOW_DEPS",
+ default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")),
+ help="directory used to store cached dependencies "
+ "(default: 'winbuild\\depends')",
+ )
+ parser.add_argument(
+ "--architecture",
+ choices=architectures,
+ default=os.environ.get(
+ "ARCHITECTURE",
+ (
+ "ARM64"
+ if platform.machine() == "ARM64"
+ else ("x86" if struct.calcsize("P") == 4 else "x64")
+ ),
+ ),
+ help="build architecture (default: same as host Python)",
+ )
+ parser.add_argument(
+ "--nmake",
+ dest="cmake_generator",
+ action="store_const",
+ const="NMake Makefiles",
+ default="Ninja",
+ help="build dependencies using NMake instead of Ninja",
+ )
+ parser.add_argument(
+ "--no-imagequant",
+ action="store_true",
+ help="skip GPL-licensed optional dependency libimagequant",
+ )
+ parser.add_argument(
+ "--no-fribidi",
+ "--no-raqm",
+ action="store_true",
+ help="skip LGPL-licensed optional dependency FriBiDi",
+ )
+ args = parser.parse_args()
- arch_prefs = architectures[architecture]
- print("Target Architecture:", architecture)
+ arch_prefs = architectures[args.architecture]
+ print("Target architecture:", args.architecture)
msvs = find_msvs()
if msvs is None:
- raise RuntimeError(
- "Visual Studio not found. Please install Visual Studio 2017 or newer."
- )
+ msg = "Visual Studio not found. Please install Visual Studio 2017 or newer."
+ raise RuntimeError(msg)
print("Found Visual Studio at:", msvs["vs_dir"])
- print("Using output directory:", build_dir)
+ # dependency cache directory
+ args.depends_dir = os.path.abspath(args.depends_dir)
+ os.makedirs(args.depends_dir, exist_ok=True)
+ print("Caching dependencies in:", args.depends_dir)
+
+ args.build_dir = os.path.abspath(args.build_dir)
+ print("Using output directory:", args.build_dir)
# build directory for *.h files
- inc_dir = os.path.join(build_dir, "inc")
+ inc_dir = os.path.join(args.build_dir, "inc")
# build directory for *.lib files
- lib_dir = os.path.join(build_dir, "lib")
+ lib_dir = os.path.join(args.build_dir, "lib")
# build directory for *.bin files
- bin_dir = os.path.join(build_dir, "bin")
+ bin_dir = os.path.join(args.build_dir, "bin")
# directory for storing project files
- sources_dir = build_dir + sources_dir
+ sources_dir = os.path.join(args.build_dir, "src")
# copy dependency licenses to this directory
- license_dir = os.path.join(build_dir, "license")
+ license_dir = os.path.join(args.build_dir, "license")
- shutil.rmtree(build_dir, ignore_errors=True)
- os.makedirs(build_dir, exist_ok=False)
+ shutil.rmtree(args.build_dir, ignore_errors=True)
+ os.makedirs(args.build_dir, exist_ok=False)
for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]:
os.makedirs(path, exist_ok=True)
+ disabled = []
+ if args.no_imagequant:
+ disabled += ["libimagequant"]
+ if args.no_fribidi:
+ disabled += ["fribidi"]
+
prefs = {
- # Python paths / preferences
- "python_dir": python_dir,
- "python_exe": python_exe,
- "architecture": architecture,
+ "architecture": args.architecture,
**arch_prefs,
# Pillow paths
- "pillow_dir": os.path.realpath(os.path.join(winbuild_dir, "..")),
+ "pillow_dir": pillow_dir,
"winbuild_dir": winbuild_dir,
# Build paths
- "build_dir": build_dir,
+ "build_dir": args.build_dir,
"inc_dir": inc_dir,
"lib_dir": lib_dir,
"bin_dir": bin_dir,
@@ -671,9 +689,8 @@ def build_pillow():
# Compilers / Tools
**msvs,
"cmake": "cmake.exe", # TODO find CMAKE automatically
+ "cmake_generator": args.cmake_generator,
# TODO find NASM automatically
- # script header
- "header": sum([header, msvs["header"], ["@echo on"]], []),
}
for k, v in deps.items():
@@ -682,5 +699,5 @@ def build_pillow():
print()
write_script(".gitignore", ["*"])
+ build_env()
build_dep_all()
- build_pillow()