diff --git a/.appveyor.yml b/.appveyor.yml
index cc4d56d0b..0f5dea9c5 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -10,7 +10,7 @@ environment:
TEST_OPTIONS:
DEPLOY: YES
matrix:
- - PYTHON: C:/Python311
+ - PYTHON: C:/Python312
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64
@@ -43,7 +43,7 @@ build_script:
test_script:
- cd c:\pillow
-- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
+- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
diff --git a/.ci/install.sh b/.ci/install.sh
index 4748feb3d..30b64349d 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -28,7 +28,8 @@ fi
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
-PYTHONOPTIMIZE=0 python3 -m pip install cffi
+# TODO Update condition when cffi supports 3.13
+if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
@@ -38,7 +39,8 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
- python3 -m pip install numpy
+ # TODO Update condition when NumPy supports 3.13
+ if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
@@ -46,6 +48,16 @@ if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install pyqt6
fi
+ # Pyroma uses non-isolated build and fails with old setuptools
+ if [[
+ $GHA_PYTHON_VERSION == pypy3.9
+ || $GHA_PYTHON_VERSION == 3.8
+ || $GHA_PYTHON_VERSION == 3.9
+ ]]; then
+ # To match pyproject.toml
+ python3 -m pip install "setuptools>=67.8"
+ fi
+
# webp
pushd depends && ./install_webp.sh && popd
diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt
new file mode 100644
index 000000000..dd61634cd
--- /dev/null
+++ b/.ci/requirements-cibw.txt
@@ -0,0 +1 @@
+cibuildwheel==2.16.2
diff --git a/.editorconfig b/.editorconfig
index d74549fe2..c3627ae4f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,7 +13,7 @@ indent_style = space
trim_trailing_whitespace = true
-[*.yml]
+[*.{toml,yml}]
# Two-space indentation
indent_size = 2
diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml
index 560d6c7df..eb73fc6a7 100644
--- a/.github/workflows/cifuzz.yml
+++ b/.github/workflows/cifuzz.yml
@@ -2,6 +2,8 @@ name: CIFuzz
on:
push:
+ branches:
+ - "**"
paths:
- ".github/workflows/cifuzz.yml"
- "**.c"
@@ -40,13 +42,13 @@ jobs:
language: python
dry-run: false
- name: Upload New Crash
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts
- name: Upload Legacy Crash
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: steps.run.outcome == 'success'
with:
name: crash
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 844c7c1ec..9fe345c8a 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -2,6 +2,8 @@ name: Docs
on:
push:
+ branches:
+ - "**"
paths:
- ".github/workflows/docs.yml"
- "docs/**"
@@ -31,7 +33,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: pip
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 78b80d26e..9069fc615 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,6 +2,9 @@ name: Lint
on: [push, pull_request, workflow_dispatch]
+env:
+ FORCE_COLOR: 1
+
permissions:
contents: read
@@ -28,7 +31,7 @@ jobs:
lint-pre-commit-
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: pip
@@ -46,3 +49,6 @@ jobs:
run: tox -e lint
env:
PRE_COMMIT_COLOR: always
+
+ - name: Mypy
+ run: tox -e mypy
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index a20838a15..f41324c4b 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -5,7 +5,9 @@ set -e
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
-PYTHONOPTIMIZE=0 python3 -m pip install cffi
+# TODO Update condition when cffi supports 3.13
+if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
+
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
@@ -14,7 +16,8 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-python3 -m pip install numpy
+# TODO Update condition when NumPy supports 3.13
+if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; 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 31f63e1c6..545c2e364 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- name: "Check issues"
- uses: actions/stale@v8
+ uses: actions/stale@v9
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 5071e5bd4..32ac6f65e 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -2,13 +2,23 @@ name: Test Cygwin
on:
push:
+ branches:
+ - "**"
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
@@ -122,7 +132,7 @@ jobs:
dash.exe -c "mkdir -p Tests/errors"
- name: Upload errors
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: failure()
with:
name: errors
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index a0c7444c6..eb27b4bf7 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -2,13 +2,23 @@ name: Test Docker
on:
push:
+ branches:
+ - "**"
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
@@ -41,8 +51,8 @@ jobs:
debian-11-bullseye-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
- fedora-37-amd64,
fedora-38-amd64,
+ fedora-39-amd64,
gentoo,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index 08dfb9a2d..115c2e9be 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -2,13 +2,23 @@ name: Test MinGW
on:
push:
+ branches:
+ - "**"
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml
index 21968ad5a..59bb958ec 100644
--- a/.github/workflows/test-valgrind.yml
+++ b/.github/workflows/test-valgrind.yml
@@ -1,9 +1,11 @@
name: Test Valgrind
-# like the docker tests, but running valgrind only on *.c/*.h changes.
+# like the Docker tests, but running valgrind only on *.c/*.h changes.
on:
push:
+ branches:
+ - "**"
paths:
- ".github/workflows/test-valgrind.yml"
- "**.c"
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index ae3cc6127..86cd5b5fa 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -4,11 +4,19 @@ on:
push:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
@@ -24,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
+ python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30
@@ -48,25 +56,26 @@ jobs:
# sets env: pythonLocation
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
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 pytest pytest-cov pytest-timeout defusedxml
- run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
+ - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
+ run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
- name: Install dependencies
id: install
run: |
- 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
+ choco install nasm --no-progress
+ echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
- choco install ghostscript --version=10.0.0.20230317
+ choco install ghostscript --version=10.0.0.20230317 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images
@@ -158,7 +167,6 @@ jobs:
- name: Build Pillow
run: |
$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
@@ -182,7 +190,7 @@ jobs:
shell: bash
- name: Upload errors
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: failure()
with:
name: errors
@@ -200,47 +208,6 @@ jobs:
flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }}
- - name: Build wheel
- id: wheel
- if: "github.event_name != 'pull_request'"
- run: |
- mkdir fribidi
- copy winbuild\build\bin\fribidi* fribidi
- setlocal EnableDelayedExpansion
- for %%f in (winbuild\build\license\*) do (
- set x=%%~nf
- rem Skip FriBiDi license, it is not included in the wheel.
- set fribidi=!x:~0,7!
- if NOT !fribidi!==fribidi (
- rem Skip imagequant license, it is not included in the wheel.
- set libimagequant=!x:~0,13!
- if NOT !libimagequant!==libimagequant (
- echo. >> LICENSE
- echo ===== %%~nf ===== >> LICENSE
- echo. >> LICENSE
- type %%f >> LICENSE
- )
- )
- )
- for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
- 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
- uses: actions/upload-artifact@v3
- if: "github.event_name != 'pull_request'"
- with:
- name: ${{ steps.wheel.outputs.dist }}
- path: "*.whl"
-
- - name: Upload fribidi.dll
- if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
- uses: actions/upload-artifact@v3
- with:
- name: fribidi
- path: fribidi\*
-
success:
permissions:
contents: none
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2f43f4b55..aa0e25138 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -2,13 +2,23 @@ name: Test
on:
push:
+ branches:
+ - "**"
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
@@ -31,7 +41,8 @@ jobs:
python-version: [
"pypy3.10",
"pypy3.9",
- "3.12-dev",
+ "3.13",
+ "3.12",
"3.11",
"3.10",
"3.9",
@@ -51,9 +62,10 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
cache: pip
cache-dependency-path: ".ci/*.sh"
@@ -100,7 +112,7 @@ jobs:
mkdir -p Tests/errors
- name: Upload errors
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: failure()
with:
name: errors
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
new file mode 100755
index 000000000..3ec314873
--- /dev/null
+++ b/.github/workflows/wheels-dependencies.sh
@@ -0,0 +1,151 @@
+#!/bin/bash
+# Define custom utilities
+# Test for macOS with [ -n "$IS_MACOS" ]
+if [ -z "$IS_MACOS" ]; then
+ export MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
+ export MB_ML_VER=${AUDITWHEEL_POLICY:9}
+fi
+export PLAT=$CIBW_ARCHS
+source wheels/multibuild/common_utils.sh
+source wheels/multibuild/library_builders.sh
+if [ -z "$IS_MACOS" ]; then
+ source wheels/multibuild/manylinux_utils.sh
+fi
+
+ARCHIVE_SDIR=pillow-depends-main
+
+# Package versions for fresh source builds
+FREETYPE_VERSION=2.13.2
+HARFBUZZ_VERSION=8.3.0
+LIBPNG_VERSION=1.6.40
+JPEGTURBO_VERSION=3.0.1
+OPENJPEG_VERSION=2.5.0
+XZ_VERSION=5.4.5
+TIFF_VERSION=4.6.0
+LCMS2_VERSION=2.16
+if [[ -n "$IS_MACOS" ]]; then
+ GIFLIB_VERSION=5.1.4
+else
+ GIFLIB_VERSION=5.2.1
+fi
+if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
+ ZLIB_VERSION=1.3
+else
+ ZLIB_VERSION=1.2.8
+fi
+LIBWEBP_VERSION=1.3.2
+BZIP2_VERSION=1.0.8
+LIBXCB_VERSION=1.16
+BROTLI_VERSION=1.1.0
+
+if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
+ function build_openjpeg {
+ local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz)
+ (cd $out_dir \
+ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
+ && make install)
+ touch openjpeg-stamp
+ }
+fi
+
+function build_brotli {
+ local cmake=$(get_modern_cmake)
+ local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz)
+ (cd $out_dir \
+ && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
+ && make install)
+ if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
+ cp /usr/local/lib64/libbrotli* /usr/local/lib
+ cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig
+ fi
+}
+
+function build {
+ if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
+ export BUILD_PREFIX="/usr/local"
+ fi
+ build_xz
+ if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
+ yum remove -y zlib-devel
+ fi
+ build_new_zlib
+
+ build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
+ if [ -n "$IS_MACOS" ]; then
+ if [[ "$CIBW_ARCHS" == "arm64" ]]; then
+ build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
+ build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
+ build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
+ if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
+ cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
+ fi
+ fi
+ else
+ sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
+ fi
+ build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
+
+ build_libjpeg_turbo
+ build_tiff
+ build_libpng
+ build_lcms2
+ if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
+ for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do
+ cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib
+ done
+ fi
+ build_openjpeg
+
+ ORIGINAL_CFLAGS=$CFLAGS
+ CFLAGS="$CFLAGS -O3 -DNDEBUG"
+ if [[ -n "$IS_MACOS" ]]; then
+ CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
+ fi
+ build_libwebp
+ CFLAGS=$ORIGINAL_CFLAGS
+
+ build_brotli
+
+ if [ -n "$IS_MACOS" ]; then
+ # Custom freetype build
+ build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
+ else
+ build_freetype
+ fi
+
+ if [ -z "$IS_MACOS" ]; then
+ export FREETYPE_LIBS=-lfreetype
+ export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
+ fi
+ build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
+ if [ -z "$IS_MACOS" ]; then
+ export FREETYPE_LIBS=""
+ export FREETYPE_CFLAGS=""
+ fi
+}
+
+# Any stuff that you need to do before you start building the wheels
+# Runs in the root directory of this repository.
+curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
+untar pillow-depends-main.zip
+
+if [[ -n "$IS_MACOS" ]]; then
+ # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
+ # libxdmcp causes an issue on macOS < 11
+ # if php is installed, brew tries to reinstall these after installing openblas
+ # remove cairo to fix building harfbuzz on arm64
+ # remove lcms2 and libpng to fix building openjpeg on arm64
+ # remove zstd to avoid inclusion on x86_64
+ # curl from brew requires zstd, use system curl
+ brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd
+
+ brew install pkg-config
+fi
+
+wrap_wheel_builder build
+
+# Append licenses
+for filename in wheels/dependency_licenses/*; do
+ echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE
+ cat $filename >> LICENSE
+done
diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1
new file mode 100644
index 000000000..f593c7228
--- /dev/null
+++ b/.github/workflows/wheels-test.ps1
@@ -0,0 +1,22 @@
+param ([string]$venv, [string]$pillow="C:\pillow")
+$ErrorActionPreference = 'Stop'
+$ProgressPreference = 'SilentlyContinue'
+Set-PSDebug -Trace 1
+if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
+ # unlike CPython, PyPy requires Visual C++ Redistributable to be installed
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe'
+ C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
+}
+$env:path += ";$pillow\winbuild\build\bin\"
+& "$venv\Scripts\activate.ps1"
+& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
+cd $pillow
+& python -VV
+if (!$?) { exit $LASTEXITCODE }
+& python selftest.py
+if (!$?) { exit $LASTEXITCODE }
+& python -m pytest -vx Tests\check_wheel.py
+if (!$?) { exit $LASTEXITCODE }
+& python -m pytest -vx Tests
+if (!$?) { exit $LASTEXITCODE }
diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh
new file mode 100755
index 000000000..207ec1567
--- /dev/null
+++ b/.github/workflows/wheels-test.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+set -e
+
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ brew install fribidi
+ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
+elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
+ apk add curl fribidi
+else
+ yum install -y fribidi
+fi
+if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
+ python3 -m pip install numpy
+fi
+
+if [ ! -d "test-images-main" ]; then
+ curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
+ unzip pillow-test-images.zip
+ mv test-images-main/* Tests/images
+fi
+
+# Runs tests
+python3 selftest.py
+python3 -m pytest Tests/check_wheel.py
+python3 -m pytest
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
new file mode 100644
index 000000000..76d42b470
--- /dev/null
+++ b/.github/workflows/wheels.yml
@@ -0,0 +1,206 @@
+name: Wheels
+
+on:
+ push:
+ paths:
+ - ".ci/requirements-cibw.txt"
+ - ".github/workflows/wheel*"
+ - "wheels/*"
+ - "winbuild/build_prepare.py"
+ - "winbuild/fribidi.cmake"
+ tags:
+ - "*"
+ pull_request:
+ paths:
+ - ".ci/requirements-cibw.txt"
+ - ".github/workflows/wheel*"
+ - "wheels/*"
+ - "winbuild/build_prepare.py"
+ - "winbuild/fribidi.cmake"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ FORCE_COLOR: 1
+
+jobs:
+ build:
+ name: ${{ matrix.name }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: "macOS x86_64"
+ os: macos-latest
+ archs: x86_64
+ macosx_deployment_target: "10.10"
+ - name: "macOS arm64"
+ os: macos-latest
+ archs: arm64
+ macosx_deployment_target: "11.0"
+ - name: "manylinux2014 and musllinux x86_64"
+ os: ubuntu-latest
+ archs: x86_64
+ - name: "manylinux_2_28 x86_64"
+ os: ubuntu-latest
+ archs: x86_64
+ build: "*manylinux*"
+ manylinux: "manylinux_2_28"
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: true
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ - name: Build wheels
+ run: |
+ python3 -m pip install -r .ci/requirements-cibw.txt
+ python3 -m cibuildwheel --output-dir wheelhouse
+ env:
+ CIBW_ARCHS: ${{ matrix.archs }}
+ CIBW_BUILD: ${{ matrix.build }}
+ CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
+ CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
+ CIBW_SKIP: pp38-*
+ CIBW_TEST_SKIP: "*-macosx_arm64"
+ MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: dist
+ path: ./wheelhouse/*.whl
+
+ windows:
+ name: Windows ${{ matrix.arch }}
+ runs-on: windows-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - arch: x86
+ cibw_arch: x86
+ - arch: x64
+ cibw_arch: AMD64
+ - arch: ARM64
+ cibw_arch: ARM64
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Checkout extra test images
+ uses: actions/checkout@v4
+ with:
+ repository: python-pillow/test-images
+ path: Tests\test-images
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ - name: Prepare for build
+ run: |
+ choco install nasm --no-progress
+ echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
+
+ # Install extra test images
+ xcopy /S /Y Tests\test-images\* Tests\images
+
+ & python.exe -m pip install -r .ci/requirements-cibw.txt
+
+ # Cannot cross-compile FriBiDi (only used for tests)
+ $FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}")
+ if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" }
+ & python.exe winbuild\build_prepare.py -v @FLAGS
+ shell: pwsh
+
+ - name: Build wheels
+ run: |
+ setlocal EnableDelayedExpansion
+ for %%f in (winbuild\build\license\*) do (
+ set x=%%~nf
+ rem Skip FriBiDi license, it is not included in the wheel.
+ set fribidi=!x:~0,7!
+ if NOT !fribidi!==fribidi (
+ rem Skip imagequant license, it is not included in the wheel.
+ set libimagequant=!x:~0,13!
+ if NOT !libimagequant!==libimagequant (
+ echo. >> LICENSE
+ echo ===== %%~nf ===== >> LICENSE
+ echo. >> LICENSE
+ type %%f >> LICENSE
+ )
+ )
+ )
+ call winbuild\\build\\build_env.cmd
+ %pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
+ env:
+ CIBW_ARCHS: ${{ matrix.cibw_arch }}
+ CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
+ CIBW_CACHE_PATH: "C:\\cibw"
+ CIBW_TEST_SKIP: "*-win_arm64"
+ CIBW_TEST_COMMAND: 'docker run --rm
+ -v {project}:C:\pillow
+ -v C:\cibw:C:\cibw
+ -v %CD%\..\venv-test:%CD%\..\venv-test
+ -e CI -e GITHUB_ACTIONS
+ mcr.microsoft.com/windows/servercore:ltsc2022
+ powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
+ shell: cmd
+
+ - name: Upload wheels
+ uses: actions/upload-artifact@v3
+ with:
+ name: dist
+ path: ./wheelhouse/*.whl
+
+ - name: Prepare to upload FriBiDi
+ if: "matrix.arch != 'ARM64'"
+ run: |
+ mkdir fribidi\${{ matrix.arch }}
+ copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }}
+ shell: cmd
+
+ - name: Upload fribidi.dll
+ if: "matrix.arch != 'ARM64'"
+ uses: actions/upload-artifact@v3
+ with:
+ name: fribidi
+ path: fribidi\*
+
+ sdist:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+ cache: pip
+ cache-dependency-path: "Makefile"
+
+ - run: make sdist
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: dist
+ path: dist/*.tar.gz
+
+ success:
+ permissions:
+ contents: none
+ needs: [build, windows, sdist]
+ runs-on: ubuntu-latest
+ name: Wheels Successful
+ steps:
+ - name: Success
+ run: echo Wheels Successful
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..80d5ab16c
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "multibuild"]
+ path = wheels/multibuild
+ url = https://github.com/multi-build/multibuild.git
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7fe5aacbe..d1c4b8015 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,54 +1,35 @@
repos:
- - repo: https://github.com/asottile/pyupgrade
- rev: v3.13.0
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.1.7
hooks:
- - id: pyupgrade
- args: [--py38-plus]
+ - id: ruff
+ args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
- rev: 23.9.1
+ rev: 23.12.0
hooks:
- id: black
- args: [--target-version=py38]
-
- - repo: https://github.com/PyCQA/isort
- rev: 5.12.0
- hooks:
- - id: isort
- repo: https://github.com/PyCQA/bandit
- rev: 1.7.5
+ rev: 1.7.6
hooks:
- id: bandit
args: [--severity-level=high]
files: ^src/
- - repo: https://github.com/asottile/yesqa
- rev: v1.5.0
- hooks:
- - id: yesqa
-
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- - repo: https://github.com/PyCQA/flake8
- rev: 6.1.0
- hooks:
- - id: flake8
- additional_dependencies:
- [flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
-
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- - id: python-check-blanket-noqa
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.5.0
hooks:
- id: check-executables-have-shebangs
- id: check-merge-conflict
@@ -61,17 +42,17 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/sphinx-contrib/sphinx-lint
- rev: v0.6.8
+ rev: v0.9.1
hooks:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: 1.1.0
+ rev: 1.5.3
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
- rev: v0.14
+ rev: v0.15
hooks:
- id: validate-pyproject
diff --git a/.readthedocs.yml b/.readthedocs.yml
index bda03d944..0c8f935d5 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -5,7 +5,7 @@ formats: [pdf]
build:
os: ubuntu-22.04
tools:
- python: "3.11"
+ python: "3"
python:
install:
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..8f8250809
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,52 @@
+if: tag IS present OR type = api
+
+env:
+ global:
+ - CIBW_ARCHS=aarch64
+ - CIBW_SKIP=pp38-*
+
+language: python
+# Default Python version is usually 3.6
+python: "3.12"
+dist: jammy
+services: docker
+
+jobs:
+ include:
+ - name: "manylinux2014 aarch64"
+ os: linux
+ arch: arm64
+ env:
+ - CIBW_BUILD="*manylinux*"
+ - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014
+ - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014
+ - name: "manylinux_2_28 aarch64"
+ os: linux
+ arch: arm64
+ env:
+ - CIBW_BUILD="*manylinux*"
+ - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28
+ - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28
+ - name: "musllinux aarch64"
+ os: linux
+ arch: arm64
+ env:
+ - CIBW_BUILD="*musllinux*"
+
+install:
+ - python3 -m pip install -r .ci/requirements-cibw.txt
+
+script:
+ - python3 -m cibuildwheel --output-dir wheelhouse
+ - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/"
+
+# Upload wheels to GitHub Releases
+deploy:
+ provider: releases
+ api_key: $GITHUB_RELEASE_TOKEN
+ file_glob: true
+ file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl"
+ on:
+ repo: python-pillow/Pillow
+ tags: true
+ skip_cleanup: true
diff --git a/CHANGES.rst b/CHANGES.rst
index 570c197fb..f69c3ffa7 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,9 +2,120 @@
Changelog (Pillow)
==================
-10.1.0 (unreleased)
+10.2.0 (unreleased)
-------------------
+- Attempt memory mapping when tile args is a string #7565
+ [radarhere]
+
+- Fill identical pixels with transparency in subsequent frames when saving GIF #7568
+ [radarhere]
+
+- Corrected duration when combining multiple GIF frames into single frame #7521
+ [radarhere]
+
+- Handle disposing GIF background from outside palette #7515
+ [radarhere]
+
+- Seek past the data when skipping a PSD layer #7483
+ [radarhere]
+
+- Import plugins relative to the module #7576
+ [deliangyang, jaxx0n]
+
+- Translate encoder error codes to strings; deprecate ``ImageFile.raise_oserror()`` #7609
+ [bgilbert, radarhere]
+
+- Support reading BC4U and DX10 BC1 images #6486
+ [REDxEYE, radarhere, hugovk]
+
+- Optimize ImageStat.Stat.extrema #7593
+ [florath, radarhere]
+
+- Handle pathlib.Path in FreeTypeFont #7578
+ [radarhere, hugovk, nulano]
+
+- Added support for reading DX10 BC4 DDS images #7603
+ [sambvfx, radarhere]
+
+- Optimized ImageStat.Stat.count #7599
+ [florath]
+
+- Correct PDF palette size when saving #7555
+ [radarhere]
+
+- Fixed closing file pointer with olefile 0.47 #7594
+ [radarhere]
+
+- Raise ValueError when TrueType font size is not greater than zero #7584, #7587
+ [akx, radarhere]
+
+- If absent, do not try to close fp when closing image #7557
+ [RaphaelVRossi, radarhere]
+
+- Allow configuring JPEG restart marker interval on save #7488
+ [bgilbert, radarhere]
+
+- Decrement reference count for PyObject #7549
+ [radarhere]
+
+- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491
+ [bgilbert, radarhere]
+
+- If save_all PNG only has one frame, do not create animated image #7522
+ [radarhere]
+
+- Fixed frombytes() for images with a zero dimension #7493
+ [radarhere]
+
+10.1.0 (2023-10-15)
+-------------------
+
+- Added TrueType default font to allow for different sizes #7354
+ [radarhere]
+
+- Fixed invalid argument warning #7442
+ [radarhere]
+
+- Added ImageOps cover method #7412
+ [radarhere, hugovk]
+
+- Catch struct.error from truncated EXIF when reading JPEG DPI #7458
+ [radarhere]
+
+- Consider default image when selecting mode for PNG save_all #7437
+ [radarhere]
+
+- Support BGR;15, BGR;16 and BGR;24 access, unpacking and putdata #7303
+ [radarhere]
+
+- Added CMYK to RGB unpacker #7310
+ [radarhere]
+
+- Improved flexibility of XMP parsing #7274
+ [radarhere]
+
+- Support reading 8-bit YCbCr TIFF images #7415
+ [radarhere]
+
+- Allow saving I;16B images as PNG #7302
+ [radarhere]
+
+- Corrected drawing I;16 points and writing I;16 text #7257
+ [radarhere]
+
+- Set blue channel to 128 for BC5S #7413
+ [radarhere]
+
+- Increase flexibility when reading IPTC fields #7319
+ [radarhere]
+
+- Set C palette to be empty by default #7289
+ [radarhere]
+
+- Added gs_binary to control Ghostscript use on all platforms #7392
+ [radarhere]
+
- Read bounding box information from the trailer of EPS files if specified #7382
[nopperl, radarhere]
@@ -2146,7 +2257,7 @@ Changelog (Pillow)
- Cache EXIF information #3498
[Glandos]
-- Added transparency for all PNG greyscale modes #3744
+- Added transparency for all PNG grayscale modes #3744
[radarhere]
- Fix deprecation warnings in Python 3.8 #3749
@@ -4648,7 +4759,7 @@ Changelog (Pillow)
- Fix Bicubic interpolation #970
[homm]
-- Support for 4-bit greyscale TIFF images #980
+- Support for 4-bit grayscale TIFF images #980
[hugovk]
- Updated manifest #957
@@ -6723,7 +6834,7 @@ The test suite includes 750 individual tests.
- You can now convert directly between all modes supported by
PIL. When converting colour images to "P", PIL defaults to
- a "web" palette and dithering. When converting greyscale
+ a "web" palette and dithering. When converting grayscale
images to "1", PIL uses a thresholding and dithering.
- Added a "dither" option to "convert". By default, "convert"
@@ -6801,13 +6912,13 @@ The test suite includes 530 individual tests.
- Fixed "paste" to allow a mask also for mode "F" images.
- The BMP driver now saves mode "1" images. When loading images, the mode
- is set to "L" for 8-bit files with greyscale palettes, and to "P" for
+ is set to "L" for 8-bit files with grayscale palettes, and to "P" for
other 8-bit files.
- The IM driver now reads and saves "1" images (file modes "0 1" or "L 1").
- The JPEG and GIF drivers now saves "1" images. For JPEG, the image
- is saved as 8-bit greyscale (it will load as mode "L"). For GIF, the
+ is saved as 8-bit grayscale (it will load as mode "L"). For GIF, the
image will be loaded as a "P" image.
- Fixed a potential buffer overrun in the GIF encoder.
@@ -7111,7 +7222,7 @@ The test suite includes 400 individual tests.
drawing capabilities can be used to render vector and metafile
formats.
-- Added restricted drivers for images from Image Tools (greyscale
+- Added restricted drivers for images from Image Tools (grayscale
only) and LabEye/IFUNC (common interchange modes only).
- Some minor improvements to the sample scripts provided in the
diff --git a/MANIFEST.in b/MANIFEST.in
index 606e7e074..af25dfd2d 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -5,8 +5,10 @@ include *.md
include *.py
include *.rst
include *.sh
+include *.toml
include *.txt
include *.yaml
+include .flake8
include LICENSE
include Makefile
include tox.ini
@@ -29,3 +31,4 @@ global-exclude .git*
global-exclude *.pyc
global-exclude *.so
prune .ci
+prune wheels
diff --git a/Makefile b/Makefile
index 57d756b47..ad0a1adab 100644
--- a/Makefile
+++ b/Makefile
@@ -49,7 +49,7 @@ help:
@echo " install make and install"
@echo " install-coverage make and install with C coverage"
@echo " lint run the lint checks"
- @echo " lint-fix run Black and isort to (mostly) fix lint issues"
+ @echo " lint-fix run Ruff to (mostly) fix lint issues"
@echo " release-test run code and package tests before release"
@echo " test run tests on installed Pillow"
@@ -118,6 +118,6 @@ lint:
.PHONY: lint-fix
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 py38 .
- python3 -m isort .
+ python3 -m black .
+ python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
+ python3 -m ruff --fix .
diff --git a/README.md b/README.md
index af1ca57c2..e11bd2faa 100644
--- a/README.md
+++ b/README.md
@@ -45,12 +45,12 @@ As of 2019, Pillow development is
-
- ![]()
+
+ src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels">
@@ -74,9 +74,9 @@ As of 2019, Pillow development is
- ![]()
+ src="https://www.bestpractices.dev/projects/6331/badge">
diff --git a/RELEASING.md b/RELEASING.md
index 604bb1b8c..74f427f03 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `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.
+* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them.
* [ ] 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.
@@ -20,12 +20,8 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
git tag 5.2.0
git push --tags
```
-* [ ] Create and check source distribution:
- ```bash
- make sdist
- ```
-* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
-* [ ] Check and upload all binaries and source distributions e.g.:
+* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
+* [ ] Check and upload all source and binary distributions e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.0*
@@ -59,8 +55,8 @@ Released as needed for security, installation or critical bug fixes.
```bash
make sdist
```
-* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
-* [ ] Check and upload all binaries and source distributions e.g.:
+* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
+* [ ] Check and upload all source and binary distributions e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.1*
@@ -90,34 +86,22 @@ Released as needed privately to individual vendors for critical security-related
```bash
make sdist
```
-* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
+* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash
git push origin 2.5.x
```
-## Binary Distributions
+## Source and Binary Distributions
-### 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
- cd pillow-wheels
- ./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/`. 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)
+* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.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
+ # select dist
```
+* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
+ and copy into `dist`.
## Publicize Release
diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py
index 69ebef9b4..36ce63296 100644
--- a/Tests/bench_cffi_access.py
+++ b/Tests/bench_cffi_access.py
@@ -45,7 +45,7 @@ def test_direct():
assert caccess[(0, 0)] == access[(0, 0)]
- print("Size: %sx%s" % im.size)
+ print(f"Size: {im.width}x{im.height}")
timer(iterate_get, "PyAccess - get", im.size, access)
timer(iterate_set, "PyAccess - set", im.size, access)
timer(iterate_get, "C-api - get", im.size, caccess)
diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py
new file mode 100644
index 000000000..cc52cb75e
--- /dev/null
+++ b/Tests/check_wheel.py
@@ -0,0 +1,41 @@
+import sys
+
+from PIL import features
+
+
+def test_wheel_modules():
+ expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
+
+ # tkinter is not available in cibuildwheel installed CPython on Windows
+ try:
+ import tkinter
+
+ assert tkinter
+ except ImportError:
+ expected_modules.remove("tkinter")
+
+ assert set(features.get_supported_modules()) == expected_modules
+
+
+def test_wheel_codecs():
+ expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
+
+ assert set(features.get_supported_codecs()) == expected_codecs
+
+
+def test_wheel_features():
+ expected_features = {
+ "webp_anim",
+ "webp_mux",
+ "transp_webp",
+ "raqm",
+ "fribidi",
+ "harfbuzz",
+ "libjpeg_turbo",
+ "xcb",
+ }
+
+ if sys.platform == "win32":
+ expected_features.remove("xcb")
+
+ assert set(features.get_supported_features()) == expected_features
diff --git a/Tests/helper.py b/Tests/helper.py
index de5468d84..cce7eca3a 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -5,6 +5,7 @@ Helper functions.
import logging
import os
import shutil
+import subprocess
import sys
import sysconfig
import tempfile
@@ -95,7 +96,7 @@ def assert_image_equal(a, b, msg=None):
except Exception:
pass
- assert False, msg or "got different content"
+ pytest.fail(msg or "got different content")
def assert_image_equal_tofile(a, filename, msg=None, mode=None):
@@ -258,11 +259,21 @@ def hopper(mode=None, cache={}):
def djpeg_available():
- return bool(shutil.which("djpeg"))
+ if shutil.which("djpeg"):
+ try:
+ subprocess.check_call(["djpeg", "-version"])
+ return True
+ except subprocess.CalledProcessError: # pragma: no cover
+ return False
def cjpeg_available():
- return bool(shutil.which("cjpeg"))
+ if shutil.which("cjpeg"):
+ try:
+ subprocess.check_call(["cjpeg", "-version"])
+ return True
+ except subprocess.CalledProcessError: # pragma: no cover
+ return False
def netpbm_available():
diff --git a/Tests/images/apng/mode_greyscale.png b/Tests/images/apng/mode_grayscale.png
similarity index 100%
rename from Tests/images/apng/mode_greyscale.png
rename to Tests/images/apng/mode_grayscale.png
diff --git a/Tests/images/apng/mode_greyscale_alpha.png b/Tests/images/apng/mode_grayscale_alpha.png
similarity index 100%
rename from Tests/images/apng/mode_greyscale_alpha.png
rename to Tests/images/apng/mode_grayscale_alpha.png
diff --git a/Tests/images/background_outside_palette.gif b/Tests/images/background_outside_palette.gif
new file mode 100644
index 000000000..63e767463
Binary files /dev/null and b/Tests/images/background_outside_palette.gif differ
diff --git a/Tests/images/bc1.dds b/Tests/images/bc1.dds
new file mode 100755
index 000000000..faec63a00
Binary files /dev/null and b/Tests/images/bc1.dds differ
diff --git a/Tests/images/bc1_typeless.dds b/Tests/images/bc1_typeless.dds
new file mode 100755
index 000000000..47a85e2d0
Binary files /dev/null and b/Tests/images/bc1_typeless.dds differ
diff --git a/Tests/images/bc4_typeless.dds b/Tests/images/bc4_typeless.dds
new file mode 100644
index 000000000..27f87889f
Binary files /dev/null and b/Tests/images/bc4_typeless.dds differ
diff --git a/Tests/images/bc4_unorm.dds b/Tests/images/bc4_unorm.dds
new file mode 100644
index 000000000..13da711bd
Binary files /dev/null and b/Tests/images/bc4_unorm.dds differ
diff --git a/Tests/images/bc4_unorm.png b/Tests/images/bc4_unorm.png
new file mode 100644
index 000000000..71d536c84
Binary files /dev/null and b/Tests/images/bc4_unorm.png differ
diff --git a/Tests/images/bc4u.dds b/Tests/images/bc4u.dds
new file mode 100644
index 000000000..7f9f050b6
Binary files /dev/null and b/Tests/images/bc4u.dds differ
diff --git a/Tests/images/bc5s.png b/Tests/images/bc5s.png
index 657d72305..5e7a1b95e 100644
Binary files a/Tests/images/bc5s.png and b/Tests/images/bc5s.png differ
diff --git a/Tests/images/default_font_freetype.png b/Tests/images/default_font_freetype.png
new file mode 100644
index 000000000..e00bb5d85
Binary files /dev/null and b/Tests/images/default_font_freetype.png differ
diff --git a/Tests/images/five_channels.psd b/Tests/images/five_channels.psd
new file mode 100644
index 000000000..021a5fa63
Binary files /dev/null and b/Tests/images/five_channels.psd differ
diff --git a/Tests/images/hopper_rle8_greyscale.bmp b/Tests/images/hopper_rle8_grayscale.bmp
similarity index 100%
rename from Tests/images/hopper_rle8_greyscale.bmp
rename to Tests/images/hopper_rle8_grayscale.bmp
diff --git a/Tests/images/imagedraw_default_font_size.png b/Tests/images/imagedraw_default_font_size.png
new file mode 100644
index 000000000..f695b5cd6
Binary files /dev/null and b/Tests/images/imagedraw_default_font_size.png differ
diff --git a/Tests/images/imagedraw_rectangle_I.png b/Tests/images/imagedraw_rectangle_I.png
index 4e94f6943..a75f12c2e 100644
Binary files a/Tests/images/imagedraw_rectangle_I.png and b/Tests/images/imagedraw_rectangle_I.png differ
diff --git a/Tests/images/truncated_exif_dpi.jpg b/Tests/images/truncated_exif_dpi.jpg
new file mode 100644
index 000000000..b41ab4004
Binary files /dev/null and b/Tests/images/truncated_exif_dpi.jpg differ
diff --git a/Tests/images/unimplemented_pixel_format.dds b/Tests/images/unimplemented_pfflags.dds
similarity index 99%
rename from Tests/images/unimplemented_pixel_format.dds
rename to Tests/images/unimplemented_pfflags.dds
index 41a343886..e3fc8344d 100755
Binary files a/Tests/images/unimplemented_pixel_format.dds and b/Tests/images/unimplemented_pfflags.dds differ
diff --git a/Tests/images/unsupported_bitcount_luminance.dds b/Tests/images/unsupported_bitcount_luminance.dds
new file mode 100644
index 000000000..f9bb82254
Binary files /dev/null and b/Tests/images/unsupported_bitcount_luminance.dds differ
diff --git a/Tests/images/unsupported_bitcount_rgb.dds b/Tests/images/unsupported_bitcount_rgb.dds
new file mode 100644
index 000000000..77d527507
Binary files /dev/null and b/Tests/images/unsupported_bitcount_rgb.dds differ
diff --git a/Tests/images/xmp_no_prefix.jpg b/Tests/images/xmp_no_prefix.jpg
new file mode 100644
index 000000000..bcd78c7ed
Binary files /dev/null and b/Tests/images/xmp_no_prefix.jpg differ
diff --git a/Tests/images/xmp_padded.jpg b/Tests/images/xmp_padded.jpg
new file mode 100644
index 000000000..9ecfb3efe
Binary files /dev/null and b/Tests/images/xmp_padded.jpg differ
diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh
index 37fad7bc8..3aa6c7f6a 100755
--- a/Tests/oss-fuzz/build.sh
+++ b/Tests/oss-fuzz/build.sh
@@ -15,7 +15,7 @@
#
################################################################################
-python3 setup.py build --build-base=/tmp/build install
+python3 -m pip install .
# Build fuzzers in $OUT.
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index 50155991d..354df246d 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -233,13 +233,13 @@ def test_apng_mode():
assert im.getpixel((0, 0)) == (0, 0, 128, 191)
assert im.getpixel((64, 32)) == (0, 0, 128, 191)
- with Image.open("Tests/images/apng/mode_greyscale.png") as im:
+ with Image.open("Tests/images/apng/mode_grayscale.png") as im:
assert im.mode == "L"
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == 128
assert im.getpixel((64, 32)) == 255
- with Image.open("Tests/images/apng/mode_greyscale_alpha.png") as im:
+ with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
assert im.mode == "LA"
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (128, 191)
@@ -352,15 +352,13 @@ def test_apng_save(tmp_path):
im.load()
assert not im.is_animated
assert im.n_frames == 1
- assert im.get_format_mimetype() == "image/apng"
+ assert im.get_format_mimetype() == "image/png"
assert im.info.get("default_image") is None
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/single_frame_default.png") as im:
- frames = []
- for frame_im in ImageSequence.Iterator(im):
- frames.append(frame_im.copy())
+ frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)]
frames[0].save(
test_file, save_all=True, default_image=True, append_images=frames[1:]
)
@@ -452,26 +450,29 @@ def test_apng_save_duration_loop(tmp_path):
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
)
with Image.open(test_file) as im:
- im.load()
assert im.n_frames == 1
- assert im.info.get("duration") == 750
+ assert "duration" not in im.info
+
+ different_frame = Image.new("RGBA", (128, 64))
+ frame.save(
+ test_file,
+ save_all=True,
+ append_images=[frame, different_frame],
+ duration=[500, 100, 150],
+ )
+ with Image.open(test_file) as im:
+ assert im.n_frames == 2
+ assert im.info["duration"] == 600
+
+ im.seek(1)
+ assert im.info["duration"] == 150
# test info duration
- frame.info["duration"] = 750
- frame.save(test_file, save_all=True)
+ frame.info["duration"] = 300
+ frame.save(test_file, save_all=True, append_images=[frame, different_frame])
with Image.open(test_file) as im:
- 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
+ assert im.n_frames == 2
+ assert im.info["duration"] == 600
def test_apng_save_disposal(tmp_path):
@@ -723,10 +724,17 @@ def test_seek_after_close():
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
-def test_different_modes_in_later_frames(mode, tmp_path):
+@pytest.mark.parametrize("default_image", (True, False))
+@pytest.mark.parametrize("duplicate", (True, False))
+def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):
test_file = str(tmp_path / "temp.png")
im = Image.new("L", (1, 1))
- im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
+ im.save(
+ test_file,
+ save_all=True,
+ default_image=default_image,
+ append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],
+ )
with Image.open(test_file) as reloaded:
assert reloaded.mode == mode
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index 9e79937e9..58a45aa0b 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -159,7 +159,7 @@ def test_rle8():
with Image.open("Tests/images/hopper_rle8.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
- with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im:
+ with Image.open("Tests/images/hopper_rle8_grayscale.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
# This test image has been manually hexedited
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index bb9af7967..0dd3d5bb9 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -12,9 +12,14 @@ TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
TEST_FILE_ATI1 = "Tests/images/ati1.dds"
TEST_FILE_ATI2 = "Tests/images/ati2.dds"
+TEST_FILE_DX10_BC4_TYPELESS = "Tests/images/bc4_typeless.dds"
+TEST_FILE_DX10_BC4_UNORM = "Tests/images/bc4_unorm.dds"
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
+TEST_FILE_DX10_BC1 = "Tests/images/bc1.dds"
+TEST_FILE_DX10_BC1_TYPELESS = "Tests/images/bc1_typeless.dds"
+TEST_FILE_BC4U = "Tests/images/bc4u.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_BC5U = "Tests/images/bc5u.dds"
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
@@ -29,11 +34,20 @@ TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
-def test_sanity_dxt1():
- """Check DXT1 images can be opened"""
+@pytest.mark.parametrize(
+ "image_path",
+ (
+ TEST_FILE_DXT1,
+ # hexeditted to use DX10 FourCC
+ TEST_FILE_DX10_BC1,
+ TEST_FILE_DX10_BC1_TYPELESS,
+ ),
+)
+def test_sanity_dxt1_bc1(image_path):
+ """Check DXT1 and BC1 images can be opened"""
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
target = target.convert("RGBA")
- with Image.open(TEST_FILE_DXT1) as im:
+ with Image.open(image_path) as im:
im.load()
assert im.format == "DDS"
@@ -69,10 +83,18 @@ def test_sanity_dxt5():
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
-def test_sanity_ati1():
- """Check ATI1 images can be opened"""
+@pytest.mark.parametrize(
+ "image_path",
+ (
+ TEST_FILE_ATI1,
+ # hexeditted to use BC4U FourCC
+ TEST_FILE_BC4U,
+ ),
+)
+def test_sanity_ati1_bc4u(image_path):
+ """Check ATI1 and BC4U images can be opened"""
- with Image.open(TEST_FILE_ATI1) as im:
+ with Image.open(image_path) as im:
im.load()
assert im.format == "DDS"
@@ -82,6 +104,27 @@ def test_sanity_ati1():
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
+@pytest.mark.parametrize(
+ "image_path",
+ (
+ TEST_FILE_DX10_BC4_UNORM,
+ # hexeditted to be typeless
+ TEST_FILE_DX10_BC4_TYPELESS,
+ ),
+)
+def test_dx10_bc4(image_path):
+ """Check DX10 BC4 images can be opened"""
+
+ with Image.open(image_path) as im:
+ im.load()
+
+ assert im.format == "DDS"
+ assert im.mode == "L"
+ assert im.size == (64, 64)
+
+ assert_image_equal_tofile(im, TEST_FILE_DX10_BC4_UNORM.replace(".dds", ".png"))
+
+
@pytest.mark.parametrize(
"image_path",
(
@@ -199,12 +242,6 @@ def test_dx10_r8g8b8a8_unorm_srgb():
)
-def test_unimplemented_dxgi_format():
- with pytest.raises(NotImplementedError):
- with Image.open("Tests/images/unimplemented_dxgi_format.dds"):
- pass
-
-
@pytest.mark.parametrize(
("mode", "size", "test_file"),
[
@@ -303,9 +340,29 @@ def test_palette():
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
-def test_unimplemented_pixel_format():
+@pytest.mark.parametrize(
+ "test_file",
+ (
+ "Tests/images/unsupported_bitcount_rgb.dds",
+ "Tests/images/unsupported_bitcount_luminance.dds",
+ ),
+)
+def test_unsupported_bitcount(test_file):
+ with pytest.raises(OSError):
+ with Image.open(test_file):
+ pass
+
+
+@pytest.mark.parametrize(
+ "test_file",
+ (
+ "Tests/images/unimplemented_dxgi_format.dds",
+ "Tests/images/unimplemented_pfflags.dds",
+ ),
+)
+def test_not_implemented(test_file):
with pytest.raises(NotImplementedError):
- with Image.open("Tests/images/unimplemented_pixel_format.dds"):
+ with Image.open(test_file):
pass
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index 4f4d26fb1..259cf75c3 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -8,6 +8,7 @@ from .helper import (
assert_image_similar,
assert_image_similar_tofile,
hopper,
+ is_win32,
mark_if_feature_version,
skip_unless_feature,
)
@@ -98,6 +99,20 @@ def test_load():
assert im.load()[0, 0] == (255, 255, 255)
+def test_binary():
+ if HAS_GHOSTSCRIPT:
+ assert EpsImagePlugin.gs_binary is not None
+ else:
+ assert EpsImagePlugin.gs_binary is False
+
+ if not is_win32():
+ assert EpsImagePlugin.gs_windows_binary is None
+ elif not HAS_GHOSTSCRIPT:
+ assert EpsImagePlugin.gs_windows_binary is False
+ else:
+ assert EpsImagePlugin.gs_windows_binary is not None
+
+
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index f2d1f4992..562868154 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -205,18 +205,39 @@ def test_optimize_full_l():
def test_optimize_if_palette_can_be_reduced_by_half():
- with Image.open("Tests/images/test.colors.gif") as im:
- # Reduce dimensions because original is too big for _get_optimize()
- im = im.resize((591, 443))
- im_rgb = im.convert("RGB")
+ im = Image.new("P", (8, 1))
+ im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
+ for i in range(8):
+ im.putpixel((i, 0), (i + 1, 0, 0))
for optimize, colors in ((False, 256), (True, 8)):
out = BytesIO()
- im_rgb.save(out, "GIF", optimize=optimize)
+ im.save(out, "GIF", optimize=optimize)
with Image.open(out) as reloaded:
assert len(reloaded.palette.palette) // 3 == colors
+def test_full_palette_second_frame(tmp_path):
+ out = str(tmp_path / "temp.gif")
+ im = Image.new("P", (1, 256))
+
+ full_palette_im = Image.new("P", (1, 256))
+ for i in range(256):
+ full_palette_im.putpixel((0, i), i)
+ full_palette_im.palette = ImagePalette.ImagePalette(
+ "RGB", bytearray(i // 3 for i in range(768))
+ )
+ full_palette_im.palette.dirty = 1
+
+ im.save(out, save_all=True, append_images=[full_palette_im])
+
+ with Image.open(out) as reloaded:
+ reloaded.seek(1)
+
+ for i in range(256):
+ reloaded.getpixel((0, i)) == i
+
+
def test_roundtrip(tmp_path):
out = str(tmp_path / "temp.gif")
im = hopper()
@@ -285,14 +306,14 @@ def test_save_all_progress():
out = BytesIO()
progress = []
- with Image.open("Tests/images/hopper.gif") as im:
- with Image.open("Tests/images/chi.gif") as im2:
- im.save(out, "GIF", save_all=True, append_images=[im2], progress=callback)
+ with Image.open("Tests/images/chi.gif") as im2:
+ im = Image.new("RGB", im2.size)
+ im.save(out, "GIF", save_all=True, append_images=[im2], progress=callback)
expected = [
{
"image_index": 0,
- "image_filename": "Tests/images/hopper.gif",
+ "image_filename": None,
"completed_frames": 1,
"total_frames": 32,
}
@@ -634,7 +655,7 @@ def test_save_dispose(tmp_path):
def test_dispose2_palette(tmp_path):
out = str(tmp_path / "temp.gif")
- # Four colors: white, grey, black, red
+ # Four colors: white, gray, black, red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
im_list = []
@@ -900,7 +921,14 @@ def test_identical_frames(tmp_path):
@pytest.mark.parametrize(
- "duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)
+ "duration",
+ (
+ [1000, 1500, 2000],
+ (1000, 1500, 2000),
+ # One more duration than the number of frames
+ [1000, 1500, 2000, 4000],
+ 1500,
+ ),
)
def test_identical_frames_to_single_frame(duration, tmp_path):
out = str(tmp_path / "temp.gif")
@@ -916,7 +944,7 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
assert reread.n_frames == 1
# Assert that the new duration is the total of the identical frames
- assert reread.info["duration"] == 8500
+ assert reread.info["duration"] == 4500
def test_loop_none(tmp_path):
@@ -1186,6 +1214,12 @@ def test_rgba_transparency(tmp_path):
assert_image_equal(hopper("P").convert("RGB"), reloaded)
+def test_background_outside_palettte(tmp_path):
+ with Image.open("Tests/images/background_outside_palette.gif") as im:
+ im.seek(1)
+ assert im.info["background"] == 255
+
+
def test_bbox(tmp_path):
out = str(tmp_path / "temp.gif")
@@ -1224,18 +1258,17 @@ def test_palette_save_L(tmp_path):
def test_palette_save_P(tmp_path):
- # Pass in a different palette, then construct what the image would look like.
- # Forcing a non-straight grayscale palette.
-
- im = hopper("P")
- palette = bytes(255 - i // 3 for i in range(768))
+ im = Image.new("P", (1, 2))
+ im.putpixel((0, 1), 1)
out = str(tmp_path / "temp.gif")
- im.save(out, palette=palette)
+ im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
with Image.open(out) as reloaded:
- im.putpalette(palette)
- assert_image_equal(reloaded, im)
+ reloaded_rgb = reloaded.convert("RGB")
+
+ assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)
+ assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
def test_palette_save_duplicate_entries(tmp_path):
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index 2d99528d3..dac35a8d0 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -1,5 +1,7 @@
import sys
-from io import StringIO
+from io import BytesIO, StringIO
+
+import pytest
from PIL import Image, IptcImagePlugin
@@ -30,6 +32,36 @@ def test_getiptcinfo_jpg_found():
assert iptc[(2, 101)] == b"Hungary"
+def test_getiptcinfo_fotostation():
+ # Arrange
+ with open(TEST_FILE, "rb") as fp:
+ data = bytearray(fp.read())
+ data[86] = 240
+ f = BytesIO(data)
+ with Image.open(f) as im:
+ # Act
+ iptc = IptcImagePlugin.getiptcinfo(im)
+
+ # Assert
+ for tag in iptc.keys():
+ if tag[0] == 240:
+ return
+ pytest.fail("FotoStation tag not found")
+
+
+def test_getiptcinfo_zero_padding():
+ # Arrange
+ with Image.open(TEST_FILE) as im:
+ im.info["photoshop"][0x0404] += b"\x00\x00\x00"
+
+ # Act
+ iptc = IptcImagePlugin.getiptcinfo(im)
+
+ # Assert
+ assert isinstance(iptc, dict)
+ assert len(iptc) == 3
+
+
def test_getiptcinfo_tiff_none():
# Arrange
with Image.open("Tests/images/hopper.tif") as im:
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index b62132613..ef070b6c5 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -643,6 +643,23 @@ class TestFileJpeg:
assert max(im2.quantization[0]) <= 255
assert max(im2.quantization[1]) <= 255
+ @pytest.mark.parametrize(
+ "blocks, rows, markers",
+ ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)),
+ )
+ def test_restart_markers(self, blocks, rows, markers):
+ im = Image.new("RGB", (32, 32)) # 16 MCUs
+ out = BytesIO()
+ im.save(
+ out,
+ format="JPEG",
+ restart_marker_blocks=blocks,
+ restart_marker_rows=rows,
+ # force 8x8 pixel MCUs
+ subsampling=0,
+ )
+ assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers
+
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg(self):
with Image.open(TEST_FILE) as img:
@@ -767,6 +784,13 @@ class TestFileJpeg:
# This should return the default
assert im.info.get("dpi") == (72, 72)
+ def test_dpi_exif_truncated(self):
+ # Arrange
+ with Image.open("Tests/images/truncated_exif_dpi.jpg") as im:
+ # Act / Assert
+ # This should return the default
+ assert im.info.get("dpi") == (72, 72)
+
def test_no_dpi_in_exif(self):
# Arrange
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
@@ -882,7 +906,10 @@ class TestFileJpeg:
def test_getxmp(self):
with Image.open("Tests/images/xmp_test.jpg") as im:
if ElementTree is None:
- with pytest.warns(UserWarning):
+ with pytest.warns(
+ UserWarning,
+ match="XMP data cannot be read without defusedxml dependency",
+ ):
assert im.getxmp() == {}
else:
xmp = im.getxmp()
@@ -905,6 +932,28 @@ class TestFileJpeg:
with Image.open("Tests/images/hopper.jpg") as im:
assert im.getxmp() == {}
+ def test_getxmp_no_prefix(self):
+ with Image.open("Tests/images/xmp_no_prefix.jpg") as im:
+ if ElementTree is None:
+ with pytest.warns(
+ UserWarning,
+ match="XMP data cannot be read without defusedxml dependency",
+ ):
+ assert im.getxmp() == {}
+ else:
+ assert im.getxmp() == {"xmpmeta": {"key": "value"}}
+
+ def test_getxmp_padded(self):
+ with Image.open("Tests/images/xmp_padded.jpg") as im:
+ if ElementTree is None:
+ with pytest.warns(
+ UserWarning,
+ match="XMP data cannot be read without defusedxml dependency",
+ ):
+ assert im.getxmp() == {}
+ else:
+ assert im.getxmp() == {"xmpmeta": None}
+
@pytest.mark.timeout(timeout=1)
def test_eof(self):
# Even though this decoder never says that it is finished
@@ -929,6 +978,28 @@ class TestFileJpeg:
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
+ def test_separate_tables(self):
+ im = hopper()
+ data = [] # [interchange, tables-only, image-only]
+ for streamtype in range(3):
+ out = BytesIO()
+ im.save(out, format="JPEG", streamtype=streamtype)
+ data.append(out.getvalue())
+
+ # SOI, EOI
+ for marker in b"\xff\xd8", b"\xff\xd9":
+ assert marker in data[1] and marker in data[2]
+ # DHT, DQT
+ for marker in b"\xff\xc4", b"\xff\xdb":
+ assert marker in data[1] and marker not in data[2]
+ # SOF0, SOS, APP0 (JFIF header)
+ for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
+ assert marker not in data[1] and marker in data[2]
+
+ with Image.open(BytesIO(data[0])) as interchange_im:
+ with Image.open(BytesIO(data[1] + data[2])) as combined_im:
+ assert_image_equal(interchange_im, combined_im)
+
def test_repr_jpeg(self):
im = hopper()
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index 99df26fc9..2016b3ccb 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -416,7 +416,7 @@ def test_plt_marker():
while True:
marker = out.read(2)
if not marker:
- assert False, "End of stream without PLT"
+ pytest.fail("End of stream without PLT")
jp2_boxid = _binary.i16be(marker)
if jp2_boxid == 0xFF4F:
@@ -426,7 +426,7 @@ def test_plt_marker():
# PLT
return
elif jp2_boxid == 0xFF93:
- assert False, "SOD without finding PLT first"
+ pytest.fail("SOD without finding PLT first")
hdr = out.read(2)
length = _binary.i16be(hdr)
diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py
index be7c8d0c8..926fdb26f 100644
--- a/Tests/test_file_palm.py
+++ b/Tests/test_file_palm.py
@@ -26,8 +26,7 @@ def open_with_magick(magick, tmp_path, f):
rc = subprocess.call(
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
)
- if rc:
- raise OSError
+ assert not rc
return Image.open(outfile)
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index f8df88d67..b8f481408 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -92,11 +92,11 @@ class TestFilePng:
assert im.format == "PNG"
assert im.get_format_mimetype() == "image/png"
- for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
+ for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
im = hopper(mode)
im.save(test_file)
with Image.open(test_file) as reloaded:
- if mode == "I;16":
+ if mode in ("I;16", "I;16B"):
reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im)
@@ -297,7 +297,7 @@ class TestFilePng:
assert_image(im, "RGBA", (10, 10))
assert im.getcolors() == [(100, (0, 0, 0, 0))]
- def test_save_greyscale_transparency(self, tmp_path):
+ def test_save_grayscale_transparency(self, tmp_path):
for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
in_file = "Tests/images/" + mode.lower() + "_trns.png"
with Image.open(in_file) as im:
@@ -665,7 +665,10 @@ class TestFilePng:
def test_getxmp(self):
with Image.open("Tests/images/color_snakes.png") as im:
if ElementTree is None:
- with pytest.warns(UserWarning):
+ with pytest.warns(
+ UserWarning,
+ match="XMP data cannot be read without defusedxml dependency",
+ ):
assert im.getxmp() == {}
else:
xmp = im.getxmp()
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index e405834b5..708212fa6 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -111,6 +111,11 @@ def test_rgba():
assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
+def test_layer_skip():
+ with Image.open("Tests/images/five_channels.psd") as im:
+ assert im.n_frames == 1
+
+
def test_icc_profile():
with Image.open(test_file) as im:
assert "icc_profile" in im.info
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index d4b545d7b..7989fad34 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -779,7 +779,10 @@ class TestFileTiff:
def test_getxmp(self):
with Image.open("Tests/images/lab.tif") as im:
if ElementTree is None:
- with pytest.warns(UserWarning):
+ with pytest.warns(
+ UserWarning,
+ match="XMP data cannot be read without defusedxml dependency",
+ ):
assert im.getxmp() == {}
else:
xmp = im.getxmp()
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index 037479f9f..dd47be8b2 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -118,7 +118,10 @@ def test_getxmp():
with Image.open("Tests/images/flower2.webp") as im:
if ElementTree is None:
- with pytest.warns(UserWarning):
+ with pytest.warns(
+ UserWarning,
+ match="XMP data cannot be read without defusedxml dependency",
+ ):
assert im.getxmp() == {}
else:
assert (
diff --git a/Tests/test_image.py b/Tests/test_image.py
index b9c57770c..f0861bb4f 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -1,4 +1,5 @@
import io
+import logging
import os
import shutil
import sys
@@ -638,8 +639,8 @@ class TestImage:
im.remap_palette(None)
def test_remap_palette_transparency(self):
- im = Image.new("P", (1, 2))
- im.putpixel((0, 1), 1)
+ im = Image.new("P", (1, 2), (0, 0, 0))
+ im.putpixel((0, 1), (255, 0, 0))
im.info["transparency"] = 0
im_remapped = im.remap_palette([1, 0])
@@ -906,6 +907,13 @@ class TestImage:
im = Image.new("RGB", size)
assert im.tobytes() == b""
+ @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
+ def test_zero_frombytes(self, size):
+ Image.frombytes("RGB", size, b"")
+
+ im = Image.new("RGB", size)
+ im.frombytes(b"")
+
def test_has_transparency_data(self):
for mode in ("1", "L", "P", "RGB"):
im = Image.new(mode, (1, 1))
@@ -992,7 +1000,7 @@ class TestImage:
with Image.open(os.path.join("Tests/images", path)) as im:
try:
im.load()
- assert False
+ pytest.fail()
except OSError as e:
buffer_overrun = str(e) == "buffer overrun when reading image file"
truncated = "image file is truncated" in str(e)
@@ -1003,10 +1011,19 @@ class TestImage:
with Image.open("Tests/images/fli_overrun2.bin") as im:
try:
im.seek(1)
- assert False
+ pytest.fail()
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
+ def test_close_graceful(self, caplog):
+ with Image.open("Tests/images/hopper.jpg") as im:
+ copy = im.copy()
+ with caplog.at_level(logging.DEBUG):
+ im.close()
+ copy.close()
+ assert len(caplog.records) == 0
+ assert im.fp is None
+
class MockEncoder:
pass
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index f80bc9c10..2b4fb7733 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -130,9 +130,16 @@ class TestImageGetPixel(AccessTest):
bands = Image.getmodebands(mode)
if bands == 1:
return 1
+ if mode in ("BGR;15", "BGR;16"):
+ # These modes have less than 8 bits per band
+ # So (1, 2, 3) cannot be roundtripped
+ return (16, 32, 49)
return tuple(range(1, bands + 1))
def check(self, mode, expected_color=None):
+ if self._need_cffi_access and mode.startswith("BGR;"):
+ pytest.skip("Support not added to deprecated module for BGR;* modes")
+
if not expected_color:
expected_color = self.color(mode)
@@ -203,6 +210,9 @@ class TestImageGetPixel(AccessTest):
"F",
"P",
"PA",
+ "BGR;15",
+ "BGR;16",
+ "BGR;24",
"RGB",
"RGBA",
"RGBX",
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 01a182cf1..f5775f09c 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -117,11 +117,11 @@ def test_trns_p(tmp_path):
f = str(tmp_path / "temp.png")
im_l = im.convert("L")
- assert im_l.info["transparency"] == 1 # undone
+ assert im_l.info["transparency"] == 0
im_l.save(f)
im_rgb = im.convert("RGB")
- assert im_rgb.info["transparency"] == (0, 1, 2) # undone
+ assert im_rgb.info["transparency"] == (0, 0, 0)
im_rgb.save(f)
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
index 0e6293349..4e40aec74 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -76,6 +76,15 @@ def test_mode_F():
assert list(im.getdata()) == target
+@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
+def test_mode_BGR(mode):
+ data = [(16, 32, 49), (32, 32, 98)]
+ im = Image.new(mode, (1, 2))
+ im.putdata(data)
+
+ assert list(im.getdata()) == data
+
+
def test_array_B():
# shouldn't segfault
# see https://github.com/python-pillow/Pillow/issues/1008
diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py
index 665e08a7e..376553344 100644
--- a/Tests/test_image_putpalette.py
+++ b/Tests/test_image_putpalette.py
@@ -84,3 +84,14 @@ def test_rgba_palette(mode, palette):
im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3]
assert im.palette.colors == {(1, 2, 3, 4): 0}
+
+
+def test_empty_palette():
+ im = Image.new("P", (1, 1))
+ assert im.getpalette() == []
+
+
+def test_undefined_palette_index():
+ im = Image.new("P", (1, 1), 3)
+ im.putpalette((1, 2, 3))
+ assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0)
diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py
index 981753eb9..3bafc4c9c 100644
--- a/Tests/test_image_quantize.py
+++ b/Tests/test_image_quantize.py
@@ -67,7 +67,7 @@ def test_quantize_no_dither():
def test_quantize_no_dither2():
im = Image.new("RGB", (9, 1))
- im.putdata(list((p,) * 3 for p in range(0, 36, 4)))
+ im.putdata([(p,) * 3 for p in range(0, 36, 4)])
palette = Image.new("P", (1, 1))
data = (0, 0, 0, 32, 32, 32)
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index 83c54cf62..b5bfa903f 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -195,7 +195,7 @@ class TestReducingGapResize:
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
)
- with pytest.raises(AssertionError):
+ with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im)
assert_image_similar(ref, im, epsilon)
@@ -210,7 +210,7 @@ class TestReducingGapResize:
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
)
- with pytest.raises(AssertionError):
+ with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im)
assert_image_similar(ref, im, epsilon)
@@ -225,7 +225,7 @@ class TestReducingGapResize:
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
)
- with pytest.raises(AssertionError):
+ with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im)
assert_image_similar(ref, im, epsilon)
diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py
index 4fd07a2b4..96a2c2662 100644
--- a/Tests/test_image_thumbnail.py
+++ b/Tests/test_image_thumbnail.py
@@ -147,7 +147,7 @@ def test_reducing_gap_values():
ref = hopper()
ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
- with pytest.raises(AssertionError):
+ with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im)
assert_image_similar(ref, im, 3.5)
diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py
index d0fea3854..e7687cc1b 100644
--- a/Tests/test_imagechops.py
+++ b/Tests/test_imagechops.py
@@ -10,7 +10,7 @@ GREEN = (0, 255, 0)
ORANGE = (255, 128, 0)
WHITE = (255, 255, 255)
-GREY = 128
+GRAY = 128
def test_sanity():
@@ -121,12 +121,12 @@ def test_constant():
im = Image.new("RGB", (20, 10))
# Act
- new = ImageChops.constant(im, GREY)
+ new = ImageChops.constant(im, GRAY)
# Assert
assert new.size == im.size
- assert new.getpixel((0, 0)) == GREY
- assert new.getpixel((19, 9)) == GREY
+ assert new.getpixel((0, 0)) == GRAY
+ assert new.getpixel((19, 9)) == GRAY
def test_darker_image():
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index ca6235447..4052c41ff 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1,8 +1,9 @@
+import contextlib
import os.path
import pytest
-from PIL import Image, ImageColor, ImageDraw, ImageFont
+from PIL import Image, ImageColor, ImageDraw, ImageFont, features
from .helper import (
assert_image_equal,
@@ -586,6 +587,18 @@ def test_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
+def test_point_I16():
+ # Arrange
+ im = Image.new("I;16", (1, 1))
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ draw.point((0, 0), fill=0x1234)
+
+ # Assert
+ assert im.getpixel((0, 0)) == 0x1234
+
+
@pytest.mark.parametrize("points", POINTS)
def test_polygon(points):
# Arrange
@@ -732,7 +745,7 @@ def test_rectangle_I16(bbox):
draw = ImageDraw.Draw(im)
# Act
- draw.rectangle(bbox, fill="black", outline="green")
+ draw.rectangle(bbox, outline=0xFFFF)
# Assert
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
@@ -1341,7 +1354,33 @@ def test_setting_default_font():
assert draw.getfont() == font
finally:
ImageDraw.ImageDraw.font = None
- assert isinstance(draw.getfont(), ImageFont.ImageFont)
+ assert isinstance(draw.getfont(), ImageFont.load_default().__class__)
+
+
+def test_default_font_size():
+ freetype_support = features.check_module("freetype2")
+ text = "Default font at a specific size."
+
+ im = Image.new("RGB", (220, 25))
+ draw = ImageDraw.Draw(im)
+ with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+ draw.text((0, 0), text, font_size=16)
+ assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
+
+ with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+ assert draw.textlength(text, font_size=16) == 216
+
+ with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+ assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
+
+ im = Image.new("RGB", (220, 25))
+ draw = ImageDraw.Draw(im)
+ with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+ draw.multiline_text((0, 0), text, font_size=16)
+ assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
+
+ with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
+ assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
@pytest.mark.parametrize("bbox", BBOX)
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index ff75b8c2a..2389c4717 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -115,8 +115,9 @@ class TestImageFile:
assert_image_equal(im1, im2)
def test_raise_oserror(self):
- with pytest.raises(OSError):
- ImageFile.raise_oserror(1)
+ with pytest.warns(DeprecationWarning):
+ with pytest.raises(OSError):
+ ImageFile.raise_oserror(1)
def test_raise_typeerror(self):
with pytest.raises(TypeError):
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 02622e721..0f1c52b66 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -4,6 +4,7 @@ import re
import shutil
import sys
from io import BytesIO
+from pathlib import Path
import pytest
from packaging.version import parse as parse_version
@@ -76,8 +77,9 @@ def _render(font, layout_engine):
return img
-def test_font_with_name(layout_engine):
- _render(FONT_PATH, layout_engine)
+@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
+def test_font_with_name(layout_engine, font):
+ _render(font, layout_engine)
def test_font_with_filelike(layout_engine):
@@ -141,7 +143,9 @@ def test_I16(font):
draw = ImageDraw.Draw(im)
txt = "Hello World!"
- draw.text((10, 10), txt, font=font)
+ draw.text((10, 10), txt, fill=0xFFFE, font=font)
+
+ assert im.getpixel((12, 14)) == 0xFFFE
target = "Tests/images/transparent_background_text_L.png"
assert_image_similar_tofile(im.convert("L"), target, 0.01)
@@ -301,8 +305,8 @@ def test_multiline_spacing(font):
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
)
def test_rotated_transposed_font(font, orientation):
- img_grey = Image.new("L", (100, 100))
- draw = ImageDraw.Draw(img_grey)
+ img_gray = Image.new("L", (100, 100))
+ draw = ImageDraw.Draw(img_gray)
word = "testing"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -342,8 +346,8 @@ def test_rotated_transposed_font(font, orientation):
),
)
def test_unrotated_transposed_font(font, orientation):
- img_grey = Image.new("L", (100, 100))
- draw = ImageDraw.Draw(img_grey)
+ img_gray = Image.new("L", (100, 100))
+ draw = ImageDraw.Draw(img_gray)
word = "testing"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -451,7 +455,7 @@ def test_load_non_font_bytes():
def test_default_font():
# Arrange
- txt = 'This is a "better than nothing" default font.'
+ txt = "This is a default font using FreeType support."
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -459,8 +463,11 @@ def test_default_font():
default_font = ImageFont.load_default()
draw.text((10, 10), txt, font=default_font)
+ larger_default_font = ImageFont.load_default(size=14)
+ draw.text((10, 60), txt, font=larger_default_font)
+
# Assert
- assert_image_equal_tofile(im, "Tests/images/default_font.png")
+ assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
@@ -483,14 +490,6 @@ def test_render_empty(font):
assert_image_equal(im, target)
-def test_unicode_pilfont():
- # should not segfault, should return UnicodeDecodeError
- # issue #2826
- font = ImageFont.load_default()
- with pytest.raises(UnicodeEncodeError):
- font.getbbox("’")
-
-
def test_unicode_extended(layout_engine):
# issue #3777
text = "A\u278A\U0001F12B"
@@ -720,14 +719,6 @@ def test_variation_set_by_axes(font):
_check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
-def test_textbbox_non_freetypefont():
- im = Image.new("RGB", (200, 200))
- d = ImageDraw.Draw(im)
- default_font = ImageFont.load_default()
- assert d.textlength("test", font=default_font) == 24
- assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
-
-
@pytest.mark.parametrize(
"anchor, left, top",
(
@@ -1082,3 +1073,9 @@ def test_raqm_missing_warning(monkeypatch):
"Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout."
)
+
+
+@pytest.mark.parametrize("size", [-1, 0])
+def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size):
+ with pytest.raises(ValueError):
+ ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py
new file mode 100644
index 000000000..c30463e81
--- /dev/null
+++ b/Tests/test_imagefontpil.py
@@ -0,0 +1,45 @@
+import pytest
+
+from PIL import Image, ImageDraw, ImageFont, features
+
+from .helper import assert_image_equal_tofile
+
+pytestmark = pytest.mark.skipif(
+ features.check_module("freetype2"),
+ reason="PILfont superseded if FreeType is supported",
+)
+
+
+def test_default_font():
+ # Arrange
+ txt = 'This is a "better than nothing" default font.'
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ default_font = ImageFont.load_default()
+ draw.text((10, 10), txt, font=default_font)
+
+ # Assert
+ assert_image_equal_tofile(im, "Tests/images/default_font.png")
+
+
+def test_size_without_freetype():
+ with pytest.raises(ImportError):
+ ImageFont.load_default(size=14)
+
+
+def test_unicode():
+ # should not segfault, should return UnicodeDecodeError
+ # issue #2826
+ font = ImageFont.load_default()
+ with pytest.raises(UnicodeEncodeError):
+ font.getbbox("’")
+
+
+def test_textbbox():
+ im = Image.new("RGB", (200, 200))
+ d = ImageDraw.Draw(im)
+ default_font = ImageFont.load_default()
+ assert d.textlength("test", font=default_font) == 24
+ assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index f8059eca4..a75cbadc4 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -11,6 +11,10 @@ from .helper import assert_image_equal_tofile, skip_unless_feature
class TestImageGrab:
+ @pytest.mark.skipif(
+ os.environ.get("USERNAME") == "ContainerAdministrator",
+ reason="can't grab screen when running in Docker",
+ )
@pytest.mark.skipif(
sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
)
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index b05785be0..a3bb536ce 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -39,6 +39,9 @@ def test_sanity():
ImageOps.contain(hopper("L"), (128, 128))
ImageOps.contain(hopper("RGB"), (128, 128))
+ ImageOps.cover(hopper("L"), (128, 128))
+ ImageOps.cover(hopper("RGB"), (128, 128))
+
ImageOps.crop(hopper("L"), 1)
ImageOps.crop(hopper("RGB"), 1)
@@ -119,6 +122,20 @@ def test_contain_round():
assert new_im.height == 5
+@pytest.mark.parametrize(
+ "image_name, expected_size",
+ (
+ ("colr_bungee.png", (1024, 256)), # landscape
+ ("imagedraw_stroke_multiline.png", (256, 640)), # portrait
+ ("hopper.png", (256, 256)), # square
+ ),
+)
+def test_cover(image_name, expected_size):
+ with Image.open("Tests/images/" + image_name) as im:
+ new_im = ImageOps.cover(im, (256, 256))
+ assert new_im.size == expected_size
+
+
def test_pad():
# Same ratio
im = hopper()
@@ -416,6 +433,12 @@ def test_exif_transpose_in_place():
assert_image_equal(im, expected)
+def test_autocontrast_unsupported_mode():
+ im = Image.new("RGBA", (1, 1))
+ with pytest.raises(OSError):
+ ImageOps.autocontrast(im)
+
+
def test_autocontrast_cutoff():
# Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:
diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py
index e54372b60..3e73339ed 100644
--- a/Tests/test_imageshow.py
+++ b/Tests/test_imageshow.py
@@ -85,7 +85,7 @@ def test_ipythonviewer():
test_viewer = viewer
break
else:
- assert False
+ pytest.fail()
im = hopper()
assert test_viewer.show(im) == 1
diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py
index f7812f62b..2a4d9acf4 100644
--- a/Tests/test_lib_pack.py
+++ b/Tests/test_lib_pack.py
@@ -340,6 +340,17 @@ class TestLibUnpack:
self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))
+ self.assert_unpack(
+ "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233)
+ )
+
+ def test_BGR(self):
+ self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8))
+ self.assert_unpack(
+ "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
+ )
+ self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
+
def test_RGBA(self):
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
self.assert_unpack(
diff --git a/_custom_build/backend.py b/_custom_build/backend.py
index 9b3265a94..23225d6b8 100644
--- a/_custom_build/backend.py
+++ b/_custom_build/backend.py
@@ -1,6 +1,6 @@
import sys
-from setuptools.build_meta import * # noqa: F401, F403
+from setuptools.build_meta import * # noqa: F403
from setuptools.build_meta import build_wheel
backend_class = build_wheel.__self__.__class__
diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh
index ab94875d8..b7cebbdbf 100755
--- a/depends/install_imagequant.sh
+++ b/depends/install_imagequant.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# install libimagequant
-archive=libimagequant-4.2.1
+archive=libimagequant-4.2.2
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
diff --git a/docs/about.rst b/docs/about.rst
index 03829c133..872ac0ea6 100644
--- a/docs/about.rst
+++ b/docs/about.rst
@@ -12,7 +12,7 @@ The fork author's goal is to foster and support active development of PIL throug
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
-.. _Travis CI: https://app.travis-ci.com/github/python-pillow/pillow-wheels
+.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow
.. _GitHub: https://github.com/python-pillow/Pillow
.. _Python Package Index: https://pypi.org/project/Pillow/
diff --git a/docs/conf.py b/docs/conf.py
index a2c825292..833dfa215 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -166,6 +166,12 @@ html_static_path = ["resources"]
# directly to the root of the documentation.
# html_extra_path = []
+html_css_files = ["css/dark.css"]
+
+html_js_files = [
+ "js/activate_tab.js",
+]
+
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
@@ -313,19 +319,15 @@ texinfo_documents = [
# texinfo_no_detailmenu = False
-def setup(app):
- app.add_css_file("css/dark.css")
-
-
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://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*",
+ r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg",
+ r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",
+ r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",
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
+ r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*",
+ r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",
+ r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",
}
# sphinx.ext.extlinks
@@ -338,6 +340,7 @@ extlinks = {
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
"issue": (_repo + "issues/%s", "#%s"),
"pr": (_repo + "pull/%s", "#%s"),
+ "pypi": ("https://pypi.org/project/%s/", "%s"),
}
# sphinxext.opengraph
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index ce956cade..75c0b73eb 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -10,7 +10,7 @@ Deprecated features
-------------------
Below are features which are considered deprecated. Where appropriate,
-a ``DeprecationWarning`` is issued.
+a :py:exc:`DeprecationWarning` is issued.
PSFile
~~~~~~
@@ -34,6 +34,16 @@ Since Pillow's C API is now faster than PyAccess on PyPy,
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
similarly deprecated.
+ImageFile.raise_oserror
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 10.2.0
+
+``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow
+12.0.0 (2025-10-15). The function is undocumented and is only useful for translating
+error codes returned by a codec's ``decode()`` method, which ImageFile already does
+automatically.
+
Removed features
----------------
@@ -267,7 +277,7 @@ ImageFile.raise_ioerror
.. deprecated:: 7.2.0
.. versionremoved:: 9.0.0
-``IOError`` was merged into ``OSError`` in Python 3.3.
+:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3.
So, ``ImageFile.raise_ioerror`` has been removed.
Use ``ImageFile.raise_oserror`` instead.
@@ -293,9 +303,9 @@ im.offset
``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead.
It was documented as deprecated in PIL 1.1.2,
-raised a ``DeprecationWarning`` since 1.1.5,
-an ``Exception`` since Pillow 3.0.0
-and ``NotImplementedError`` since 3.3.0.
+raised a :py:exc:`DeprecationWarning` since 1.1.5,
+an :py:exc:`Exception` since Pillow 3.0.0
+and :py:exc:`NotImplementedError` since 3.3.0.
Image.fromstring, im.fromstring and im.tostring
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -307,9 +317,9 @@ Image.fromstring, im.fromstring and im.tostring
* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead.
* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead.
-They issued a ``DeprecationWarning`` since 2.0.0,
-an ``Exception`` since 3.0.0
-and ``NotImplementedError`` since 3.3.0.
+They issued a :py:exc:`DeprecationWarning` since 2.0.0,
+an :py:exc:`Exception` since 3.0.0
+and :py:exc:`NotImplementedError` since 3.3.0.
ImageCms.CmsProfile attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -318,7 +328,7 @@ ImageCms.CmsProfile attributes
.. versionremoved:: 8.0.0
Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0,
-they issued a ``DeprecationWarning``:
+they issued a :py:exc:`DeprecationWarning`:
======================== ===================================================
Removed Use instead
@@ -442,7 +452,7 @@ PIL.OleFileIO
.. deprecated:: 4.0.0
.. versionremoved:: 6.0.0
-PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
-the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0
+``PIL.OleFileIO`` was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
+the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
PyPI (eg. ``python3 -m pip install olefile``).
diff --git a/docs/example/image_thumbnail.png b/docs/example/image_thumbnail.png
new file mode 100644
index 000000000..293b05794
Binary files /dev/null and b/docs/example/image_thumbnail.png differ
diff --git a/docs/example/imageops_contain.png b/docs/example/imageops_contain.png
new file mode 100644
index 000000000..293b05794
Binary files /dev/null and b/docs/example/imageops_contain.png differ
diff --git a/docs/example/imageops_cover.png b/docs/example/imageops_cover.png
new file mode 100644
index 000000000..929e1d874
Binary files /dev/null and b/docs/example/imageops_cover.png differ
diff --git a/docs/example/imageops_fit.png b/docs/example/imageops_fit.png
new file mode 100644
index 000000000..13a3d5e3f
Binary files /dev/null and b/docs/example/imageops_fit.png differ
diff --git a/docs/example/imageops_pad.png b/docs/example/imageops_pad.png
new file mode 100644
index 000000000..69649d6e5
Binary files /dev/null and b/docs/example/imageops_pad.png differ
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 1f785bc21..9cd65fd48 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -115,8 +115,13 @@ in ``L``, ``RGB`` and ``CMYK`` modes.
Loading
~~~~~~~
+To use Ghostscript, Pillow searches for the "gs" executable. On Windows, it
+also searches for "gswin32c" and "gswin64c". To customise this behaviour,
+``EpsImagePlugin.gs_binary = "gswin64"`` will set the name of the executable to
+use. ``EpsImagePlugin.gs_binary = False`` will prevent Ghostscript use.
+
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
-method with the following parameters to affect how Ghostscript renders the EPS
+method with the following parameters to affect how Ghostscript renders the EPS.
**scale**
Affects the scale of the resultant rasterized image. If the EPS suggests
@@ -261,9 +266,13 @@ following options are available::
:py:class:`PIL.ImagePalette.ImagePalette` object.
**optimize**
- If present and true, attempt to compress the palette by
- eliminating unused colors. This is only useful if the palette can
- be compressed to the next smaller power of 2 elements.
+ Whether to attempt to compress the palette by eliminating unused colors
+ (this is only useful if the palette can be compressed to the next smaller
+ power of 2 elements) and whether to mark all pixels that are not new in the
+ next frame as transparent.
+
+ This is attempted by default, unless a palette is specified as an option or
+ as part of the first image's :py:attr:`~PIL.Image.Image.info` dictionary.
Note that if the image you are saving comes from an existing GIF, it may have
the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary.
@@ -489,6 +498,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
If absent, the setting will be determined by libjpeg or libjpeg-turbo.
+**restart_marker_blocks**
+ If present, emit a restart marker whenever the specified number of MCU
+ blocks has been produced.
+
+ .. versionadded:: 10.2.0
+
+**restart_marker_rows**
+ If present, emit a restart marker whenever the specified number of MCU
+ rows has been produced.
+
+ .. versionadded:: 10.2.0
+
**qtables**
If present, sets the qtables for the encoder. This is listed as an
advanced option for wizards in the JPEG documentation. Use with
@@ -501,6 +522,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
.. versionadded:: 2.5.0
+**streamtype**
+ Allows storing images without quantization and Huffman tables, or with
+ these tables but without image data. This is useful for container formats
+ or network protocols that handle tables separately and share them between
+ images.
+
+ * ``0`` (default): interchange datastream, with tables and image data
+ * ``1``: abbreviated table specification (tables-only) datastream
+
+ .. versionadded:: 10.2.0
+
+ * ``2``: abbreviated image (image-only) datastream
+
**comment**
A comment about the image.
@@ -1291,6 +1325,8 @@ Pillow reads Kodak FlashPix files. In the current version, only the highest
resolution image is read from the file, and the viewing transform is not taken
into account.
+To enable FPX support, you must install :pypi:`olefile`.
+
.. note::
To enable full FlashPix support, you need to build and install the IJG JPEG
@@ -1367,6 +1403,8 @@ the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.s
Note that there may be an embedded gamma of 2.2 in MIC files.
+To enable MIC support, you must install :pypi:`olefile`.
+
MPO
^^^
diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst
index 50133f15e..d79f2465f 100644
--- a/docs/handbook/tutorial.rst
+++ b/docs/handbook/tutorial.rst
@@ -26,7 +26,7 @@ image. If the image was not read from a file, it is set to None. The size
attribute is a 2-tuple containing width and height (in pixels). The
:py:attr:`~PIL.Image.Image.mode` attribute defines the number and names of the
bands in the image, and also the pixel type and depth. Common modes are “L”
-(luminance) for greyscale images, “RGB” for true color images, and “CMYK” for
+(luminance) for grayscale images, “RGB” for true color images, and “CMYK” for
pre-press images.
If the file cannot be opened, an :py:exc:`OSError` exception is raised.
@@ -268,6 +268,37 @@ true, to provide for the same changes to the image's size.
A more general form of image transformations can be carried out via the
:py:meth:`~PIL.Image.Image.transform` method.
+Relative resizing
+^^^^^^^^^^^^^^^^^
+
+Instead of calculating the size of the new image when resizing, you can also
+choose to resize relative to a given size.
+
+::
+
+ from PIL import Image, ImageOps
+ size = (100, 150)
+ with Image.open("Tests/images/hopper.png") as im:
+ ImageOps.contain(im, size).save("imageops_contain.png")
+ ImageOps.cover(im, size).save("imageops_cover.png")
+ ImageOps.fit(im, size).save("imageops_fit.png")
+ ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
+
+ # thumbnail() can also be used,
+ # but will modify the image object in place
+ im.thumbnail(size)
+ im.save("imageops_thumbnail.png")
+
++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
+| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
++================+===========================================+============================================+==========================================+========================================+========================================+
+|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
+|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
+|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
+
.. _color-transforms:
Color transforms
@@ -568,7 +599,7 @@ Controlling the decoder
Some decoders allow you to manipulate the image while reading it from a file.
This can often be used to speed up decoding when creating thumbnails (when
speed is usually more important than quality) and printing to a monochrome
-laser printer (when only a greyscale version of the image is needed).
+laser printer (when only a grayscale version of the image is needed).
The :py:meth:`~PIL.Image.Image.draft` method manipulates an opened but not yet
loaded image so it as closely as possible matches the given mode and size. This
diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst
index ca16fccda..956d63aa7 100644
--- a/docs/handbook/writing-your-own-image-plugin.rst
+++ b/docs/handbook/writing-your-own-image-plugin.rst
@@ -26,14 +26,14 @@ Pillow decodes files in two stages:
it.
An image plugin should contain a format handler derived from the
-:py:class:`PIL.ImageFile.ImageFile` base class. This class should
-provide an ``_open`` method, which reads the file header and
-sets up at least the :py:attr:`~PIL.Image.Image.mode` and
-:py:attr:`~PIL.Image.Image.size` attributes. To be able to load the
-file, the method must also create a list of ``tile`` descriptors,
-which contain a decoder name, extents of the tile, and
-any decoder-specific data. The format handler class must be explicitly
-registered, via a call to the :py:mod:`~PIL.Image` module.
+:py:class:`PIL.ImageFile.ImageFile` base class. This class should provide an
+``_open`` method, which reads the file header and set at least the internal
+``_size`` and ``_mode`` attributes so that :py:attr:`~PIL.Image.Image.mode` and
+:py:attr:`~PIL.Image.Image.size` are populated. To be able to load the file,
+the method must also create a list of ``tile`` descriptors, which contain a
+decoder name, extents of the tile, and any decoder-specific data. The format
+handler class must be explicitly registered, via a call to the
+:py:mod:`~PIL.Image` module.
.. note:: For performance reasons, it is important that the
``_open`` method quickly rejects files that do not have the
@@ -45,7 +45,7 @@ Example
The following plugin supports a simple format, which has a 128-byte header
consisting of the words “SPAM” followed by the width, height, and pixel size in
bits. The header fields are separated by spaces. The image data follows
-directly after the header, and can be either bi-level, greyscale, or 24-bit
+directly after the header, and can be either bi-level, grayscale, or 24-bit
true color.
**SpamImagePlugin.py**::
@@ -96,13 +96,13 @@ true color.
)
-The format handler must always set the
-:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.Image.Image.mode`
-attributes. If these are not set, the file cannot be opened. To
-simplify the plugin, the calling code considers exceptions like
-:py:exc:`SyntaxError`, :py:exc:`KeyError`, :py:exc:`IndexError`,
-:py:exc:`EOFError` and :py:exc:`struct.error` as a failure to identify
-the file.
+The format handler must always set the internal ``_size`` and ``_mode``
+attributes so that :py:attr:`~PIL.Image.Image.size` and
+:py:attr:`~PIL.Image.Image.mode` are populated. If these are not set, the file
+cannot be opened. To simplify the plugin, the calling code considers exceptions
+like :py:exc:`SyntaxError`, :py:exc:`KeyError`, :py:exc:`IndexError`,
+:py:exc:`EOFError` and :py:exc:`struct.error` as a failure to identify the
+file.
Note that the image plugin must be explicitly registered using
:py:func:`PIL.Image.register_open`. Although not required, it is also a good
@@ -211,9 +211,9 @@ table describes some commonly used **raw modes**:
| ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the |
| | | least significant bit. 0 means black, 1 means white. |
+-----------+-------------------------------------------------------------------+
-| ``L`` | 8-bit greyscale. 0 means black, 255 means white. |
+| ``L`` | 8-bit grayscale. 0 means black, 255 means white. |
+-----------+-------------------------------------------------------------------+
-| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. |
+| ``L;I`` | 8-bit inverted grayscale. 0 means white, 255 means black. |
+-----------+-------------------------------------------------------------------+
| ``P`` | 8-bit palette-mapped image. |
+-----------+-------------------------------------------------------------------+
diff --git a/docs/index.rst b/docs/index.rst
index 418844ba7..4f577fe9c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -37,12 +37,12 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more
+ document.addEventListener('DOMContentLoaded', function() {
+ activateTab(getOS());
+ });
+
+
Warnings
--------
@@ -42,6 +50,11 @@ Install Pillow with :command:`pip`::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
+Optionally, install :pypi:`defusedxml` for Pillow to read XMP data,
+and :pypi:`olefile` for Pillow to read FPX and MIC images::
+
+ python3 -m pip install --upgrade defusedxml olefile
+
.. tab:: Linux
@@ -82,9 +95,10 @@ Install Pillow with :command:`pip`::
.. tab:: Windows
- We provide Pillow binaries for Windows compiled for the matrix of
- supported Pythons in 64-bit versions in the wheel format. These binaries include
- support for all optional libraries except libimagequant and libxcb. Raqm support
+ We provide Pillow binaries for Windows compiled for the matrix of supported
+ Pythons in the wheel format. These include x86, x86-64 and arm64 versions
+ (with the exception of Python 3.8 on arm64). These binaries include support
+ for all optional libraries except libimagequant and libxcb. Raqm support
requires FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
@@ -144,24 +158,24 @@ Many of Pillow's features require external libraries:
* Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and
libjpeg-turbo version **8**.
- * Starting with Pillow 3.0.0, libjpeg is required by default, but
- may be disabled with the ``--disable-jpeg`` flag.
+ * Starting with Pillow 3.0.0, libjpeg is required by default. It can be
+ disabled with the ``-C jpeg=disable`` flag.
* **zlib** provides access to compressed PNGs
- * Starting with Pillow 3.0.0, zlib is required by default, but may
- be disabled with the ``--disable-zlib`` flag.
+ * Starting with Pillow 3.0.0, zlib is required by default. It can be
+ disabled with the ``-C zlib=disable`` flag.
* **libtiff** provides compressed TIFF functionality
- * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5.1**
+ * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
* **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.15**.
+ above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
* **libwebp** provides the WebP format.
@@ -169,8 +183,6 @@ Many of Pillow's features require external libraries:
transparent WebP files. Versions **0.3.0** and above support
transparency.
-* **tcl/tk** provides support for tkinter bitmap and photo images.
-
* **openjpeg** provides JPEG 2000 functionality.
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
@@ -180,7 +192,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
- * Pillow has been tested with libimagequant **2.6-4.2.1**
+ * Pillow has been tested with libimagequant **2.6-4.2.2**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.
@@ -354,7 +366,7 @@ 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
+:file:`pyproject.toml`, or by adding environment variables on the command
line::
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
@@ -454,10 +466,10 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Fedora 37 | 3.11 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
+| Fedora 39 | 3.12 | x86-64 |
++----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
@@ -476,7 +488,7 @@ These platforms are built and tested for every change.
| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, PyPy3 | |
| +----------------------------+---------------------+
-| | 3.11 | x86 |
+| | 3.12 | x86 |
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+
@@ -494,93 +506,97 @@ These platforms have been reported to work at the versions mentioned.
Contributors please test Pillow on your platform then update this
document and send a pull request.
-+----------------------------------+---------------------------+------------------+--------------+
-| Operating system | | Tested Python | | Latest tested | | Tested |
-| | | versions | | Pillow version | | processors |
-+==================================+===========================+==================+==============+
-| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
-+----------------------------------+---------------------------+------------------+--------------+
-| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
-| +---------------------------+------------------+ |
-| | 3.7 | 9.5.0 | |
-+----------------------------------+---------------------------+------------------+--------------+
-| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
-+----------------------------------+---------------------------+------------------+--------------+
-| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
-| +---------------------------+------------------+--------------+
-| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
-| +---------------------------+------------------+ |
-| | 3.6 | 8.4.0 | |
-+----------------------------------+---------------------------+------------------+--------------+
-| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
-| +---------------------------+------------------+ |
-| | 3.5 | 7.2.0 | |
-+----------------------------------+---------------------------+------------------+--------------+
-| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
-| +---------------------------+------------------+ |
-| | 2.7 | 6.0.0 | |
-| +---------------------------+------------------+ |
-| | 3.4 | 5.4.1 | |
-+----------------------------------+---------------------------+------------------+--------------+
-| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
-| +---------------------------+------------------+ |
-| | 3.3 | 4.1.0 | |
-+----------------------------------+---------------------------+------------------+--------------+
-| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Redhat Linux 6 | 2.6 | |x86 |
-+----------------------------------+---------------------------+------------------+--------------+
-| CentOS 6.3 | 2.7, 3.3 | |x86 |
-+----------------------------------+---------------------------+------------------+--------------+
-| CentOS 8 | 3.9 | 9.0.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
-| | | PyPy5.3.1, PyPy3 v2.4.0 | | |
-| +---------------------------+------------------+--------------+
-| | 2.7 | 4.3.0 |x86-64 |
-| +---------------------------+------------------+--------------+
-| | 2.7, 3.2 | 3.4.1 |ppc |
-+----------------------------------+---------------------------+------------------+--------------+
-| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
-+----------------------------------+---------------------------+------------------+--------------+
-| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
-+----------------------------------+---------------------------+------------------+--------------+
-| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm |
-| +---------------------------+------------------+ |
-| | 2.7 | 6.2.2 | |
-+----------------------------------+---------------------------+------------------+--------------+
-| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Windows 10 | 3.7 | 7.1.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
-| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
-+----------------------------------+---------------------------+------------------+--------------+
++----------------------------------+----------------------------+------------------+--------------+
+| Operating system | | Tested Python | | Latest tested | | Tested |
+| | | versions | | Pillow version | | processors |
++==================================+============================+==================+==============+
+| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
+| +----------------------------+------------------+ |
+| | 3.7 | 9.5.0 | |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
+| +----------------------------+------------------+--------------+
+| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
+| +----------------------------+------------------+ |
+| | 3.6 | 8.4.0 | |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
+| +----------------------------+------------------+ |
+| | 3.5 | 7.2.0 | |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
+| +----------------------------+------------------+ |
+| | 2.7 | 6.0.0 | |
+| +----------------------------+------------------+ |
+| | 3.4 | 5.4.1 | |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
+| +----------------------------+------------------+ |
+| | 3.3 | 4.1.0 | |
++----------------------------------+----------------------------+------------------+--------------+
+| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Redhat Linux 6 | 2.6 | |x86 |
++----------------------------------+----------------------------+------------------+--------------+
+| CentOS 6.3 | 2.7, 3.3 | |x86 |
++----------------------------------+----------------------------+------------------+--------------+
+| CentOS 8 | 3.9 | 9.0.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
+| | | PyPy5.3.1, PyPy3 v2.4.0 | | |
+| +----------------------------+------------------+--------------+
+| | 2.7 | 4.3.0 |x86-64 |
+| +----------------------------+------------------+--------------+
+| | 2.7, 3.2 | 3.4.1 |ppc |
++----------------------------------+----------------------------+------------------+--------------+
+| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
++----------------------------------+----------------------------+------------------+--------------+
+| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
++----------------------------------+----------------------------+------------------+--------------+
+| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm |
+| +----------------------------+------------------+ |
+| | 2.7 | 6.2.2 | |
++----------------------------------+----------------------------+------------------+--------------+
+| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 11 Pro | 3.11, 3.12 | 10.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 10 | 3.7 | 7.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
Old Versions
------------
diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst
index 20237eccf..31faeac78 100644
--- a/docs/reference/ImageColor.rst
+++ b/docs/reference/ImageColor.rst
@@ -59,7 +59,7 @@ Functions
.. py:method:: getcolor(color, mode)
Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
- greyscale value if the mode is not color or a palette image. If the string
+ grayscale value if the mode is not color or a palette image. If the string
cannot be parsed, this function raises a :py:exc:`ValueError` exception.
.. versionadded:: 1.1.4
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index 95a40007b..4ccfacae7 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -351,7 +351,7 @@ Methods
Draw a shape.
-.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
+.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None)
Draws the string at the given position.
@@ -362,9 +362,10 @@ Methods
:param fill: Color to use for the text.
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values. This parameter is
- ignored for non-TrueType fonts.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+ This parameter is ignored for non-TrueType fonts.
.. note:: This parameter was present in earlier versions
of Pillow, but implemented only in version 8.0.0.
@@ -416,8 +417,14 @@ Methods
.. versionadded:: 8.0.0
+ :param font_size: If ``font`` is not provided, then the size to use for the default
+ font.
+ Keyword-only argument.
-.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
+ .. versionadded:: 10.1.0
+
+
+.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None)
Draws the string at the given position.
@@ -427,9 +434,10 @@ Methods
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values. This parameter is
- ignored for non-TrueType fonts.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+ This parameter is ignored for non-TrueType fonts.
.. note:: This parameter was present in earlier versions
of Pillow, but implemented only in version 8.0.0.
@@ -477,7 +485,13 @@ Methods
.. versionadded:: 8.0.0
-.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False)
+ :param font_size: If ``font`` is not provided, then the size to use for the default
+ font.
+ Keyword-only argument.
+
+ .. versionadded:: 10.1.0
+
+.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False, font_size=None)
Returns length (in pixels with 1/64 precision) of given text when rendered
in font with provided direction, features, and language.
@@ -538,9 +552,15 @@ Methods
It should be a `BCP 47 language code`_.
Requires libraqm.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
+ :param font_size: If ``font`` is not provided, then the size to use for the default
+ font.
+ Keyword-only argument.
+
+ .. versionadded:: 10.1.0
+
:return: Either width for horizontal text, or height for vertical text.
-.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
+.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None)
Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language.
@@ -558,9 +578,10 @@ Methods
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`.
:param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values. This parameter is
- ignored for non-TrueType fonts.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+ This parameter is ignored for non-TrueType fonts.
:param spacing: If the text is passed on to
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
the number of pixels between lines.
@@ -588,9 +609,15 @@ Methods
Requires libraqm.
:param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
+ :param font_size: If ``font`` is not provided, then the size to use for the default
+ font.
+ Keyword-only argument.
+
+ .. versionadded:: 10.1.0
+
:return: ``(left, top, right, bottom)`` bounding box
-.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
+.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None)
Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language.
@@ -606,9 +633,10 @@ Methods
:param text: Text to be measured.
:param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values. This parameter is
- ignored for non-TrueType fonts.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+ This parameter is ignored for non-TrueType fonts.
:param spacing: The number of pixels between lines.
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
@@ -632,6 +660,12 @@ Methods
Requires libraqm.
:param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
+ :param font_size: If ``font`` is not provided, then the size to use for the default
+ font.
+ Keyword-only argument.
+
+ .. versionadded:: 10.1.0
+
:return: ``(left, top, right, bottom)`` bounding box
.. py:method:: getdraw(im=None, hints=None)
diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst
index 457f0d4df..529acad4a 100644
--- a/docs/reference/ImageEnhance.rst
+++ b/docs/reference/ImageEnhance.rst
@@ -58,7 +58,7 @@ method:
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
+ :ref:`enhancement factor ` of 0.0 gives a solid gray
image, a factor of 1.0 gives the original image, and greater values
increase the contrast of the image.
diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst
index 2abfa0cc9..6edf4b05c 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -10,7 +10,7 @@ this class store bitmap fonts, and are used with the
PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use
`pilfont.py `_
-from `pillow-scripts `_ to convert BDF and
+from :pypi:`pillow-scripts` to convert BDF and
PCF font descriptors (X window font formats) to this format.
Starting with version 1.1.4, PIL can be configured to support TrueType and
@@ -20,7 +20,7 @@ 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
+ text input, Pillow will raise a :py:exc:`ValueError` if the number of characters
is over a certain limit, :py:data:`MAX_STRING_LENGTH`.
This threshold can be changed by setting
@@ -70,24 +70,23 @@ Methods
Constants
---------
-.. data:: PIL.ImageFont.Layout.BASIC
+.. class:: Layout
- Use basic text layout for TrueType font.
- Advanced features such as text direction are not supported.
+ .. py:attribute:: BASIC
-.. data:: PIL.ImageFont.Layout.RAQM
+ Use basic text layout for TrueType font.
+ Advanced features such as text direction are not supported.
- Use Raqm text layout for TrueType font.
- Advanced features are supported.
+ .. py:attribute:: RAQM
- Requires Raqm, you can check support using
- :py:func:`PIL.features.check_feature` with ``feature="raqm"``.
+ Use Raqm text layout for TrueType font.
+ Advanced features are supported.
-Constants
----------
+ Requires Raqm, you can check support using
+ :py:func:`PIL.features.check_feature` with ``feature="raqm"``.
.. 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
+ raise a :py:exc:`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/ImageMath.rst b/docs/reference/ImageMath.rst
index 118d988d6..ee07efa01 100644
--- a/docs/reference/ImageMath.rst
+++ b/docs/reference/ImageMath.rst
@@ -72,7 +72,7 @@ pixel bits.
Note that the operands are converted to 32-bit signed integers before the
bitwise operation is applied. This means that you’ll get negative values if
-you invert an ordinary greyscale image. You can use the and (&) operator to
+you invert an ordinary grayscale image. You can use the and (&) operator to
mask off unwanted bits.
Bitwise operators don’t work on floating point images.
diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst
index d1c43cf60..475253078 100644
--- a/docs/reference/ImageOps.rst
+++ b/docs/reference/ImageOps.rst
@@ -12,14 +12,11 @@ only work on L and RGB images.
.. autofunction:: autocontrast
.. autofunction:: colorize
-.. autofunction:: contain
-.. autofunction:: pad
.. autofunction:: crop
.. autofunction:: scale
.. autofunction:: deform
.. autofunction:: equalize
.. autofunction:: expand
-.. autofunction:: fit
.. autofunction:: flip
.. autofunction:: grayscale
.. autofunction:: invert
@@ -27,3 +24,38 @@ only work on L and RGB images.
.. autofunction:: posterize
.. autofunction:: solarize
.. autofunction:: exif_transpose
+
+.. _relative-resize:
+
+Resize relative to a given size
+-------------------------------
+
+::
+
+ from PIL import Image, ImageOps
+ size = (100, 150)
+ with Image.open("Tests/images/hopper.png") as im:
+ ImageOps.contain(im, size).save("imageops_contain.png")
+ ImageOps.cover(im, size).save("imageops_cover.png")
+ ImageOps.fit(im, size).save("imageops_fit.png")
+ ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
+
+ # thumbnail() can also be used,
+ # but will modify the image object in place
+ im.thumbnail(size)
+ im.save("imageops_thumbnail.png")
+
++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
+| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
++================+===========================================+============================================+==========================================+========================================+========================================+
+|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
+|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
+|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
++----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
+
+.. autofunction:: contain
+.. autofunction:: cover
+.. autofunction:: fit
+.. autofunction:: pad
diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst
index fcf4514a8..18cd99cf3 100644
--- a/docs/reference/plugins.rst
+++ b/docs/reference/plugins.rst
@@ -33,6 +33,14 @@ Plugin reference
:undoc-members:
:show-inheritance:
+:mod:`~PIL.DdsImagePlugin` Module
+---------------------------------
+
+.. automodule:: PIL.DdsImagePlugin
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
:mod:`~PIL.EpsImagePlugin` Module
---------------------------------
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index 06acfc7af..a3f238119 100644
--- a/docs/releasenotes/10.0.0.rst
+++ b/docs/releasenotes/10.0.0.rst
@@ -173,8 +173,8 @@ 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
+:cve:`2023-44271`: To protect against potential DOS attacks when using arbitrary strings as text
+input, Pillow will now raise a :py:exc:`ValueError` if the number of characters
passed into ImageFont methods is over a certain limit,
:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.
diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst
index 78c52380c..fd556bdf1 100644
--- a/docs/releasenotes/10.1.0.rst
+++ b/docs/releasenotes/10.1.0.rst
@@ -1,37 +1,51 @@
10.1.0
------
-Backwards Incompatible Changes
-==============================
+API Changes
+===========
Setting image mode
^^^^^^^^^^^^^^^^^^
If you attempt to set the mode of an image directly, e.g.
-``im.mode = "RGBA"``, you will now receive an ``AttributeError``. This is
+``im.mode = "RGBA"``, you will now receive an :py:exc:`AttributeError`. This is
not about removing existing functionality, but instead about raising an
explicit error to prevent later consequences. The ``convert`` method is the
correct way to change an image's mode.
-Deprecations
-============
+Accept a list in getpixel()
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
-^^^^
+:py:meth:`~PIL.Image.Image.getpixel` now accepts a list of coordinates, as well
+as a tuple. ::
-TODO
+ from PIL import Image
+ im = Image.new("RGB", (1, 1))
+ im.getpixel((0, 0))
+ im.getpixel([0, 0])
-API Changes
-===========
+BoxBlur and GaussianBlur allow for different x and y radii
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
-^^^^
+:py:class:`~PIL.ImageFilter.BoxBlur` and
+:py:class:`~PIL.ImageFilter.GaussianBlur` now allow a sequence of x and y radii
+to be specified, rather than a single number for both dimensions. ::
-TODO
+ from PIL import ImageFilter
+ ImageFilter.BoxBlur((2, 5))
+ ImageFilter.GaussianBlur((2, 5))
API Additions
=============
+EpsImagePlugin.gs_binary
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+``EpsImagePlugin.gs_windows_binary`` stores the name of the Ghostscript
+executable on Windows. ``EpsImagePlugin.gs_binary`` has now been added for all
+platforms, and can be used to customise the name of the executable, or disable
+use entirely through ``EpsImagePlugin.gs_binary = False``.
+
has_transparency_data
^^^^^^^^^^^^^^^^^^^^^
@@ -43,18 +57,53 @@ channel, a palette with an alpha channel, or a "transparency" key in the
Even if this attribute is true, the image might still appear solid, if all of
the values shown within are opaque.
-Security
-========
+ImageOps.cover
+^^^^^^^^^^^^^^
-TODO
-^^^^
+Returns a resized version of the image, so that the requested size is covered,
+while maintaining the original aspect ratio.
-TODO
+See :ref:`relative-resize` for a comparison between this and similar ``ImageOps``
+methods.
+
+size and font_size arguments when using default font
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pillow has had a "better than nothing" default font, which can only be drawn at
+one font size. Now, if FreeType support is available, a version of
+`Aileron Regular `_ is loaded, which can be
+drawn at chosen font sizes.
+
+The following ``size`` and ``font_size`` arguments can now be used to specify a
+font size for this new builtin font::
+
+ ImageFont.load_default(size=24)
+ draw.text((0, 0), "test", font_size=24)
+ draw.textlength((0, 0), "test", font_size=24)
+ draw.textbbox((0, 0), "test", font_size=24)
+ draw.multiline_text((0, 0), "test", font_size=24)
+ draw.multiline_textbbox((0, 0), "test", font_size=24)
Other Changes
=============
-Added support for DDS 8-bit color indexed images
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Python 3.12
+^^^^^^^^^^^
-Support has been added to read PALETTEINDEXED8 DDS files as P mode images.
+Pillow 10.0.0 had wheels built against Python 3.12 beta, available as a preview to help
+others prepare for 3.12, and to ensure Pillow could be used immediately at the release
+of 3.12.0 final (2023-10-02, :pep:`693`).
+
+Pillow 10.1.0 now officially supports Python 3.12.
+
+Added support for DDS BC5U and 8-bit color indexed images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added to read BC5U DDS files as RGB images, and
+PALETTEINDEXED8 DDS files as P mode images.
+
+Support reading signed 8-bit YCbCr TIFF images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+TIFF images with unsigned integer data, 8 bits per sample and a photometric
+interpretation of YCbCr can now be read.
diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst
new file mode 100644
index 000000000..9883f10ba
--- /dev/null
+++ b/docs/releasenotes/10.2.0.rst
@@ -0,0 +1,93 @@
+10.2.0
+------
+
+Backwards Incompatible Changes
+==============================
+
+TODO
+^^^^
+
+TODO
+
+Deprecations
+============
+
+ImageFile.raise_oserror
+^^^^^^^^^^^^^^^^^^^^^^^
+
+``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow
+12.0.0 (2025-10-15). The function is undocumented and is only useful for translating
+error codes returned by a codec's ``decode()`` method, which ImageFile already does
+automatically.
+
+TODO
+^^^^
+
+TODO
+
+API Changes
+===========
+
+Zero or negative font size error
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When creating a :py:class:`~PIL.ImageFont.FreeTypeFont` instance, either directly or
+through :py:func:`~PIL.ImageFont.truetype`, if the font size is zero or less, a
+:py:exc:`ValueError` will now be raised.
+
+API Additions
+=============
+
+Added DdsImagePlugin enums
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:class:`~PIL.DdsImagePlugin.DDSD`, :py:class:`~PIL.DdsImagePlugin.DDSCAPS`,
+:py:class:`~PIL.DdsImagePlugin.DDSCAPS2`, :py:class:`~PIL.DdsImagePlugin.DDPF`,
+:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT`
+enums have been added to :py:class:`PIL.DdsImagePlugin`.
+
+JPEG restart marker interval
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When saving JPEG files, ``restart_marker_blocks`` and ``restart_marker_rows`` can now
+be used to emit restart markers whenever the specified number of MCU blocks or rows
+have been produced.
+
+JPEG tables-only streamtype
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When saving JPEG files, ``streamtype`` can now be set to 1, for tables-only. This will
+output only the quantization and Huffman tables for the image.
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+Other Changes
+=============
+
+Added DDS BC4U and DX10 BC1 and BC4 reading
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added to read the BC4U format of DDS images.
+
+Support has also been added to read DX10 BC1 and BC4, whether UNORM or
+TYPELESS.
+
+Optimized ImageStat.Stat count and extrema
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and
+:py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the
+histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3x as fast
+on average and ``st.extrema`` is 12x as fast on average.
+
+Encoder errors now report error detail as string
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:exc:`OSError` exceptions from image encoders now include a textual description of
+the error instead of a numeric error code.
diff --git a/docs/releasenotes/2.8.0.rst b/docs/releasenotes/2.8.0.rst
index c522fe8b0..4dbbc0bdd 100644
--- a/docs/releasenotes/2.8.0.rst
+++ b/docs/releasenotes/2.8.0.rst
@@ -10,7 +10,7 @@ operations. As a result PIL was unable to open them as images, requiring a wrap
``cStringIO`` or ``BytesIO``.
Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and
-catch on exception ``AttributeError`` or ``io.UnsupportedOperation``. If this is caught we
+catch on exception :py:exc:`AttributeError` or :py:exc:`io.UnsupportedOperation`. If this is caught we
attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like
objects).
diff --git a/docs/releasenotes/3.4.0.rst b/docs/releasenotes/3.4.0.rst
index dc5e2e295..2bbafe741 100644
--- a/docs/releasenotes/3.4.0.rst
+++ b/docs/releasenotes/3.4.0.rst
@@ -19,7 +19,7 @@ Deprecation Warning when Saving JPEGs
JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0
silently drops the alpha channel. With this release Pillow will now
-issue a ``DeprecationWarning`` when attempting to save a ``RGBA`` mode
+issue a :py:exc:`DeprecationWarning` when attempting to save a ``RGBA`` mode
image as a JPEG. This will become an error in Pillow 4.2.
New DDS Decoders
diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst
index cbf131c93..5778de26a 100644
--- a/docs/releasenotes/4.0.0.rst
+++ b/docs/releasenotes/4.0.0.rst
@@ -17,8 +17,8 @@ Pillow 4.0 supports Python 3.6.
OleFileIO.py
============
-OleFileIO.py has been removed as a vendored file and is now installed
-from the upstream olefile pypi package. All internal dependencies are
+``OleFileIO.py`` has been removed as a vendored file and is now installed
+from the upstream :pypi:`olefile` PyPI package. All internal dependencies are
redirected to the olefile package. Direct accesses to
``PIL.OlefileIO`` raises a deprecation warning, then patches the
upstream olefile into ``sys.modules`` in its place.
diff --git a/docs/releasenotes/5.0.0.rst b/docs/releasenotes/5.0.0.rst
index 509edbe6d..be00a45cd 100644
--- a/docs/releasenotes/5.0.0.rst
+++ b/docs/releasenotes/5.0.0.rst
@@ -28,7 +28,7 @@ Scripts
The scripts formerly installed by Pillow have been split into a
separate package, pillow-scripts, living at
-https://github.com/python-pillow/pillow-scripts .
+https://github.com/python-pillow/pillow-scripts.
API Changes
@@ -37,7 +37,7 @@ API Changes
OleFileIO.py
^^^^^^^^^^^^
-The olefile module is no longer a required dependency when installing Pillow.
+The :pypi:`olefile` module is no longer a required dependency when installing Pillow.
Support for plugins requiring olefile will not be loaded if it is not
installed. This allows library consumers to avoid installing this dependency
if they choose. Some library consumers have little interest in the format
diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst
index bff56566b..8f276da24 100644
--- a/docs/releasenotes/5.3.0.rst
+++ b/docs/releasenotes/5.3.0.rst
@@ -8,7 +8,7 @@ Image size
^^^^^^^^^^
If you attempt to set the size of an image directly, e.g.
-``im.size = (100, 100)``, you will now receive an ``AttributeError``. This is
+``im.size = (100, 100)``, you will now receive an :py:exc:`AttributeError`. This is
not about removing existing functionality, but instead about raising an
explicit error to prevent later consequences. The ``resize`` method is the
correct way to change an image's size.
@@ -16,7 +16,8 @@ correct way to change an image's size.
The exceptions to this are:
* The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage.
-* The TIFF image format, which now has a ``DeprecationWarning`` for this action, as direct image size setting was previously necessary to work around an issue with tile extents.
+* The TIFF image format, which now has a :py:exc:`DeprecationWarning` for this action,
+ as direct image size setting was previously necessary to work around an issue with tile extents.
API Additions
diff --git a/docs/releasenotes/5.4.1.rst b/docs/releasenotes/5.4.1.rst
index 78f483db6..bbabd6520 100644
--- a/docs/releasenotes/5.4.1.rst
+++ b/docs/releasenotes/5.4.1.rst
@@ -15,7 +15,7 @@ PNG: Handle IDAT chunks after image end
Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop
reading image data before the IDAT chunks finish. A regression caused an
-``EOFError`` exception when previously there was none. This is now fixed, and
+:py:exc:`EOFError` exception when previously there was none. This is now fixed, and
file reading continues in case there are subsequent text chunks.
PNG: MIME type
@@ -30,7 +30,7 @@ File closing
^^^^^^^^^^^^
A regression caused an unsupported image file to report a
-``ValueError: seek of closed file`` exception instead of an ``OSError``. This
+``ValueError: seek of closed file`` exception instead of an :py:exc:`OSError`. This
has been fixed by ensuring that image plugins only close their internal ``__fp``
if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own
file pointers.
diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst
index 3e3b945a0..5e69f0b6b 100644
--- a/docs/releasenotes/6.0.0.rst
+++ b/docs/releasenotes/6.0.0.rst
@@ -14,8 +14,8 @@ Pillow for Python 3.4 is 5.4.1.
Removed deprecated PIL.OleFileIO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of
-the upstream olefile Python package, and replaced with an ``ImportError``. The
+``PIL.OleFileIO`` was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of
+the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError`. The
deprecated file has now been removed from Pillow. If needed, install from PyPI (eg.
``python3 -m pip install olefile``).
@@ -103,7 +103,7 @@ ImageCms.CmsProfile attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From
-6.0.0, they issue a ``DeprecationWarning``:
+6.0.0, they issue a :py:exc:`DeprecationWarning`:
======================== ===============================
Deprecated Use instead
diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst
index 76e13b061..ce3edc5fa 100644
--- a/docs/releasenotes/6.1.0.rst
+++ b/docs/releasenotes/6.1.0.rst
@@ -29,10 +29,10 @@ API Additions
Image.entropy
^^^^^^^^^^^^^
Calculates and returns the entropy for the image. A bilevel image (mode "1") is treated
-as a greyscale ("L") image by this method. If a mask is provided, the method employs
+as a grayscale ("L") image by this method. If a mask is provided, the method employs
the histogram for those parts of the image where the mask image is non-zero. The mask
image must have the same size as the image, and be either a bi-level image (mode "1") or
-a greyscale image ("L").
+a grayscale image ("L").
ImageGrab.grab
^^^^^^^^^^^^^^
@@ -58,7 +58,7 @@ file. ``ImageFont.FreeTypeFont`` has four new methods,
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and
:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes
-instead. An ``IOError`` will be raised if the font is not a variation font. FreeType
+instead. An :py:exc:`IOError` will be raised if the font is not a variation font. FreeType
2.9.1 or greater is required.
Other Changes
diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst
index f2e235289..ed6026593 100644
--- a/docs/releasenotes/7.0.0.rst
+++ b/docs/releasenotes/7.0.0.rst
@@ -85,7 +85,7 @@ Custom unidentified image error
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be
-identified. For backwards compatibility, this will inherit from ``OSError``.
+identified. For backwards compatibility, this will inherit from :py:exc:`OSError`.
New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/docs/releasenotes/7.1.2.rst b/docs/releasenotes/7.1.2.rst
index b12d84e33..ec0063e79 100644
--- a/docs/releasenotes/7.1.2.rst
+++ b/docs/releasenotes/7.1.2.rst
@@ -7,7 +7,7 @@ Fix another regression seeking PNG files
This fixes a regression introduced in 7.1.0 when adding support for APNG files.
When calling ``seek(n)`` on a regular PNG where ``n > 0``, it failed to raise an
-``EOFError`` as it should have done, resulting in:
+:py:exc:`EOFError` as it should have done, resulting in:
.. code-block:: pycon
diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst
index ff1b7c9e7..91e54da19 100644
--- a/docs/releasenotes/7.2.0.rst
+++ b/docs/releasenotes/7.2.0.rst
@@ -53,6 +53,6 @@ a custom :py:class:`~PIL.ImageShow.Viewer` class.
ImageFile.raise_ioerror
~~~~~~~~~~~~~~~~~~~~~~~
-``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror``
+:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror``
is now deprecated and will be removed in a future release. Use
``ImageFile.raise_oserror`` instead.
diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst
index 00c691a74..2bf299dd3 100644
--- a/docs/releasenotes/8.0.0.rst
+++ b/docs/releasenotes/8.0.0.rst
@@ -168,7 +168,7 @@ offset.
Error for large BMP files
^^^^^^^^^^^^^^^^^^^^^^^^^
-Previously, if a BMP file was too large, an ``OSError`` would be raised. Now,
+Previously, if a BMP file was too large, an :py:exc:`OSError` would be raised. Now,
``DecompressionBombError`` is used instead, as Pillow already uses for other formats.
Dark theme for docs
diff --git a/docs/releasenotes/8.3.1.rst b/docs/releasenotes/8.3.1.rst
index e97070c11..6af2b37bf 100644
--- a/docs/releasenotes/8.3.1.rst
+++ b/docs/releasenotes/8.3.1.rst
@@ -22,9 +22,10 @@ Catch OSError when checking if destination is sys.stdout
========================================================
In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was
-updated. This lead to an OSError being raised if the environment restricted access.
+updated. This lead to an :py:exc:`OSError` being raised if the environment restricted
+access.
-The OSError is now silently caught.
+The :py:exc:`OSError` is now silently caught.
Fixed removing orientation in ImageOps.exif_transpose
=====================================================
@@ -34,7 +35,7 @@ original image EXIF data was not modified, and the orientation was only removed
the modified copy.
However, for certain images the orientation was already missing from the modified
-image, leading to a KeyError.
+image, leading to a :py:exc:`KeyError`.
This error has been resolved, and the copying of metadata to the modified image
improved.
diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst
index 73e77ad3e..090ec8024 100644
--- a/docs/releasenotes/9.0.0.rst
+++ b/docs/releasenotes/9.0.0.rst
@@ -63,7 +63,7 @@ a custom :py:class:`~PIL.ImageShow.Viewer` class.
ImageFile.raise_ioerror
^^^^^^^^^^^^^^^^^^^^^^^
-``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror``
+:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror``
has been removed. Use ``ImageFile.raise_oserror`` instead.
diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst
index 19690ca59..02da702a7 100644
--- a/docs/releasenotes/9.1.0.rst
+++ b/docs/releasenotes/9.1.0.rst
@@ -8,14 +8,14 @@ Raise an error when performing a negative crop
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now
-it will raise a ``ValueError``, to help reduce confusion if a user has unintentionally
+it will raise a :py:exc:`ValueError`, to help reduce confusion if a user has unintentionally
provided the wrong arguments.
Added specific error if path coordinate type is incorrect
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Rather than returning a ``SystemError``, passing the incorrect types of coordinates into
-a path will now raise a more specific ``ValueError``, with the message "incorrect
+Rather than returning a :py:exc:`SystemError`, passing the incorrect types of coordinates into
+a path will now raise a more specific :py:exc:`ValueError`, with the message "incorrect
coordinate type".
Replace requirements.txt with extras
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index 1b1c353fd..d8034853c 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 10.2.0
10.1.0
10.0.1
10.0.0
diff --git a/docs/resources/js/activate_tab.js b/docs/resources/js/activate_tab.js
new file mode 100644
index 000000000..92522b5ce
--- /dev/null
+++ b/docs/resources/js/activate_tab.js
@@ -0,0 +1,36 @@
+// Based on https://stackoverflow.com/a/38241481/724176
+function getOS() {
+ const userAgent = window.navigator.userAgent,
+ platform = window.navigator.userAgentData?.platform || window.navigator.platform,
+ macosPlatforms = ["macOS", "Macintosh", "MacIntel", "MacPPC", "Mac68K"],
+ windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
+
+ if (macosPlatforms.includes(platform)) {
+ return "macOS";
+ } else if (windowsPlatforms.includes(platform)) {
+ return "Windows";
+ } else if (/Android/.test(userAgent)) {
+ return "Android";
+ } else if (/Linux/.test(platform)) {
+ return "Linux";
+ }
+}
+
+function activateTab(tabName) {
+ // Find all label elements with the specified tab name
+ const labels = document.querySelectorAll(".tab-label");
+
+ labels.forEach((label) => {
+ if (label.textContent == tabName) {
+ // Find the associated input element using the "for" attribute
+ const tabInputId = label.getAttribute("for");
+ const tabInput = document.getElementById(tabInputId);
+
+ // Check if the input element exists before attempting to set the "checked" attribute
+ if (tabInput) {
+ // Activate the tab by setting its "checked" attribute to true
+ tabInput.checked = true;
+ }
+ }
+ });
+}
diff --git a/pyproject.toml b/pyproject.toml
index fd9c05f92..a48a4fcc6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,3 +6,152 @@ requires = [
backend-path = [
"_custom_build",
]
+
+[project]
+name = "pillow"
+description = "Python Imaging Library (Fork)"
+readme = "README.md"
+keywords = [
+ "Imaging",
+]
+license = {text = "HPND"}
+authors = [{name = "Jeffrey A. Clark (Alex)", email = "aclark@aclark.net"}]
+requires-python = ">=3.8"
+classifiers = [
+ "Development Status :: 6 - Mature",
+ "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Topic :: Multimedia :: Graphics",
+ "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
+ "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
+ "Topic :: Multimedia :: Graphics :: Graphics Conversion",
+ "Topic :: Multimedia :: Graphics :: Viewers",
+]
+dynamic = [
+ "version",
+]
+[project.optional-dependencies]
+docs = [
+ "furo",
+ "olefile",
+ "sphinx>=2.4",
+ "sphinx-copybutton",
+ "sphinx-inline-tabs",
+ "sphinx-removed-in",
+ "sphinxext-opengraph",
+]
+fpx = [
+ "olefile",
+]
+mic = [
+ "olefile",
+]
+tests = [
+ "check-manifest",
+ "coverage",
+ "defusedxml",
+ "markdown2",
+ "olefile",
+ "packaging",
+ "pyroma",
+ "pytest",
+ "pytest-cov",
+ "pytest-timeout",
+]
+xmp = [
+ "defusedxml",
+]
+[project.urls]
+Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst"
+Documentation = "https://pillow.readthedocs.io"
+Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi"
+Homepage = "https://python-pillow.org"
+Mastodon = "https://fosstodon.org/@pillow"
+"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
+Source = "https://github.com/python-pillow/Pillow"
+Twitter = "https://twitter.com/PythonPillow"
+
+[tool.setuptools]
+packages = ["PIL"]
+include-package-data = true
+package-dir = {"" = "src"}
+
+[tool.setuptools.dynamic]
+version = {attr = "PIL.__version__"}
+
+[tool.cibuildwheel]
+before-all = ".github/workflows/wheels-dependencies.sh"
+build-verbosity = 1
+config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
+test-command = "cd {project} && .github/workflows/wheels-test.sh"
+test-extras = "tests"
+
+[tool.ruff]
+select = [
+ "C4", # flake8-comprehensions
+ "E", # pycodestyle errors
+ "EM", # flake8-errmsg
+ "F", # pyflakes errors
+ "I", # isort
+ "ISC", # flake8-implicit-str-concat
+ "PGH", # pygrep-hooks
+ "RUF100", # unused noqa (yesqa)
+ "UP", # pyupgrade
+ "W", # pycodestyle warnings
+ "YTT", # flake8-2020
+ # "LOG", # TODO: enable flake8-logging when it's not in preview anymore
+]
+extend-ignore = [
+ "E203", # Whitespace before ':'
+ "E221", # Multiple spaces before operator
+ "E226", # Missing whitespace around arithmetic operator
+ "E241", # Multiple spaces after ','
+]
+
+[tool.ruff.per-file-ignores]
+"Tests/*.py" = ["I001"]
+
+[tool.ruff.isort]
+known-first-party = ["PIL"]
+
+[tool.pytest.ini_options]
+addopts = "-ra --color=yes"
+testpaths = ["Tests"]
+
+[tool.mypy]
+python_version = "3.8"
+pretty = true
+disallow_any_generics = true
+enable_error_code = "ignore-without-code"
+extra_checks = true
+follow_imports = "silent"
+warn_redundant_casts = true
+warn_unreachable = true
+warn_unused_ignores = true
+exclude = [
+ '^src/PIL/_tkinter_finder.py$',
+ '^src/PIL/DdsImagePlugin.py$',
+ '^src/PIL/FpxImagePlugin.py$',
+ '^src/PIL/Image.py$',
+ '^src/PIL/ImageCms.py$',
+ '^src/PIL/ImageFile.py$',
+ '^src/PIL/ImageFont.py$',
+ '^src/PIL/ImageMath.py$',
+ '^src/PIL/ImageMorph.py$',
+ '^src/PIL/ImageQt.py$',
+ '^src/PIL/ImageShow.py$',
+ '^src/PIL/ImImagePlugin.py$',
+ '^src/PIL/MicImagePlugin.py$',
+ '^src/PIL/PdfParser.py$',
+ '^src/PIL/PyAccess.py$',
+ '^src/PIL/TiffImagePlugin.py$',
+ '^src/PIL/TiffTags.py$',
+ '^src/PIL/WebPImagePlugin.py$',
+]
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index e560f9516..000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,74 +0,0 @@
-[metadata]
-name = Pillow
-description = Python Imaging Library (Fork)
-long_description = file: README.md
-long_description_content_type = text/markdown
-url = https://python-pillow.org
-author = 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.8
- Programming Language :: Python :: 3.9
- Programming Language :: Python :: 3.10
- Programming Language :: Python :: 3.11
- Programming Language :: Python :: 3.12
- Programming Language :: Python :: Implementation :: CPython
- Programming Language :: Python :: Implementation :: PyPy
- Topic :: Multimedia :: Graphics
- Topic :: Multimedia :: Graphics :: Capture :: Digital Camera
- Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
- Topic :: Multimedia :: Graphics :: Graphics Conversion
- Topic :: Multimedia :: Graphics :: Viewers
-keywords = Imaging
-project_urls =
- Documentation=https://pillow.readthedocs.io
- Source=https://github.com/python-pillow/Pillow
- Funding=https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi
- Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html
- Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
- Twitter=https://twitter.com/PythonPillow
- Mastodon=https://fosstodon.org/@pillow
-
-[options]
-packages = PIL
-python_requires = >=3.8
-include_package_data = True
-package_dir =
- = src
-
-[options.extras_require]
-docs =
- furo
- olefile
- sphinx>=2.4
- sphinx-copybutton
- sphinx-inline-tabs
- sphinx-removed-in
- sphinxext-opengraph
-tests =
- check-manifest
- coverage
- defusedxml
- markdown2
- olefile
- packaging
- pyroma
- pytest
- pytest-cov
- pytest-timeout
-
-[flake8]
-extend-ignore = E203
-max-line-length = 88
-
-[isort]
-profile = black
-
-[tool:pytest]
-addopts = -ra --color=yes
-testpaths = Tests
diff --git a/setup.py b/setup.py
index 935166716..2a364ba97 100755
--- a/setup.py
+++ b/setup.py
@@ -440,17 +440,17 @@ class pil_build_ext(build_ext):
#
# add configured kits
- for root_name, lib_name in dict(
- JPEG_ROOT="libjpeg",
- JPEG2K_ROOT="libopenjp2",
- TIFF_ROOT=("libtiff-5", "libtiff-4"),
- ZLIB_ROOT="zlib",
- FREETYPE_ROOT="freetype2",
- HARFBUZZ_ROOT="harfbuzz",
- FRIBIDI_ROOT="fribidi",
- LCMS_ROOT="lcms2",
- IMAGEQUANT_ROOT="libimagequant",
- ).items():
+ for root_name, lib_name in {
+ "JPEG_ROOT": "libjpeg",
+ "JPEG2K_ROOT": "libopenjp2",
+ "TIFF_ROOT": ("libtiff-5", "libtiff-4"),
+ "ZLIB_ROOT": "zlib",
+ "FREETYPE_ROOT": "freetype2",
+ "HARFBUZZ_ROOT": "harfbuzz",
+ "FRIBIDI_ROOT": "fribidi",
+ "LCMS_ROOT": "lcms2",
+ "IMAGEQUANT_ROOT": "libimagequant",
+ }.items():
root = globals()[root_name]
if root is None and root_name in os.environ:
@@ -986,7 +986,6 @@ ext_modules = [
try:
setup(
- version=PILLOW_VERSION,
cmdclass={"build_ext": pil_build_ext},
ext_modules=ext_modules,
zip_safe=not (debug_build() or PLATFORM_MINGW),
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index a5cfad5f4..398696d5c 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -419,9 +419,11 @@ class BLPEncoder(ImageFile.PyEncoder):
def _write_palette(self):
data = b""
palette = self.im.getpalette("RGBA", "RGBA")
- for i in range(256):
+ for i in range(len(palette) // 4):
r, g, b, a = palette[i * 4 : (i + 1) * 4]
data += struct.pack("<4B", b, g, r, a)
+ while len(data) < 256 * 4:
+ data += b"\x00" * 4
return data
def encode(self, bufsize):
@@ -442,7 +444,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data
-def _save(im, fp, filename, save_all=False):
+def _save(im, fp, filename):
if im.mode != "P":
msg = "Unsupported BLP image mode"
raise ValueError(msg)
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index 9abfd0b5b..b51019c66 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -230,21 +230,21 @@ class BmpImageFile(ImageFile.ImageFile):
else:
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
- greyscale = True
+ grayscale = True
indices = (
(0, 255)
if file_info["colors"] == 2
else list(range(file_info["colors"]))
)
- # ----------------- Check if greyscale and ignore palette if so
+ # ----------------- Check if grayscale and ignore palette if so
for ind, val in enumerate(indices):
rgb = palette[ind * padding : ind * padding + 3]
if rgb != o8(val) * 3:
- greyscale = False
+ grayscale = False
- # ------- If all colors are grey, white or black, ditch palette
- if greyscale:
+ # ------- If all colors are gray, white or black, ditch palette
+ if grayscale:
self._mode = "1" if file_info["colors"] == 2 else "L"
raw_mode = self.mode
else:
@@ -396,7 +396,7 @@ def _save(im, fp, filename, bitmap_header=True):
dpi = info.get("dpi", (96, 96))
# 1 meter == 39.3701 inches
- ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi))
+ ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
header = 40 # or 64 for OS/2 version 2
diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py
index 94efff341..fc0dae44b 100644
--- a/src/PIL/CurImagePlugin.py
+++ b/src/PIL/CurImagePlugin.py
@@ -64,8 +64,6 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
d, e, o, a = self.tile[0]
self.tile[0] = d, (0, 0) + self.size, o, a
- return
-
#
# --------------------------------------------------------------------
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index 54f358c7f..1758c9a4d 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -3,109 +3,321 @@ A Pillow loader for .dds files (S3TC-compressed aka DXTC)
Jerome Leclanche
Documentation:
- https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
+https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
- https://creativecommons.org/publicdomain/zero/1.0/
+https://creativecommons.org/publicdomain/zero/1.0/
"""
+import io
import struct
-from io import BytesIO
+import sys
+from enum import IntEnum, IntFlag
from . import Image, ImageFile, ImagePalette
+from ._binary import i32le as i32
from ._binary import o32le as o32
# Magic ("DDS ")
DDS_MAGIC = 0x20534444
+
# DDS flags
-DDSD_CAPS = 0x1
-DDSD_HEIGHT = 0x2
-DDSD_WIDTH = 0x4
-DDSD_PITCH = 0x8
-DDSD_PIXELFORMAT = 0x1000
-DDSD_MIPMAPCOUNT = 0x20000
-DDSD_LINEARSIZE = 0x80000
-DDSD_DEPTH = 0x800000
+class DDSD(IntFlag):
+ CAPS = 0x1
+ HEIGHT = 0x2
+ WIDTH = 0x4
+ PITCH = 0x8
+ PIXELFORMAT = 0x1000
+ MIPMAPCOUNT = 0x20000
+ LINEARSIZE = 0x80000
+ DEPTH = 0x800000
+
# DDS caps
-DDSCAPS_COMPLEX = 0x8
-DDSCAPS_TEXTURE = 0x1000
-DDSCAPS_MIPMAP = 0x400000
+class DDSCAPS(IntFlag):
+ COMPLEX = 0x8
+ TEXTURE = 0x1000
+ MIPMAP = 0x400000
+
+
+class DDSCAPS2(IntFlag):
+ CUBEMAP = 0x200
+ CUBEMAP_POSITIVEX = 0x400
+ CUBEMAP_NEGATIVEX = 0x800
+ CUBEMAP_POSITIVEY = 0x1000
+ CUBEMAP_NEGATIVEY = 0x2000
+ CUBEMAP_POSITIVEZ = 0x4000
+ CUBEMAP_NEGATIVEZ = 0x8000
+ VOLUME = 0x200000
-DDSCAPS2_CUBEMAP = 0x200
-DDSCAPS2_CUBEMAP_POSITIVEX = 0x400
-DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800
-DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000
-DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000
-DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000
-DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000
-DDSCAPS2_VOLUME = 0x200000
# Pixel Format
-DDPF_ALPHAPIXELS = 0x1
-DDPF_ALPHA = 0x2
-DDPF_FOURCC = 0x4
-DDPF_PALETTEINDEXED8 = 0x20
-DDPF_RGB = 0x40
-DDPF_LUMINANCE = 0x20000
-
-
-# dds.h
-
-DDS_FOURCC = DDPF_FOURCC
-DDS_RGB = DDPF_RGB
-DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS
-DDS_LUMINANCE = DDPF_LUMINANCE
-DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS
-DDS_ALPHA = DDPF_ALPHA
-DDS_PAL8 = DDPF_PALETTEINDEXED8
-
-DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
-DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT
-DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH
-DDS_HEADER_FLAGS_PITCH = DDSD_PITCH
-DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE
-
-DDS_HEIGHT = DDSD_HEIGHT
-DDS_WIDTH = DDSD_WIDTH
-
-DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE
-DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP
-DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX
-
-DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX
-DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX
-DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY
-DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY
-DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ
-DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ
-
-
-# DXT1
-DXT1_FOURCC = 0x31545844
-
-# DXT3
-DXT3_FOURCC = 0x33545844
-
-# DXT5
-DXT5_FOURCC = 0x35545844
+class DDPF(IntFlag):
+ ALPHAPIXELS = 0x1
+ ALPHA = 0x2
+ FOURCC = 0x4
+ PALETTEINDEXED8 = 0x20
+ RGB = 0x40
+ LUMINANCE = 0x20000
# dxgiformat.h
+class DXGI_FORMAT(IntEnum):
+ UNKNOWN = 0
+ R32G32B32A32_TYPELESS = 1
+ R32G32B32A32_FLOAT = 2
+ R32G32B32A32_UINT = 3
+ R32G32B32A32_SINT = 4
+ R32G32B32_TYPELESS = 5
+ R32G32B32_FLOAT = 6
+ R32G32B32_UINT = 7
+ R32G32B32_SINT = 8
+ R16G16B16A16_TYPELESS = 9
+ R16G16B16A16_FLOAT = 10
+ R16G16B16A16_UNORM = 11
+ R16G16B16A16_UINT = 12
+ R16G16B16A16_SNORM = 13
+ R16G16B16A16_SINT = 14
+ R32G32_TYPELESS = 15
+ R32G32_FLOAT = 16
+ R32G32_UINT = 17
+ R32G32_SINT = 18
+ R32G8X24_TYPELESS = 19
+ D32_FLOAT_S8X24_UINT = 20
+ R32_FLOAT_X8X24_TYPELESS = 21
+ X32_TYPELESS_G8X24_UINT = 22
+ R10G10B10A2_TYPELESS = 23
+ R10G10B10A2_UNORM = 24
+ R10G10B10A2_UINT = 25
+ R11G11B10_FLOAT = 26
+ R8G8B8A8_TYPELESS = 27
+ R8G8B8A8_UNORM = 28
+ R8G8B8A8_UNORM_SRGB = 29
+ R8G8B8A8_UINT = 30
+ R8G8B8A8_SNORM = 31
+ R8G8B8A8_SINT = 32
+ R16G16_TYPELESS = 33
+ R16G16_FLOAT = 34
+ R16G16_UNORM = 35
+ R16G16_UINT = 36
+ R16G16_SNORM = 37
+ R16G16_SINT = 38
+ R32_TYPELESS = 39
+ D32_FLOAT = 40
+ R32_FLOAT = 41
+ R32_UINT = 42
+ R32_SINT = 43
+ R24G8_TYPELESS = 44
+ D24_UNORM_S8_UINT = 45
+ R24_UNORM_X8_TYPELESS = 46
+ X24_TYPELESS_G8_UINT = 47
+ R8G8_TYPELESS = 48
+ R8G8_UNORM = 49
+ R8G8_UINT = 50
+ R8G8_SNORM = 51
+ R8G8_SINT = 52
+ R16_TYPELESS = 53
+ R16_FLOAT = 54
+ D16_UNORM = 55
+ R16_UNORM = 56
+ R16_UINT = 57
+ R16_SNORM = 58
+ R16_SINT = 59
+ R8_TYPELESS = 60
+ R8_UNORM = 61
+ R8_UINT = 62
+ R8_SNORM = 63
+ R8_SINT = 64
+ A8_UNORM = 65
+ R1_UNORM = 66
+ R9G9B9E5_SHAREDEXP = 67
+ R8G8_B8G8_UNORM = 68
+ G8R8_G8B8_UNORM = 69
+ BC1_TYPELESS = 70
+ BC1_UNORM = 71
+ BC1_UNORM_SRGB = 72
+ BC2_TYPELESS = 73
+ BC2_UNORM = 74
+ BC2_UNORM_SRGB = 75
+ BC3_TYPELESS = 76
+ BC3_UNORM = 77
+ BC3_UNORM_SRGB = 78
+ BC4_TYPELESS = 79
+ BC4_UNORM = 80
+ BC4_SNORM = 81
+ BC5_TYPELESS = 82
+ BC5_UNORM = 83
+ BC5_SNORM = 84
+ B5G6R5_UNORM = 85
+ B5G5R5A1_UNORM = 86
+ B8G8R8A8_UNORM = 87
+ B8G8R8X8_UNORM = 88
+ R10G10B10_XR_BIAS_A2_UNORM = 89
+ B8G8R8A8_TYPELESS = 90
+ B8G8R8A8_UNORM_SRGB = 91
+ B8G8R8X8_TYPELESS = 92
+ B8G8R8X8_UNORM_SRGB = 93
+ BC6H_TYPELESS = 94
+ BC6H_UF16 = 95
+ BC6H_SF16 = 96
+ BC7_TYPELESS = 97
+ BC7_UNORM = 98
+ BC7_UNORM_SRGB = 99
+ AYUV = 100
+ Y410 = 101
+ Y416 = 102
+ NV12 = 103
+ P010 = 104
+ P016 = 105
+ OPAQUE_420 = 106
+ YUY2 = 107
+ Y210 = 108
+ Y216 = 109
+ NV11 = 110
+ AI44 = 111
+ IA44 = 112
+ P8 = 113
+ A8P8 = 114
+ B4G4R4A4_UNORM = 115
+ P208 = 130
+ V208 = 131
+ V408 = 132
+ SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189
+ SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190
-DXGI_FORMAT_R8G8B8A8_TYPELESS = 27
-DXGI_FORMAT_R8G8B8A8_UNORM = 28
-DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
-DXGI_FORMAT_BC5_TYPELESS = 82
-DXGI_FORMAT_BC5_UNORM = 83
-DXGI_FORMAT_BC5_SNORM = 84
-DXGI_FORMAT_BC6H_UF16 = 95
-DXGI_FORMAT_BC6H_SF16 = 96
-DXGI_FORMAT_BC7_TYPELESS = 97
-DXGI_FORMAT_BC7_UNORM = 98
-DXGI_FORMAT_BC7_UNORM_SRGB = 99
+
+class D3DFMT(IntEnum):
+ UNKNOWN = 0
+ R8G8B8 = 20
+ A8R8G8B8 = 21
+ X8R8G8B8 = 22
+ R5G6B5 = 23
+ X1R5G5B5 = 24
+ A1R5G5B5 = 25
+ A4R4G4B4 = 26
+ R3G3B2 = 27
+ A8 = 28
+ A8R3G3B2 = 29
+ X4R4G4B4 = 30
+ A2B10G10R10 = 31
+ A8B8G8R8 = 32
+ X8B8G8R8 = 33
+ G16R16 = 34
+ A2R10G10B10 = 35
+ A16B16G16R16 = 36
+ A8P8 = 40
+ P8 = 41
+ L8 = 50
+ A8L8 = 51
+ A4L4 = 52
+ V8U8 = 60
+ L6V5U5 = 61
+ X8L8V8U8 = 62
+ Q8W8V8U8 = 63
+ V16U16 = 64
+ A2W10V10U10 = 67
+ D16_LOCKABLE = 70
+ D32 = 71
+ D15S1 = 73
+ D24S8 = 75
+ D24X8 = 77
+ D24X4S4 = 79
+ D16 = 80
+ D32F_LOCKABLE = 82
+ D24FS8 = 83
+ D32_LOCKABLE = 84
+ S8_LOCKABLE = 85
+ L16 = 81
+ VERTEXDATA = 100
+ INDEX16 = 101
+ INDEX32 = 102
+ Q16W16V16U16 = 110
+ R16F = 111
+ G16R16F = 112
+ A16B16G16R16F = 113
+ R32F = 114
+ G32R32F = 115
+ A32B32G32R32F = 116
+ CxV8U8 = 117
+ A1 = 118
+ A2B10G10R10_XR_BIAS = 119
+ BINARYBUFFER = 199
+
+ UYVY = i32(b"UYVY")
+ R8G8_B8G8 = i32(b"RGBG")
+ YUY2 = i32(b"YUY2")
+ G8R8_G8B8 = i32(b"GRGB")
+ DXT1 = i32(b"DXT1")
+ DXT2 = i32(b"DXT2")
+ DXT3 = i32(b"DXT3")
+ DXT4 = i32(b"DXT4")
+ DXT5 = i32(b"DXT5")
+ DX10 = i32(b"DX10")
+ BC4S = i32(b"BC4S")
+ BC4U = i32(b"BC4U")
+ BC5S = i32(b"BC5S")
+ BC5U = i32(b"BC5U")
+ ATI1 = i32(b"ATI1")
+ ATI2 = i32(b"ATI2")
+ MULTI2_ARGB8 = i32(b"MET1")
+
+
+# Backward compatibility layer
+module = sys.modules[__name__]
+for item in DDSD:
+ setattr(module, "DDSD_" + item.name, item.value)
+for item in DDSCAPS:
+ setattr(module, "DDSCAPS_" + item.name, item.value)
+for item in DDSCAPS2:
+ setattr(module, "DDSCAPS2_" + item.name, item.value)
+for item in DDPF:
+ setattr(module, "DDPF_" + item.name, item.value)
+
+DDS_FOURCC = DDPF.FOURCC
+DDS_RGB = DDPF.RGB
+DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS
+DDS_LUMINANCE = DDPF.LUMINANCE
+DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS
+DDS_ALPHA = DDPF.ALPHA
+DDS_PAL8 = DDPF.PALETTEINDEXED8
+
+DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
+DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT
+DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH
+DDS_HEADER_FLAGS_PITCH = DDSD.PITCH
+DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE
+
+DDS_HEIGHT = DDSD.HEIGHT
+DDS_WIDTH = DDSD.WIDTH
+
+DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE
+DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP
+DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX
+
+DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX
+DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX
+DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY
+DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY
+DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ
+DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ
+
+DXT1_FOURCC = D3DFMT.DXT1
+DXT3_FOURCC = D3DFMT.DXT3
+DXT5_FOURCC = D3DFMT.DXT5
+
+DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS
+DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM
+DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB
+DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS
+DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM
+DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM
+DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16
+DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16
+DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS
+DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM
+DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB
class DdsImageFile(ImageFile.ImageFile):
@@ -124,114 +336,140 @@ class DdsImageFile(ImageFile.ImageFile):
if len(header_bytes) != 120:
msg = f"Incomplete header: {len(header_bytes)} bytes"
raise OSError(msg)
- header = BytesIO(header_bytes)
+ header = io.BytesIO(header_bytes)
flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height)
- self._mode = "RGBA"
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved
# pixel format
- pfsize, pfflags = struct.unpack("<2I", header.read(8))
- fourcc = header.read(4)
- (bitcount,) = struct.unpack(" WIDTH:
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index a878cbfd2..a0999130e 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -97,16 +97,15 @@ class FpxImageFile(ImageFile.ImageFile):
s = prop[0x2000002 | id]
- colors = []
bands = i32(s, 4)
if bands > 4:
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)
- self._mode, self.rawmode = MODES[tuple(colors)]
+ # note: for now, we ignore the "uncalibrated" flag
+ colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands))
+
+ self._mode, self.rawmode = MODES[colors]
# load JPEG tables, if any
self.jpeg = {}
@@ -227,6 +226,7 @@ class FpxImageFile(ImageFile.ImageFile):
break # isn't really required
self.stream = stream
+ self._fp = self.fp
self.fp = None
def load(self):
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index f3a55c2fe..c22b42711 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -29,7 +29,15 @@ import os
import subprocess
from enum import IntEnum
-from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
+from . import (
+ Image,
+ ImageChops,
+ ImageFile,
+ ImageMath,
+ ImageOps,
+ ImagePalette,
+ ImageSequence,
+)
from ._binary import i16le as i16
from ._binary import o8
from ._binary import o16le as o16
@@ -182,7 +190,8 @@ class GifImageFile(ImageFile.ImageFile):
s = self.fp.read(1)
if not s or s == b";":
- raise EOFError
+ msg = "no more images in GIF file"
+ raise EOFError(msg)
palette = None
@@ -279,15 +288,11 @@ class GifImageFile(ImageFile.ImageFile):
bits = self.fp.read(1)[0]
self.__offset = self.fp.tell()
break
-
- else:
- pass
- # raise OSError, "illegal GIF tag `%x`" % s[0]
s = None
if interlace is None:
- # self._fp = None
- raise EOFError
+ msg = "image not found in GIF frame"
+ raise EOFError(msg)
self.__frame = frame
if not update_image:
@@ -332,6 +337,8 @@ class GifImageFile(ImageFile.ImageFile):
def _rgb(color):
if self._frame_palette:
+ if color * 3 + 3 > len(self._frame_palette.palette):
+ color = 0
color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
else:
color = (color, color, color)
@@ -536,7 +543,15 @@ def _normalize_palette(im, palette, info):
else:
used_palette_colors = _get_optimize(im, info)
if used_palette_colors is not None:
- return im.remap_palette(used_palette_colors, source_palette)
+ im = im.remap_palette(used_palette_colors, source_palette)
+ if "transparency" in info:
+ try:
+ info["transparency"] = used_palette_colors.index(
+ info["transparency"]
+ )
+ except ValueError:
+ del info["transparency"]
+ return im
im.palette.palette = source_palette
return im
@@ -564,13 +579,11 @@ def _write_single_frame(im, fp, palette):
def _getbbox(base_im, im_frame):
- if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):
- delta = ImageChops.subtract_modulo(im_frame, base_im)
- else:
- delta = ImageChops.subtract_modulo(
- im_frame.convert("RGBA"), base_im.convert("RGBA")
- )
- return delta.getbbox(alpha_only=False)
+ if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im):
+ im_frame = im_frame.convert("RGBA")
+ base_im = base_im.convert("RGBA")
+ delta = ImageChops.subtract_modulo(im_frame, base_im)
+ return delta, delta.getbbox(alpha_only=False)
def _write_multiple_frames(im, fp, palette):
@@ -585,6 +598,7 @@ def _write_multiple_frames(im, fp, palette):
total += getattr(imSequence, "n_frames", 1)
im_frames = []
+ previous_im = None
frame_count = 0
background_im = None
for i, imSequence in enumerate(imSequences):
@@ -598,9 +612,9 @@ def _write_multiple_frames(im, fp, palette):
im.encoderinfo.setdefault(k, v)
encoderinfo = im.encoderinfo.copy()
- im_frame = _normalize_palette(im_frame, palette, encoderinfo)
if "transparency" in im_frame.info:
encoderinfo.setdefault("transparency", im_frame.info["transparency"])
+ im_frame = _normalize_palette(im_frame, palette, encoderinfo)
if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count]
elif duration is None and "duration" in im_frame.info:
@@ -609,14 +623,16 @@ def _write_multiple_frames(im, fp, palette):
encoderinfo["disposal"] = disposal[frame_count]
frame_count += 1
+ diff_frame = None
if im_frames:
# delta frame
- previous = im_frames[-1]
- bbox = _getbbox(previous["im"], im_frame)
+ delta, bbox = _getbbox(previous_im, im_frame)
if not bbox:
# This frame is identical to the previous frame
if encoderinfo.get("duration"):
- previous["encoderinfo"]["duration"] += encoderinfo["duration"]
+ im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[
+ "duration"
+ ]
if progress:
im._save_all_progress(imSequence, i, frame_count, total)
continue
@@ -628,35 +644,69 @@ def _write_multiple_frames(im, fp, palette):
background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background)
background_im.putpalette(im_frames[0]["im"].palette)
- bbox = _getbbox(background_im, im_frame)
+ delta, bbox = _getbbox(background_im, im_frame)
+ if encoderinfo.get("optimize") and im_frame.mode != "1":
+ if "transparency" not in encoderinfo:
+ try:
+ encoderinfo[
+ "transparency"
+ ] = im_frame.palette._new_color_index(im_frame)
+ except ValueError:
+ pass
+ if "transparency" in encoderinfo:
+ # When the delta is zero, fill the image with transparency
+ diff_frame = im_frame.copy()
+ fill = Image.new(
+ "P", diff_frame.size, encoderinfo["transparency"]
+ )
+ if delta.mode == "RGBA":
+ r, g, b, a = delta.split()
+ mask = ImageMath.eval(
+ "convert(max(max(max(r, g), b), a) * 255, '1')",
+ r=r,
+ g=g,
+ b=b,
+ a=a,
+ )
+ else:
+ if delta.mode == "P":
+ # Convert to L without considering palette
+ delta_l = Image.new("L", delta.size)
+ delta_l.putdata(delta.getdata())
+ delta = delta_l
+ mask = ImageMath.eval("convert(im * 255, '1')", im=delta)
+ diff_frame.paste(fill, mask=ImageOps.invert(mask))
else:
bbox = None
- im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
+ previous_im = im_frame
+ im_frames.append(
+ {"im": diff_frame or im_frame, "bbox": bbox, "encoderinfo": encoderinfo}
+ )
if progress:
im._save_all_progress(imSequence, i, frame_count, total)
- if len(im_frames) > 1:
- for frame_data in im_frames:
- im_frame = frame_data["im"]
- if not frame_data["bbox"]:
- # global header
- for s in _get_global_header(im_frame, frame_data["encoderinfo"]):
- fp.write(s)
- offset = (0, 0)
- else:
- # compress difference
- if not palette:
- frame_data["encoderinfo"]["include_color_table"] = True
+ if len(im_frames) == 1:
+ if "duration" in im.encoderinfo:
+ # Since multiple frames will not be written, use the combined duration
+ im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"]
+ return
- im_frame = im_frame.crop(frame_data["bbox"])
- offset = frame_data["bbox"][:2]
- _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
- return True
- elif "duration" in im.encoderinfo and isinstance(
- im.encoderinfo["duration"], (list, tuple)
- ):
- # Since multiple frames will not be written, add together the frame durations
- im.encoderinfo["duration"] = sum(im.encoderinfo["duration"])
+ for frame_data in im_frames:
+ im_frame = frame_data["im"]
+ if not frame_data["bbox"]:
+ # global header
+ for s in _get_global_header(im_frame, frame_data["encoderinfo"]):
+ fp.write(s)
+ offset = (0, 0)
+ else:
+ # compress difference
+ if not palette:
+ frame_data["encoderinfo"]["include_color_table"] = True
+
+ im_frame = im_frame.crop(frame_data["bbox"])
+ offset = frame_data["bbox"][:2]
+ _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
+ return True
def _save_all(im, fp, filename):
@@ -669,7 +719,7 @@ def _save(im, fp, filename, save_all=False):
palette = im.encoderinfo.get("palette", im.info.get("palette"))
else:
palette = None
- im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
+ im.encoderinfo.setdefault("optimize", True)
if not save_all or not _write_multiple_frames(im, fp, palette):
_write_single_frame(im, fp, palette)
@@ -691,22 +741,10 @@ def get_interlace(im):
def _write_local_header(fp, im, offset, flags):
- transparent_color_exists = False
try:
- transparency = int(im.encoderinfo["transparency"])
- except (KeyError, ValueError):
- pass
- else:
- # optimize the block away if transparent color is not used
- transparent_color_exists = True
-
- used_palette_colors = _get_optimize(im, im.encoderinfo)
- if used_palette_colors is not None:
- # adjust the transparency index after optimize
- try:
- transparency = used_palette_colors.index(transparency)
- except ValueError:
- transparent_color_exists = False
+ transparency = im.encoderinfo["transparency"]
+ except KeyError:
+ transparency = None
if "duration" in im.encoderinfo:
duration = int(im.encoderinfo["duration"] / 10)
@@ -715,11 +753,9 @@ def _write_local_header(fp, im, offset, flags):
disposal = int(im.encoderinfo.get("disposal", 0))
- if transparent_color_exists or duration != 0 or disposal:
- packed_flag = 1 if transparent_color_exists else 0
+ if transparency is not None or duration != 0 or disposal:
+ packed_flag = 1 if transparency is not None else 0
packed_flag |= disposal << 2
- if not transparent_color_exists:
- transparency = 0
fp.write(
b"!"
@@ -727,7 +763,7 @@ def _write_local_header(fp, im, offset, flags):
+ o8(4) # length
+ o8(packed_flag) # packed fields
+ o16(duration) # duration
- + o8(transparency) # transparency index
+ + o8(transparency or 0) # transparency index
+ o8(0)
)
@@ -815,7 +851,7 @@ def _get_optimize(im, info):
:param info: encoderinfo
:returns: list of indexes of palette entries in use, or None
"""
- if im.mode in ("P", "L") and info and info.get("optimize", 0):
+ if im.mode in ("P", "L") and info and info.get("optimize"):
# Potentially expensive operation.
# The palette saves 3 bytes per color not used, but palette
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index 0aa4f7a84..b415a3219 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -391,8 +391,8 @@ if __name__ == "__main__":
with open(sys.argv[1], "rb") as fp:
imf = IcnsImageFile(fp)
for size in imf.info["sizes"]:
- imf.size = size
- imf.save("out-%s-%s-%s.png" % size)
+ width, height, scale = imf.size = size
+ imf.save(f"out-{width}-{height}-{scale}.png")
with Image.open(sys.argv[1]) as im:
im.save("out.png")
if sys.platform == "windows":
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index 0445a2ab2..7f0f0047c 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -174,9 +174,7 @@ class IcoFile:
self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
# ICO images are usually squares
- # self.entry = sorted(self.entry, key=lambda x: x['width'])
- self.entry = sorted(self.entry, key=lambda x: x["square"])
- self.entry.reverse()
+ self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True)
def sizes(self):
"""
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index c41348680..ab8f1d4a0 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -24,6 +24,8 @@
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
import atexit
import builtins
import io
@@ -40,7 +42,7 @@ from enum import IntEnum
from pathlib import Path
try:
- import defusedxml.ElementTree as ElementTree
+ from defusedxml import ElementTree
except ImportError:
ElementTree = None
@@ -355,10 +357,11 @@ def init():
if _initialized >= 2:
return 0
+ parent_name = __name__.rpartition(".")[0]
for plugin in _plugins:
try:
logger.debug("Importing %s", plugin)
- __import__(f"PIL.{plugin}", globals(), locals(), [])
+ __import__(f"{parent_name}.{plugin}", globals(), locals(), [])
except ImportError as e:
logger.debug("Image: failed to import %s: %s", plugin, e)
@@ -476,8 +479,8 @@ class Image:
* :py:func:`~PIL.Image.frombytes`
"""
- format = None
- format_description = None
+ format: str | None = None
+ format_description: str | None = None
_close_exclusive_fp_after_loading = True
def __init__(self):
@@ -549,16 +552,17 @@ class Image:
:py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
more information.
"""
- try:
- if getattr(self, "_fp", False):
- if self._fp != self.fp:
- self._fp.close()
- self._fp = DeferredError(ValueError("Operation on closed image"))
- if self.fp:
- self.fp.close()
- self.fp = None
- except Exception as msg:
- logger.debug("Error closing: %s", msg)
+ if hasattr(self, "fp"):
+ try:
+ if getattr(self, "_fp", False):
+ if self._fp != self.fp:
+ self._fp.close()
+ self._fp = DeferredError(ValueError("Operation on closed image"))
+ if self.fp:
+ self.fp.close()
+ self.fp = None
+ except Exception as msg:
+ logger.debug("Error closing: %s", msg)
if getattr(self, "map", None):
self.map = None
@@ -791,6 +795,9 @@ class Image:
but loads data into this image instead of creating a new image object.
"""
+ if self.width == 0 or self.height == 0:
+ return
+
# may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple):
args = args[0]
@@ -878,12 +885,12 @@ class Image:
"L", "RGB" and "CMYK". The ``matrix`` argument only supports "L"
and "RGB".
- When translating a color image to greyscale (mode "L"),
+ When translating a color image to grayscale (mode "L"),
the library uses the ITU-R 601-2 luma transform::
L = R * 299/1000 + G * 587/1000 + B * 114/1000
- The default method of converting a greyscale ("L") or "RGB"
+ The default method of converting a grayscale ("L") or "RGB"
image into a bilevel (mode "1") image uses Floyd-Steinberg
dither to approximate the original image luminosity levels. If
dither is ``None``, all values larger than 127 are set to 255 (white),
@@ -933,9 +940,9 @@ class Image:
msg = "illegal conversion"
raise ValueError(msg)
im = self.im.convert_matrix(mode, matrix)
- new = self._new(im)
+ new_im = self._new(im)
if has_transparency and self.im.bands == 3:
- transparency = new.info["transparency"]
+ transparency = new_im.info["transparency"]
def convert_transparency(m, v):
v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
@@ -948,8 +955,8 @@ class Image:
convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
for i in range(0, len(transparency))
)
- new.info["transparency"] = transparency
- return new
+ new_im.info["transparency"] = transparency
+ return new_im
if mode == "P" and self.mode == "RGBA":
return self.quantize(colors)
@@ -980,7 +987,7 @@ class Image:
else:
# get the new transparency color.
# use existing conversions
- trns_im = Image()._new(core.new(self.mode, (1, 1)))
+ trns_im = new(self.mode, (1, 1))
if self.mode == "P":
trns_im.putpalette(self.palette)
if isinstance(t, tuple):
@@ -1021,23 +1028,25 @@ class Image:
if mode == "P" and palette == Palette.ADAPTIVE:
im = self.im.quantize(colors)
- new = self._new(im)
+ new_im = self._new(im)
from . import ImagePalette
- new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB"))
+ new_im.palette = ImagePalette.ImagePalette(
+ "RGB", new_im.im.getpalette("RGB")
+ )
if delete_trns:
# This could possibly happen if we requantize to fewer colors.
# The transparency would be totally off in that case.
- del new.info["transparency"]
+ del new_im.info["transparency"]
if trns is not None:
try:
- new.info["transparency"] = new.palette.getcolor(trns, new)
+ new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
except Exception:
# if we can't make a transparent color, don't leave the old
# transparency hanging around to mess us up.
- del new.info["transparency"]
+ del new_im.info["transparency"]
warnings.warn("Couldn't allocate palette entry for transparency")
- return new
+ return new_im
if "LAB" in (self.mode, mode):
other_mode = mode if self.mode == "LAB" else self.mode
@@ -1074,7 +1083,7 @@ class Image:
if mode == "P" and palette != Palette.ADAPTIVE:
from . import ImagePalette
- new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
+ new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB"))
if delete_trns:
# crash fail if we leave a bytes transparency in an rgb/l mode.
del new_im.info["transparency"]
@@ -1154,7 +1163,7 @@ class Image:
if palette.mode != "P":
msg = "bad mode for palette image"
raise ValueError(msg)
- if self.mode != "RGB" and self.mode != "L":
+ if self.mode not in {"RGB", "L"}:
msg = "only RGB or L mode images can be quantized to a palette"
raise ValueError(msg)
im = self.im.convert("P", dither, palette.im)
@@ -1236,7 +1245,7 @@ class Image:
Configures the image file loader so it returns a version of the
image that as closely as possible matches the given mode and
size. For example, you can use this method to convert a color
- JPEG to greyscale while loading it.
+ JPEG to grayscale while loading it.
If any changes are made, returns a tuple with the chosen ``mode`` and
``box`` with coordinates of the original image within the altered one.
@@ -1282,9 +1291,9 @@ class Image:
if self.im.bands == 1 or multiband:
return self._new(filter.filter(self.im))
- ims = []
- for c in range(self.im.bands):
- ims.append(self._new(filter.filter(self.im.getband(c))))
+ ims = [
+ self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands)
+ ]
return merge(self.mode, ims)
def getbands(self):
@@ -1333,10 +1342,7 @@ class Image:
self.load()
if self.mode in ("1", "L", "P"):
h = self.im.histogram()
- out = []
- for i in range(256):
- if h[i]:
- out.append((h[i], i))
+ out = [(h[i], i) for i in range(256) if h[i]]
if len(out) > maxcolors:
return None
return out
@@ -1377,15 +1383,12 @@ class Image:
self.load()
if self.im.bands > 1:
- extrema = []
- for i in range(self.im.bands):
- extrema.append(self.im.getband(i).getextrema())
- return tuple(extrema)
+ return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
return self.im.getextrema()
def _getxmp(self, xmp_tags):
def get_name(tag):
- return tag.split("}")[1]
+ return re.sub("^{[^}]+}", "", tag)
def get_value(element):
value = {get_name(k): v for k, v in element.attrib.items()}
@@ -1608,13 +1611,13 @@ class Image:
than one band, the histograms for all bands are concatenated (for
example, the histogram for an "RGB" image contains 768 values).
- A bilevel image (mode "1") is treated as a greyscale ("L") image
+ A bilevel image (mode "1") is treated as a grayscale ("L") image
by this method.
If a mask is provided, the method returns a histogram for those
parts of the image where the mask image is non-zero. The mask
image must have the same size as the image, and be either a
- bi-level image (mode "1") or a greyscale image ("L").
+ bi-level image (mode "1") or a grayscale image ("L").
:param mask: An optional mask.
:param extrema: An optional tuple of manually-specified extrema.
@@ -1634,13 +1637,13 @@ class Image:
"""
Calculates and returns the entropy for the image.
- A bilevel image (mode "1") is treated as a greyscale ("L")
+ A bilevel image (mode "1") is treated as a grayscale ("L")
image by this method.
If a mask is provided, the method employs the histogram for
those parts of the image where the mask image is non-zero.
The mask image must have the same size as the image, and be
- either a bi-level image (mode "1") or a greyscale image ("L").
+ either a bi-level image (mode "1") or a grayscale image ("L").
:param mask: An optional mask.
:param extrema: An optional tuple of manually-specified extrema.
@@ -1860,7 +1863,8 @@ class Image:
# do things the hard way
im = self.im.convert(mode)
if im.mode not in ("LA", "PA", "RGBA"):
- raise ValueError from e # sanity check
+ msg = "alpha channel could not be added"
+ raise ValueError(msg) from e # sanity check
self.im = im
self.pyaccess = None
self._mode = self.im.mode
@@ -2482,7 +2486,8 @@ class Image:
# overridden by file handlers
if frame != 0:
- raise EOFError
+ msg = "no more images in file"
+ raise EOFError(msg)
def show(self, title=None):
"""
@@ -2889,7 +2894,7 @@ class ImageTransformHandler:
def _wedge():
- """Create greyscale wedge (for debugging only)"""
+ """Create grayscale wedge (for debugging only)"""
return Image()._new(core.wedge("L"))
@@ -2980,15 +2985,16 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
_check_size(size)
- # may pass tuple instead of argument list
- if len(args) == 1 and isinstance(args[0], tuple):
- args = args[0]
-
- if decoder_name == "raw" and args == ():
- args = mode
-
im = new(mode, size)
- im.frombytes(data, decoder_name, args)
+ if im.width != 0 and im.height != 0:
+ # may pass tuple instead of argument list
+ if len(args) == 1 and isinstance(args[0], tuple):
+ args = args[0]
+
+ if decoder_name == "raw" and args == ():
+ args = mode
+
+ im.frombytes(data, decoder_name, args)
return im
@@ -3109,7 +3115,8 @@ def fromarray(obj, mode=None):
try:
mode, rawmode = _fromarray_typemap[typekey]
except KeyError as e:
- msg = "Cannot handle this data type: %s, %s" % typekey
+ typekey_shape, typestr = typekey
+ msg = f"Cannot handle this data type: {typekey_shape}, {typestr}"
raise TypeError(msg) from e
else:
rawmode = mode
@@ -3766,6 +3773,7 @@ class Exif(MutableMapping):
self.endian = self._info._endian
if offset is None:
offset = self._info.next
+ self.fp.tell()
self.fp.seek(offset)
self._info.load(self.fp)
diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py
index 701200317..29a5c995f 100644
--- a/src/PIL/ImageChops.py
+++ b/src/PIL/ImageChops.py
@@ -15,11 +15,13 @@
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
from . import Image
-def constant(image, value):
- """Fill a channel with a given grey level.
+def constant(image: Image.Image, value: int) -> Image.Image:
+ """Fill a channel with a given gray level.
:rtype: :py:class:`~PIL.Image.Image`
"""
@@ -27,7 +29,7 @@ def constant(image, value):
return Image.new("L", image.size, value)
-def duplicate(image):
+def duplicate(image: Image.Image) -> Image.Image:
"""Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`.
:rtype: :py:class:`~PIL.Image.Image`
@@ -36,7 +38,7 @@ def duplicate(image):
return image.copy()
-def invert(image):
+def invert(image: Image.Image) -> Image.Image:
"""
Invert an image (channel). ::
@@ -49,7 +51,7 @@ def invert(image):
return image._new(image.im.chop_invert())
-def lighter(image1, image2):
+def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Compares the two images, pixel by pixel, and returns a new image containing
the lighter values. ::
@@ -64,7 +66,7 @@ def lighter(image1, image2):
return image1._new(image1.im.chop_lighter(image2.im))
-def darker(image1, image2):
+def darker(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Compares the two images, pixel by pixel, and returns a new image containing
the darker values. ::
@@ -79,7 +81,7 @@ def darker(image1, image2):
return image1._new(image1.im.chop_darker(image2.im))
-def difference(image1, image2):
+def difference(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Returns the absolute value of the pixel-by-pixel difference between the two
images. ::
@@ -94,7 +96,7 @@ def difference(image1, image2):
return image1._new(image1.im.chop_difference(image2.im))
-def multiply(image1, image2):
+def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two images on top of each other.
@@ -111,7 +113,7 @@ def multiply(image1, image2):
return image1._new(image1.im.chop_multiply(image2.im))
-def screen(image1, image2):
+def screen(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two inverted images on top of each other. ::
@@ -125,7 +127,7 @@ def screen(image1, image2):
return image1._new(image1.im.chop_screen(image2.im))
-def soft_light(image1, image2):
+def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two images on top of each other using the Soft Light algorithm
@@ -137,7 +139,7 @@ def soft_light(image1, image2):
return image1._new(image1.im.chop_soft_light(image2.im))
-def hard_light(image1, image2):
+def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two images on top of each other using the Hard Light algorithm
@@ -149,7 +151,7 @@ def hard_light(image1, image2):
return image1._new(image1.im.chop_hard_light(image2.im))
-def overlay(image1, image2):
+def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two images on top of each other using the Overlay algorithm
@@ -161,7 +163,9 @@ def overlay(image1, image2):
return image1._new(image1.im.chop_overlay(image2.im))
-def add(image1, image2, scale=1.0, offset=0):
+def add(
+ image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
+) -> Image.Image:
"""
Adds two images, dividing the result by scale and adding the
offset. If omitted, scale defaults to 1.0, and offset to 0.0. ::
@@ -176,7 +180,9 @@ def add(image1, image2, scale=1.0, offset=0):
return image1._new(image1.im.chop_add(image2.im, scale, offset))
-def subtract(image1, image2, scale=1.0, offset=0):
+def subtract(
+ image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
+) -> Image.Image:
"""
Subtracts two images, dividing the result by scale and adding the offset.
If omitted, scale defaults to 1.0, and offset to 0.0. ::
@@ -191,7 +197,7 @@ def subtract(image1, image2, scale=1.0, offset=0):
return image1._new(image1.im.chop_subtract(image2.im, scale, offset))
-def add_modulo(image1, image2):
+def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Add two images, without clipping the result. ::
out = ((image1 + image2) % MAX)
@@ -204,7 +210,7 @@ def add_modulo(image1, image2):
return image1._new(image1.im.chop_add_modulo(image2.im))
-def subtract_modulo(image1, image2):
+def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Subtract two images, without clipping the result. ::
out = ((image1 - image2) % MAX)
@@ -217,7 +223,7 @@ def subtract_modulo(image1, image2):
return image1._new(image1.im.chop_subtract_modulo(image2.im))
-def logical_and(image1, image2):
+def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Logical AND between two images.
Both of the images must have mode "1". If you would like to perform a
@@ -235,7 +241,7 @@ def logical_and(image1, image2):
return image1._new(image1.im.chop_and(image2.im))
-def logical_or(image1, image2):
+def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Logical OR between two images.
Both of the images must have mode "1". ::
@@ -250,7 +256,7 @@ def logical_or(image1, image2):
return image1._new(image1.im.chop_or(image2.im))
-def logical_xor(image1, image2):
+def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Logical XOR between two images.
Both of the images must have mode "1". ::
@@ -265,7 +271,7 @@ def logical_xor(image1, image2):
return image1._new(image1.im.chop_xor(image2.im))
-def blend(image1, image2, alpha):
+def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image:
"""Blend images using constant transparency weight. Alias for
:py:func:`PIL.Image.blend`.
@@ -275,7 +281,9 @@ def blend(image1, image2, alpha):
return Image.blend(image1, image2, alpha)
-def composite(image1, image2, mask):
+def composite(
+ image1: Image.Image, image2: Image.Image, mask: Image.Image
+) -> Image.Image:
"""Create composite using transparency mask. Alias for
:py:func:`PIL.Image.composite`.
@@ -285,7 +293,7 @@ def composite(image1, image2, mask):
return Image.composite(image1, image2, mask)
-def offset(image, xoffset, yoffset=None):
+def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image:
"""Returns a copy of the image where data has been offset by the given
distances. Data wraps around the edges. If ``yoffset`` is omitted, it
is assumed to be equal to ``xoffset``.
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 3a337f9f2..0df3a4c6c 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -787,11 +787,8 @@ def getProfileInfo(profile):
# info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
description = profile.profile.profile_description
cpright = profile.profile.copyright
- arr = []
- for elt in (description, cpright):
- if elt:
- arr.append(elt)
- return "\r\n\r\n".join(arr) + "\r\n\r\n"
+ elements = [element for element in (description, cpright) if element]
+ return "\r\n\r\n".join(elements) + "\r\n\r\n"
except (AttributeError, OSError, TypeError, ValueError) as v:
raise PyCMSError(v) from v
diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py
index befc1fd1d..894461c83 100644
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -124,7 +124,7 @@ def getcolor(color, mode):
"""
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.
+ not color or a palette image, converts the RGB value to a grayscale value.
If the string cannot be parsed, this function raises a :py:exc:`ValueError`
exception.
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index 7d1790faa..6509d4c8e 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -113,6 +113,15 @@ class ImageDraw:
self.font = ImageFont.load_default()
return self.font
+ def _getfont(self, font_size):
+ if font_size is not None:
+ from . import ImageFont
+
+ font = ImageFont.load_default(font_size)
+ else:
+ font = self.getfont()
+ return font
+
def _getink(self, ink, fill=None):
if ink is None and fill is None:
if self.fill:
@@ -456,6 +465,13 @@ class ImageDraw:
**kwargs,
):
"""Draw text."""
+ if embedded_color and self.mode not in ("RGB", "RGBA"):
+ msg = "Embedded color supported only in RGB and RGBA modes"
+ raise ValueError(msg)
+
+ if font is None:
+ font = self._getfont(kwargs.get("font_size"))
+
if self._multiline_check(text):
return self.multiline_text(
xy,
@@ -473,13 +489,6 @@ class ImageDraw:
embedded_color,
)
- if embedded_color and self.mode not in ("RGB", "RGBA"):
- msg = "Embedded color supported only in RGB and RGBA modes"
- raise ValueError(msg)
-
- if font is None:
- font = self.getfont()
-
def getink(fill):
ink, fill = self._getink(fill)
if ink is None:
@@ -570,6 +579,8 @@ class ImageDraw:
stroke_width=0,
stroke_fill=None,
embedded_color=False,
+ *,
+ font_size=None,
):
if direction == "ttb":
msg = "ttb direction is unsupported for multiline text"
@@ -584,6 +595,9 @@ class ImageDraw:
msg = "anchor not supported for multiline text"
raise ValueError(msg)
+ if font is None:
+ font = self._getfont(font_size)
+
widths = []
max_width = 0
lines = self._multiline_split(text)
@@ -645,6 +659,8 @@ class ImageDraw:
features=None,
language=None,
embedded_color=False,
+ *,
+ font_size=None,
):
"""Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text):
@@ -655,7 +671,7 @@ class ImageDraw:
raise ValueError(msg)
if font is None:
- font = self.getfont()
+ font = self._getfont(font_size)
mode = "RGBA" if embedded_color else self.fontmode
return font.getlength(text, mode, direction, features, language)
@@ -672,12 +688,17 @@ class ImageDraw:
language=None,
stroke_width=0,
embedded_color=False,
+ *,
+ font_size=None,
):
"""Get the bounding box of a given string, in pixels."""
if embedded_color and self.mode not in ("RGB", "RGBA"):
msg = "Embedded color supported only in RGB and RGBA modes"
raise ValueError(msg)
+ if font is None:
+ font = self._getfont(font_size)
+
if self._multiline_check(text):
return self.multiline_textbbox(
xy,
@@ -693,8 +714,6 @@ class ImageDraw:
embedded_color,
)
- if font is None:
- font = self.getfont()
mode = "RGBA" if embedded_color else self.fontmode
bbox = font.getbbox(
text, mode, direction, features, language, stroke_width, anchor
@@ -714,6 +733,8 @@ class ImageDraw:
language=None,
stroke_width=0,
embedded_color=False,
+ *,
+ font_size=None,
):
if direction == "ttb":
msg = "ttb direction is unsupported for multiline text"
@@ -728,6 +749,9 @@ class ImageDraw:
msg = "anchor not supported for multiline text"
raise ValueError(msg)
+ if font is None:
+ font = self._getfont(font_size)
+
widths = []
max_width = 0
lines = self._multiline_split(text)
@@ -897,7 +921,7 @@ def floodfill(image, xy, value, border=None, thresh=0):
if border is None:
fill = _color_diff(p, background) <= thresh
else:
- fill = p != value and p != border
+ fill = p not in (value, border)
if fill:
pixel[s, t] = value
new_edge.add((s, t))
diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py
index 3b79d5c46..d7fdec262 100644
--- a/src/PIL/ImageEnhance.py
+++ b/src/PIL/ImageEnhance.py
@@ -59,7 +59,7 @@ class Contrast(_Enhance):
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.
+ gives a solid gray image. A factor of 1.0 gives the original image.
"""
def __init__(self, image):
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 8e4f7dfb2..ae4e23db1 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -26,13 +26,16 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import io
import itertools
import struct
import sys
+from typing import NamedTuple
from . import Image
+from ._deprecate import deprecate
from ._util import is_path
MAXBLOCK = 65536
@@ -61,15 +64,25 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`,
# Helpers
-def raise_oserror(error):
+def _get_oserror(error, *, encoder):
try:
msg = Image.core.getcodecstatus(error)
except AttributeError:
msg = ERRORS.get(error)
if not msg:
- msg = f"decoder error {error}"
- msg += " when reading image file"
- raise OSError(msg)
+ msg = f"{'encoder' if encoder else 'decoder'} error {error}"
+ msg += f" when {'writing' if encoder else 'reading'} image file"
+ return OSError(msg)
+
+
+def raise_oserror(error):
+ deprecate(
+ "raise_oserror",
+ 12,
+ action="It is only useful for translating error codes returned by a codec's "
+ "decode() method, which ImageFile already does automatically.",
+ )
+ raise _get_oserror(error, encoder=False)
def _tilesort(t):
@@ -77,6 +90,13 @@ def _tilesort(t):
return t[2]
+class _Tile(NamedTuple):
+ encoder_name: str
+ extents: tuple[int, int, int, int]
+ offset: int
+ args: tuple | str | None
+
+
#
# --------------------------------------------------------------------
# ImageFile base class
@@ -187,6 +207,8 @@ class ImageFile(Image.Image):
if use_mmap:
# try memory mapping
decoder_name, extents, offset, args = self.tile[0]
+ if isinstance(args, str):
+ args = (args, 0, 1)
if (
decoder_name == "raw"
and len(args) >= 3
@@ -200,8 +222,8 @@ class ImageFile(Image.Image):
with open(self.filename) as fp:
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
if offset + self.size[1] * args[1] > self.map.size():
- # buffer is not large enough
- raise OSError
+ msg = "buffer is not large enough"
+ raise OSError(msg)
self.im = Image.core.map_buffer(
self.map, self.size, decoder_name, offset, args
)
@@ -285,7 +307,7 @@ class ImageFile(Image.Image):
if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0:
# still raised if decoder fails to return anything
- raise_oserror(err_code)
+ raise _get_oserror(err_code, encoder=False)
return Image.Image.load(self)
@@ -412,7 +434,7 @@ class Parser:
if e < 0:
# decoding error
self.image = None
- raise_oserror(e)
+ raise _get_oserror(e, encoder=False)
else:
# end of image
return
@@ -430,7 +452,6 @@ class Parser:
with io.BytesIO(self.data) as fp:
im = Image.open(fp)
except OSError:
- # traceback.print_exc()
pass # not enough data
else:
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
@@ -521,13 +542,13 @@ def _save(im, fp, tile, bufsize=0):
fp.flush()
-def _encode_tile(im, fp, tile, bufsize, fh, exc=None):
- for e, b, o, a in tile:
- if o > 0:
- fp.seek(o)
- encoder = Image._getencoder(im.mode, e, a, im.encoderconfig)
+def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
+ for encoder_name, extents, offset, args in tile:
+ if offset > 0:
+ fp.seek(offset)
+ encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig)
try:
- encoder.setimage(im.im, b)
+ encoder.setimage(im.im, extents)
if encoder.pushes_fd:
encoder.setfd(fp)
errcode = encoder.encode_to_pyfd()[1]
@@ -543,8 +564,7 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None):
# slight speedup: compress to real file object
errcode = encoder.encode_to_file(fh, bufsize)
if errcode < 0:
- msg = f"encoder error {errcode} when writing image file"
- raise OSError(msg) from exc
+ raise _get_oserror(errcode, encoder=True) from exc
finally:
encoder.cleanup()
@@ -690,7 +710,8 @@ class PyDecoder(PyCodec):
If finished with decoding return -1 for the bytes consumed.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
- raise NotImplementedError()
+ msg = "unavailable in base decoder"
+ raise NotImplementedError(msg)
def set_as_raw(self, data, rawmode=None):
"""
@@ -739,7 +760,8 @@ class PyEncoder(PyCodec):
If finished with encoding return 1 for the error code.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
- raise NotImplementedError()
+ msg = "unavailable in base encoder"
+ raise NotImplementedError(msg)
def encode_to_pyfd(self):
"""
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 57268b8f5..c24f86ef3 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -222,7 +222,7 @@ class UnsharpMask(MultibandFilter):
.. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking
- """ # noqa: E501
+ """
name = "UnsharpMask"
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index b77dc6ab1..6db7cc4ec 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -25,12 +25,16 @@
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
import base64
import os
import sys
import warnings
from enum import IntEnum
from io import BytesIO
+from pathlib import Path
+from typing import IO
from . import Image
from ._util import is_directory, is_path
@@ -185,9 +189,20 @@ class ImageFont:
class FreeTypeFont:
"""FreeType font wrapper (requires _imagingft service)"""
- def __init__(self, font=None, size=10, index=0, encoding="", layout_engine=None):
+ def __init__(
+ self,
+ font: bytes | str | Path | IO | None = None,
+ size: float = 10,
+ index: int = 0,
+ encoding: str = "",
+ layout_engine: Layout | None = None,
+ ) -> None:
# FIXME: use service provider instead
+ if size <= 0:
+ msg = "font size must be greater than 0"
+ raise ValueError(msg)
+
self.path = font
self.size = size
self.index = index
@@ -213,6 +228,8 @@ class FreeTypeFont:
)
if is_path(font):
+ if isinstance(font, Path):
+ font = str(font)
if sys.platform == "win32":
font_bytes_path = font if isinstance(font, bytes) else font.encode()
try:
@@ -375,8 +392,9 @@ class FreeTypeFont:
:param stroke_width: The width of the text stroke.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
:return: ``(left, top, right, bottom)`` bounding box
"""
@@ -449,8 +467,9 @@ class FreeTypeFont:
.. versionadded:: 6.2.0
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
.. versionadded:: 8.0.0
@@ -541,8 +560,9 @@ class FreeTypeFont:
.. versionadded:: 6.2.0
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
.. versionadded:: 8.0.0
@@ -563,14 +583,21 @@ class FreeTypeFont:
if start is None:
start = (0, 0)
im = None
+ size = None
- def fill(mode, size):
- nonlocal im
+ def fill(width, height):
+ nonlocal im, size
- im = Image.core.fill(mode, size)
+ size = (width, height)
+ if Image.MAX_IMAGE_PIXELS is not None:
+ pixels = max(1, width) * max(1, height)
+ if pixels > 2 * Image.MAX_IMAGE_PIXELS:
+ return
+
+ im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
return im
- size, offset = self.font.render(
+ offset = self.font.render(
text,
fill,
mode,
@@ -582,7 +609,6 @@ class FreeTypeFont:
ink,
start[0],
start[1],
- Image.MAX_IMAGE_PIXELS,
)
Image._decompression_bomb_check(size)
return im, offset
@@ -706,7 +732,6 @@ class TransposedFont:
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
msg = "text length is undefined for text rotated by 90 or 270 degrees"
raise ValueError(msg)
- _string_length_check(text)
return self.font.getlength(text, *args, **kwargs)
@@ -769,7 +794,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
This specifies the character set to use. It does not alter the
encoding of any text provided in subsequent operations.
:param layout_engine: Which layout engine to use, if available:
- :data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`.
+ :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`.
If it is available, Raqm layout will be used by default.
Otherwise, basic layout will be used.
@@ -782,6 +807,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
.. versionadded:: 4.2.0
:return: A font object.
:exception OSError: If the file could not be read.
+ :exception ValueError: If the font size is not greater than zero.
"""
def freetype(font):
@@ -855,19 +881,258 @@ def load_path(filename):
raise OSError(msg)
-def load_default():
- """Load a "better than nothing" default font.
+def load_default(size=None):
+ """If FreeType support is available, load a version of Aileron Regular,
+ https://dotcolon.net/font/aileron, with a more limited character set.
+
+ Otherwise, load a "better than nothing" font.
.. versionadded:: 1.1.4
+ :param size: The font size of Aileron Regular.
+
+ .. versionadded:: 10.1.0
+
:return: A font object.
"""
- f = ImageFont()
- f._load_pilfont_data(
- # courB08
- BytesIO(
- base64.b64decode(
- b"""
+ if core.__class__.__name__ == "module" or size is not None:
+ f = truetype(
+ BytesIO(
+ base64.b64decode(
+ b"""
+AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA
+AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA
+MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh
+tdHjjERZ8AAAB2AAAAdBsb2NhuOexrgAABVQAAADqbWF4cAC7AEYAAAFYAAAAIG5hbWUr+h5lAAAk
+OAAAA6Jwb3N0D3oPTQAAJ9wAAAEKAAEAAAABGhxJDqIhXw889QALA+gAAAAA0Bqf2QAAAADhCh2h/
+2r/LgOxAyAAAAAIAAIAAAAAAAAAAQAAA8r/GgAAA7j/av9qA7EAAQAAAAAAAAAAAAAAAAAAAHQAAQ
+AAAHQAQwAFAAAAAAACAAAAAQABAAAAQAAAAAAAAAADAfoBkAAFAAgCigJYAAAASwKKAlgAAAFeADI
+BPgAAAAAFAAAAAAAAAAAAAAcAAAAAAAAAAAAAAABVS1dOAEAAIPsCAwL/GgDIA8oA5iAAAJMAAAAA
+AhICsgAAACAAAwH0AAAAAAAAAU0AAADYAAAA8gA5AVMAVgJEAEYCRAA1AuQAKQKOAEAAsAArATsAZ
+AE7AB4CMABVAkQAUADc/+EBEgAgANwAJQEv//sCRAApAkQAggJEADwCRAAtAkQAIQJEADkCRAArAk
+QAMgJEACwCRAAxANwAJQDc/+ECRABnAkQAUAJEAEQB8wAjA1QANgJ/AB0CcwBkArsALwLFAGQCSwB
+kAjcAZALGAC8C2gBkAQgAZAIgADcCYQBkAj8AZANiAGQCzgBkAuEALwJWAGQC3QAvAmsAZAJJADQC
+ZAAiAqoAXgJuACADuAAaAnEAGQJFABMCTwAuATMAYgEv//sBJwAiAkQAUAH0ADIBLAApAhMAJAJjA
+EoCEQAeAmcAHgIlAB4BIgAVAmcAHgJRAEoA7gA+AOn/8wIKAEoA9wBGA1cASgJRAEoCSgAeAmMASg
+JnAB4BSgBKAcsAGAE5ABQCUABCAgIAAQMRAAEB4v/6AgEAAQHOABQBLwBAAPoAYAEvACECRABNA0Y
+AJAItAHgBKgAcAkQAUAEsAHQAygAgAi0AOQD3ADYA9wAWAaEANgGhABYCbAAlAYMAeAGDADkA6/9q
+AhsAFAIKABUB/QAVAAAAAwAAAAMAAAAcAAEAAAAAAKQAAwABAAAAHAAEAIgAAAAeABAAAwAOAH4Aq
+QCrALEAtAC3ALsgGSAdICYgOiBEISL7Av//AAAAIACpAKsAsAC0ALcAuyAYIBwgJiA5IEQhIvsB//
+//4/+5/7j/tP+y/7D/reBR4E/gR+A14CzfTwVxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAMEBQYHCAkKCwwNDg8QERIT
+FBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMT
+U5PUFFSU1RVVldYWVpbXF1eX2BhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAA
+AAAAAAYnFmAAAAAABlAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2htAAAAAAAAAABrbGlqAAAAAHAAbm9
+ycwBnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACYAJgAmAD4AUgCCAMoBCgFO
+AVwBcgGIAaYBvAHKAdYB6AH2AgwCIAJKAogCpgLWAw4DIgNkA5wDugPUA+gD/AQQBEYEogS8BPoFJ
+gVSBWoFgAWwBcoF1gX6BhQGJAZMBmgGiga0BuIHGgdUB2YHkAeiB8AH3AfyCAoIHAgqCDoITghcCG
+oIogjSCPoJKglYCXwJwgnqCgIKKApACl4Klgq8CtwLDAs8C1YLjAuyC9oL7gwMDCYMSAxgDKAMrAz
+qDQoNTA1mDYQNoA2uDcAN2g3oDfYODA4iDkoOXA5sDnoOnA7EDvwAAAAFAAAAAAH0ArwAAwAGAAkA
+DAAPAAAxESERAxMhExcRASELARETAfT6qv6syKr+jgFUqsiqArz9RAGLAP/+1P8B/v3VAP8BLP4CA
+P8AAgA5//IAuQKyAAMACwAANyMDMwIyFhQGIiY0oE4MZk84JCQ4JLQB/v3AJDgkJDgAAgBWAeUBPA
+LfAAMABwAAEyMnMxcjJzOmRgpagkYKWgHl+vr6AAAAAAIARgAAAf4CsgAbAB8AAAEHMxUjByM3Iwc
+jNyM1MzcjNTM3MwczNzMHMxUrAQczAZgdZXEvOi9bLzovWmYdZXEvOi9bLzovWp9bHlsBn4w429vb
+2ziMONvb29s4jAAAAAMANf+mAg4DDAAfACYALAAAJRQGBxUjNS4BJzMeARcRLgE0Njc1MxUeARcjJ
+icVHgEBFBYXNQ4BExU+ATU0Ag5xWDpgcgRcBz41Xl9oVTpVYwpcC1ttXP6cLTQuM5szOrVRZwlOTQ
+ZqVzZECAEAGlukZAlOTQdrUG8O7iNlAQgxNhDlCDj+8/YGOjReAAAAAAUAKf/yArsCvAAHAAsAFQA
+dACcAABIyFhQGIiY0EyMBMwQiBhUUFjI2NTQSMhYUBiImNDYiBhUUFjI2NTR5iFBQiFCVVwHAV/5c
+OiMjOiPmiFBQiFCxOiMjOiMCvFaSVlaS/ZoCsjIzMC80NC8w/uNWklZWkhozMC80NC8wAAAAAgBA/
+/ICbgLAACIALgAAARUjEQYjIiY1NDY3LgE1NDYzMhcVJiMiBhUUFhcWOwE1MxUFFBYzMjc1IyIHDg
+ECbmBcYYOOVkg7R4hsQjY4Q0RNRD4SLDxW/pJUXzksPCkUUk0BgUb+zBVUZ0BkDw5RO1huCkULQzp
+COAMBcHDHRz0J/AIHRQAAAAEAKwHlAIUC3wADAAATIycze0YKWgHl+gAAAAABAGT/sAEXAwwACQAA
+EzMGEBcjLgE0Nt06dXU6OUBAAwzG/jDGVePs4wAAAAEAHv+wANEDDAAJAAATMx4BFAYHIzYQHjo5Q
+EA5OnUDDFXj7ONVxgHQAAAAAQBVAFIB2wHbAA4AAAE3FwcXBycHJzcnNxcnMwEtmxOfcTJjYzJxnx
+ObCj4BKD07KYolmZkliik7PbMAAQBQAFUB9AIlAAsAAAEjFSM1IzUzNTMVMwH0tTq1tTq1AR/Kyjj
+OzgAAAAAB/+H/iACMAGQABAAANwcjNzOMWlFOXVrS3AAAAQAgAP8A8gE3AAMAABMjNTPy0tIA/zgA
+AQAl//IApQByAAcAADYyFhQGIiY0STgkJDgkciQ4JCQ4AAAAAf/7/+IBNALQAAMAABcjEzM5Pvs+H
+gLuAAAAAAIAKf/yAhsCwAADAAcAABIgECA2IBAgKQHy/g5gATL+zgLA/TJEAkYAAAAAAQCCAAABlg
+KyAAgAAAERIxEHNTc2MwGWVr6SIygCsv1OAldxW1sWAAEAPAAAAg4CwAAZAAA3IRUhNRM+ATU0JiM
+iDwEjNz4BMzIWFRQGB7kBUv4x+kI2QTt+EAFWAQp8aGVtSl5GRjEA/0RVLzlLmAoKa3FsUkNxXQAA
+AAEALf/yAhYCwAAqAAABHgEVFAYjIi8BMxceATMyNjU0KwE1MzI2NTQmIyIGDwEjNz4BMzIWFRQGA
+YxBSZJo2RUBVgEHV0JBUaQREUBUQzc5TQcBVgEKfGhfcEMBbxJbQl1x0AoKRkZHPn9GSD80QUVCCg
+pfbGBPOlgAAAACACEAAAIkArIACgAPAAAlIxUjNSE1ATMRMyMRBg8BAiRXVv6qAVZWV60dHLCurq4
+rAdn+QgFLMibzAAABADn/8gIZArIAHQAAATIWFRQGIyIvATMXFjMyNjU0JiMiByMTIRUhBzc2ATNv
+d5Fl1RQBVgIad0VSTkVhL1IwAYj+vh8rMAHHgGdtgcUKCoFXTU5bYgGRRvAuHQAAAAACACv/8gITA
+sAAFwAjAAABMhYVFAYjIhE0NjMyFh8BIycmIyIDNzYTMjY1NCYjIgYVFBYBLmp7imr0l3RZdAgBXA
+IYZ5wKJzU6QVNJSz5SUAHSgWltiQFGxcNlVQoKdv7sPiz+ZF1LTmJbU0lhAAAAAQAyAAACGgKyAAY
+AAAEVASMBITUCGv6oXAFL/oECsij9dgJsRgAAAAMALP/xAhgCwAAWACAALAAAAR4BFRQGIyImNTQ2
+Ny4BNTQ2MhYVFAYmIgYVFBYyNjU0AzI2NTQmIyIGFRQWAZQ5S5BmbIpPOjA7ecp5P2F8Q0J8RIVJS
+0pLTEtOAW0TXTxpZ2ZqPF0SE1A3VWVlVTdQ/UU0N0RENzT9/ko+Ok1NOj1LAAIAMf/yAhkCwAAXAC
+MAAAEyERQGIyImLwEzFxYzMhMHBiMiJjU0NhMyNjU0JiMiBhUUFgEl9Jd0WXQIAVwCGGecCic1SWp
+7imo+UlBAQVNJAsD+usXDZVUKCnYBFD4sgWltif5kW1NJYV1LTmIAAAACACX/8gClAiAABwAPAAAS
+MhYUBiImNBIyFhQGIiY0STgkJDgkJDgkJDgkAiAkOCQkOP52JDgkJDgAAAAC/+H/iAClAiAABwAMA
+AASMhYUBiImNBMHIzczSTgkJDgkaFpSTl4CICQ4JCQ4/mba5gAAAQBnAB4B+AH0AAYAAAENARUlNS
+UB+P6qAVb+bwGRAbCmpkbJRMkAAAIAUAC7AfQBuwADAAcAAAEhNSERITUhAfT+XAGk/lwBpAGDOP8
+AOAABAEQAHgHVAfQABgAAARUFNS0BNQHV/m8BVv6qAStEyUSmpkYAAAAAAgAj//IB1ALAABgAIAAA
+ATIWFRQHDgEHIz4BNz4BNTQmIyIGByM+ARIyFhQGIiY0AQRibmktIAJWBSEqNig+NTlHBFoDezQ4J
+CQ4JALAZ1BjaS03JS1DMD5LLDQ/SUVgcv2yJDgkJDgAAAAAAgA2/5gDFgKYADYAQgAAAQMGFRQzMj
+Y1NCYjIg4CFRQWMzI2NxcGIyImNTQ+AjMyFhUUBiMiJwcGIyImNTQ2MzIfATcHNzYmIyIGFRQzMjY
+Cej8EJjJJlnBAfGQ+oHtAhjUYg5OPx0h2k06Os3xRWQsVLjY5VHtdPBwJETcJDyUoOkZEJz8B0f74
+EQ8kZl6EkTFZjVOLlyknMVm1pmCiaTq4lX6CSCknTVRmmR8wPdYnQzxuSWVGAAIAHQAAAncCsgAHA
+AoAACUjByMTMxMjATMDAcj+UVz4dO5d/sjPZPT0ArL9TgE6ATQAAAADAGQAAAJMArIAEAAbACcAAA
+EeARUUBgcGKwERMzIXFhUUJRUzMjc2NTQnJiMTPgE1NCcmKwEVMzIBvkdHZkwiNt7LOSGq/oeFHBt
+hahIlSTM+cB8Yj5UWAW8QT0VYYgwFArIEF5Fv1eMED2NfDAL93AU+N24PBP0AAAAAAQAv//ICjwLA
+ABsAAAEyFh8BIycmIyIGFRQWMzI/ATMHDgEjIiY1NDYBdX+PCwFWAiKiaHx5ZaIiAlYBCpWBk6a0A
+sCAagoKpqN/gaOmCgplhcicn8sAAAIAZAAAAp8CsgAMABkAAAEeARUUBgcGKwERMzITPgE1NCYnJi
+sBETMyAY59lJp8IzXN0jUVWmdjWRs5d3I4Aq4QqJWUug8EArL9mQ+PeHGHDgX92gAAAAABAGQAAAI
+vArIACwAAJRUhESEVIRUhFSEVAi/+NQHB/pUBTf6zRkYCskbwRvAAAAABAGQAAAIlArIACQAAExUh
+FSERIxEhFboBQ/69VgHBAmzwRv7KArJGAAAAAAEAL//yAo8CwAAfAAABMxEjNQcGIyImNTQ2MzIWH
+wEjJyYjIgYVFBYzMjY1IwGP90wfPnWTprSSf48LAVYCIqJofHllVG+hAU3+s3hARsicn8uAagoKpq
+N/gaN1XAAAAAEAZAAAAowCsgALAAABESMRIREjETMRIRECjFb+hFZWAXwCsv1OAS7+0gKy/sQBPAA
+AAAABAGQAAAC6ArIAAwAAMyMRM7pWVgKyAAABADf/8gHoArIAEwAAAREUBw4BIyImLwEzFxYzMjc2
+NREB6AIFcGpgbQIBVgIHfXQKAQKy/lYxIltob2EpKYyEFD0BpwAAAAABAGQAAAJ0ArIACwAACQEjA
+wcVIxEzEQEzATsBJ3ntQlZWAVVlAWH+nwEnR+ACsv6RAW8AAQBkAAACLwKyAAUAACUVIREzEQIv/j
+VWRkYCsv2UAAABAGQAAAMUArIAFAAAAREjETQ3BgcDIwMmJxYVESMRMxsBAxRWAiMxemx8NxsCVo7
+MywKy/U4BY7ZLco7+nAFmoFxLtP6dArL9lwJpAAAAAAEAZAAAAoACsgANAAAhIwEWFREjETMBJjUR
+MwKAhP67A1aEAUUDVAJeeov+pwKy/aJ5jAFZAAAAAgAv//ICuwLAAAkAEwAAEiAWFRQGICY1NBIyN
+jU0JiIGFRTbATSsrP7MrNrYenrYegLAxaKhxsahov47nIeIm5uIhwACAGQAAAJHArIADgAYAAABHg
+EVFAYHBisBESMRMzITNjQnJisBETMyAZRUX2VOHzuAVtY7GlxcGDWIiDUCrgtnVlVpCgT+5gKy/rU
+V1BUF/vgAAAACAC//zAK9AsAAEgAcAAAlFhcHJiMiBwYjIiY1NDYgFhUUJRQWMjY1NCYiBgI9PUMx
+UDcfKh8omqysATSs/dR62Hp62HpICTg7NgkHxqGixcWitbWHnJyHiJubAAIAZAAAAlgCsgAXACMAA
+CUWFyMmJyYnJisBESMRMzIXHgEVFAYHFiUzMjc+ATU0JyYrAQIqDCJfGQwNWhAhglbiOx9QXEY1Tv
+6bhDATMj1lGSyMtYgtOXR0BwH+1wKyBApbU0BSESRAAgVAOGoQBAABADT/8gIoAsAAJQAAATIWFyM
+uASMiBhUUFhceARUUBiMiJiczHgEzMjY1NCYnLgE1NDYBOmd2ClwGS0E6SUNRdW+HZnKKC1wPWkQ9
+Uk1cZGuEAsBwXUJHNjQ3OhIbZVZZbm5kREo+NT5DFRdYUFdrAAAAAAEAIgAAAmQCsgAHAAABIxEjE
+SM1IQJk9lb2AkICbP2UAmxGAAEAXv/yAmQCsgAXAAABERQHDgEiJicmNREzERQXHgEyNjc2NRECZA
+IIgfCBCAJWAgZYmlgGAgKy/k0qFFxzc1wUKgGz/lUrEkRQUEQSKwGrAAAAAAEAIAAAAnoCsgAGAAA
+hIwMzGwEzAYJ07l3N1FwCsv2PAnEAAAEAGgAAA7ECsgAMAAABAyMLASMDMxsBMxsBA7HAcZyicrZi
+kaB0nJkCsv1OAlP9rQKy/ZsCW/2kAmYAAAEAGQAAAm8CsgALAAAhCwEjEwMzGwEzAxMCCsrEY/bkY
+re+Y/D6AST+3AFcAVb+5gEa/q3+oQAAAQATAAACUQKyAAgAAAERIxEDMxsBMwFdVvRjwLphARD+8A
+EQAaL+sQFPAAABAC4AAAI5ArIACQAAJRUhNQEhNSEVAQI5/fUBof57Aen+YUZGQgIqRkX92QAAAAA
+BAGL/sAEFAwwABwAAARUjETMVIxEBBWlpowMMOP0UOANcAAAB//v/4gE0AtAAAwAABSMDMwE0Pvs+
+HgLuAAAAAQAi/7AAxQMMAAcAABcjNTMRIzUzxaNpaaNQOALsOAABAFAA1wH0AmgABgAAJQsBIxMzE
+wGwjY1GsESw1wFZ/qcBkf5vAAAAAQAy/6oBwv/iAAMAAAUhNSEBwv5wAZBWOAAAAAEAKQJEALYCsg
+ADAAATIycztjhVUAJEbgAAAAACACT/8gHQAiAAHQAlAAAhJwcGIyImNTQ2OwE1NCcmIyIHIz4BMzI
+XFh0BFBcnMjY9ASYVFAF6CR0wVUtgkJoiAgdgaQlaBm1Zrg4DCuQ9R+5MOSFQR1tbDiwUUXBUXowf
+J8c9SjRORzYSgVwAAAAAAgBK//ICRQLfABEAHgAAATIWFRQGIyImLwEVIxEzETc2EzI2NTQmIyIGH
+QEUFgFUcYCVbiNJEyNWVigySElcU01JXmECIJd4i5QTEDRJAt/+3jkq/hRuZV55ZWsdX14AAQAe//
+IB9wIgABgAAAEyFhcjJiMiBhUUFjMyNjczDgEjIiY1NDYBF152DFocbEJXU0A1Rw1aE3pbaoKQAiB
+oWH5qZm1tPDlaXYuLgZcAAAACAB7/8gIZAt8AEQAeAAABESM1BwYjIiY1NDYzMhYfAREDMjY9ATQm
+IyIGFRQWAhlWKDJacYCVbiNJEyOnSV5hQUlcUwLf/SFVOSqXeIuUExA0ARb9VWVrHV9ebmVeeQACA
+B7/8gH9AiAAFQAbAAABFAchHgEzMjY3Mw4BIyImNTQ2MzIWJyIGByEmAf0C/oAGUkA1SwlaD4FXbI
+WObmt45UBVBwEqDQEYFhNjWD84W16Oh3+akU9aU60AAAEAFQAAARoC8gAWAAATBh0BMxUjESMRIzU
+zNTQ3PgEzMhcVJqcDbW1WOTkDB0k8Hx5oAngVITRC/jQBzEIsJRs5PwVHEwAAAAIAHv8uAhkCIAAi
+AC8AAAERFAcOASMiLwEzFx4BMzI2NzY9AQcGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZAQSEd
+NwRAVcBBU5DTlUDASgyWnGAlW4jSRMjp0leYUFJXFMCEv5wSh1zeq8KCTI8VU0ZIQk5Kpd4i5QTED
+RJ/iJlax1fXm5lXnkAAQBKAAACCgLkABcAAAEWFREjETQnLgEHDgEdASMRMxE3NjMyFgIIAlYCBDs
+6RVRWViE5UVViAYUbQP7WASQxGzI7AQJyf+kC5P7TPSxUAAACAD4AAACsAsAABwALAAASMhYUBiIm
+NBMjETNeLiAgLiBiVlYCwCAuICAu/WACEgAC//P/LgCnAsAABwAVAAASMhYUBiImNBcRFAcGIyInN
+RY3NjURWS4gIC4gYgMLcRwNSgYCAsAgLiAgLo79wCUbZAJGBzMOHgJEAAAAAQBKAAACCALfAAsAAC
+EnBxUjETMREzMHEwGTwTJWVvdu9/rgN6kC3/4oAQv6/ugAAQBG//wA3gLfAA8AABMRFBceATcVBiM
+iJicmNRGcAQIcIxkkKi4CAQLf/bkhERoSBD4EJC8SNAJKAAAAAQBKAAADEAIgACQAAAEWFREjETQn
+JiMiFREjETQnJiMiFREjETMVNzYzMhYXNzYzMhYDCwVWBAxedFYEDF50VlYiJko7ThAvJkpEVAGfI
+jn+vAEcQyRZ1v76ARxDJFnW/voCEk08HzYtRB9HAAAAAAEASgAAAgoCIAAWAAABFhURIxE0JyYjIg
+YdASMRMxU3NjMyFgIIAlYCCXBEVVZWITlRVWIBhRtA/tYBJDEbbHR/6QISWz0sVAAAAAACAB7/8gI
+sAiAABwARAAASIBYUBiAmNBIyNjU0JiIGFRSlAQCHh/8Ah7ieWlqeWgIgn/Cfn/D+s3ZfYHV1YF8A
+AgBK/zwCRQIgABEAHgAAATIWFRQGIyImLwERIxEzFTc2EzI2NTQmIyIGHQEUFgFUcYCVbiNJEyNWV
+igySElcU01JXmECIJd4i5QTEDT+8wLWVTkq/hRuZV55ZWsdX14AAgAe/zwCGQIgABEAHgAAAREjEQ
+cGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZVigyWnGAlW4jSRMjp0leYUFJXFMCEv0qARk5Kpd
+4i5QTEDRJ/iJlax1fXm5lXnkAAQBKAAABPgIeAA0AAAEyFxUmBhURIxEzFTc2ARoWDkdXVlYwIwIe
+B0EFVlf+0gISU0cYAAEAGP/yAa0CIAAjAAATMhYXIyYjIgYVFBYXHgEVFAYjIiYnMxYzMjY1NCYnL
+gE1NDbkV2MJWhNdKy04PF1XbVhWbgxaE2ktOjlEUllkAiBaS2MrJCUoEBlPQkhOVFZoKCUmLhIWSE
+BIUwAAAAEAFP/4ARQCiQAXAAATERQXHgE3FQYjIiYnJjURIzUzNTMVMxWxAQMmMx8qMjMEAUdHVmM
+BzP7PGw4mFgY/BSwxDjQBNUJ7e0IAAAABAEL/8gICAhIAFwAAAREjNQcGIyImJyY1ETMRFBceATMy
+Nj0BAgJWITlRT2EKBVYEBkA1RFECEv3uWj4qTToiOQE+/tIlJC43c4DpAAAAAAEAAQAAAfwCEgAGA
+AABAyMDMxsBAfzJaclfop8CEv3uAhL+LQHTAAABAAEAAAMLAhIADAAAAQMjCwEjAzMbATMbAQMLqW
+Z2dmapY3t0a3Z7AhL97gG+/kICEv5AAcD+QwG9AAAB//oAAAHWAhIACwAAARMjJwcjEwMzFzczARq
+8ZIuKY763ZoWFYwEO/vLV1QEMAQbNzQAAAQAB/y4B+wISABEAAAEDDgEjIic1FjMyNj8BAzMbAQH7
+2iFZQB8NDRIpNhQH02GenQIS/cFVUAJGASozEwIt/i4B0gABABQAAAGxAg4ACQAAJRUhNQEhNSEVA
+QGx/mMBNP7iAYL+zkREQgGIREX+ewAAAAABAED/sAEOAwwALAAAASMiBhUUFxYVFAYHHgEVFAcGFR
+QWOwEVIyImNTQ3NjU0JzU2NTQnJjU0NjsBAQ4MKiMLDS4pKS4NCyMqDAtERAwLUlILDERECwLUGBk
+WTlsgKzUFBTcrIFtOFhkYOC87GFVMIkUIOAhFIkxVGDsvAAAAAAEAYP84AJoDIAADAAAXIxEzmjo6
+yAPoAAEAIf+wAO8DDAAsAAATFQYVFBcWFRQGKwE1MzI2NTQnJjU0NjcuATU0NzY1NCYrATUzMhYVF
+AcGFRTvUgsMREQLDCojCw0uKSkuDQsjKgwLREQMCwF6OAhFIkxVGDsvOBgZFk5bICs1BQU3KyBbTh
+YZGDgvOxhVTCJFAAABAE0A3wH2AWQAEwAAATMUIyImJyYjIhUjNDMyFhcWMzIBvjhuGywtQR0xOG4
+bLC1BHTEBZIURGCNMhREYIwAAAwAk/94DIgLoAAcAEQApAAAAIBYQBiAmECQgBhUUFiA2NTQlMhYX
+IyYjIgYUFjMyNjczDgEjIiY1NDYBAQFE3d3+vN0CB/7wubkBELn+xVBnD1wSWDo+QTcqOQZcEmZWX
+HN2Aujg/rbg4AFKpr+Mjb6+jYxbWEldV5ZZNShLVn5na34AAgB4AFIB9AGeAAUACwAAAQcXIyc3Mw
+cXIyc3AUqJiUmJifOJiUmJiQGepqampqampqYAAAIAHAHSAQ4CwAAHAA8AABIyFhQGIiY0NiIGFBY
+yNjRgakREakSTNCEhNCECwEJqQkJqCiM4IyM4AAAAAAIAUAAAAfQCCwALAA8AAAEzFSMVIzUjNTM1
+MxMhNSEBP7W1OrW1OrX+XAGkAVs4tLQ4sP31OAAAAQB0AkQBAQKyAAMAABMjNzOsOD1QAkRuAAAAA
+AEAIADsAKoBdgAHAAASMhYUBiImNEg6KCg6KAF2KDooKDoAAAIAOQBSAbUBngAFAAsAACUHIzcnMw
+UHIzcnMwELiUmJiUkBM4lJiYlJ+KampqampqYAAAABADYB5QDhAt8ABAAAEzczByM2Xk1OXQHv8Po
+AAQAWAeUAwQLfAAQAABMHIzczwV5NTl0C1fD6AAIANgHlAYsC3wAEAAkAABM3MwcjPwEzByM2Xk1O
+XapeTU5dAe/w+grw+gAAAgAWAeUBawLfAAQACQAAEwcjNzMXByM3M8FeTU5dql5NTl0C1fD6CvD6A
+AADACX/8gI1AHIABwAPABcAADYyFhQGIiY0NjIWFAYiJjQ2MhYUBiImNEk4JCQ4JOw4JCQ4JOw4JC
+Q4JHIkOCQkOCQkOCQkOCQkOCQkOAAAAAEAeABSAUoBngAFAAABBxcjJzcBSomJSYmJAZ6mpqamAAA
+AAAEAOQBSAQsBngAFAAAlByM3JzMBC4lJiYlJ+KampgAAAf9qAAABgQKyAAMAACsBATM/VwHAVwKy
+AAAAAAIAFAHIAdwClAAHABQAABMVIxUjNSM1BRUjNwcjJxcjNTMXN9pKMkoByDICKzQqATJLKysCl
+CmjoykBy46KiY3Lm5sAAQAVAAABvALyABgAAAERIxEjESMRIzUzNTQ3NjMyFxUmBgcGHQEBvFbCVj
+k5AxHHHx5iVgcDAg798gHM/jQBzEIOJRuWBUcIJDAVIRYAAAABABX//AHkAvIAJQAAJR4BNxUGIyI
+mJyY1ESYjIgcGHQEzFSMRIxEjNTM1NDc2MzIXERQBowIcIxkkKi4CAR4nXgwDbW1WLy8DEbNdOmYa
+EQQ/BCQvEjQCFQZWFSEWQv40AcxCDiUblhP9uSEAAAAAAAAWAQ4AAQAAAAAAAAATACgAAQAAAAAAA
+QAHAEwAAQAAAAAAAgAHAGQAAQAAAAAAAwAaAKIAAQAAAAAABAAHAM0AAQAAAAAABQA8AU8AAQAAAA
+AABgAPAawAAQAAAAAACAALAdQAAQAAAAAACQALAfgAAQAAAAAACwAXAjQAAQAAAAAADAAXAnwAAwA
+BBAkAAAAmAAAAAwABBAkAAQAOADwAAwABBAkAAgAOAFQAAwABBAkAAwA0AGwAAwABBAkABAAOAL0A
+AwABBAkABQB4ANUAAwABBAkABgAeAYwAAwABBAkACAAWAbwAAwABBAkACQAWAeAAAwABBAkACwAuA
+gQAAwABBAkADAAuAkwATgBvACAAUgBpAGcAaAB0AHMAIABSAGUAcwBlAHIAdgBlAGQALgAATm8gUm
+lnaHRzIFJlc2VydmVkLgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAUgBlAGcAdQBsAGEAcgAAUmV
+ndWxhcgAAMQAuADEAMAAyADsAVQBLAFcATgA7AEEAaQBsAGUAcgBvAG4ALQBSAGUAZwB1AGwAYQBy
+AAAxLjEwMjtVS1dOO0FpbGVyb24tUmVndWxhcgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAVgBlA
+HIAcwBpAG8AbgAgADEALgAxADAAMgA7AFAAUwAgADAAMAAxAC4AMQAwADIAOwBoAG8AdABjAG8Abg
+B2ACAAMQAuADAALgA3ADAAOwBtAGEAawBlAG8AdABmAC4AbABpAGIAMgAuADUALgA1ADgAMwAyADk
+AAFZlcnNpb24gMS4xMDI7UFMgMDAxLjEwMjtob3Rjb252IDEuMC43MDttYWtlb3RmLmxpYjIuNS41
+ODMyOQAAQQBpAGwAZQByAG8AbgAtAFIAZQBnAHUAbABhAHIAAEFpbGVyb24tUmVndWxhcgAAUwBvA
+HIAYQAgAFMAYQBnAGEAbgBvAABTb3JhIFNhZ2FubwAAUwBvAHIAYQAgAFMAYQBnAGEAbgBvAABTb3
+JhIFNhZ2FubwAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBsAG8AbgAuAG4AZQB0AAB
+odHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBs
+AG8AbgAuAG4AZQB0AABodHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAAAACAAAAAAAA/4MAMgAAAAAAA
+AAAAAAAAAAAAAAAAAAAAHQAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATAB
+QAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAA
+xADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0A
+TgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAIsAqQCDAJMAjQDDAKoAtgC3A
+LQAtQCrAL4AvwC8AIwAwADBAAAAAAAB//8AAgABAAAADAAAABwAAAACAAIAAwBxAAEAcgBzAAIABA
+AAAAIAAAABAAAACgBMAGYAAkRGTFQADmxhdG4AGgAEAAAAAP//AAEAAAAWAANDQVQgAB5NT0wgABZ
+ST00gABYAAP//AAEAAAAA//8AAgAAAAEAAmxpZ2EADmxvY2wAFAAAAAEAAQAAAAEAAAACAAYAEAAG
+AAAAAgASADQABAAAAAEATAADAAAAAgAQABYAAQAcAAAAAQABAE8AAQABAGcAAQABAE8AAwAAAAIAE
+AAWAAEAHAAAAAEAAQAvAAEAAQBnAAEAAQAvAAEAGgABAAgAAgAGAAwAcwACAE8AcgACAEwAAQABAE
+kAAAABAAAACgBGAGAAAkRGTFQADmxhdG4AHAAEAAAAAP//AAIAAAABABYAA0NBVCAAFk1PTCAAFlJ
+PTSAAFgAA//8AAgAAAAEAAmNwc3AADmtlcm4AFAAAAAEAAAAAAAEAAQACAAYADgABAAAAAQASAAIA
+AAACAB4ANgABAAoABQAFAAoAAgABACQAPQAAAAEAEgAEAAAAAQAMAAEAOP/nAAEAAQAkAAIGigAEA
+AAFJAXKABoAGQAA//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAD/sv+4/+z/7v/MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAD/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9T/6AAAAAD/8QAA
+ABD/vQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7gAAAAAAAAAAAAAAAAAA//MAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAP/5AAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAD/4AAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//L/9AAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAA/+gAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAD
+/4gAA//AAAAAA//YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAP/OAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/zv/qAAAAAP/0AAAACAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAD/egAA/1kAAAAA/5D/rgAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAD/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAD/8AAA/7b/8P+wAAD/8P/E/98AAAAA/8P/+P/0//oAAAAAAAAAAAAA//gA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/w//C/9MAAP/SAAD/9wAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yAAA/+kAAAAA//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9wAAAAD//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAP/cAAAAAAAAAAAAAAAA/7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAkAFAAEAAAAAQACwAAABcA
+BgAAAAAAAAAIAA4AAAAAAAsAEgAAAAAAAAATABkAAwANAAAAAQAJAAAAAAAAAAAAAAAAAAAAGAAAA
+AAABwAAAAAAAAAAAAAAFQAFAAAAAAAYABgAAAAUAAAACgAAAAwAAgAPABEAFgAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAEAEQBdAAYAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAABwAAAAAACAAAAAAAAAAAAAcAAAAHAAAAEwAJ
+ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA
+gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC
+YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA
+AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
+"""
+ )
+ ),
+ 10 if size is None else size,
+ layout_engine=Layout.BASIC,
+ )
+ else:
+ f = ImageFont()
+ f._load_pilfont_data(
+ # courB08
+ BytesIO(
+ base64.b64decode(
+ b"""
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
@@ -960,12 +1225,12 @@ pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
+QAGAAIAzgAKANUAEw==
"""
- )
- ),
- Image.open(
- BytesIO(
- base64.b64decode(
- b"""
+ )
+ ),
+ Image.open(
+ BytesIO(
+ base64.b64decode(
+ b"""
iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
@@ -990,8 +1255,8 @@ AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
w7IkEbzhVQAAAABJRU5ErkJggg==
"""
+ )
)
- )
- ),
- )
+ ),
+ )
return f
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index eb6bbe6c6..047187195 100644
--- a/src/PIL/ImageMath.py
+++ b/src/PIL/ImageMath.py
@@ -20,10 +20,6 @@ import builtins
from . import Image, _imagingmath
-def _isconstant(v):
- return isinstance(v, (int, float))
-
-
class _Operand:
"""Wraps an image operand, providing standard operators"""
@@ -43,7 +39,7 @@ class _Operand:
raise ValueError(msg)
else:
# argument was a constant
- if _isconstant(im1) and self.im.mode in ("1", "L", "I"):
+ if isinstance(im1, (int, float)) and self.im.mode in ("1", "L", "I"):
return Image.new("I", self.im.size, im1)
else:
return Image.new("F", self.im.size, im1)
@@ -239,7 +235,7 @@ def eval(expression, _dict={}, **kw):
args = ops.copy()
args.update(_dict)
args.update(kw)
- for k, v in list(args.items()):
+ for k, v in args.items():
if hasattr(v, "im"):
args[k] = _Operand(v)
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 1231ad6eb..f183c8f27 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -56,7 +56,7 @@ def _lut(image, lut):
lut = lut + lut + lut
return image.point(lut)
else:
- msg = "not supported for this image mode"
+ msg = f"not supported for mode {image.mode}"
raise OSError(msg)
@@ -242,7 +242,7 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
Returns a resized version of the image, set to the maximum width and height
within the requested size, while maintaining the original aspect ratio.
- :param image: The image to resize and crop.
+ :param image: The image to resize.
:param size: The requested output size in pixels, given as a
(width, height) tuple.
:param method: Resampling method to use. Default is
@@ -266,6 +266,35 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
return image.resize(size, resample=method)
+def cover(image, size, method=Image.Resampling.BICUBIC):
+ """
+ Returns a resized version of the image, so that the requested size is
+ covered, while maintaining the original aspect ratio.
+
+ :param image: The image to resize.
+ :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.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
+ :return: An image.
+ """
+
+ im_ratio = image.width / image.height
+ dest_ratio = size[0] / size[1]
+
+ if im_ratio != dest_ratio:
+ if im_ratio < dest_ratio:
+ new_height = round(image.height / image.width * size[0])
+ if new_height != size[1]:
+ size = (size[0], new_height)
+ else:
+ new_width = round(image.width / image.height * size[1])
+ if new_width != size[0]:
+ size = (new_width, size[1])
+ return image.resize(size, resample=method)
+
+
def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
"""
Returns a resized and padded version of the image, expanded to fill the
@@ -528,9 +557,7 @@ def invert(image):
:param image: The image to invert.
:return: An image.
"""
- lut = []
- for i in range(256):
- lut.append(255 - i)
+ lut = list(range(255, -1, -1))
return image.point(lut) if image.mode == "1" else _lut(image, lut)
@@ -552,10 +579,8 @@ def posterize(image, bits):
:param bits: The number of bits to keep for each channel (1-8).
:return: An image.
"""
- lut = []
mask = ~(2 ** (8 - bits) - 1)
- for i in range(256):
- lut.append(i & mask)
+ lut = [i & mask for i in range(256)]
return _lut(image, lut)
@@ -564,7 +589,7 @@ def solarize(image, threshold=128):
Invert all pixel values above a threshold.
:param image: The image to solarize.
- :param threshold: All pixels above this greyscale level are inverted.
+ :param threshold: All pixels above this grayscale level are inverted.
:return: An image.
"""
lut = []
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index f0c094708..212948793 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -102,6 +102,30 @@ class ImagePalette:
# Declare tostring as an alias for tobytes
tostring = tobytes
+ def _new_color_index(self, image=None, e=None):
+ if not isinstance(self.palette, bytearray):
+ self._palette = bytearray(self.palette)
+ index = len(self.palette) // 3
+ special_colors = ()
+ if image:
+ special_colors = (
+ image.info.get("background"),
+ image.info.get("transparency"),
+ )
+ while index in special_colors:
+ index += 1
+ if index >= 256:
+ if image:
+ # Search for an unused index
+ for i, count in reversed(list(enumerate(image.histogram()))):
+ if count == 0 and i not in special_colors:
+ index = i
+ break
+ if index >= 256:
+ msg = "cannot allocate more than 256 colors"
+ raise ValueError(msg) from e
+ return index
+
def getcolor(self, color, image=None):
"""Given an rgb tuple, allocate palette entry.
@@ -124,27 +148,7 @@ class ImagePalette:
return self.colors[color]
except KeyError as e:
# allocate new color slot
- if not isinstance(self.palette, bytearray):
- self._palette = bytearray(self.palette)
- index = len(self.palette) // 3
- special_colors = ()
- if image:
- special_colors = (
- image.info.get("background"),
- image.info.get("transparency"),
- )
- while index in special_colors:
- index += 1
- if index >= 256:
- if image:
- # Search for an unused index
- for i, count in reversed(list(enumerate(image.histogram()))):
- if count == 0 and i not in special_colors:
- index = i
- break
- if index >= 256:
- msg = "cannot allocate more than 256 colors"
- raise ValueError(msg) from e
+ index = self._new_color_index(image, e)
self.colors[color] = index
if index * 3 < len(self.palette):
self._palette = (
@@ -200,20 +204,15 @@ def raw(rawmode, data):
def make_linear_lut(black, white):
- lut = []
if black == 0:
- for i in range(256):
- lut.append(white * i // 255)
- else:
- raise NotImplementedError # FIXME
- return lut
+ return [white * i // 255 for i in range(256)]
+
+ msg = "unavailable when black is non-zero"
+ raise NotImplementedError(msg) # FIXME
def make_gamma_lut(exp):
- lut = []
- for i in range(256):
- lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
- return lut
+ return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
def negative(mode="RGB"):
@@ -225,9 +224,7 @@ def negative(mode="RGB"):
def random(mode="RGB"):
from random import randint
- palette = []
- for i in range(256 * len(mode)):
- palette.append(randint(0, 255))
+ palette = [randint(0, 255) for _ in range(256 * len(mode))]
return ImagePalette(mode, palette)
@@ -256,8 +253,6 @@ def load(filename):
if lut:
break
except (SyntaxError, ValueError):
- # import traceback
- # traceback.print_exc()
pass
else:
msg = "cannot load palette"
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index 9b7245454..56c1aa525 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -83,16 +83,6 @@ def fromqimage(im):
def fromqpixmap(im):
return fromqimage(im)
- # buffer = QBuffer()
- # buffer.open(QIODevice.ReadWrite)
- # # im.save(buffer)
- # # What if png doesn't support some image features like animation?
- # im.save(buffer, 'ppm')
- # bytes_io = BytesIO()
- # bytes_io.write(buffer.data())
- # buffer.close()
- # bytes_io.seek(0)
- # return Image.open(bytes_io)
def align8to32(bytes, width, mode):
@@ -113,12 +103,10 @@ def align8to32(bytes, width, mode):
if not extra_padding:
return bytes
- new_data = []
- for i in range(len(bytes) // bytes_per_line):
- new_data.append(
- bytes[i * bytes_per_line : (i + 1) * bytes_per_line]
- + b"\x00" * extra_padding
- )
+ new_data = [
+ bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding
+ for i in range(len(bytes) // bytes_per_line)
+ ]
return b"".join(new_data)
@@ -141,15 +129,11 @@ def _toqclass_helper(im):
format = qt_format.Format_Mono
elif im.mode == "L":
format = qt_format.Format_Indexed8
- colortable = []
- for i in range(256):
- colortable.append(rgb(i, i, i))
+ colortable = [rgb(i, i, i) for i in range(256)]
elif im.mode == "P":
format = qt_format.Format_Indexed8
- colortable = []
palette = im.getpalette()
- for i in range(0, len(palette), 3):
- colortable.append(rgb(*palette[i : i + 3]))
+ colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)]
elif im.mode == "RGB":
# Populate the 4th channel with 255
im = im.convert("RGBA")
@@ -208,9 +192,5 @@ def toqimage(im):
def toqpixmap(im):
- # # This doesn't work. For now using a dumb approach.
- # im_data = _toqclass_helper(im)
- # result = QPixmap(im_data["size"][0], im_data["size"][1])
- # result.loadFromData(im_data["data"])
qimage = toqimage(im)
return QPixmap.fromImage(qimage)
diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py
index c4bb6334a..2d96b8b13 100644
--- a/src/PIL/ImageSequence.py
+++ b/src/PIL/ImageSequence.py
@@ -40,7 +40,8 @@ class Iterator:
self.im.seek(ix)
return self.im
except EOFError as e:
- raise IndexError from e # end of sequence
+ msg = "end of sequence"
+ raise IndexError(msg) from e
def __iter__(self):
return self
@@ -51,7 +52,8 @@ class Iterator:
self.position += 1
return self.im
except EOFError as e:
- raise StopIteration from e
+ msg = "end of sequence"
+ raise StopIteration(msg) from e
def all_frames(im, func=None):
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index 8b1c3f8bb..3d8fa2e40 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -99,7 +99,8 @@ class Viewer:
Returns the command used to display the file.
Not implemented in the base class.
"""
- raise NotImplementedError
+ msg = "unavailable in base viewer"
+ raise NotImplementedError(msg)
def save_image(self, image):
"""Save to temporary file and return filename."""
diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py
index b7ebddf06..edc39fb53 100644
--- a/src/PIL/ImageStat.py
+++ b/src/PIL/ImageStat.py
@@ -21,9 +21,7 @@
# See the README file for information on usage and redistribution.
#
-import functools
import math
-import operator
class Stat:
@@ -53,26 +51,22 @@ class Stat:
"""Get min/max values for each band in the image"""
def minmax(histogram):
- n = 255
- x = 0
+ res_min, res_max = 255, 0
for i in range(256):
if histogram[i]:
- n = min(n, i)
- x = max(x, i)
- return n, x # returns (255, 0) if there's no data in the histogram
+ res_min = i
+ break
+ for i in range(255, -1, -1):
+ if histogram[i]:
+ res_max = i
+ break
+ return res_min, res_max
- v = []
- for i in range(0, len(self.h), 256):
- v.append(minmax(self.h[i:]))
- return v
+ return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)]
def _getcount(self):
"""Get total number of pixels in each layer"""
-
- v = []
- for i in range(0, len(self.h), 256):
- v.append(functools.reduce(operator.add, self.h[i : i + 256]))
- return v
+ return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)]
def _getsum(self):
"""Get sum of all pixels in each layer"""
@@ -98,11 +92,7 @@ class Stat:
def _getmean(self):
"""Get average pixel level for each layer"""
-
- v = []
- for i in self.bands:
- v.append(self.sum[i] / self.count[i])
- return v
+ return [self.sum[i] / self.count[i] for i in self.bands]
def _getmedian(self):
"""Get median pixel level for each layer"""
@@ -121,28 +111,18 @@ class Stat:
def _getrms(self):
"""Get RMS for each layer"""
-
- v = []
- for i in self.bands:
- v.append(math.sqrt(self.sum2[i] / self.count[i]))
- return v
+ return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
def _getvar(self):
"""Get variance for each layer"""
-
- v = []
- for i in self.bands:
- n = self.count[i]
- v.append((self.sum2[i] - (self.sum[i] ** 2.0) / n) / n)
- return v
+ return [
+ (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
+ for i in self.bands
+ ]
def _getstddev(self):
"""Get standard deviation for each layer"""
-
- v = []
- for i in self.bands:
- v.append(math.sqrt(self.var[i]))
- return v
+ return [math.sqrt(self.var[i]) for i in self.bands]
Global = Stat # compatibility
diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py
index ca9b14c8a..c7c64b35a 100644
--- a/src/PIL/ImageWin.py
+++ b/src/PIL/ImageWin.py
@@ -54,9 +54,9 @@ class Dib:
"L", "P", or "RGB".
If the display requires a palette, this constructor creates a suitable
- palette and associates it with the image. For an "L" image, 128 greylevels
+ palette and associates it with the image. For an "L" image, 128 graylevels
are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together
- with 20 greylevels.
+ with 20 graylevels.
To make sure that palettes work properly under Windows, you must call the
``palette`` method upon certain events from Windows.
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index 6ce4975c0..3a40cf987 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -18,10 +18,9 @@ import os
import tempfile
from . import Image, ImageFile
-from ._binary import i8
+from ._binary import i8, o8
from ._binary import i16be as i16
from ._binary import i32be as i32
-from ._binary import o8
COMPRESSION = {1: "raw", 5: "jpeg"}
@@ -58,13 +57,13 @@ class IptcImageFile(ImageFile.ImageFile):
#
# get a IPTC field header
s = self.fp.read(5)
- if not len(s):
+ if not s.strip(b"\x00"):
return None, 0
tag = s[1], s[2]
# syntax
- if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9:
+ if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]:
msg = "invalid IPTC/NAA file"
raise SyntaxError(msg)
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 963d6c1a3..bb0cb676a 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -334,10 +334,7 @@ def _save(im, fp, filename):
if quality_layers is not None and not (
isinstance(quality_layers, (list, tuple))
and all(
- [
- isinstance(quality_layer, (int, float))
- for quality_layer in quality_layers
- ]
+ isinstance(quality_layer, (int, float)) for quality_layer in quality_layers
)
):
msg = "quality_layers must be a sequence of numbers"
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 74130854f..5add65f45 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -165,16 +165,25 @@ def APP(self, marker):
except TypeError:
dpi = x_resolution
if math.isnan(dpi):
- raise ValueError
+ msg = "DPI is not a number"
+ raise ValueError(msg)
if resolution_unit == 3: # cm
# 1 dpcm = 2.54 dpi
dpi *= 2.54
self.info["dpi"] = dpi, dpi
- except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError):
- # SyntaxError for invalid/unreadable EXIF
+ except (
+ struct.error,
+ KeyError,
+ SyntaxError,
+ TypeError,
+ ValueError,
+ ZeroDivisionError,
+ ):
+ # struct.error for truncated EXIF
# KeyError for dpi not included
- # ZeroDivisionError for invalid dpi rational value
+ # SyntaxError for invalid/unreadable EXIF
# ValueError or TypeError for dpi being an invalid float
+ # ZeroDivisionError for invalid dpi rational value
self.info["dpi"] = 72, 72
@@ -224,9 +233,7 @@ def SOF(self, marker):
# fixup icc profile
self.icclist.sort() # sort by sequence number
if self.icclist[0][13] == len(self.icclist):
- profile = []
- for p in self.icclist:
- profile.append(p[14:])
+ profile = [p[14:] for p in self.icclist]
icc_profile = b"".join(profile)
else:
icc_profile = None # wrong number of fragments
@@ -388,7 +395,7 @@ class JpegImageFile(ImageFile.ImageFile):
# self.__offset = self.fp.tell()
break
s = self.fp.read(1)
- elif i == 0 or i == 0xFFFF:
+ elif i in {0, 0xFFFF}:
# padded marker or junk; move on
s = b"\xff"
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
@@ -496,7 +503,7 @@ class JpegImageFile(ImageFile.ImageFile):
for segment, content in self.applist:
if segment == "APP1":
- marker, xmp_tags = content.rsplit(b"\x00", 1)
+ marker, xmp_tags = content.split(b"\x00")[:2]
if marker == b"http://ns.adobe.com/xap/1.0/":
return self._getxmp(xmp_tags)
return {}
@@ -711,7 +718,8 @@ def _save(im, fp, filename):
for idx, table in enumerate(qtables):
try:
if len(table) != 64:
- raise TypeError
+ msg = "Invalid quantization table"
+ raise TypeError(msg)
table = array.array("H", table)
except TypeError as e:
msg = "Invalid quantization table"
@@ -777,6 +785,8 @@ def _save(im, fp, filename):
dpi[0],
dpi[1],
subsampling,
+ info.get("restart_marker_blocks", 0),
+ info.get("restart_marker_rows", 0),
qtables,
comment,
extra,
diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py
index 801318930..9300d3545 100644
--- a/src/PIL/MicImagePlugin.py
+++ b/src/PIL/MicImagePlugin.py
@@ -51,10 +51,11 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
# find ACI subfiles with Image members (maybe not the
# best way to identify MIC files, but what the... ;-)
- self.images = []
- for path in self.ole.listdir():
- if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image":
- self.images.append(path)
+ self.images = [
+ path
+ for path in self.ole.listdir()
+ if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image"
+ ]
# if we didn't find any images, this is probably not
# an MIC file.
@@ -66,6 +67,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1
+ self.__fp = self.fp
self.seek(0)
def seek(self, frame):
@@ -87,10 +89,12 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
return self.frame
def close(self):
+ self.__fp.close()
self.ole.close()
super().close()
def __exit__(self, *args):
+ self.__fp.close()
self.ole.close()
super().__exit__()
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index c36dc9b95..75f599843 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -32,9 +32,6 @@ from . import (
from ._binary import i16be as i16
from ._binary import o32le
-# def _accept(prefix):
-# return JpegImagePlugin._accept(prefix)
-
def _save(im, fp, filename):
JpegImagePlugin._save(im, fp, filename)
diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py
index 13b3048f6..c01534bbf 100644
--- a/src/PIL/PSDraw.py
+++ b/src/PIL/PSDraw.py
@@ -109,7 +109,7 @@ class PSDraw:
if im.mode == "1":
dpi = 200 # fax
else:
- dpi = 100 # greyscale
+ dpi = 100 # grayscale
# image size (on paper)
x = im.size[0] * 72 / dpi
y = im.size[1] * 72 / dpi
diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py
index a88a90791..606739c87 100644
--- a/src/PIL/PalmImagePlugin.py
+++ b/src/PIL/PalmImagePlugin.py
@@ -124,7 +124,7 @@ def _save(im, fp, filename):
if im.encoderinfo.get("bpp") in (1, 2, 4):
# this is 8-bit grayscale, so we shift it to get the high-order bits,
# and invert it because
- # Palm does greyscale from white (0) to black (1)
+ # Palm does grayscale from white (0) to black (1)
bpp = im.encoderinfo["bpp"]
im = im.point(
lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift)
diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py
index 8db5822fe..8b0014f3a 100644
--- a/src/PIL/PcfFontFile.py
+++ b/src/PIL/PcfFontFile.py
@@ -129,9 +129,8 @@ class PcfFontFile(FontFile.FontFile):
nprops = i32(fp.read(4))
# read property description
- p = []
- for i in range(nprops):
- p.append((i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))))
+ p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)]
+
if nprops & 3:
fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad
@@ -186,8 +185,6 @@ class PcfFontFile(FontFile.FontFile):
#
# bitmap data
- bitmaps = []
-
fp, format, i16, i32 = self._getformat(PCF_BITMAPS)
nbitmaps = i32(fp.read(4))
@@ -196,13 +193,9 @@ class PcfFontFile(FontFile.FontFile):
msg = "Wrong number of bitmaps"
raise OSError(msg)
- offsets = []
- for i in range(nbitmaps):
- offsets.append(i32(fp.read(4)))
+ offsets = [i32(fp.read(4)) for _ in range(nbitmaps)]
- bitmap_sizes = []
- for i in range(4):
- bitmap_sizes.append(i32(fp.read(4)))
+ bitmap_sizes = [i32(fp.read(4)) for _ in range(4)]
# byteorder = format & 4 # non-zero => MSB
bitorder = format & 8 # non-zero => MSB
@@ -218,6 +211,7 @@ class PcfFontFile(FontFile.FontFile):
if bitorder:
mode = "1"
+ bitmaps = []
for i in range(nbitmaps):
xsize, ysize = metrics[i][:2]
b, e = offsets[i : i + 2]
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 854d9e83e..67990b0ad 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -91,7 +91,7 @@ class PcxImageFile(ImageFile.ImageFile):
self.fp.seek(-769, io.SEEK_END)
s = self.fp.read(769)
if len(s) == 769 and s[0] == 12:
- # check if the palette is linear greyscale
+ # check if the palette is linear grayscale
for i in range(256):
if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3:
mode = rawmode = "P"
@@ -203,7 +203,7 @@ def _save(im, fp, filename):
palette += b"\x00" * (768 - len(palette))
fp.write(palette) # 768 bytes
elif im.mode == "L":
- # greyscale palette
+ # grayscale palette
fp.write(o8(12))
for i in range(256):
fp.write(o8(i) * 3)
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
index 7c24c00fe..5ef813b7a 100644
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -96,7 +96,7 @@ def _write_image(im, filename, existing_pdf, image_refs):
dict_obj["ColorSpace"] = [
PdfParser.PdfName("Indexed"),
PdfParser.PdfName("DeviceRGB"),
- 255,
+ len(palette) // 3 - 1,
PdfParser.PdfBinary(palette),
]
procset = "ImageI" # indexed color
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index dc1012f54..8bdb65cce 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -82,7 +82,7 @@ class IndirectReference(
collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"])
):
def __str__(self):
- return "%s %s R" % self
+ return f"{self.object_id} {self.generation} R"
def __bytes__(self):
return self.__str__().encode("us-ascii")
@@ -103,7 +103,7 @@ class IndirectReference(
class IndirectObjectDef(IndirectReference):
def __str__(self):
- return "%s %s obj" % self
+ return f"{self.object_id} {self.generation} obj"
class XrefTable:
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 395601b99..423856ae9 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -56,7 +56,7 @@ _MAGIC = b"\211PNG\r\n\032\n"
_MODES = {
# supported bits/color combinations, and corresponding modes/rawmodes
- # Greyscale
+ # Grayscale
(1, 0): ("1", "1"),
(2, 0): ("L", "L;2"),
(4, 0): ("L", "L;4"),
@@ -70,7 +70,7 @@ _MODES = {
(2, 3): ("P", "P;2"),
(4, 3): ("P", "P;4"),
(8, 3): ("P", "P"),
- # Greyscale with alpha
+ # Grayscale with alpha
(8, 4): ("LA", "LA"),
(16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available
# Truecolour with alpha
@@ -438,11 +438,12 @@ class PngStream(ChunkStream):
tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)]
self.im_tile = tile
self.im_idat = length
- raise EOFError
+ msg = "image data found"
+ raise EOFError(msg)
def chunk_IEND(self, pos, length):
- # end of PNG image
- raise EOFError
+ msg = "end of PNG image"
+ raise EOFError(msg)
def chunk_PLTE(self, pos, length):
# palette
@@ -891,7 +892,8 @@ class PngImageFile(ImageFile.ImageFile):
self.dispose_extent = self.info.get("bbox")
if not self.tile:
- raise EOFError
+ msg = "image not found in APNG frame"
+ raise EOFError(msg)
# setup frame disposal (actual disposal done when needed in the next _seek())
if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
@@ -1042,6 +1044,7 @@ _OUTMODES = {
"LA": ("LA", b"\x08\x04"),
"I": ("I;16B", b"\x10\x00"),
"I;16": ("I;16B", b"\x10\x00"),
+ "I;16B": ("I;16B", b"\x10\x00"),
"P;1": ("P;1", b"\x01\x03"),
"P;2": ("P;2", b"\x02\x03"),
"P;4": ("P;4", b"\x04\x03"),
@@ -1109,10 +1112,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
if im_frame.mode == rawmode:
im_frame = im_frame.copy()
else:
- if rawmode == "P":
- im_frame = im_frame.convert(rawmode, palette=im.palette)
- else:
- im_frame = im_frame.convert(rawmode)
+ im_frame = im_frame.convert(rawmode)
encoderinfo = im.encoderinfo.copy()
if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count]
@@ -1165,6 +1165,9 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
if progress:
im._save_all_progress(imSequence, i, frame_count, total)
+ if len(im_frames) == 1 and not default_image:
+ return im_frames[0]["im"]
+
# animation control
chunk(
fp,
@@ -1175,6 +1178,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
# default image IDAT (if it exists)
if default_image:
+ if im.mode != rawmode:
+ im = im.convert(rawmode)
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
seq_num = 0
@@ -1236,11 +1241,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
)
modes = set()
append_images = im.encoderinfo.get("append_images", [])
- if default_image:
- chain = itertools.chain(append_images)
- else:
- chain = itertools.chain([im], append_images)
- for im_seq in chain:
+ for im_seq in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(im_seq):
modes.add(im_frame.mode)
for mode in ("RGBA", "RGB", "P"):
@@ -1402,8 +1403,10 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, b"eXIf", exif)
if save_all:
- _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
- else:
+ im = _write_multiple_frames(
+ im, fp, chunk, rawmode, default_image, append_images
+ )
+ if im:
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
if info:
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index e480ab055..93f1528c5 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -328,9 +328,6 @@ def _save(im, fp, filename):
fp.write(b"65535\n")
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
- # ALTERNATIVE: save via builtin debug function
- # im._dump(filename)
-
#
# --------------------------------------------------------------------
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index 2f019bb8c..46970da04 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -186,6 +186,9 @@ def _layerinfo(fp, ct_bytes):
ct_types = i16(read(2))
types = list(range(ct_types))
if len(types) > 4:
+ fp.seek(len(types) * 6 + 12, io.SEEK_CUR)
+ size = i32(read(4))
+ fp.seek(size, io.SEEK_CUR)
continue
for _ in types:
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
index 99b46a4a6..24d30d2a6 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -244,7 +244,7 @@ class _PyAccessI16_L(PyAccess):
except TypeError:
color = min(color[0], 65535)
- pixel.l = color & 0xFF # noqa: E741
+ pixel.l = color & 0xFF
pixel.r = color >> 8
@@ -265,7 +265,7 @@ class _PyAccessI16_B(PyAccess):
except Exception:
color = min(color[0], 65535)
- pixel.l = color >> 8 # noqa: E741
+ pixel.l = color >> 8
pixel.r = color & 0xFF
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index acb9ce5a3..a2a259c89 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -123,7 +123,7 @@ class SgiImageFile(ImageFile.ImageFile):
def _save(im, fp, filename):
- if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L":
+ if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode"
raise ValueError(msg)
@@ -155,7 +155,7 @@ def _save(im, fp, filename):
# Z Dimension: Number of channels
z = len(im.mode)
- if dim == 1 or dim == 2:
+ if dim in {1, 2}:
z = 1
# assert we've got the right number of bands.
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 408b982b5..14cad8f9a 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -238,9 +238,7 @@ def makeSpiderHeader(im):
if nvalues < 23:
return []
- hdr = []
- for i in range(nvalues):
- hdr.append(0.0)
+ hdr = [0.0] * nvalues
# NB these are Fortran indices
hdr[1] = 1.0 # nslice (=1 for an image)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 921992cca..5106b4508 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -251,6 +251,8 @@ OPEN_INFO = {
(II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
(MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
(II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
+ (II, 6, (1,), 1, (8,), ()): ("L", "L"),
+ (MM, 6, (1,), 1, (8,), ()): ("L", "L"),
# JPEG compressed images handled by LibTiff and auto-converted to RGBX
# Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
@@ -1883,13 +1885,14 @@ class AppendingTiffWriter:
8, # long8
]
- # StripOffsets = 273
- # FreeOffsets = 288
- # TileOffsets = 324
- # JPEGQTables = 519
- # JPEGDCTables = 520
- # JPEGACTables = 521
- Tags = {273, 288, 324, 519, 520, 521}
+ Tags = {
+ 273, # StripOffsets
+ 288, # FreeOffsets
+ 324, # TileOffsets
+ 519, # JPEGQTables
+ 520, # JPEGDCTables
+ 521, # JPEGACTables
+ }
def __init__(self, fn, new=False):
if hasattr(fn, "read"):
@@ -1939,8 +1942,6 @@ class AppendingTiffWriter:
iimm = self.f.read(4)
if not iimm:
- # msg = "nothing written into new page"
- # raise RuntimeError(msg)
# Make it easy to finish a frame without committing to a new one.
return
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 30b05e4e1..b02c637b6 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -56,7 +56,7 @@ def lookup(tag, group=None):
##
# Map tag numbers to tag info.
#
-# id: (Name, Type, Length, enum_values)
+# id: (Name, Type, Length[, enum_values])
#
# The length here differs from the length in the tiff spec. For
# numbers, the tiff spec is for the number of fields returned. We
@@ -427,7 +427,7 @@ def _populate():
TAGS_V2[k] = TagInfo(k, *v)
- for group, tags in TAGS_V2_GROUPS.items():
+ for tags in TAGS_V2_GROUPS.values():
for k, v in tags.items():
tags[k] = TagInfo(k, *v)
@@ -438,22 +438,6 @@ _populate()
TYPES = {}
-# was:
-# TYPES = {
-# 1: "byte",
-# 2: "ascii",
-# 3: "short",
-# 4: "long",
-# 5: "rational",
-# 6: "signed byte",
-# 7: "undefined",
-# 8: "signed short",
-# 9: "signed long",
-# 10: "signed rational",
-# 11: "float",
-# 12: "double",
-# }
-
#
# These tags are handled by default in libtiff, without
# adding to the custom dictionary. From tif_dir.c, searching for
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index c35350a75..7c0ed86fa 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -168,6 +168,9 @@ class WebPImageFile(ImageFile.ImageFile):
return super().load()
+ def load_seek(self, pos):
+ pass
+
def tell(self):
if not _webp.HAVE_WEBPANIM:
return super().tell()
diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py
index 2f2a3df13..33a0e07b3 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -47,6 +47,8 @@ def deprecate(
raise RuntimeError(msg)
elif when == 11:
removed = "Pillow 11 (2024-10-15)"
+ elif when == 12:
+ removed = "Pillow 12 (2025-10-15)"
else:
msg = f"Unknown removal version: {when}. Update {__name__}?"
raise ValueError(msg)
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index cf5019e80..279b6e228 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,2 +1,2 @@
# Master version for Pillow
-__version__ = "10.1.0.dev0"
+__version__ = "10.2.0.dev0"
diff --git a/src/_imaging.c b/src/_imaging.c
index 95da2772d..2270c77fe 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -475,8 +475,10 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) {
case IMAGING_TYPE_FLOAT32:
return PyFloat_FromDouble(pixel.f);
case IMAGING_TYPE_SPECIAL:
- if (strncmp(im->mode, "I;16", 4) == 0) {
+ if (im->bands == 1) {
return PyLong_FromLong(pixel.h);
+ } else {
+ return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]);
}
break;
}
@@ -599,7 +601,7 @@ getink(PyObject *color, Imaging im, char *ink) {
} 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)) {
+ } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) {
return NULL;
}
if (!strcmp(im->mode, "BGR;15")) {
@@ -1571,21 +1573,46 @@ 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;
- 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 (image->bands == 1) {
+ int bigendian = 0;
+ if (image->type == IMAGING_TYPE_SPECIAL) {
+ // I;16*
+ bigendian = strcmp(image->mode, "I;16B") == 0;
}
- if (endian == 0) {
- image->image8[y][x] = (UINT8)CLIP8(value);
- } 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);
+ 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 (image->type == IMAGING_TYPE_SPECIAL) {
+ image->image8[y][x * 2 + (bigendian ? 1 : 0)] = CLIP8((int)value % 256);
+ image->image8[y][x * 2 + (bigendian ? 0 : 1)] = CLIP8((int)value >> 8);
+ } else {
+ image->image8[y][x] = (UINT8)CLIP8(value);
+ }
+ if (++x >= (int)image->xsize) {
+ x = 0, y++;
+ }
}
- if (++x >= (int)image->xsize) {
- x = 0, y++;
+ } else {
+ // BGR;*
+ int b;
+ for (i = x = y = 0; i < n; i++) {
+ char ink[4];
+
+ op = PySequence_Fast_GET_ITEM(seq, i);
+ if (!op || !getink(op, image, ink)) {
+ Py_DECREF(seq);
+ return NULL;
+ }
+ /* FIXME: what about scale and offset? */
+ for (b = 0; b < image->pixelsize; b++) {
+ image->image8[y][x * image->pixelsize + b] = ink[b];
+ }
+ if (++x >= (int)image->xsize) {
+ x = 0, y++;
+ }
}
}
PyErr_Clear(); /* Avoid weird exceptions */
diff --git a/src/_imagingft.c b/src/_imagingft.c
index 2165fbc7a..4925dc233 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -815,7 +815,6 @@ font_render(FontObject *self, PyObject *args) {
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) */
@@ -833,8 +832,7 @@ font_render(FontObject *self, PyObject *args) {
&anchor,
&foreground_ink_long,
&x_start,
- &y_start,
- &max_image_pixels)) {
+ &y_start)) {
return NULL;
}
@@ -879,26 +877,24 @@ font_render(FontObject *self, PyObject *args) {
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) {
+ image = PyObject_CallFunction(fill, "ii", width, height);
+ if (image == Py_None) {
+ PyMem_Del(glyph_info);
+ return Py_BuildValue("ii", 0, 0);
+ } else if (image == NULL) {
PyMem_Del(glyph_info);
return NULL;
}
- id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id"));
+ PyObject *imageId = PyObject_GetAttrString(image, "id");
+ id = PyLong_AsSsize_t(imageId);
+ Py_XDECREF(imageId);
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);
+ return Py_BuildValue("ii", x_offset, y_offset);
}
if (stroke_width) {
@@ -1116,7 +1112,7 @@ font_render(FontObject *self, PyObject *args) {
Py_DECREF(image);
FT_Stroker_Done(stroker);
PyMem_Del(glyph_info);
- return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset);
+ return Py_BuildValue("ii", x_offset, y_offset);
glyph_error:
if (im->destroy) {
diff --git a/src/encode.c b/src/encode.c
index 08544aede..4664ad0f3 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -1045,6 +1045,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
Py_ssize_t xdpi = 0, ydpi = 0;
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
+ Py_ssize_t restart_marker_blocks = 0;
+ Py_ssize_t restart_marker_rows = 0;
PyObject *qtables = NULL;
unsigned int *qarrays = NULL;
int qtablesLen = 0;
@@ -1057,7 +1059,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "ss|nnnnnnnnOz#y#y#",
+ "ss|nnnnnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
@@ -1068,6 +1070,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
&xdpi,
&ydpi,
&subsampling,
+ &restart_marker_blocks,
+ &restart_marker_rows,
&qtables,
&comment,
&comment_size,
@@ -1156,6 +1160,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)->restart_marker_blocks = restart_marker_blocks;
+ ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = restart_marker_rows;
((JPEGENCODERSTATE *)encoder->state.context)->comment = comment;
((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size;
((JPEGENCODERSTATE *)encoder->state.context)->extra = extra;
diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c
index dd0418696..091c84e18 100644
--- a/src/libImaging/Access.c
+++ b/src/libImaging/Access.c
@@ -12,8 +12,8 @@
#include "Imaging.h"
/* use make_hash.py from the pillow-scripts repository to calculate these values */
-#define ACCESS_TABLE_SIZE 27
-#define ACCESS_TABLE_HASH 33051
+#define ACCESS_TABLE_SIZE 35
+#define ACCESS_TABLE_HASH 8940
static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE];
@@ -87,6 +87,31 @@ get_pixel_16(Imaging im, int x, int y, void *color) {
memcpy(color, in, sizeof(UINT16));
}
+static void
+get_pixel_BGR15(Imaging im, int x, int y, void *color) {
+ UINT8 *in = (UINT8 *)&im->image8[y][x * 2];
+ UINT16 pixel = in[0] + (in[1] << 8);
+ char *out = color;
+ out[0] = (pixel & 31) * 255 / 31;
+ out[1] = ((pixel >> 5) & 31) * 255 / 31;
+ out[2] = ((pixel >> 10) & 31) * 255 / 31;
+}
+
+static void
+get_pixel_BGR16(Imaging im, int x, int y, void *color) {
+ UINT8 *in = (UINT8 *)&im->image8[y][x * 2];
+ UINT16 pixel = in[0] + (in[1] << 8);
+ char *out = color;
+ out[0] = (pixel & 31) * 255 / 31;
+ out[1] = ((pixel >> 5) & 63) * 255 / 63;
+ out[2] = ((pixel >> 11) & 31) * 255 / 31;
+}
+
+static void
+get_pixel_BGR24(Imaging im, int x, int y, void *color) {
+ memcpy(color, &im->image8[y][x * 3], sizeof(UINT8) * 3);
+}
+
static void
get_pixel_32(Imaging im, int x, int y, void *color) {
memcpy(color, &im->image32[y][x], sizeof(INT32));
@@ -134,6 +159,16 @@ put_pixel_16B(Imaging im, int x, int y, const void *color) {
out[1] = in[0];
}
+static void
+put_pixel_BGR1516(Imaging im, int x, int y, const void *color) {
+ memcpy(&im->image8[y][x * 2], color, 2);
+}
+
+static void
+put_pixel_BGR24(Imaging im, int x, int y, const void *color) {
+ memcpy(&im->image8[y][x * 3], color, 3);
+}
+
static void
put_pixel_32L(Imaging im, int x, int y, const void *color) {
memcpy(&im->image8[y][x * 4], color, 4);
@@ -178,6 +213,9 @@ ImagingAccessInit() {
ADD("F", get_pixel_32, put_pixel_32);
ADD("P", get_pixel_8, put_pixel_8);
ADD("PA", get_pixel_32_2bands, put_pixel_32);
+ ADD("BGR;15", get_pixel_BGR15, put_pixel_BGR1516);
+ ADD("BGR;16", get_pixel_BGR16, put_pixel_BGR1516);
+ ADD("BGR;24", get_pixel_BGR24, put_pixel_BGR24);
ADD("RGB", get_pixel_32, put_pixel_32);
ADD("RGBA", get_pixel_32, put_pixel_32);
ADD("RGBa", get_pixel_32, put_pixel_32);
diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c
index dfd4b3e07..5e4296eeb 100644
--- a/src/libImaging/BcnDecode.c
+++ b/src/libImaging/BcnDecode.c
@@ -850,10 +850,12 @@ decode_bcn(
DECODE_LOOP(3, 16, rgba);
DECODE_LOOP(4, 8, lum);
case 5:
+ {
+ int sign = strcmp(pixel_format, "BC5S") == 0 ? 1 : 0;
while (bytes >= 16) {
rgba col[16];
- memset(col, 0, 16 * sizeof(col[0]));
- decode_bc5_block(col, ptr, strcmp(pixel_format, "BC5S") == 0 ? 1 : 0);
+ memset(col, sign ? 128 : 0, 16 * sizeof(col[0]));
+ decode_bc5_block(col, ptr, sign);
put_block(im, state, (const char *)col, sizeof(col[0]), C);
ptr += 16;
bytes -= 16;
@@ -862,10 +864,13 @@ decode_bcn(
}
}
break;
+ }
case 6:
+ {
+ int sign = strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0;
while (bytes >= 16) {
rgba col[16];
- decode_bc6_block(col, ptr, strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0);
+ decode_bc6_block(col, ptr, sign);
put_block(im, state, (const char *)col, sizeof(col[0]), C);
ptr += 16;
bytes -= 16;
@@ -874,6 +879,7 @@ decode_bcn(
}
}
break;
+ }
DECODE_LOOP(7, 16, rgba);
#undef DECODE_LOOP
}
diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c
index 7fe24a639..99d2a4ada 100644
--- a/src/libImaging/Convert.c
+++ b/src/libImaging/Convert.c
@@ -564,7 +564,7 @@ rgb2cmyk(UINT8 *out, const UINT8 *in, int xsize) {
}
}
-static void
+void
cmyk2rgb(UINT8 *out, const UINT8 *in, int xsize) {
int x, nk, tmp;
for (x = 0; x < xsize; x++) {
@@ -1013,7 +1013,7 @@ static struct {
static void
p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++) {
*out++ = (L(&palette->palette[in[x] * 4]) >= 128000) ? 255 : 0;
}
@@ -1022,7 +1022,7 @@ p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
pa2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++, in += 4) {
*out++ = (L(&palette->palette[in[0] * 4]) >= 128000) ? 255 : 0;
}
@@ -1031,7 +1031,7 @@ pa2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
p2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++) {
*out++ = L24(&palette->palette[in[x] * 4]) >> 16;
}
@@ -1040,7 +1040,7 @@ p2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++, in += 4) {
*out++ = L24(&palette->palette[in[0] * 4]) >> 16;
}
@@ -1070,7 +1070,7 @@ p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
p2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++, out += 4) {
const UINT8 *rgba = &palette->palette[*in++ * 4];
out[0] = out[1] = out[2] = L24(rgba) >> 16;
@@ -1081,7 +1081,7 @@ p2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
pa2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++, in += 4, out += 4) {
out[0] = out[1] = out[2] = L24(&palette->palette[in[0] * 4]) >> 16;
out[3] = in[3];
@@ -1295,7 +1295,6 @@ topalette(
int alpha;
int x, y;
ImagingPalette palette = inpalette;
- ;
/* Map L or RGB/RGBX/RGBA to palette image */
if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) {
@@ -1307,7 +1306,14 @@ topalette(
if (palette == NULL) {
/* FIXME: make user configurable */
if (imIn->bands == 1) {
- palette = ImagingPaletteNew("RGB"); /* Initialised to grey ramp */
+ palette = ImagingPaletteNew("RGB");
+
+ palette->size = 256;
+ int i;
+ for (i = 0; i < 256; i++) {
+ palette->palette[i * 4] = palette->palette[i * 4 + 1] =
+ palette->palette[i * 4 + 2] = (UINT8)i;
+ }
} else {
palette = ImagingPaletteNewBrowser(); /* Standard colour cube */
}
@@ -1329,9 +1335,9 @@ topalette(
imOut->palette = ImagingPaletteDuplicate(palette);
if (imIn->bands == 1) {
- /* greyscale image */
+ /* grayscale image */
- /* Greyscale palette: copy data as is */
+ /* Grayscale palette: copy data as is */
ImagingSectionEnter(&cookie);
for (y = 0; y < imIn->ysize; y++) {
if (alpha) {
diff --git a/src/libImaging/Convert.h b/src/libImaging/Convert.h
new file mode 100644
index 000000000..e688e3018
--- /dev/null
+++ b/src/libImaging/Convert.h
@@ -0,0 +1,2 @@
+extern void
+cmyk2rgb(UINT8 *out, const UINT8 *in, int xsize);
diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c
index f8a2901b8..1b5bfe132 100644
--- a/src/libImaging/Dib.c
+++ b/src/libImaging/Dib.c
@@ -142,9 +142,9 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) {
GetSystemPaletteEntries(dib->dc, 0, 256, pal->palPalEntry);
if (strcmp(mode, "L") == 0) {
- /* Greyscale DIB. Fill all 236 slots with a greyscale ramp
+ /* Grayscale DIB. Fill all 236 slots with a grayscale ramp
* (this is usually overkill on Windows since VGA only offers
- * 6 bits greyscale resolution). Ignore the slots already
+ * 6 bits grayscale resolution). Ignore the slots already
* allocated by Windows */
i = 10;
@@ -160,7 +160,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) {
#ifdef CUBE216
/* Colour DIB. Create a 6x6x6 colour cube (216 entries) and
- * add 20 extra greylevels for best result with greyscale
+ * add 20 extra graylevels for best result with grayscale
* images. */
i = 10;
diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c
index 82f290bd0..0ccf22d58 100644
--- a/src/libImaging/Draw.c
+++ b/src/libImaging/Draw.c
@@ -41,6 +41,7 @@
#define FLOOR(v) ((v) >= 0.0 ? (int)(v) : (int)floor(v))
#define INK8(ink) (*(UINT8 *)ink)
+#define INK16(ink) (*(UINT16 *)ink)
/*
* Rounds around zero (up=away from zero, down=towards zero)
@@ -68,8 +69,13 @@ static inline void
point8(Imaging im, int x, int y, int ink) {
if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) {
if (strncmp(im->mode, "I;16", 4) == 0) {
- im->image8[y][x * 2] = (UINT8)ink;
+#ifdef WORDS_BIGENDIAN
+ im->image8[y][x * 2] = (UINT8)(ink >> 8);
im->image8[y][x * 2 + 1] = (UINT8)ink;
+#else
+ im->image8[y][x * 2] = (UINT8)ink;
+ im->image8[y][x * 2 + 1] = (UINT8)(ink >> 8);
+#endif
} else {
im->image8[y][x] = (UINT8)ink;
}
@@ -631,13 +637,17 @@ DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba, polygon32rgba};
/* Interface */
/* -------------------------------------------------------------------- */
-#define DRAWINIT() \
- if (im->image8) { \
- draw = &draw8; \
- ink = INK8(ink_); \
- } else { \
- draw = (op) ? &draw32rgba : &draw32; \
- memcpy(&ink, ink_, sizeof(ink)); \
+#define DRAWINIT() \
+ if (im->image8) { \
+ draw = &draw8; \
+ if (strncmp(im->mode, "I;16", 4) == 0) { \
+ ink = INK16(ink_); \
+ } else { \
+ ink = INK8(ink_); \
+ } \
+ } else { \
+ draw = (op) ? &draw32rgba : &draw32; \
+ memcpy(&ink, ink_, sizeof(ink)); \
}
int
diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h
index 1d7550818..5cc74e69b 100644
--- a/src/libImaging/Jpeg.h
+++ b/src/libImaging/Jpeg.h
@@ -83,6 +83,10 @@ typedef struct {
/* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */
int subsampling;
+ /* Restart marker interval, in MCU blocks or MCU rows, or 0 for none */
+ unsigned int restart_marker_blocks;
+ unsigned int restart_marker_rows;
+
/* Converter input mode (input to the shuffler) */
char rawmode[8 + 1];
diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c
index 2a24eff39..9da830b18 100644
--- a/src/libImaging/JpegEncode.c
+++ b/src/libImaging/JpegEncode.c
@@ -210,6 +210,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
context->cinfo.smoothing_factor = context->smooth;
context->cinfo.optimize_coding = (boolean)context->optimize;
+ context->cinfo.restart_interval = context->restart_marker_blocks;
+ context->cinfo.restart_in_rows = context->restart_marker_rows;
if (context->xdpi > 0 && context->ydpi > 0) {
context->cinfo.write_JFIF_header = TRUE;
context->cinfo.density_unit = 1; /* dots per inch */
@@ -218,9 +220,9 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
switch (context->streamtype) {
case 1:
- /* tables only -- not yet implemented */
- state->errcode = IMAGING_CODEC_CONFIG;
- return -1;
+ /* tables only */
+ jpeg_write_tables(&context->cinfo);
+ goto cleanup;
case 2:
/* image only */
jpeg_suppress_tables(&context->cinfo, TRUE);
@@ -316,6 +318,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
jpeg_finish_compress(&context->cinfo);
+cleanup:
/* Clean up */
if (context->comment) {
free(context->comment);
diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c
index 14c8f1461..d47344245 100644
--- a/src/libImaging/Pack.c
+++ b/src/libImaging/Pack.c
@@ -549,16 +549,16 @@ static struct {
{"1", "1;IR", 1, pack1IR},
{"1", "L", 8, pack1L},
- /* greyscale */
+ /* grayscale */
{"L", "L", 8, copy1},
{"L", "L;16", 16, packL16},
{"L", "L;16B", 16, packL16B},
- /* greyscale w. alpha */
+ /* grayscale w. alpha */
{"LA", "LA", 16, packLA},
{"LA", "LA;L", 16, packLAL},
- /* greyscale w. alpha premultiplied */
+ /* grayscale w. alpha premultiplied */
{"La", "La", 16, packLA},
/* palette */
diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c
index 71a095c2c..78916bca5 100644
--- a/src/libImaging/Palette.c
+++ b/src/libImaging/Palette.c
@@ -39,11 +39,8 @@ ImagingPaletteNew(const char *mode) {
strncpy(palette->mode, mode, IMAGING_MODE_LENGTH - 1);
palette->mode[IMAGING_MODE_LENGTH - 1] = 0;
- /* Initialize to ramp */
- palette->size = 256;
+ palette->size = 0;
for (i = 0; i < 256; i++) {
- palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] =
- palette->palette[i * 4 + 2] = (UINT8)i;
palette->palette[i * 4 + 3] = 255; /* opaque */
}
@@ -62,16 +59,10 @@ ImagingPaletteNewBrowser(void) {
return NULL;
}
- /* Blank out unused entries */
/* FIXME: Add 10-level windows palette here? */
- for (i = 0; i < 10; i++) {
- palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] =
- palette->palette[i * 4 + 2] = 0;
- }
-
/* Simple 6x6x6 colour cube */
-
+ i = 10;
for (b = 0; b < 256; b += 51) {
for (g = 0; g < 256; g += 51) {
for (r = 0; r < 256; r += 51) {
@@ -82,14 +73,9 @@ ImagingPaletteNewBrowser(void) {
}
}
}
+ palette->size = i;
- /* Blank out unused entries */
- /* FIXME: add 30-level greyscale wedge here? */
-
- for (; i < 256; i++) {
- palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] =
- palette->palette[i * 4 + 2] = 0;
- }
+ /* FIXME: add 30-level grayscale wedge here? */
return palette;
}
diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c
index acf5202e5..6684b11ef 100644
--- a/src/libImaging/Paste.c
+++ b/src/libImaging/Paste.c
@@ -425,7 +425,7 @@ fill_mask_L(
*out = BLEND(*mask, *out, ink[0], tmp1);
if (strncmp(imOut->mode, "I;16", 4) == 0) {
out++;
- *out = BLEND(*mask, *out, ink[0], tmp1);
+ *out = BLEND(*mask, *out, ink[1], tmp1);
}
out++, mask++;
}
diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c
index 02a4a5c76..398fbf6db 100644
--- a/src/libImaging/Quant.c
+++ b/src/libImaging/Quant.c
@@ -1697,7 +1697,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
image data? */
if (!strcmp(im->mode, "L")) {
- /* greyscale */
+ /* grayscale */
/* FIXME: converting a "L" image to "P" with 256 colors
should be done by a simple copy... */
@@ -1825,6 +1825,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
free(newData);
+ imOut->palette->size = (int)paletteLength;
pp = imOut->palette->palette;
for (i = j = 0; i < (int)paletteLength; i++) {
@@ -1832,16 +1833,9 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
*pp++ = palette[i].c.g;
*pp++ = palette[i].c.b;
if (withAlpha) {
- *pp++ = palette[i].c.a;
- } else {
- *pp++ = 255;
+ *pp = palette[i].c.a;
}
- }
- for (; i < 256; i++) {
- *pp++ = 0;
- *pp++ = 0;
- *pp++ = 0;
- *pp++ = 255;
+ pp++;
}
if (withAlpha) {
diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c
index 128595f65..b1b03c515 100644
--- a/src/libImaging/Storage.c
+++ b/src/libImaging/Storage.c
@@ -80,18 +80,18 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->palette = ImagingPaletteNew("RGB");
} else if (strcmp(mode, "L") == 0) {
- /* 8-bit greyscale (luminance) images */
+ /* 8-bit grayscale (luminance) images */
im->bands = im->pixelsize = 1;
im->linesize = xsize;
} else if (strcmp(mode, "LA") == 0) {
- /* 8-bit greyscale (luminance) with alpha */
+ /* 8-bit grayscale (luminance) with alpha */
im->bands = 2;
im->pixelsize = 4; /* store in image32 memory */
im->linesize = xsize * 4;
} else if (strcmp(mode, "La") == 0) {
- /* 8-bit greyscale (luminance) with premultiplied alpha */
+ /* 8-bit grayscale (luminance) with premultiplied alpha */
im->bands = 2;
im->pixelsize = 4; /* store in image32 memory */
im->linesize = xsize * 4;
diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c
index 35122f182..e3b81590e 100644
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -14,6 +14,10 @@
#ifdef HAVE_LIBTIFF
+#ifdef HAVE_UNISTD_H
+#include /* lseek */
+#endif
+
#ifndef uint
#define uint uint32
#endif
diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h
index c7c7d48ed..02454ba03 100644
--- a/src/libImaging/TiffDecode.h
+++ b/src/libImaging/TiffDecode.h
@@ -13,12 +13,6 @@
#include
#endif
-/* UNDONE -- what are we using from this? */
-/*#ifndef _UNISTD_H
- # include
- # endif
-*/
-
#ifndef min
#define min(x, y) ((x > y) ? y : x)
#define max(x, y) ((x < y) ? y : x)
diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c
index 206403ba6..6c7d52f58 100644
--- a/src/libImaging/Unpack.c
+++ b/src/libImaging/Unpack.c
@@ -31,6 +31,7 @@
*/
#include "Imaging.h"
+#include "Convert.h"
#define R 0
#define G 1
@@ -818,7 +819,7 @@ ImagingUnpackXBGR(UINT8 *_out, const UINT8 *in, int pixels) {
static void
unpackRGBALA(UINT8 *_out, const UINT8 *in, int pixels) {
int i;
- /* greyscale with alpha */
+ /* grayscale with alpha */
for (i = 0; i < pixels; i++) {
UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]);
memcpy(_out, &iv, sizeof(iv));
@@ -830,7 +831,7 @@ unpackRGBALA(UINT8 *_out, const UINT8 *in, int pixels) {
static void
unpackRGBALA16B(UINT8 *_out, const UINT8 *in, int pixels) {
int i;
- /* 16-bit greyscale with alpha, big-endian */
+ /* 16-bit grayscale with alpha, big-endian */
for (i = 0; i < pixels; i++) {
UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[2]);
memcpy(_out, &iv, sizeof(iv));
@@ -1107,7 +1108,7 @@ unpackCMYKI(UINT8 *_out, const UINT8 *in, int pixels) {
/* There are two representations of LAB images for whatever precision:
L: Uint (in PS, it's 0-100)
A: Int (in ps, -128 .. 128, or elsewhere 0..255, with 128 as middle.
- Channels in PS display a 0 value as middle grey,
+ Channels in PS display a 0 value as middle gray,
LCMS appears to use 128 as the 0 value for these channels)
B: Int (as above)
@@ -1171,7 +1172,7 @@ unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) {
static void
unpackI12_I16(UINT8 *out, const UINT8 *in, int pixels) {
- /* Fillorder 1/MSB -> LittleEndian, for 12bit integer greyscale tiffs.
+ /* Fillorder 1/MSB -> LittleEndian, for 12bit integer grayscale tiffs.
According to the TIFF spec:
@@ -1238,6 +1239,12 @@ copy2(UINT8 *out, const UINT8 *in, int pixels) {
memcpy(out, in, pixels * 2);
}
+static void
+copy3(UINT8 *out, const UINT8 *in, int pixels) {
+ /* BGR;24 */
+ memcpy(out, in, pixels * 3);
+}
+
static void
copy4(UINT8 *out, const UINT8 *in, int pixels) {
/* RGBA, CMYK quadruples */
@@ -1520,7 +1527,7 @@ static struct {
{"1", "1;IR", 1, unpack1IR},
{"1", "1;8", 8, unpack18},
- /* greyscale */
+ /* grayscale */
{"L", "L;2", 2, unpackL2},
{"L", "L;2I", 2, unpackL2I},
{"L", "L;2R", 2, unpackL2R},
@@ -1537,11 +1544,11 @@ static struct {
{"L", "L;16", 16, unpackL16},
{"L", "L;16B", 16, unpackL16B},
- /* greyscale w. alpha */
+ /* grayscale w. alpha */
{"LA", "LA", 16, unpackLA},
{"LA", "LA;L", 16, unpackLAL},
- /* greyscale w. alpha premultiplied */
+ /* grayscale w. alpha premultiplied */
{"La", "La", 16, unpackLA},
/* palette */
@@ -1589,6 +1596,11 @@ static struct {
{"RGB", "R;16B", 16, band016B},
{"RGB", "G;16B", 16, band116B},
{"RGB", "B;16B", 16, band216B},
+ {"RGB", "CMYK", 32, cmyk2rgb},
+
+ {"BGR;15", "BGR;15", 16, copy2},
+ {"BGR;16", "BGR;16", 16, copy2},
+ {"BGR;24", "BGR;24", 24, copy3},
/* true colour w. alpha */
{"RGBA", "LA", 16, unpackRGBALA},
diff --git a/tox.ini b/tox.ini
index 5388ed243..034d89372 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,3 +29,11 @@ pass_env =
commands =
pre-commit run --all-files --show-diff-on-failure
check-manifest
+
+[testenv:mypy]
+skip_install = true
+deps =
+ mypy==1.7.1
+ numpy
+commands =
+ mypy src {posargs}
diff --git a/wheels/README.md b/wheels/README.md
new file mode 100644
index 000000000..8b412b7fe
--- /dev/null
+++ b/wheels/README.md
@@ -0,0 +1,35 @@
+README
+------
+
+[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux
+wheels for tagged versions of Pillow.
+
+This directory contains [multibuild](https://github.com/multi-build/multibuild) to
+build dependencies for the wheels, and dependency licenses to be included.
+
+Archives
+--------
+
+https://github.com/python-pillow/pillow-depends contains archives for libraries
+that will be built as part of the Pillow build.
+
+In general, there is no need to put library archives there, because the
+`multibuild` scripts will download them from their respective URLs.
+
+But, the build will look in that repository before downloading from the
+URL, so if there is a library that often fails to download, or you think might
+fail to download, then download it and add it to the Git repository.
+
+See `build` in `.github/workflows/wheels-dependencies.sh` and the `fetch_unpack`
+routine in `multibuild/common_utils.sh` for the logic, and the build recipes in
+`multibuild/library_builders.sh` for the filename to give to the downloaded
+archive.
+
+Wheels
+------
+
+Wheels are
+[GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml).
+
+Windows wheels are created separately. They are
+[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain).
diff --git a/wheels/dependency_licenses/BROTLI.txt b/wheels/dependency_licenses/BROTLI.txt
new file mode 100644
index 000000000..33b7cdd2d
--- /dev/null
+++ b/wheels/dependency_licenses/BROTLI.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/wheels/dependency_licenses/BZIP2.txt b/wheels/dependency_licenses/BZIP2.txt
new file mode 100644
index 000000000..d3edf477d
--- /dev/null
+++ b/wheels/dependency_licenses/BZIP2.txt
@@ -0,0 +1,42 @@
+
+--------------------------------------------------------------------------
+
+This program, "bzip2", the associated library "libbzip2", and all
+documentation, are copyright (C) 1996-2019 Julian R Seward. All
+rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. The origin of this software must not be misrepresented; you must
+ not claim that you wrote the original software. If you use this
+ software in a product, an acknowledgment in the product
+ documentation would be appreciated but is not required.
+
+3. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+4. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Julian Seward, jseward@acm.org
+bzip2/libbzip2 version 1.0.8 of 13 July 2019
+
+--------------------------------------------------------------------------
diff --git a/wheels/dependency_licenses/FREETYPE2.txt b/wheels/dependency_licenses/FREETYPE2.txt
new file mode 100644
index 000000000..93efc6126
--- /dev/null
+++ b/wheels/dependency_licenses/FREETYPE2.txt
@@ -0,0 +1,652 @@
+The FreeType 2 font engine is copyrighted work and cannot be used
+legally without a software license. In order to make this project
+usable to a vast majority of developers, we distribute it under two
+mutually exclusive open-source licenses.
+
+This means that *you* must choose *one* of the two licenses described
+below, then obey all its terms and conditions when using FreeType 2 in
+any of your projects or products.
+
+ - The FreeType License, found in the file `docs/FTL.TXT`, which is
+ similar to the original BSD license *with* an advertising clause
+ that forces you to explicitly cite the FreeType project in your
+ product's documentation. All details are in the license file.
+ This license is suited to products which don't use the GNU General
+ Public License.
+
+ Note that this license is compatible to the GNU General Public
+ License version 3, but not version 2.
+
+ - The GNU General Public License version 2, found in
+ `docs/GPLv2.TXT` (any later version can be used also), for
+ programs which already use the GPL. Note that the FTL is
+ incompatible with GPLv2 due to its advertisement clause.
+
+The contributed BDF and PCF drivers come with a license similar to
+that of the X Window System. It is compatible to the above two
+licenses (see files `src/bdf/README` and `src/pcf/README`). The same
+holds for the source code files `src/base/fthash.c` and
+`include/freetype/internal/fthash.h`; they were part of the BDF driver
+in earlier FreeType versions.
+
+The gzip module uses the zlib license (see `src/gzip/zlib.h`) which
+too is compatible to the above two licenses.
+
+The files `src/autofit/ft-hb.c` and `src/autofit/ft-hb.h` contain code
+taken almost verbatim from the HarfBuzz file `hb-ft.cc`, which uses
+the 'Old MIT' license, compatible to the above two licenses.
+
+The MD5 checksum support (only used for debugging in development
+builds) is in the public domain.
+
+--------------------------------------------------------------------------
+
+ The FreeType Project LICENSE
+ ----------------------------
+
+ 2006-Jan-27
+
+ Copyright 1996-2002, 2006 by
+ David Turner, Robert Wilhelm, and Werner Lemberg
+
+
+
+Introduction
+============
+
+ The FreeType Project is distributed in several archive packages;
+ some of them may contain, in addition to the FreeType font engine,
+ various tools and contributions which rely on, or relate to, the
+ FreeType Project.
+
+ This license applies to all files found in such packages, and
+ which do not fall under their own explicit license. The license
+ affects thus the FreeType font engine, the test programs,
+ documentation and makefiles, at the very least.
+
+ This license was inspired by the BSD, Artistic, and IJG
+ (Independent JPEG Group) licenses, which all encourage inclusion
+ and use of free software in commercial and freeware products
+ alike. As a consequence, its main points are that:
+
+ o We don't promise that this software works. However, we will be
+ interested in any kind of bug reports. (`as is' distribution)
+
+ o You can use this software for whatever you want, in parts or
+ full form, without having to pay us. (`royalty-free' usage)
+
+ o You may not pretend that you wrote this software. If you use
+ it, or only parts of it, in a program, you must acknowledge
+ somewhere in your documentation that you have used the
+ FreeType code. (`credits')
+
+ We specifically permit and encourage the inclusion of this
+ software, with or without modifications, in commercial products.
+ We disclaim all warranties covering The FreeType Project and
+ assume no liability related to The FreeType Project.
+
+
+ Finally, many people asked us for a preferred form for a
+ credit/disclaimer to use in compliance with this license. We thus
+ encourage you to use the following text:
+
+ """
+ Portions of this software are copyright © The FreeType
+ Project (www.freetype.org). All rights reserved.
+ """
+
+ Please replace with the value from the FreeType version you
+ actually use.
+
+
+Legal Terms
+===========
+
+0. Definitions
+--------------
+
+ Throughout this license, the terms `package', `FreeType Project',
+ and `FreeType archive' refer to the set of files originally
+ distributed by the authors (David Turner, Robert Wilhelm, and
+ Werner Lemberg) as the `FreeType Project', be they named as alpha,
+ beta or final release.
+
+ `You' refers to the licensee, or person using the project, where
+ `using' is a generic term including compiling the project's source
+ code as well as linking it to form a `program' or `executable'.
+ This program is referred to as `a program using the FreeType
+ engine'.
+
+ This license applies to all files distributed in the original
+ FreeType Project, including all source code, binaries and
+ documentation, unless otherwise stated in the file in its
+ original, unmodified form as distributed in the original archive.
+ If you are unsure whether or not a particular file is covered by
+ this license, you must contact us to verify this.
+
+ The FreeType Project is copyright (C) 1996-2000 by David Turner,
+ Robert Wilhelm, and Werner Lemberg. All rights reserved except as
+ specified below.
+
+1. No Warranty
+--------------
+
+ THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO
+ USE, OF THE FREETYPE PROJECT.
+
+2. Redistribution
+-----------------
+
+ This license grants a worldwide, royalty-free, perpetual and
+ irrevocable right and license to use, execute, perform, compile,
+ display, copy, create derivative works of, distribute and
+ sublicense the FreeType Project (in both source and object code
+ forms) and derivative works thereof for any purpose; and to
+ authorize others to exercise some or all of the rights granted
+ herein, subject to the following conditions:
+
+ o Redistribution of source code must retain this license file
+ (`FTL.TXT') unaltered; any additions, deletions or changes to
+ the original files must be clearly indicated in accompanying
+ documentation. The copyright notices of the unaltered,
+ original files must be preserved in all copies of source
+ files.
+
+ o Redistribution in binary form must provide a disclaimer that
+ states that the software is based in part of the work of the
+ FreeType Team, in the distribution documentation. We also
+ encourage you to put an URL to the FreeType web page in your
+ documentation, though this isn't mandatory.
+
+ These conditions apply to any software derived from or based on
+ the FreeType Project, not just the unmodified files. If you use
+ our work, you must acknowledge us. However, no fee need be paid
+ to us.
+
+3. Advertising
+--------------
+
+ Neither the FreeType authors and contributors nor you shall use
+ the name of the other for commercial, advertising, or promotional
+ purposes without specific prior written permission.
+
+ We suggest, but do not require, that you use one or more of the
+ following phrases to refer to this software in your documentation
+ or advertising materials: `FreeType Project', `FreeType Engine',
+ `FreeType library', or `FreeType Distribution'.
+
+ As you have not signed this license, you are not required to
+ accept it. However, as the FreeType Project is copyrighted
+ material, only this license, or another one contracted with the
+ authors, grants you the right to use, distribute, and modify it.
+ Therefore, by using, distributing, or modifying the FreeType
+ Project, you indicate that you understand and accept all the terms
+ of this license.
+
+4. Contacts
+-----------
+
+ There are two mailing lists related to FreeType:
+
+ o freetype@nongnu.org
+
+ Discusses general use and applications of FreeType, as well as
+ future and wanted additions to the library and distribution.
+ If you are looking for support, start in this list if you
+ haven't found anything to help you in the documentation.
+
+ o freetype-devel@nongnu.org
+
+ Discusses bugs, as well as engine internals, design issues,
+ specific licenses, porting, etc.
+
+ Our home page can be found at
+
+ https://www.freetype.org
+
+
+--- end of FTL.TXT ---
+
+--------------------------------------------------------------------------
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
+
+--------------------------------------------------------------------------
+
+The following license details are part of `src/bdf/README`:
+
+```
+License
+*******
+
+Copyright (C) 2001-2002 by Francesco Zappa Nardelli
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*** Portions of the driver (that is, bdflib.c and bdf.h):
+
+Copyright 2000 Computing Research Labs, New Mexico State University
+Copyright 2001-2002, 2011 Francesco Zappa Nardelli
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE COMPUTING RESEARCH LAB OR NEW MEXICO STATE UNIVERSITY BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
+OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+Credits
+*******
+
+This driver is based on excellent Mark Leisher's bdf library. If you
+find something good in this driver you should probably thank him, not
+me.
+```
+
+The following license details are part of `src/pcf/README`:
+
+```
+License
+*******
+
+Copyright (C) 2000 by Francesco Zappa Nardelli
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+Credits
+*******
+
+Keith Packard wrote the pcf driver found in XFree86. His work is at
+the same time the specification and the sample implementation of the
+PCF format. Undoubtedly, this driver is inspired from his work.
+```
diff --git a/wheels/dependency_licenses/HARFBUZZ.txt b/wheels/dependency_licenses/HARFBUZZ.txt
new file mode 100644
index 000000000..1dd917e9f
--- /dev/null
+++ b/wheels/dependency_licenses/HARFBUZZ.txt
@@ -0,0 +1,42 @@
+HarfBuzz is licensed under the so-called "Old MIT" license. Details follow.
+For parts of HarfBuzz that are licensed under different licenses see individual
+files names COPYING in subdirectories where applicable.
+
+Copyright © 2010-2022 Google, Inc.
+Copyright © 2015-2020 Ebrahim Byagowi
+Copyright © 2019,2020 Facebook, Inc.
+Copyright © 2012,2015 Mozilla Foundation
+Copyright © 2011 Codethink Limited
+Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies)
+Copyright © 2009 Keith Stribley
+Copyright © 2011 Martin Hosken and SIL International
+Copyright © 2007 Chris Wilson
+Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod
+Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc.
+Copyright © 1998-2005 David Turner and Werner Lemberg
+Copyright © 2016 Igalia S.L.
+Copyright © 2022 Matthias Clasen
+Copyright © 2018,2021 Khaled Hosny
+Copyright © 2018,2019,2020 Adobe, Inc
+Copyright © 2013-2015 Alexei Podtelezhnikov
+
+For full copyright notices consult the individual files in the package.
+
+
+Permission is hereby granted, without written agreement and without
+license or royalty fees, to use, copy, modify, and distribute this
+software and its documentation for any purpose, provided that the
+above copyright notice and the following two paragraphs appear in
+all copies of this software.
+
+IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
+DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
+IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
+THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
+ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
+PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
diff --git a/wheels/dependency_licenses/LCMS2.txt b/wheels/dependency_licenses/LCMS2.txt
new file mode 100644
index 000000000..21ed6fb86
--- /dev/null
+++ b/wheels/dependency_licenses/LCMS2.txt
@@ -0,0 +1,8 @@
+Little CMS
+Copyright (c) 1998-2020 Marti Maria Saguer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/wheels/dependency_licenses/LIBJPEG.txt b/wheels/dependency_licenses/LIBJPEG.txt
new file mode 100644
index 000000000..b5451f405
--- /dev/null
+++ b/wheels/dependency_licenses/LIBJPEG.txt
@@ -0,0 +1,43 @@
+1. We don't promise that this software works. (But if you find any bugs,
+ please let us know!)
+2. You can use this software for whatever you want. You don't have to pay us.
+3. You may not pretend that you wrote this software. If you use it in a
+ program, you must acknowledge somewhere in your documentation that
+ you've used the IJG code.
+
+In legalese:
+
+The authors make NO WARRANTY or representation, either express or implied,
+with respect to this software, its quality, accuracy, merchantability, or
+fitness for a particular purpose. This software is provided "AS IS", and you,
+its user, assume the entire risk as to its quality and accuracy.
+
+This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding.
+All Rights Reserved except as specified below.
+
+Permission is hereby granted to use, copy, modify, and distribute this
+software (or portions thereof) for any purpose, without fee, subject to these
+conditions:
+(1) If any part of the source code for this software is distributed, then this
+README file must be included, with this copyright and no-warranty notice
+unaltered; and any additions, deletions, or changes to the original files
+must be clearly indicated in accompanying documentation.
+(2) If only executable code is distributed, then the accompanying
+documentation must state that "this software is based in part on the work of
+the Independent JPEG Group".
+(3) Permission for use of this software is granted only if the user accepts
+full responsibility for any undesirable consequences; the authors accept
+NO LIABILITY for damages of any kind.
+
+These conditions apply to any software derived from or based on the IJG code,
+not just to the unmodified library. If you use our work, you ought to
+acknowledge us.
+
+Permission is NOT granted for the use of any IJG author's name or company name
+in advertising or publicity relating to this software or products derived from
+it. This software may be referred to only as "the Independent JPEG Group's
+software".
+
+We specifically permit and encourage the use of this software as the basis of
+commercial products, provided that all warranty or liability claims are
+assumed by the product vendor.
diff --git a/wheels/dependency_licenses/LIBLZMA.txt b/wheels/dependency_licenses/LIBLZMA.txt
new file mode 100644
index 000000000..43c7a23ba
--- /dev/null
+++ b/wheels/dependency_licenses/LIBLZMA.txt
@@ -0,0 +1,63 @@
+XZ Utils Licensing
+==================
+
+ Different licenses apply to different files in this package. Here
+ is a rough summary of which licenses apply to which parts of this
+ package (but check the individual files to be sure!):
+
+ - liblzma is in the public domain.
+
+ - xz, xzdec, and lzmadec command line tools are in the public
+ domain unless GNU getopt_long had to be compiled and linked
+ in from the lib directory. The getopt_long code is under
+ GNU LGPLv2.1+.
+
+ - The scripts to grep, diff, and view compressed files have been
+ adapted from gzip. These scripts and their documentation are
+ under GNU GPLv2+.
+
+ - All the documentation in the doc directory and most of the
+ XZ Utils specific documentation files in other directories
+ are in the public domain.
+
+ - Translated messages are in the public domain.
+
+ - The build system contains public domain files, and files that
+ are under GNU GPLv2+ or GNU GPLv3+. None of these files end up
+ in the binaries being built.
+
+ - Test files and test code in the tests directory, and debugging
+ utilities in the debug directory are in the public domain.
+
+ - The extra directory may contain public domain files, and files
+ that are under various free software licenses.
+
+ You can do whatever you want with the files that have been put into
+ the public domain. If you find public domain legally problematic,
+ take the previous sentence as a license grant. If you still find
+ the lack of copyright legally problematic, you have too many
+ lawyers.
+
+ As usual, this software is provided "as is", without any warranty.
+
+ If you copy significant amounts of public domain code from XZ Utils
+ into your project, acknowledging this somewhere in your software is
+ polite (especially if it is proprietary, non-free software), but
+ naturally it is not legally required. Here is an example of a good
+ notice to put into "about box" or into documentation:
+
+ This software includes code from XZ Utils .
+
+ The following license texts are included in the following files:
+ - COPYING.LGPLv2.1: GNU Lesser General Public License version 2.1
+ - COPYING.GPLv2: GNU General Public License version 2
+ - COPYING.GPLv3: GNU General Public License version 3
+
+ Note that the toolchain (compiler, linker etc.) may add some code
+ pieces that are copyrighted. Thus, it is possible that e.g. liblzma
+ binary wouldn't actually be in the public domain in its entirety
+ even though it contains no copyrighted code from the XZ Utils source
+ package.
+
+ If you have questions, don't hesitate to ask the author(s) for more
+ information.
diff --git a/wheels/dependency_licenses/LIBPNG.txt b/wheels/dependency_licenses/LIBPNG.txt
new file mode 100644
index 000000000..c8ad24eec
--- /dev/null
+++ b/wheels/dependency_licenses/LIBPNG.txt
@@ -0,0 +1,134 @@
+COPYRIGHT NOTICE, DISCLAIMER, and LICENSE
+=========================================
+
+PNG Reference Library License version 2
+---------------------------------------
+
+ * Copyright (c) 1995-2022 The PNG Reference Library Authors.
+ * Copyright (c) 2018-2022 Cosmin Truta.
+ * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson.
+ * Copyright (c) 1996-1997 Andreas Dilger.
+ * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+The software is supplied "as is", without warranty of any kind,
+express or implied, including, without limitation, the warranties
+of merchantability, fitness for a particular purpose, title, and
+non-infringement. In no event shall the Copyright owners, or
+anyone distributing the software, be liable for any damages or
+other liability, whether in contract, tort or otherwise, arising
+from, out of, or in connection with the software, or the use or
+other dealings in the software, even if advised of the possibility
+of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute
+this software, or portions hereof, for any purpose, without fee,
+subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you
+ must not claim that you wrote the original software. If you
+ use this software in a product, an acknowledgment in the product
+ documentation would be appreciated, but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+
+PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35)
+-----------------------------------------------------------------------
+
+libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are
+Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are
+derived from libpng-1.0.6, and are distributed according to the same
+disclaimer and license as libpng-1.0.6 with the following individuals
+added to the list of Contributing Authors:
+
+ Simon-Pierre Cadieux
+ Eric S. Raymond
+ Mans Rullgard
+ Cosmin Truta
+ Gilles Vollant
+ James Yu
+ Mandar Sahastrabuddhe
+ Google Inc.
+ Vadim Barkov
+
+and with the following additions to the disclaimer:
+
+ There is no warranty against interference with your enjoyment of
+ the library or against infringement. There is no warranty that our
+ efforts or the library will fulfill any of your particular purposes
+ or needs. This library is provided with all faults, and the entire
+ risk of satisfactory quality, performance, accuracy, and effort is
+ with the user.
+
+Some files in the "contrib" directory and some configure-generated
+files that are distributed with libpng have other copyright owners, and
+are released under other open source licenses.
+
+libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
+Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from
+libpng-0.96, and are distributed according to the same disclaimer and
+license as libpng-0.96, with the following individuals added to the
+list of Contributing Authors:
+
+ Tom Lane
+ Glenn Randers-Pehrson
+ Willem van Schaik
+
+libpng versions 0.89, June 1996, through 0.96, May 1997, are
+Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88,
+and are distributed according to the same disclaimer and license as
+libpng-0.88, with the following individuals added to the list of
+Contributing Authors:
+
+ John Bowler
+ Kevin Bracey
+ Sam Bushell
+ Magnus Holmgren
+ Greg Roelofs
+ Tom Tanner
+
+Some files in the "scripts" directory have other copyright owners,
+but are released under this license.
+
+libpng versions 0.5, May 1995, through 0.88, January 1996, are
+Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+For the purposes of this copyright and license, "Contributing Authors"
+is defined as the following set of individuals:
+
+ Andreas Dilger
+ Dave Martindale
+ Guy Eric Schalnat
+ Paul Schmidt
+ Tim Wegner
+
+The PNG Reference Library is supplied "AS IS". The Contributing
+Authors and Group 42, Inc. disclaim all warranties, expressed or
+implied, including, without limitation, the warranties of
+merchantability and of fitness for any purpose. The Contributing
+Authors and Group 42, Inc. assume no liability for direct, indirect,
+incidental, special, exemplary, or consequential damages, which may
+result from the use of the PNG Reference Library, even if advised of
+the possibility of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute this
+source code, or portions hereof, for any purpose, without fee, subject
+to the following restrictions:
+
+ 1. The origin of this source code must not be misrepresented.
+
+ 2. Altered versions must be plainly marked as such and must not
+ be misrepresented as being the original source.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+The Contributing Authors and Group 42, Inc. specifically permit,
+without fee, and encourage the use of this source code as a component
+to supporting the PNG file format in commercial products. If you use
+this source code in a product, acknowledgment is not required but would
+be appreciated.
diff --git a/wheels/dependency_licenses/LIBTIFF.txt b/wheels/dependency_licenses/LIBTIFF.txt
new file mode 100644
index 000000000..dc255dec6
--- /dev/null
+++ b/wheels/dependency_licenses/LIBTIFF.txt
@@ -0,0 +1,21 @@
+Copyright (c) 1988-1997 Sam Leffler
+Copyright (c) 1991-1997 Silicon Graphics, Inc.
+
+Permission to use, copy, modify, distribute, and sell this software and
+its documentation for any purpose is hereby granted without fee, provided
+that (i) the above copyright notices and this permission notice appear in
+all copies of the software and related documentation, and (ii) the names of
+Sam Leffler and Silicon Graphics may not be used in any advertising or
+publicity relating to the software without the specific, prior written
+permission of Sam Leffler and Silicon Graphics.
+
+THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
+EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
+WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
+
+IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
+ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF
+LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
+OF THIS SOFTWARE.
diff --git a/wheels/dependency_licenses/LIBWEBP.txt b/wheels/dependency_licenses/LIBWEBP.txt
new file mode 100644
index 000000000..83e4e6f6d
--- /dev/null
+++ b/wheels/dependency_licenses/LIBWEBP.txt
@@ -0,0 +1,29 @@
+Copyright (c) 2010, Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/wheels/dependency_licenses/OPENJPEG.txt b/wheels/dependency_licenses/OPENJPEG.txt
new file mode 100644
index 000000000..c41fc21d8
--- /dev/null
+++ b/wheels/dependency_licenses/OPENJPEG.txt
@@ -0,0 +1,39 @@
+*
+ * The copyright in this software is being made available under the 2-clauses
+ * BSD License, included below. This software may be subject to other third
+ * party and contributor rights, including patent rights, and no such rights
+ * are granted under this license.
+ *
+ * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium
+ * Copyright (c) 2002-2014, Professor Benoit Macq
+ * Copyright (c) 2003-2014, Antonin Descampe
+ * Copyright (c) 2003-2009, Francois-Olivier Devaux
+ * Copyright (c) 2005, Herve Drolon, FreeImage Team
+ * Copyright (c) 2002-2003, Yannick Verschueren
+ * Copyright (c) 2001-2003, David Janssens
+ * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France
+ * Copyright (c) 2012, CS Systemes d'Information, France
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS'
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
diff --git a/wheels/dependency_licenses/RAQM.txt b/wheels/dependency_licenses/RAQM.txt
new file mode 100644
index 000000000..196511ef6
--- /dev/null
+++ b/wheels/dependency_licenses/RAQM.txt
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright © 2015 Information Technology Authority (ITA)
+Copyright © 2016 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
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/wheels/dependency_licenses/XAU.txt b/wheels/dependency_licenses/XAU.txt
new file mode 100644
index 000000000..64492ad80
--- /dev/null
+++ b/wheels/dependency_licenses/XAU.txt
@@ -0,0 +1,21 @@
+Copyright 1988, 1993, 1994, 1998 The Open Group
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation.
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the name of The Open Group shall not be
+used in advertising or otherwise to promote the sale, use or other dealings
+in this Software without prior written authorization from The Open Group.
diff --git a/wheels/dependency_licenses/XCB.txt b/wheels/dependency_licenses/XCB.txt
new file mode 100644
index 000000000..54bfbe5b0
--- /dev/null
+++ b/wheels/dependency_licenses/XCB.txt
@@ -0,0 +1,30 @@
+Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett.
+All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall
+be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the names of the authors
+or their institutions shall not be used in advertising or
+otherwise to promote the sale, use or other dealings in this
+Software without prior written authorization from the
+authors.
diff --git a/wheels/dependency_licenses/XDMCP.txt b/wheels/dependency_licenses/XDMCP.txt
new file mode 100644
index 000000000..5532d143c
--- /dev/null
+++ b/wheels/dependency_licenses/XDMCP.txt
@@ -0,0 +1,23 @@
+Copyright 1989, 1998 The Open Group
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation.
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the name of The Open Group shall not be
+used in advertising or otherwise to promote the sale, use or other dealings
+in this Software without prior written authorization from The Open Group.
+
+Author: Keith Packard, MIT X Consortium
diff --git a/wheels/dependency_licenses/ZLIB.txt b/wheels/dependency_licenses/ZLIB.txt
new file mode 100644
index 000000000..84def6dc6
--- /dev/null
+++ b/wheels/dependency_licenses/ZLIB.txt
@@ -0,0 +1,29 @@
+ (C) 1995-2017 Jean-loup Gailly and Mark Adler
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+
+ Jean-loup Gailly Mark Adler
+ jloup@gzip.org madler@alumni.caltech.edu
+
+If you use the zlib library in a product, we would appreciate *not* receiving
+lengthy legal documents to sign. The sources are provided for free but without
+warranty of any kind. The library has been entirely written by Jean-loup
+Gailly and Mark Adler; it does not include third-party code.
+
+If you redistribute modified sources, we would appreciate that you include in
+the file ChangeLog history information documenting your changes. Please read
+the FAQ for more information on the distribution of modified source versions.
diff --git a/wheels/multibuild b/wheels/multibuild
new file mode 160000
index 000000000..452dd2d17
--- /dev/null
+++ b/wheels/multibuild
@@ -0,0 +1 @@
+Subproject commit 452dd2d1705f6b2375369a6570c415beb3163f70
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 776173e1c..f7e145fb9 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -110,14 +110,20 @@ ARCHITECTURES = {
DEPS = {
"libjpeg": {
"url": SF_PROJECTS
- + "/libjpeg-turbo/files/3.0.0/libjpeg-turbo-3.0.0.tar.gz/download",
- "filename": "libjpeg-turbo-3.0.0.tar.gz",
- "dir": "libjpeg-turbo-3.0.0",
+ + "/libjpeg-turbo/files/3.0.1/libjpeg-turbo-3.0.1.tar.gz/download",
+ "filename": "libjpeg-turbo-3.0.1.tar.gz",
+ "dir": "libjpeg-turbo-3.0.1",
"license": ["README.ijg", "LICENSE.md"],
"license_pattern": (
"(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n=========="
".+(libjpeg-turbo Licenses\n======================\n\n.+)$"
),
+ "patch": {
+ r"CMakeLists.txt": {
+ # libjpeg-turbo does not detect MSVC x86_arm64 cross-compiler correctly
+ 'if(MSVC_IDE AND CMAKE_GENERATOR_PLATFORM MATCHES "arm64")': "if({architecture} STREQUAL ARM64)", # noqa: E501
+ },
+ },
"build": [
*cmds_cmake(
("jpeg-static", "cjpeg-static", "djpeg-static"),
@@ -148,9 +154,9 @@ DEPS = {
"libs": [r"*.lib"],
},
"xz": {
- "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.4.tar.gz/download",
- "filename": "xz-5.4.4.tar.gz",
- "dir": "xz-5.4.4",
+ "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.5.tar.gz/download",
+ "filename": "xz-5.4.5.tar.gz",
+ "dir": "xz-5.4.5",
"license": "COPYING",
"build": [
*cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),
@@ -184,9 +190,9 @@ DEPS = {
"libs": [r"output\release-static\{architecture}\lib\*.lib"],
},
"libtiff": {
- "url": "https://download.osgeo.org/libtiff/tiff-4.5.1.tar.gz",
- "filename": "tiff-4.5.1.tar.gz",
- "dir": "tiff-4.5.1",
+ "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz",
+ "filename": "tiff-4.6.0.tar.gz",
+ "dir": "tiff-4.6.0",
"license": "LICENSE.md",
"patch": {
r"libtiff\tif_lzma.c": {
@@ -213,7 +219,6 @@ DEPS = {
],
"headers": [r"libtiff\tiff*.h"],
"libs": [r"libtiff\*.lib"],
- # "bins": [r"libtiff\*.dll"],
},
"libpng": {
"url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download",
@@ -228,18 +233,18 @@ DEPS = {
"libs": [r"libpng16.lib"],
},
"brotli": {
- "url": "https://github.com/google/brotli/archive/refs/tags/v1.0.9.tar.gz",
- "filename": "brotli-1.0.9.tar.gz",
- "dir": "brotli-1.0.9",
+ "url": "https://github.com/google/brotli/archive/refs/tags/v1.1.0.tar.gz",
+ "filename": "brotli-1.1.0.tar.gz",
+ "dir": "brotli-1.1.0",
"license": "LICENSE",
"build": [
- *cmds_cmake(("brotlicommon-static", "brotlidec-static")),
+ *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"),
cmd_xcopy(r"c\include", "{inc_dir}"),
],
"libs": ["*.lib"],
},
"freetype": {
- "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.gz", # noqa: E501
+ "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.gz",
"filename": "freetype-2.13.2.tar.gz",
"dir": "freetype-2.13.2",
"license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"],
@@ -254,7 +259,7 @@ DEPS = {
"": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501
"": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501
"": "{lib_dir}", # noqa: E501
- "": "zlib.lib;libpng16.lib;brotlicommon-static.lib;brotlidec-static.lib", # noqa: E501
+ "": "zlib.lib;libpng16.lib;brotlicommon.lib;brotlidec.lib", # noqa: E501
},
r"src/autofit/afshaper.c": {
# link against harfbuzz.lib
@@ -272,13 +277,12 @@ DEPS = {
cmd_xcopy("include", "{inc_dir}"),
],
"libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"],
- # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"],
},
"lcms2": {
- "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",
+ "url": SF_PROJECTS + "/lcms/files/lcms/2.16/lcms2-2.16.tar.gz/download",
+ "filename": "lcms2-2.16.tar.gz",
+ "dir": "lcms2-2.16",
+ "license": "LICENSE",
"patch": {
r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": {
# default is /MD for x86 and /MT for x64, we need /MD always
@@ -321,7 +325,7 @@ DEPS = {
},
"libimagequant": {
# commit: Merge branch 'master' into msvc (matches 2.17.0 tag)
- "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", # noqa: E501
+ "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
"filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
"dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab",
"license": "COPYRIGHT",
@@ -329,6 +333,8 @@ DEPS = {
"CMakeLists.txt": {
"if(OPENMP_FOUND)": "if(false)",
"install": "#install",
+ # libimagequant does not detect MSVC x86_arm64 cross-compiler correctly
+ "if(${{CMAKE_SYSTEM_PROCESSOR}} STREQUAL ARM64)": "if({architecture} STREQUAL ARM64)", # noqa: E501
}
},
"build": [
@@ -339,9 +345,9 @@ DEPS = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/8.2.0.zip",
- "filename": "harfbuzz-8.2.0.zip",
- "dir": "harfbuzz-8.2.0",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/8.3.0.zip",
+ "filename": "harfbuzz-8.3.0.zip",
+ "dir": "harfbuzz-8.3.0",
"license": "COPYING",
"build": [
*cmds_cmake(
@@ -369,12 +375,17 @@ DEPS = {
# based on distutils._msvccompiler from CPython 3.7.4
-def find_msvs() -> dict[str, str] | None:
+def find_msvs(architecture: str) -> dict[str, str] | None:
root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
if not root:
print("Program Files not found")
return None
+ if architecture == "ARM64":
+ tools = "Microsoft.VisualStudio.Component.VC.Tools.ARM64"
+ else:
+ tools = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
+
try:
vspath = (
subprocess.check_output(
@@ -385,7 +396,7 @@ def find_msvs() -> dict[str, str] | None:
"-latest",
"-prerelease",
"-requires",
- "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
+ tools,
"-property",
"installationPath",
"-products",
@@ -471,7 +482,7 @@ def extract_dep(url: str, filename: str) -> None:
msg = "Attempted Path Traversal in Zip File"
raise RuntimeError(msg)
zf.extractall(sources_dir)
- elif filename.endswith(".tar.gz") or filename.endswith(".tgz"):
+ elif filename.endswith((".tar.gz", ".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))
@@ -575,14 +586,19 @@ def build_dep(name: str) -> str:
def build_dep_all() -> None:
lines = [r'call "{build_dir}\build_env.cmd"']
+ gha_groups = "GITHUB_ACTIONS" in os.environ
for dep_name in DEPS:
print()
if dep_name in disabled:
print(f"Skipping disabled dependency {dep_name}")
continue
script = build_dep(dep_name)
+ if gha_groups:
+ lines.append(f"@echo ::group::Running {script}")
lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"')
lines.append("if errorlevel 1 echo Build failed! && exit /B 1")
+ if gha_groups:
+ lines.append("@echo ::endgroup::")
print()
lines.append("@echo All Pillow dependencies built successfully!")
write_script("build_dep_all.cmd", lines)
@@ -656,7 +672,7 @@ if __name__ == "__main__":
arch_prefs = ARCHITECTURES[args.architecture]
print("Target architecture:", args.architecture)
- msvs = find_msvs()
+ msvs = find_msvs(args.architecture)
if msvs is None:
msg = "Visual Studio not found. Please install Visual Studio 2017 or newer."
raise RuntimeError(msg)
@@ -691,6 +707,11 @@ if __name__ == "__main__":
disabled += ["libimagequant"]
if args.no_fribidi:
disabled += ["fribidi"]
+ elif args.architecture == "ARM64" and platform.machine() != "ARM64":
+ import warnings
+
+ warnings.warn("Cross-compiling FriBiDi is currently not supported, disabling")
+ disabled += ["fribidi"]
prefs = {
"architecture": args.architecture,