Merge branch 'main' into progress

This commit is contained in:
Andrew Murray 2025-03-06 07:57:45 +11:00 committed by GitHub
commit af7954bc6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
182 changed files with 2505 additions and 2248 deletions

View File

@ -1,99 +0,0 @@
skip_commits:
files:
- ".github/**/*"
- ".gitmodules"
- "docs/**/*"
- "wheels/**/*"
version: '{build}'
clone_folder: c:\pillow
init:
- ECHO %PYTHON%
#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
# Uncomment previous line to get RDP access during the build.
environment:
COVERAGE_CORE: sysmon
EXECUTABLE: python.exe
TEST_OPTIONS:
DEPLOY: YES
matrix:
- PYTHON: C:/Python313
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python39-x64
ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
install:
- '%PYTHON%\%EXECUTABLE% --version'
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-test-images.zip -oc:\
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.4.0
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
c:\pillow\winbuild\build\build_dep_all.cmd
$host.SetShouldExit(0)
- path C:\pillow\winbuild\build\bin;%PATH%
build_script:
- cd c:\pillow
- winbuild\build\build_env.cmd
- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .'
- '%PYTHON%\%EXECUTABLE% selftest.py --installed'
test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- path %PYTHON%;%PATH%
- .ci\test.cmd
after_test:
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe
- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor
matrix:
fast_finish: true
cache:
- '%LOCALAPPDATA%\pip\Cache'
artifacts:
- path: pillow\*.egg
name: egg
- path: pillow\*.whl
name: wheel
before_deploy:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .'
- ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
deploy:
provider: S3
region: us-west-2
access_key_id: AKIAIRAXC62ZNTVQJMOQ
secret_access_key:
secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi
bucket: pillow-nightly
folder: win/$(APPVEYOR_BUILD_NUMBER)/
artifact: /.*egg|wheel/
on:
APPVEYOR_REPO_NAME: python-pillow/Pillow
branch: main
deploy: YES
# Uncomment the following lines to get RDP access after the build/test and block for
# up to the timeout limit (~1hr)
#
#on_finish:
#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))

View File

@ -2,8 +2,4 @@
# gather the coverage data
python3 -m pip install coverage
if [[ $MATRIX_DOCKER ]]; then
python3 -m coverage xml --ignore-errors
else
python3 -m coverage xml
fi
python3 -m coverage xml

View File

@ -3,8 +3,5 @@
set -e
python3 -m coverage erase
if [ $(uname) == "Darwin" ]; then
export CPPFLAGS="-I/usr/local/miniconda/include";
fi
make clean
make install-coverage

View File

@ -2,12 +2,12 @@
aptget_update()
{
if [ ! -z $1 ]; then
if [ -n "$1" ]; then
echo ""
echo "Retrying apt-get update..."
echo ""
fi
output=`sudo apt-get update 2>&1`
output=$(sudo apt-get update 2>&1)
echo "$output"
if [[ $output == *[WE]:\ * ]]; then
return 1

View File

@ -1 +1 @@
cibuildwheel==2.22.0
cibuildwheel==2.23.0

View File

@ -1,4 +1,4 @@
mypy==1.14.0
mypy==1.15.0
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython

View File

@ -9,7 +9,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Fork the Pillow repository.
- Create a branch from `main`.
- Develop bug fixes, features, tests, etc.
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
- Create a pull request to pull the changes from your branch to the Pillow `main`.
### Guidelines
@ -17,7 +17,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Separate code commits from reformatting commits.
- Provide tests for any newly added code.
- Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests.
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
## Reporting Issues

1
.github/mergify.yml vendored
View File

@ -9,7 +9,6 @@ pull_request_rules:
- status-success=Windows Test Successful
- status-success=MinGW
- status-success=Cygwin Test Successful
- status-success=continuous-integration/appveyor/pr
actions:
merge:
method: merge

View File

@ -10,15 +10,11 @@ brew install \
ghostscript \
jpeg-turbo \
libimagequant \
libraqm \
libtiff \
little-cms2 \
openjpeg \
webp
if [[ "$ImageOS" == "macos13" ]]; then
brew install --ignore-dependencies libraqm
else
brew install libraqm
fi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
python3 -m pip install coverage

View File

@ -52,7 +52,7 @@ jobs:
persist-credentials: false
- name: Install Cygwin
uses: cygwin/cygwin-install-action@v4
uses: cygwin/cygwin-install-action@v5
with:
packages: >
gcc-g++

View File

@ -29,21 +29,18 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
docker: [
# Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-22.04-jammy-arm64v8,
ubuntu-24.04-noble-ppc64le,
ubuntu-24.04-noble-s390x,
# Then run the remainder
alpine,
amazon-2-amd64,
amazon-2023-amd64,
arch,
centos-stream-9-amd64,
centos-stream-10-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-40-amd64,
@ -54,12 +51,17 @@ jobs:
]
dockerTag: [main]
include:
- docker: "ubuntu-22.04-jammy-arm64v8"
qemu-arch: "aarch64"
- docker: "ubuntu-24.04-noble-ppc64le"
os: "ubuntu-22.04"
qemu-arch: "ppc64le"
dockerTag: main
- docker: "ubuntu-24.04-noble-s390x"
os: "ubuntu-22.04"
qemu-arch: "s390x"
dockerTag: main
- docker: "ubuntu-24.04-noble-arm64v8"
os: "ubuntu-24.04-arm"
dockerTag: main
name: ${{ matrix.docker }}
@ -89,15 +91,15 @@ jobs:
- name: After success
run: |
PATH="$PATH:~/.local/bin"
docker start pillow_container
sudo docker cp pillow_container:/Pillow /Pillow
sudo chown -R runner /Pillow
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
docker stop pillow_container
sudo mkdir -p $pil_path
sudo cp src/PIL/*.py $pil_path
cd /Pillow
.ci/after_success.sh
env:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage
uses: codecov/codecov-action@v5

View File

@ -66,9 +66,9 @@ jobs:
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python3-pip \
mingw-w64-x86_64-python-numpy \
mingw-w64-x86_64-python-olefile \
mingw-w64-x86_64-python-pip \
mingw-w64-x86_64-python-pytest \
mingw-w64-x86_64-python-pytest-cov \
mingw-w64-x86_64-python-pytest-timeout \

View File

@ -31,15 +31,20 @@ env:
jobs:
build:
runs-on: windows-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
architecture: ["x64"]
os: ["windows-latest"]
include:
# Test the oldest Python on 32-bit
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
timeout-minutes: 30
name: Python ${{ matrix.python-version }}
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
steps:
- name: Checkout Pillow
@ -67,6 +72,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
architecture: ${{ matrix.architecture }}
cache: pip
cache-dependency-path: ".github/workflows/test-windows.yml"
@ -78,7 +84,7 @@ jobs:
python3 -m pip install --upgrade pip
- name: Install CPython dependencies
if: "!contains(matrix.python-version, 'pypy')"
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
run: |
python3 -m pip install PyQt6

View File

@ -41,7 +41,9 @@ jobs:
"ubuntu-latest",
]
python-version: [
"pypy3.11",
"pypy3.10",
"3.14",
"3.13t",
"3.13",
"3.12",

View File

@ -37,20 +37,15 @@ fi
ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=10.1.0
LIBPNG_VERSION=1.6.44
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.4.0
LIBPNG_VERSION=1.6.47
JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3
XZ_VERSION=5.6.4
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then
GIFLIB_VERSION=5.2.2
else
GIFLIB_VERSION=5.2.1
fi
ZLIB_NG_VERSION=2.2.2
LCMS2_VERSION=2.17
ZLIB_NG_VERSION=2.2.4
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
@ -59,13 +54,10 @@ BROTLI_VERSION=1.1.0
function build_pkg_config {
if [ -e pkg-config-stamp ]; then return; fi
# This essentially duplicates the Homebrew recipe
ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -Wno-int-conversion"
build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
--disable-debug --disable-host-tool --with-internal-glib \
--with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \
--with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include
CFLAGS=$ORIGINAL_CFLAGS
export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config
touch pkg-config-stamp
}
@ -77,6 +69,14 @@ function build_zlib_ng {
&& ./configure --prefix=$BUILD_PREFIX --zlib-compat \
&& make -j4 \
&& make install)
if [ -n "$IS_MACOS" ]; then
# Ensure that on macOS, the library name is an absolute path, not an
# @rpath, so that delocate picks up the right library (and doesn't need
# DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an
# option to control the install_name.
install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
fi
touch zlib-stamp
}
@ -103,7 +103,7 @@ function build_harfbuzz {
function build {
build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel
fi
build_zlib_ng
@ -135,13 +135,13 @@ function build {
build_lcms2
build_openjpeg
ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -O3 -DNDEBUG"
webp_cflags="-O3 -DNDEBUG"
if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
fi
build_libwebp
CFLAGS=$ORIGINAL_CFLAGS
CFLAGS="$CFLAGS $webp_cflags" build_simple libwebp $LIBWEBP_VERSION \
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
--enable-libwebpmux --enable-libwebpdemux
build_brotli

View File

@ -11,6 +11,9 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
$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
if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
& python -m pip install numpy
}
cd $pillow
& python -VV
if (!$?) { exit $LASTEXITCODE }

View File

@ -13,6 +13,7 @@ on:
paths:
- ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- "pyproject.toml"
- "setup.py"
- "wheels/*"
- "winbuild/build_prepare.py"
@ -23,6 +24,7 @@ on:
paths:
- ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- "pyproject.toml"
- "setup.py"
- "wheels/*"
- "winbuild/build_prepare.py"
@ -40,62 +42,7 @@ env:
FORCE_COLOR: 1
jobs:
build-1-QEMU-emulated-wheels:
if: github.event_name != 'schedule'
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- pp310
- cp3{9,10,11}
- cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
- musllinux
exclude:
- { python-version: pp310, spec: musllinux }
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: true
- uses: actions/setup-python@v5
with:
python-version: "3.x"
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Install cibuildwheel
run: |
python3 -m pip install -r .ci/requirements-cibw.txt
- name: Build wheels
run: |
python3 -m cibuildwheel --output-dir wheelhouse
env:
# Build only the currently selected Linux architecture (so we can
# parallelise for speed).
CIBW_ARCHS: "aarch64"
# Likewise, select only one Python version per job to speed this up.
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
CIBW_ENABLE: cpython-prerelease
# Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
- uses: actions/upload-artifact@v4
with:
name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }}
path: ./wheelhouse/*.whl
build-2-native-wheels:
build-native-wheels:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
@ -116,7 +63,7 @@ jobs:
- name: "macOS 10.15 x86_64"
os: macos-13
cibw_arch: x86_64
build: "pp310*"
build: "pp3*"
macosx_deployment_target: "10.15"
- name: "macOS arm64"
os: macos-latest
@ -130,6 +77,14 @@ jobs:
cibw_arch: x86_64
build: "*manylinux*"
manylinux: "manylinux_2_28"
- name: "manylinux2014 and musllinux aarch64"
os: ubuntu-24.04-arm
cibw_arch: aarch64
- name: "manylinux_2_28 aarch64"
os: ubuntu-24.04-arm
cibw_arch: aarch64
build: "*manylinux*"
manylinux: "manylinux_2_28"
steps:
- uses: actions/checkout@v4
with:
@ -150,7 +105,9 @@ jobs:
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_ENABLE: cpython-prerelease cpython-freethreading
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_SKIP: pp39-*
@ -227,7 +184,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_ENABLE: cpython-prerelease cpython-freethreading
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
@ -263,8 +220,6 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: pip
cache-dependency-path: "Makefile"
- run: make sdist
@ -275,7 +230,7 @@ jobs:
scientific-python-nightly-wheels-publish:
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [build-2-native-wheels, windows]
needs: [build-native-wheels, windows]
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
@ -292,7 +247,7 @@ jobs:
pypi-publish:
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
needs: [build-native-wheels, windows, sdist]
runs-on: ubuntu-latest
name: Upload release to PyPI
environment:

View File

@ -1,17 +1,17 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.1
rev: v0.9.9
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.10.0
rev: 25.1.0
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.8.0
rev: 1.8.3
hooks:
- id: bandit
args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.4
rev: v19.1.7
hooks:
- id: clang-format
types: [c]
@ -50,19 +50,24 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.30.0
rev: 0.31.2
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.4.1
hooks:
- id: zizmor
- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.0
hooks:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.0
rev: v2.5.1
hooks:
- id: pyproject-fmt
@ -73,7 +78,7 @@ repos:
additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.4.1
rev: 1.5.0
hooks:
- id: tox-ini-fmt

View File

@ -1,5 +1,8 @@
version: 2
sphinx:
configuration: docs/conf.py
formats: [pdf]
build:

View File

@ -20,7 +20,6 @@ graft docs
graft _custom_build
# build/src control detritus
exclude .appveyor.yml
exclude .clang-format
exclude .coveragerc
exclude .editorconfig

View File

@ -42,9 +42,6 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
alt="AppVeyor CI build status (Windows)"
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
alt="GitHub Actions build status (Wheels)"
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>

View File

@ -9,7 +9,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 [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch.
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
@ -38,7 +38,7 @@ Released as needed for security, installation or critical bug fixes.
git checkout -t remotes/origin/5.2.x
```
* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Run pre-release check via `make release-test`.
* [ ] Create tag for release e.g.:

View File

@ -3,26 +3,25 @@ from __future__ import annotations
import zlib
from io import BytesIO
import pytest
from PIL import Image, ImageFile, PngImagePlugin
TEST_FILE = "Tests/images/png_decompression_dos.png"
def test_ignore_dos_text() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
def test_ignore_dos_text(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
try:
im = Image.open(TEST_FILE)
with Image.open(TEST_FILE) as im:
im.load()
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
assert isinstance(im, PngImagePlugin.PngImageFile)
for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
assert isinstance(im, PngImagePlugin.PngImageFile)
for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
for s in im.info.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
for s in im.info.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_text() -> None:

View File

@ -9,7 +9,6 @@ import os
import shutil
import subprocess
import sys
import sysconfig
import tempfile
from collections.abc import Sequence
from functools import lru_cache
@ -140,18 +139,11 @@ def assert_image_similar_tofile(
filename: str,
epsilon: float,
msg: str | None = None,
mode: str | None = None,
) -> None:
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg)
def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) == len(items), msg
def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) != len(items), msg
@ -327,16 +319,7 @@ def magick_command() -> list[str] | None:
return None
def on_appveyor() -> bool:
return "APPVEYOR" in os.environ
def on_github_actions() -> bool:
return "GITHUB_ACTIONS" in os.environ
def on_ci() -> bool:
# GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ
@ -358,10 +341,6 @@ def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info")
def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw"
class CachedProperty:
def __init__(self, func: Callable[[Any], Any]) -> None:
self.func = func

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -7,7 +7,7 @@ import fuzzers
import packaging
import pytest
from PIL import Image, UnidentifiedImageError, features
from PIL import Image, features
from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"):
@ -32,21 +32,17 @@ def test_fuzz_images(path: str) -> None:
fuzzers.fuzz_image(f.read())
assert True
except (
# Known exceptions from Pillow
OSError,
SyntaxError,
MemoryError,
ValueError,
NotImplementedError,
OverflowError,
):
# Known exceptions that are through from Pillow
assert True
except (
# Known Image.* exceptions
Image.DecompressionBombError,
Image.DecompressionBombWarning,
UnidentifiedImageError,
):
# Known Image.* exceptions
assert True
finally:
fuzzers.disable_decompressionbomb_error()

View File

@ -19,7 +19,7 @@ except ImportError:
class TestColorLut3DCoreAPI:
def generate_identity_table(
self, channels: int, size: int | tuple[int, int, int]
) -> tuple[int, int, int, int, list[float]]:
) -> tuple[int, tuple[int, int, int], list[float]]:
if isinstance(size, tuple):
size_1d, size_2d, size_3d = size
else:
@ -39,9 +39,7 @@ class TestColorLut3DCoreAPI:
]
return (
channels,
size_1d,
size_2d,
size_3d,
(size_1d, size_2d, size_3d),
[item for sublist in table for item in sublist],
)
@ -89,21 +87,21 @@ class TestColorLut3DCoreAPI:
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7
"RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 7
)
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9
"RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 9
)
with pytest.raises(TypeError):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8
"RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, "0"] * 8
)
with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), 16)
@pytest.mark.parametrize(
"lut_mode, table_channels, table_size",
@ -264,7 +262,7 @@ class TestColorLut3DCoreAPI:
assert_image_equal(
Image.merge('RGB', im.split()[::-1]),
im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2, [
3, (2, 2, 2), [
0, 0, 0, 0, 0, 1,
0, 1, 0, 0, 1, 1,
@ -286,7 +284,7 @@ class TestColorLut3DCoreAPI:
# fmt: off
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2,
3, (2, 2, 2),
[
-1, -1, -1, 2, -1, -1,
-1, 2, -1, 2, 2, -1,
@ -307,7 +305,7 @@ class TestColorLut3DCoreAPI:
# fmt: off
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2,
3, (2, 2, 2),
[
-3, -3, -3, 5, -3, -3,
-3, 5, -3, 5, 5, -3,

View File

@ -12,19 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb:
def teardown_method(self) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def test_no_warning_small_file(self) -> None:
# Implicit assert: no warning.
# A warning would cause a failure.
with Image.open(TEST_FILE):
pass
def test_no_warning_no_limit(self) -> None:
def test_no_warning_no_limit(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
# Turn limit off
Image.MAX_IMAGE_PIXELS = None
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None)
assert Image.MAX_IMAGE_PIXELS is None
# Act / Assert
@ -33,18 +30,18 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE):
pass
def test_warning(self) -> None:
def test_warning(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Set limit to trigger warning on the test file
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 128 * 128 - 1)
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
with pytest.warns(Image.DecompressionBombWarning):
with Image.open(TEST_FILE):
pass
def test_exception(self) -> None:
def test_exception(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Set limit to trigger exception on the test file
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 64 * 128 - 1)
assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1
with pytest.raises(Image.DecompressionBombError):
@ -66,9 +63,9 @@ class TestDecompressionBomb:
with pytest.raises(Image.DecompressionBombError):
im.seek(1)
def test_exception_gif_zero_width(self) -> None:
def test_exception_gif_zero_width(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Set limit to trigger exception on the test file
Image.MAX_IMAGE_PIXELS = 4 * 64 * 128
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 4 * 64 * 128)
assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128
with pytest.raises(Image.DecompressionBombError):

View File

@ -308,13 +308,8 @@ def test_apng_syntax_errors() -> None:
im.load()
# we can handle this case gracefully
exception = None
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
try:
im.seek(im.n_frames - 1)
except Exception as e:
exception = e
assert exception is None
im.seek(im.n_frames - 1)
with pytest.raises(OSError):
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
@ -406,13 +401,8 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
append_images=frames,
)
with Image.open(test_file) as im:
exception = None
try:
im.seek(im.n_frames - 1)
im.load()
except Exception as e:
exception = e
assert exception is None
im.seek(im.n_frames - 1)
im.load()
def test_apng_save_duration_loop(tmp_path: Path) -> None:

View File

@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from PIL import Image
from PIL import BlpImagePlugin, Image
from .helper import (
assert_image_equal,
@ -19,6 +19,7 @@ def test_load_blp1() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
assert im.mode == "RGBA"
im.load()
@ -37,6 +38,13 @@ def test_load_blp2_dxt1a() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(BlpImagePlugin.BLPFormatError):
BlpImagePlugin.BlpImageFile(invalid_file)
def test_save(tmp_path: Path) -> None:
f = str(tmp_path / "temp.blp")

View File

@ -26,12 +26,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(TEST_FILE)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:

View File

@ -331,11 +331,13 @@ def test_dxt5_colorblock_alpha_issue_4142() -> None:
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
px = im.getpixel((0, 0))
assert isinstance(px, tuple)
assert px[0] != 0
assert px[1] != 0
assert px[2] != 0
px = im.getpixel((1, 0))
assert isinstance(px, tuple)
assert px[0] != 0
assert px[1] != 0
assert px[2] != 0

View File

@ -95,10 +95,14 @@ def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_load() -> None:
with Image.open(FILE1) as im:
assert im.load()[0, 0] == (255, 255, 255)
px = im.load()
assert px is not None
assert px[0, 0] == (255, 255, 255)
# Test again now that it has already been loaded once
assert im.load()[0, 0] == (255, 255, 255)
px = im.load()
assert px is not None
assert px[0, 0] == (255, 255, 255)
def test_binary() -> None:

View File

@ -35,32 +35,29 @@ def test_sanity() -> None:
assert im.is_animated
def test_prefix_chunk() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
with Image.open(animated_test_file_with_prefix_chunk) as im:
assert im.mode == "P"
assert im.size == (320, 200)
assert im.format == "FLI"
assert im.info["duration"] == 171
assert im.is_animated
def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open(animated_test_file_with_prefix_chunk) as im:
assert im.mode == "P"
assert im.size == (320, 200)
assert im.format == "FLI"
assert im.info["duration"] == 171
assert im.is_animated
palette = im.getpalette()
assert palette[3:6] == [255, 255, 255]
assert palette[381:384] == [204, 204, 12]
assert palette[765:] == [252, 0, 0]
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
palette = im.getpalette()
assert palette[3:6] == [255, 255, 255]
assert palette[381:384] == [204, 204, 12]
assert palette[765:] == [252, 0, 0]
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(static_test_file)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:

View File

@ -1,5 +1,8 @@
from __future__ import annotations
import io
import struct
import pytest
from PIL import FtexImagePlugin, Image
@ -23,3 +26,15 @@ def test_invalid_file() -> None:
with pytest.raises(SyntaxError):
FtexImagePlugin.FtexImageFile(invalid_file)
def test_invalid_texture() -> None:
with open("Tests/images/ftex_dxt1.ftc", "rb") as fp:
data = fp.read()
# Change texture compression format
data = data[:24] + struct.pack("<i", 2) + data[28:]
with pytest.raises(ValueError, match="Invalid texture compression format: 2"):
with Image.open(io.BytesIO(data)):
pass

View File

@ -14,10 +14,14 @@ def test_gbr_file() -> None:
def test_load() -> None:
with Image.open("Tests/images/gbr.gbr") as im:
assert im.load()[0, 0] == (0, 0, 0, 0)
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
# Test again now that it has already been loaded once
assert im.load()[0, 0] == (0, 0, 0, 0)
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
def test_multiple_load_operations() -> None:

View File

@ -4,6 +4,8 @@ import pytest
from PIL import GdImageFile, UnidentifiedImageError
from .helper import assert_image_similar_tofile
TEST_GD_FILE = "Tests/images/hopper.gd"
@ -11,6 +13,7 @@ def test_sanity() -> None:
with GdImageFile.open(TEST_GD_FILE) as im:
assert im.size == (128, 128)
assert im.format == "GD"
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14)
def test_bad_mode() -> None:

View File

@ -22,9 +22,6 @@ from .helper import (
# sample gif stream
TEST_GIF = "Tests/images/hopper.gif"
with open(TEST_GIF, "rb") as f:
data = f.read()
def test_sanity() -> None:
with Image.open(TEST_GIF) as im:
@ -37,12 +34,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(TEST_GIF)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:
@ -86,12 +83,12 @@ def test_invalid_file() -> None:
def test_l_mode_transparency() -> None:
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
assert im.mode == "L"
assert im.load()[0, 0] == 128
assert im.getpixel((0, 0)) == 128
assert im.info["transparency"] == 255
im.seek(1)
assert im.mode == "L"
assert im.load()[0, 0] == 128
assert im.getpixel((0, 0)) == 128
def test_l_mode_after_rgb() -> None:
@ -109,7 +106,7 @@ def test_palette_not_needed_for_second_frame() -> None:
assert_image_similar(im, hopper("L").convert("RGB"), 8)
def test_strategy() -> None:
def test_strategy(monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/iss634.gif") as im:
expected_rgb_always = im.convert("RGB")
@ -119,35 +116,36 @@ def test_strategy() -> None:
im.seek(1)
expected_different = im.convert("RGB")
try:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "RGB"
assert_image_equal(im, expected_rgb_always)
monkeypatch.setattr(
GifImagePlugin, "LOADING_STRATEGY", GifImagePlugin.LoadingStrategy.RGB_ALWAYS
)
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "RGB"
assert_image_equal(im, expected_rgb_always)
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGBA"
assert_image_equal(im, expected_rgb_always_rgba)
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGBA"
assert_image_equal(im, expected_rgb_always_rgba)
GifImagePlugin.LOADING_STRATEGY = (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
)
# Stay in P mode with only a global palette
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "P"
monkeypatch.setattr(
GifImagePlugin,
"LOADING_STRATEGY",
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
)
# Stay in P mode with only a global palette
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "P"
im.seek(1)
assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_different)
im.seek(1)
assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_different)
# Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "P"
# Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "P"
im.seek(1)
assert im.mode == "RGB"
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
im.seek(1)
assert im.mode == "RGB"
def test_optimize() -> None:
@ -357,8 +355,9 @@ def test_save_all_progress():
def test_loading_multiple_palettes(path: str, mode: str) -> None:
with Image.open(path) as im:
assert im.mode == "P"
assert im.palette is not None
first_frame_colors = im.palette.colors.keys()
original_color = im.convert("RGB").load()[0, 0]
original_color = im.convert("RGB").getpixel((0, 0))
im.seek(1)
assert im.mode == mode
@ -366,10 +365,10 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None:
im = im.convert("RGB")
# Check a color only from the old palette
assert im.load()[0, 0] == original_color
assert im.getpixel((0, 0)) == original_color
# Check a color from the new palette
assert im.load()[24, 24] not in first_frame_colors
assert im.getpixel((24, 24)) not in first_frame_colors
def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
@ -535,8 +534,7 @@ def test_eoferror() -> None:
def test_first_frame_transparency() -> None:
with Image.open("Tests/images/first_frame_transparency.gif") as im:
px = im.load()
assert px[0, 0] == im.info["transparency"]
assert im.getpixel((0, 0)) == im.info["transparency"]
def test_dispose_none() -> None:
@ -576,6 +574,7 @@ def test_dispose_background_transparency() -> None:
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
img.seek(2)
px = img.load()
assert px is not None
assert px[35, 30][3] == 0
@ -603,17 +602,15 @@ def test_dispose_background_transparency() -> None:
def test_transparent_dispose(
loading_strategy: GifImagePlugin.LoadingStrategy,
expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],
monkeypatch: pytest.MonkeyPatch,
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3):
img.seek(frame)
for x in range(3):
color = img.getpixel((x, 0))
assert color == expected_colors[frame][x]
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3):
img.seek(frame)
for x in range(3):
color = img.getpixel((x, 0))
assert color == expected_colors[frame][x]
def test_dispose_previous() -> None:
@ -652,7 +649,7 @@ def test_save_dispose(tmp_path: Path) -> None:
Image.new("L", (100, 100), "#111"),
Image.new("L", (100, 100), "#222"),
]
for method in range(0, 4):
for method in range(4):
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
with Image.open(out) as img:
for _ in range(2):
@ -812,6 +809,21 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None:
assert im.getpixel((0, 0)) == (0, 0, 0, 255)
def test_dispose2_without_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("P", (100, 100))
im2 = Image.new("P", (100, 100), (0, 0, 0))
im2.putpixel((50, 50), (255, 0, 0))
im.save(out, save_all=True, append_images=[im2], disposal=2)
with Image.open(out) as reloaded:
reloaded.seek(1)
assert reloaded.tile[0].extents == (0, 0, 100, 100)
def test_transparency_in_second_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im:
@ -1361,6 +1373,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
with Image.open(out) as im:
# Assert that the frames are correct, and each frame has the same palette
assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
assert im.palette is not None
assert im.palette.palette == im.global_palette.palette
im.seek(1)
@ -1395,32 +1408,30 @@ def test_save_I(tmp_path: Path) -> None:
assert_image_equal(reloaded.convert("L"), im.convert("L"))
def test_getdata() -> None:
def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None:
# Test getheader/getdata against legacy values.
# Create a 'P' image with holes in the palette.
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
im = Image.linear_gradient(mode="L").resize((16, 16), Image.Resampling.NEAREST)
im.putpalette(ImagePalette.ImagePalette("RGB"))
im.info = {"background": 0}
passed_palette = bytes(255 - i // 3 for i in range(768))
GifImagePlugin._FORCE_OPTIMIZE = True
try:
h = GifImagePlugin.getheader(im, passed_palette)
d = GifImagePlugin.getdata(im)
monkeypatch.setattr(GifImagePlugin, "_FORCE_OPTIMIZE", True)
import pickle
h = GifImagePlugin.getheader(im, passed_palette)
d = GifImagePlugin.getdata(im)
# Enable to get target values on pre-refactor version
# with open('Tests/images/gif_header_data.pkl', 'wb') as f:
# pickle.dump((h, d), f, 1)
with open("Tests/images/gif_header_data.pkl", "rb") as f:
(h_target, d_target) = pickle.load(f)
import pickle
assert h == h_target
assert d == d_target
finally:
GifImagePlugin._FORCE_OPTIMIZE = False
# Enable to get target values on pre-refactor version
# with open('Tests/images/gif_header_data.pkl', 'wb') as f:
# pickle.dump((h, d), f, 1)
with open("Tests/images/gif_header_data.pkl", "rb") as f:
(h_target, d_target) = pickle.load(f)
assert h == h_target
assert d == d_target
def test_lzw_bits() -> None:
@ -1446,24 +1457,23 @@ def test_lzw_bits() -> None:
),
)
def test_extents(
test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy
test_file: str,
loading_strategy: GifImagePlugin.LoadingStrategy,
monkeypatch: pytest.MonkeyPatch,
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/" + test_file) as im:
assert im.size == (100, 100)
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
with Image.open("Tests/images/" + test_file) as im:
assert im.size == (100, 100)
# Check that n_frames does not change the size
assert im.n_frames == 2
assert im.size == (100, 100)
# Check that n_frames does not change the size
assert im.n_frames == 2
assert im.size == (100, 100)
im.seek(1)
assert im.size == (150, 150)
im.seek(1)
assert im.size == (150, 150)
im.load()
assert im.im.size == (150, 150)
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
im.load()
assert im.im.size == (150, 150)
def test_missing_background() -> None:

View File

@ -32,10 +32,14 @@ def test_sanity() -> None:
def test_load() -> None:
with Image.open(TEST_FILE) as im:
assert im.load()[0, 0] == (0, 0, 0, 0)
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
# Test again now that it has already been loaded once
assert im.load()[0, 0] == (0, 0, 0, 0)
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
def test_save(tmp_path: Path) -> None:

View File

@ -24,7 +24,9 @@ def test_sanity() -> None:
def test_load() -> None:
with Image.open(TEST_ICO_FILE) as im:
assert im.load()[0, 0] == (1, 1, 9, 255)
px = im.load()
assert px is not None
assert px[0, 0] == (1, 1, 9, 255)
def test_mask() -> None:
@ -243,27 +245,23 @@ def test_draw_reloaded(tmp_path: Path) -> None:
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
def test_truncated_mask() -> None:
def test_truncated_mask(monkeypatch: pytest.MonkeyPatch) -> None:
# 1 bpp
with open("Tests/images/hopper_mask.ico", "rb") as fp:
data = fp.read()
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
data = data[:-3]
try:
with Image.open(io.BytesIO(data)) as im:
with Image.open("Tests/images/hopper_mask.png") as expected:
assert im.mode == "1"
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "1"
# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")
# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")
data = output.getvalue()[:-1]
data = output.getvalue()[:-1]
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"

View File

@ -31,12 +31,12 @@ def test_name_limit(tmp_path: Path) -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(TEST_IM)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:

View File

@ -58,10 +58,7 @@ def test_getiptcinfo_fotostation() -> None:
# Assert
assert iptc is not None
for tag in iptc.keys():
if tag[0] == 240:
return
pytest.fail("FotoStation tag not found")
assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found"
def test_getiptcinfo_zero_padding() -> None:

View File

@ -181,6 +181,10 @@ class TestFileJpeg:
assert test(100, 200) == (100, 200)
assert test(0) is None # square pixels
def test_dpi_jfif_cm(self) -> None:
with Image.open("Tests/images/jfif_unit_cm.jpg") as im:
assert im.info["dpi"] == (2.54, 5.08)
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@ -277,7 +281,10 @@ class TestFileJpeg:
assert not im2.info.get("progressive")
assert im3.info.get("progressive")
assert_image_equal(im1, im3)
if features.check_feature("mozjpeg"):
assert_image_similar(im1, im3, 9.39)
else:
assert_image_equal(im1, im3)
assert im1_bytes >= im3_bytes
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
@ -349,7 +356,6 @@ class TestFileJpeg:
assert exif.get_ifd(0x8825) == {}
transposed = ImageOps.exif_transpose(im)
assert transposed is not None
exif = transposed.getexif()
assert exif.get_ifd(0x8825) == {}
@ -420,8 +426,12 @@ class TestFileJpeg:
im2 = self.roundtrip(hopper(), progressive=1)
im3 = self.roundtrip(hopper(), progression=1) # compatibility
assert_image_equal(im1, im2)
assert_image_equal(im1, im3)
if features.check_feature("mozjpeg"):
assert_image_similar(im1, im2, 9.39)
assert_image_similar(im1, im3, 9.39)
else:
assert_image_equal(im1, im2)
assert_image_equal(im1, im3)
assert im2.info.get("progressive")
assert im2.info.get("progression")
assert im3.info.get("progressive")
@ -520,12 +530,13 @@ class TestFileJpeg:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
def test_truncated_jpeg_should_read_all_the_data(self) -> None:
def test_truncated_jpeg_should_read_all_the_data(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
filename = "Tests/images/truncated_jpeg.jpg"
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open(filename) as im:
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
assert im.getbbox() is not None
def test_truncated_jpeg_throws_oserror(self) -> None:
@ -923,7 +934,7 @@ class TestFileJpeg:
def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer = BytesIO(b"\xff" * size) # Many xff bytes
max_pos = 0
orig_read = buffer.read
@ -1014,7 +1025,7 @@ class TestFileJpeg:
im.save(f, xmp=b"1" * 65505)
@pytest.mark.timeout(timeout=1)
def test_eof(self) -> None:
def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Even though this decoder never says that it is finished
# the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
@ -1027,11 +1038,10 @@ class TestFileJpeg:
with Image.open(TEST_FILE) as im:
im.tile = [
("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
]
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_separate_tables(self) -> None:
im = hopper()

View File

@ -63,6 +63,7 @@ def test_sanity() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0)
assert im.mode == "RGB"
assert im.size == (640, 480)
@ -181,14 +182,11 @@ def test_load_dpi() -> None:
assert "dpi" not in im.info
def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
# JPEG2000 image with a restricted ICC profile and a known colorspace
with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_restricted_icc_profile(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
# JPEG2000 image with a restricted ICC profile and a known colorspace
with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im:
assert im.mode == "RGB"
@pytest.mark.skipif(
@ -315,6 +313,18 @@ def test_rgba(ext: str) -> None:
assert im.mode == "RGBA"
def test_grayscale_four_channels() -> None:
with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp:
data = fp.read()
# Change color space to OPJ_CLRSPC_GRAY
data = data[:76] + b"\x11" + data[77:]
with Image.open(BytesIO(data)) as im:
im.load()
assert im.mode == "RGBA"
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
@ -325,6 +335,18 @@ def test_cmyk() -> None:
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
@skip_unless_feature_version("jpg_2000", "2.5.3")
def test_cmyk_save() -> None:
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2:
assert jp2.mode == "CMYK"
im = roundtrip(jp2)
assert_image_equal(im, jp2)
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im:
@ -412,6 +434,7 @@ def test_subsampling_decode(name: str) -> None:
def test_pclr() -> None:
with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im:
assert im.mode == "P"
assert im.palette is not None
assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0
@ -419,13 +442,15 @@ def test_pclr() -> None:
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
assert im.palette is not None
assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0
def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"):
with Image.open(path) as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
# Test an image that is truncated partway through a codestream
with open("Tests/images/comment.jp2", "rb") as fp:
@ -479,8 +504,7 @@ def test_plt_marker(card: ImageFile.ImageFile) -> None:
out.seek(0)
while True:
marker = out.read(2)
if not marker:
pytest.fail("End of stream without PLT")
assert marker, "End of stream without PLT"
jp2_boxid = _binary.i16be(marker)
if jp2_boxid == 0xFF4F:

View File

@ -36,11 +36,7 @@ class LibTiffTestCase:
im.load()
im.getdata()
try:
assert im._compression == "group4"
except AttributeError:
print("No _compression")
print(dir(im))
assert im._compression == "group4"
# can we write it back out, in a different form.
out = str(tmp_path / "temp.png")
@ -313,7 +309,7 @@ class TestFileLibTiff(LibTiffTestCase):
}
def check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str],
) -> None:
im = hopper()
@ -1107,13 +1103,15 @@ class TestFileLibTiff(LibTiffTestCase):
)
def test_buffering(self, test_file: str) -> None:
# load exif first
with Image.open(open(test_file, "rb", buffering=1048576)) as im:
exif = dict(im.getexif())
with open(test_file, "rb", buffering=1048576) as f:
with Image.open(f) as im:
exif = dict(im.getexif())
# load image before exif
with Image.open(open(test_file, "rb", buffering=1048576)) as im2:
im2.load()
exif_after_load = dict(im2.getexif())
with open(test_file, "rb", buffering=1048576) as f:
with Image.open(f) as im2:
im2.load()
exif_after_load = dict(im2.getexif())
assert exif == exif_after_load
@ -1142,11 +1140,9 @@ class TestFileLibTiff(LibTiffTestCase):
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e:
im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "-9"
with pytest.raises(OSError, match="decoder error -9"):
im.load()
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
@ -1160,23 +1156,22 @@ class TestFileLibTiff(LibTiffTestCase):
assert len(im.tag_v2[STRIPOFFSETS]) > 1
@pytest.mark.parametrize("argument", (True, False))
def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None:
def test_save_single_strip(
self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif")
if not argument:
TiffImagePlugin.STRIP_SIZE = 2**18
try:
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, "TIFF", **arguments)
monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18)
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, "TIFF", **arguments)
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[STRIPOFFSETS]) == 1
finally:
TiffImagePlugin.STRIP_SIZE = 65536
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[STRIPOFFSETS]) == 1
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
def test_save_zero(self, compression: str | None, tmp_path: Path) -> None:

View File

@ -29,21 +29,26 @@ def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
@pytest.mark.parametrize("test_file", test_files)
def test_sanity(test_file: str) -> None:
with Image.open(test_file) as im:
def check(im: ImageFile.ImageFile) -> None:
im.load()
assert im.mode == "RGB"
assert im.size == (640, 480)
assert im.format == "MPO"
with Image.open(test_file) as im:
check(im)
with MpoImagePlugin.MpoImageFile(test_file) as im:
check(im)
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(test_files[0])
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:
@ -77,8 +82,8 @@ def test_app(test_file: str) -> None:
with Image.open(test_file) as im:
assert im.applist[0][0] == "APP1"
assert im.applist[1][0] == "APP2"
assert (
im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
assert im.applist[1][1].startswith(
b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
)
assert len(im.applist) == 2

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import io
from pathlib import Path
import pytest
@ -36,6 +37,28 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f)
def test_bad_image_size() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()
data = data[:4] + b"\xff\xff" + data[6:]
b = io.BytesIO(data)
with pytest.raises(SyntaxError, match="bad PCX image size"):
with PcxImagePlugin.PcxImageFile(b):
pass
def test_unknown_mode() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()
data = data[:3] + b"\xff" + data[4:]
b = io.BytesIO(data)
with pytest.raises(OSError, match="unknown PCX mode"):
with Image.open(b):
pass
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"

View File

@ -306,7 +306,7 @@ def test_pdf_append(tmp_path: Path) -> None:
# append some info
pdf.info.Title = "abc"
pdf.info.Author = "def"
pdf.info.Subject = "ghi\uABCD"
pdf.info.Subject = "ghi\uabcd"
pdf.info.Keywords = "qw)e\\r(ty"
pdf.info.Creator = "hopper()"
pdf.start_writing()
@ -334,7 +334,7 @@ def test_pdf_append(tmp_path: Path) -> None:
assert pdf.info.Title == "abc"
assert pdf.info.Producer == "PdfParser"
assert pdf.info.Keywords == "qw)e\\r(ty"
assert pdf.info.Subject == "ghi\uABCD"
assert pdf.info.Subject == "ghi\uabcd"
assert b"CreationDate" in pdf.info
assert b"ModDate" in pdf.info
check_pdf_pages_consistency(pdf)

View File

@ -363,7 +363,7 @@ class TestFilePng:
with pytest.raises((OSError, SyntaxError)):
im.verify()
def test_verify_ignores_crc_error(self) -> None:
def test_verify_ignores_crc_error(self, monkeypatch: pytest.MonkeyPatch) -> None:
# check ignores crc errors in ancillary chunks
chunk_data = chunk(b"tEXt", b"spam")
@ -373,24 +373,20 @@ class TestFilePng:
with pytest.raises(SyntaxError):
PngImagePlugin.PngImageFile(BytesIO(image_data))
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
im = load(image_data)
assert im is not None
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im = load(image_data)
assert im is not None
def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None:
def test_verify_not_ignores_crc_error_in_required_chunk(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# check does not ignore crc errors in required chunks
image_data = MAGIC + IHDR[:-1] + b"q" + TAIL
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
with pytest.raises(SyntaxError):
PngImagePlugin.PngImageFile(BytesIO(image_data))
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with pytest.raises(SyntaxError):
PngImagePlugin.PngImageFile(BytesIO(image_data))
def test_roundtrip_dpi(self) -> None:
# Check dpi roundtripping
@ -600,7 +596,7 @@ class TestFilePng:
(b"prIV", b"VALUE3", True),
]
def test_textual_chunks_after_idat(self) -> None:
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/hopper.png") as im:
assert "comment" in im.text
for k, v in {
@ -614,18 +610,17 @@ class TestFilePng:
with pytest.raises(OSError):
assert isinstance(im.text, dict)
# Raises an EOFError in load_end
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
# Raises a UnicodeDecodeError in load_end
with Image.open("Tests/images/truncated_image.png") as im:
# The file is truncated
with pytest.raises(OSError):
im.text()
ImageFile.LOAD_TRUNCATED_IMAGES = True
im.text
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
assert isinstance(im.text, dict)
ImageFile.LOAD_TRUNCATED_IMAGES = False
# Raises an EOFError in load_end
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
def test_unknown_compression_method(self) -> None:
with pytest.raises(SyntaxError, match="Unknown compression method"):
@ -651,15 +646,16 @@ class TestFilePng:
@pytest.mark.parametrize(
"cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT")
)
def test_truncated_chunks(self, cid: bytes) -> None:
def test_truncated_chunks(
self, cid: bytes, monkeypatch: pytest.MonkeyPatch
) -> None:
fp = BytesIO()
with PngImagePlugin.PngStream(fp) as png:
with pytest.raises(ValueError):
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.parametrize("save_all", (True, False))
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
@ -772,38 +768,31 @@ class TestFilePng:
im.seek(1)
@pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG")
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
def test_truncated_end_chunk(self) -> None:
def test_truncated_end_chunk(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/truncated_end_chunk.png") as im:
with pytest.raises(OSError):
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
with Image.open("Tests/images/truncated_end_chunk.png") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.png")
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open("Tests/images/truncated_end_chunk.png") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.png")
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@ -812,11 +801,11 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
mem_limit = 2 * 1024 # max increase in K
iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs
def test_leak_load(self) -> None:
def test_leak_load(self, monkeypatch: pytest.MonkeyPatch) -> None:
with open("Tests/images/hopper.png", "rb") as f:
DATA = BytesIO(f.read(16 * 1024))
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open(DATA) as im:
im.load()
@ -824,7 +813,4 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
with Image.open(DATA) as im:
im.load()
try:
self._test_leak(core)
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
self._test_leak(core)

View File

@ -49,7 +49,7 @@ def test_sanity() -> None:
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
# P6 with maxval < 255
(
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11",
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0a\x0f\x10\x11",
"RGB",
(
(0, 15, 30),
@ -60,7 +60,7 @@ def test_sanity() -> None:
# P6 with maxval > 255
(
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xff\xff",
"RGB",
(
(0, 1, 2),
@ -79,6 +79,7 @@ def test_arbitrary_maxval(
assert im.mode == mode
px = im.load()
assert px is not None
assert tuple(px[x, 0] for x in range(3)) == pixels
@ -292,12 +293,10 @@ def test_header_token_too_long(tmp_path: Path) -> None:
with open(path, "wb") as f:
f.write(b"P6\n 01234567890")
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Token too long in file header: 01234567890"):
with Image.open(path):
pass
assert str(e.value) == "Token too long in file header: 01234567890"
def test_truncated_file(tmp_path: Path) -> None:
# Test EOF in header
@ -305,12 +304,10 @@ def test_truncated_file(tmp_path: Path) -> None:
with open(path, "wb") as f:
f.write(b"P6")
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Reached EOF while reading header"):
with Image.open(path):
pass
assert str(e.value) == "Reached EOF while reading header"
# Test EOF for PyDecoder
fp = BytesIO(b"P5 3 1 4")
with Image.open(fp) as im:
@ -334,12 +331,12 @@ def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:
with open(path, "wb") as f:
f.write(b"P6\n3 1 " + maxval)
with pytest.raises(ValueError) as e:
with pytest.raises(
ValueError, match="maxval must be greater than 0 and less than 65536"
):
with Image.open(path):
pass
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
def test_neg_ppm() -> None:
# Storage.c accepted negative values for xsize, ysize. the
@ -367,22 +364,18 @@ def test_mimetypes(tmp_path: Path) -> None:
@pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout
def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM")
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:

View File

@ -25,12 +25,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(test_file)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:

View File

@ -7,7 +7,7 @@ from pathlib import Path
import pytest
from PIL import Image, ImageSequence, SpiderImagePlugin
from PIL import Image, SpiderImagePlugin
from .helper import assert_image_equal, hopper, is_pypy
@ -24,12 +24,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(TEST_FILE)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:
@ -153,8 +153,8 @@ def test_nonstack_file() -> None:
def test_nonstack_dos() -> None:
with Image.open(TEST_FILE) as im:
for i, frame in enumerate(ImageSequence.Iterator(im)):
assert i <= 1, "Non-stack DOS file test failed"
with pytest.raises(EOFError):
im.seek(0)
# for issue #4093

View File

@ -1,10 +1,11 @@
from __future__ import annotations
import io
import os
import pytest
from PIL import Image, SunImagePlugin
from PIL import Image, SunImagePlugin, _binary
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -33,6 +34,60 @@ def test_im1() -> None:
assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png")
def _sun_header(
depth: int = 0, file_type: int = 0, palette_length: int = 0
) -> io.BytesIO:
return io.BytesIO(
_binary.o32be(0x59A66A95)
+ b"\x00" * 8
+ _binary.o32be(depth)
+ b"\x00" * 4
+ _binary.o32be(file_type)
+ b"\x00" * 4
+ _binary.o32be(palette_length)
)
def test_unsupported_mode_bit_depth() -> None:
with pytest.raises(SyntaxError, match="Unsupported Mode/Bit Depth"):
with SunImagePlugin.SunImageFile(_sun_header()):
pass
def test_unsupported_color_palette_length() -> None:
with pytest.raises(SyntaxError, match="Unsupported Color Palette Length"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1025)):
pass
def test_unsupported_palette_type() -> None:
with pytest.raises(SyntaxError, match="Unsupported Palette Type"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1)):
pass
def test_unsupported_file_type() -> None:
with pytest.raises(SyntaxError, match="Unsupported Sun Raster file type"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, file_type=6)):
pass
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_rgbx() -> None:
with open(os.path.join(EXTRA_DIR, "32bpp.ras"), "rb") as fp:
data = fp.read()
# Set file type to 3
data = data[:20] + _binary.o32be(3) + data[24:]
with Image.open(io.BytesIO(data)) as im:
r, g, b = im.split()
im = Image.merge("RGB", (b, g, r))
assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import warnings
from pathlib import Path
import pytest
@ -29,6 +30,22 @@ def test_sanity(codec: str, test_path: str, format: str) -> None:
assert im.format == format
def test_unexpected_end(tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.tar")
with open(tmpfile, "w"):
pass
with pytest.raises(OSError, match="unexpected end of tar file"):
with TarIO.TarIO(tmpfile, "test"):
pass
def test_cannot_find_subfile() -> None:
with pytest.raises(OSError, match="cannot find subfile"):
with TarIO.TarIO(TEST_TAR_FILE, "test"):
pass
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
with pytest.warns(ResourceWarning):

View File

@ -72,6 +72,7 @@ def test_palette_depth_8(tmp_path: Path) -> None:
def test_palette_depth_16(tmp_path: Path) -> None:
with Image.open("Tests/images/p_16.tga") as im:
assert im.palette is not None
assert im.palette.mode == "RGBA"
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
@ -213,10 +214,14 @@ def test_save_orientation(tmp_path: Path) -> None:
def test_horizontal_orientations() -> None:
# These images have been manually hexedited to have the relevant orientations
with Image.open("Tests/images/rgb32rle_top_right.tga") as im:
assert im.load()[90, 90][:3] == (0, 0, 0)
px = im.load()
assert px is not None
assert px[90, 90][:3] == (0, 0, 0)
with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im:
assert im.load()[90, 90][:3] == (0, 255, 0)
px = im.load()
assert px is not None
assert px[90, 90][:3] == (0, 255, 0)
def test_save_rle(tmp_path: Path) -> None:

View File

@ -63,12 +63,12 @@ class TestFileTiff:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(self) -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open("Tests/images/multipage.tiff")
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file(self) -> None:
with warnings.catch_warnings():
@ -115,15 +115,27 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_bigtiff_save(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
im = hopper()
im.save(outfile, big_tiff=True)
with Image.open(outfile) as reloaded:
assert reloaded.tag_v2._bigtiff is True
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
with Image.open(outfile) as reloaded:
assert reloaded.tag_v2._bigtiff is True
def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif")
def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="Not allowing setting of legacy api"):
ifd.legacy_api = False
assert str(e.value) == "Not allowing setting of legacy api"
def test_xyres_tiff(self) -> None:
filename = "Tests/images/pil168.tif"
@ -648,6 +660,18 @@ class TestFileTiff:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[278] == 256
im = hopper()
im2 = Image.new("L", (128, 128))
im2.encoderinfo = {"tiffinfo": {278: 256}}
im.save(outfile, save_all=True, append_images=[im2])
with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[278] == 128
im.seek(1)
assert im.tag_v2[278] == 256
def test_strip_raw(self) -> None:
infile = "Tests/images/tiff_strip_raw.tif"
with Image.open(infile) as im:
@ -797,6 +821,39 @@ class TestFileTiff:
with pytest.raises(RuntimeError):
a.fixOffsets(1)
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**16
b.seek(0)
a.fixOffsets(1, isShort=True)
b = BytesIO(b"II\x2b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**32
b.seek(0)
a.fixOffsets(1, isShort=True)
b.seek(0)
a.fixOffsets(1, isLong=True)
def test_appending_tiff_writer_writelong(self) -> None:
data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.seek(-4, os.SEEK_CUR)
a.writeLong(2**32 - 1)
assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff"
def test_appending_tiff_writer_rewritelastshorttolong(self) -> None:
data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.seek(-2, os.SEEK_CUR)
a.rewriteLastShortToLong(2**32 - 1)
assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff"
def test_saving_icc_profile(self, tmp_path: Path) -> None:
# Tests saving TIFF with icc_profile set.
# At the time of writing this will only work for non-compressed tiffs
@ -946,11 +1003,10 @@ class TestFileTiff:
@pytest.mark.timeout(6)
@pytest.mark.filterwarnings("ignore:Truncated File Read")
def test_timeout(self) -> None:
def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/timeout-6646305047838720") as im:
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.parametrize(
"test_file",

View File

@ -21,7 +21,11 @@ def test_open() -> None:
def test_load() -> None:
with WalImageFile.open(TEST_FILE) as im:
assert im.load()[0, 0] == 122
px = im.load()
assert px is not None
assert px[0, 0] == 122
# Test again now that it has already been loaded once
assert im.load()[0, 0] == 122
px = im.load()
assert px is not None
assert px[0, 0] == 122

View File

@ -28,9 +28,9 @@ except ImportError:
class TestUnsupportedWebp:
def test_unsupported(self) -> None:
def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
if HAVE_WEBP:
WebPImagePlugin.SUPPORTED = False
monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False)
file_path = "Tests/images/hopper.webp"
with pytest.warns(UserWarning):
@ -38,9 +38,6 @@ class TestUnsupportedWebp:
with Image.open(file_path):
pass
if HAVE_WEBP:
WebPImagePlugin.SUPPORTED = True
@skip_unless_feature("webp")
class TestFileWebp:
@ -208,9 +205,8 @@ class TestFileWebp:
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
im = Image.new("RGB", (15000, 15000))
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="encoding error 6"):
im.save(tmp_path / "temp.webp", method=0)
assert str(e.value) == "encoding error 6"
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
@ -285,7 +281,7 @@ class TestFileWebp:
with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1))
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3))
assert difference < 5
def test_duration(self, tmp_path: Path) -> None:

View File

@ -40,7 +40,7 @@ def test_read_exif_metadata() -> None:
def test_read_exif_metadata_without_prefix() -> None:
with Image.open("Tests/images/flower2.webp") as im:
# Assert prefix is not present
assert im.info["exif"][:6] != b"Exif\x00\x00"
assert not im.info["exif"].startswith(b"Exif\x00\x00")
exif = im.getexif()
assert exif[305] == "Adobe Photoshop CS6 (Macintosh)"

View File

@ -32,7 +32,9 @@ def test_load_raw() -> None:
def test_load() -> None:
with Image.open("Tests/images/drawing.emf") as im:
if hasattr(Image.core, "drawwmf"):
assert im.load()[0, 0] == (255, 255, 255)
px = im.load()
assert px is not None
assert px[0, 0] == (255, 255, 255)
def test_load_zero_inch() -> None:
@ -71,7 +73,7 @@ def test_load_float_dpi() -> None:
with open("Tests/images/drawing.emf", "rb") as fp:
data = fp.read()
b = BytesIO(data[:8] + b"\x06\xFA" + data[10:])
b = BytesIO(data[:8] + b"\x06\xfa" + data[10:])
with Image.open(b) as im:
assert im.info["dpi"][0] == 2540

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import io
import pytest
from PIL import BdfFontFile, FontFile
@ -8,13 +10,20 @@ filename = "Tests/images/courB08.bdf"
def test_sanity() -> None:
with open(filename, "rb") as test_file:
font = BdfFontFile.BdfFontFile(test_file)
with open(filename, "rb") as fp:
font = BdfFontFile.BdfFontFile(fp)
assert isinstance(font, FontFile.FontFile)
assert len([_f for _f in font.glyph if _f]) == 190
def test_zero_width_chars() -> None:
with open(filename, "rb") as fp:
data = fp.read()
data = data[:2650] + b"\x00\x00" + data[2652:]
BdfFontFile.BdfFontFile(io.BytesIO(data))
def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):

View File

@ -4,7 +4,20 @@ from pathlib import Path
import pytest
from PIL import FontFile
from PIL import FontFile, Image
def test_compile() -> None:
font = FontFile.FontFile()
font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0)))
font.compile()
assert font.ysize == 1
font.ysize = 2
font.compile()
# Assert that compiling again did not change anything
assert font.ysize == 2
def test_save(tmp_path: Path) -> None:

View File

@ -22,28 +22,26 @@ def test_sanity() -> None:
Image.new("HSV", (100, 100))
def wedge() -> Image.Image:
w = Image._wedge()
w90 = w.rotate(90)
def linear_gradient() -> Image.Image:
im = Image.linear_gradient(mode="L")
im90 = im.rotate(90)
(px, h) = w.size
(px, h) = im.size
r = Image.new("L", (px * 3, h))
g = r.copy()
b = r.copy()
r.paste(w, (0, 0))
r.paste(w90, (px, 0))
r.paste(im, (0, 0))
r.paste(im90, (px, 0))
g.paste(w90, (0, 0))
g.paste(w, (2 * px, 0))
g.paste(im90, (0, 0))
g.paste(im, (2 * px, 0))
b.paste(w, (px, 0))
b.paste(w90, (2 * px, 0))
b.paste(im, (px, 0))
b.paste(im90, (2 * px, 0))
img = Image.merge("RGB", (r, g, b))
return img
return Image.merge("RGB", (r, g, b))
def to_xxx_colorsys(
@ -79,8 +77,8 @@ def to_rgb_colorsys(im: Image.Image) -> Image.Image:
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
def test_wedge() -> None:
src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR)
def test_linear_gradient() -> None:
src = linear_gradient().resize((3 * 32, 32), Image.Resampling.BILINEAR)
im = src.convert("HSV")
comparable = to_hsv_colorsys(src)

View File

@ -65,21 +65,20 @@ class TestImage:
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None:
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="unrecognized image mode"):
Image.new(mode, (1, 1))
assert str(e.value) == "unrecognized image mode"
def test_exception_inheritance(self) -> None:
assert issubclass(UnidentifiedImageError, OSError)
def test_sanity(self) -> None:
im = Image.new("L", (100, 100))
assert repr(im)[:45] == "<PIL.Image.Image image mode=L size=100x100 at"
assert repr(im).startswith("<PIL.Image.Image image mode=L size=100x100 at")
assert im.mode == "L"
assert im.size == (100, 100)
im = Image.new("RGB", (100, 100))
assert repr(im)[:45] == "<PIL.Image.Image image mode=RGB size=100x100 "
assert repr(im).startswith("<PIL.Image.Image image mode=RGB size=100x100 ")
assert im.mode == "RGB"
assert im.size == (100, 100)
@ -189,8 +188,6 @@ class TestImage:
if ext == ".jp2" and not features.check_codec("jpg_2000"):
pytest.skip("jpg_2000 not available")
temp_file = str(tmp_path / ("temp." + ext))
if os.path.exists(temp_file):
os.remove(temp_file)
im.save(Path(temp_file))
def test_fp_name(self, tmp_path: Path) -> None:
@ -580,9 +577,7 @@ class TestImage:
def test_one_item_tuple(self) -> None:
for mode in ("I", "F", "L"):
im = Image.new(mode, (100, 100), (5,))
px = im.load()
assert px is not None
assert px[0, 0] == 5
assert im.getpixel((0, 0)) == 5
def test_linear_gradient_wrong_mode(self) -> None:
# Arrange
@ -662,12 +657,13 @@ class TestImage:
im.putpalette(list(range(256)) * 4, "RGBA")
im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im, im_remapped)
assert im.palette is not None
assert im.palette.palette == im_remapped.palette.palette
# Test illegal image mode
with hopper() as im:
with pytest.raises(ValueError):
im.remap_palette(None)
im.remap_palette([])
def test_remap_palette_transparency(self) -> None:
im = Image.new("P", (1, 2), (0, 0, 0))
@ -770,7 +766,7 @@ class TestImage:
assert dict(exif)
# Test that exif data is cleared after another load
exif.load(None)
exif.load(b"")
assert not dict(exif)
# Test loading just the EXIF header
@ -793,6 +789,10 @@ class TestImage:
ifd[36864] = b"0220"
assert exif.get_ifd(0x8769) == {36864: b"0220"}
reloaded_exif = Image.Exif()
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"}
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@ -987,6 +987,11 @@ class TestImage:
else:
assert im.getxmp() == {"xmpmeta": None}
def test_get_child_images(self) -> None:
im = Image.new("RGB", (1, 1))
with pytest.warns(DeprecationWarning):
assert im.get_child_images() == []
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size)

View File

@ -271,13 +271,25 @@ class TestImagePutPixelError:
class TestEmbeddable:
@pytest.mark.xfail(reason="failing test")
@pytest.mark.xfail(not (sys.version_info >= (3, 13)), reason="failing test")
@pytest.mark.skipif(not is_win32(), reason="requires Windows")
def test_embeddable(self) -> None:
import ctypes
from setuptools.command import build_ext
compiler = getattr(build_ext, "new_compiler")()
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
"INCLUDEPY"
).replace("include", "libs")
compiler.add_library_dir(libdir)
try:
compiler.initialize()
except Exception:
pytest.skip("Compiler could not be initialized")
with open("embed_pil.c", "w", encoding="utf-8") as fh:
home = sys.prefix.replace("\\", "\\\\")
fh.write(
@ -305,13 +317,6 @@ int main(int argc, char* argv[])
"""
)
compiler = getattr(build_ext, "new_compiler")()
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
"INCLUDEPY"
).replace("include", "libs")
compiler.add_library_dir(libdir)
objects = compiler.compile(["embed_pil.c"])
compiler.link_executable(objects, "embed_pil")

View File

@ -222,9 +222,7 @@ def test_l_macro_rounding(convert_mode: str) -> None:
im.palette.getcolor((0, 1, 2))
converted_im = im.convert(convert_mode)
px = converted_im.load()
assert px is not None
converted_color = px[0, 0]
converted_color = converted_im.getpixel((0, 0))
if convert_mode == "LA":
assert isinstance(converted_color, tuple)
converted_color = converted_color[0]
@ -236,6 +234,7 @@ def test_gif_with_rgba_palette_to_p() -> None:
with Image.open("Tests/images/hopper.gif") as im:
im.info["transparency"] = 255
im.load()
assert im.palette is not None
assert im.palette.mode == "RGB"
im_p = im.convert("P")

View File

@ -148,10 +148,8 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color)
converted = im.quantize(method=method)
converted_px = converted.load()
assert converted_px is not None
assert converted.palette is not None
assert converted_px[0, 0] == converted.palette.colors[color]
assert converted.getpixel((0, 0)) == converted.palette.colors[color]
def test_small_palette() -> None:

View File

@ -309,7 +309,7 @@ class TestImageResize:
# Test unknown resampling filter
with hopper() as im:
with pytest.raises(ValueError):
im.resize((10, 10), "unknown")
im.resize((10, 10), -1)
@skip_unless_feature("libtiff")
def test_transposed(self) -> None:

View File

@ -47,6 +47,7 @@ class TestImageTransform:
transformed = im.transform(
im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0]
)
assert im.palette is not None
assert im.palette.palette == transformed.palette.palette
def test_extent(self) -> None:

View File

@ -39,6 +39,8 @@ BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X
POINTS = (
((10, 10), (20, 40), (30, 30)),
[(10, 10), (20, 40), (30, 30)],
([10, 10], [20, 40], [30, 30]),
[[10, 10], [20, 40], [30, 30]],
(10, 10, 20, 40, 30, 30),
[10, 10, 20, 40, 30, 30],
)
@ -46,6 +48,8 @@ POINTS = (
KITE_POINTS = (
((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)),
[(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)],
([10, 50], [70, 10], [90, 50], [70, 90], [10, 50]),
[[10, 50], [70, 10], [90, 50], [70, 90], [10, 50]],
)
@ -448,7 +452,6 @@ def test_shape1() -> None:
x3, y3 = 95, 5
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -470,7 +473,6 @@ def test_shape2() -> None:
x3, y3 = 5, 95
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -489,7 +491,6 @@ def test_transform() -> None:
draw = ImageDraw.Draw(im)
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.line(0, 0)
s.transform((0, 0, 0, 0, 0, 0))
@ -812,7 +813,7 @@ def test_rounded_rectangle(
tuple[int, int, int, int]
| tuple[list[int]]
| tuple[tuple[int, int], tuple[int, int]]
)
),
) -> None:
# Arrange
im = Image.new("RGB", (200, 200))
@ -1047,8 +1048,8 @@ def create_base_image_draw(
background2: tuple[int, int, int] = GRAY,
) -> tuple[Image.Image, ImageDraw.ImageDraw]:
img = Image.new(mode, size, background1)
for x in range(0, size[0]):
for y in range(0, size[1]):
for x in range(size[0]):
for y in range(size[1]):
if (x + y) % 2 == 0:
img.putpixel((x, y), background2)
return img, ImageDraw.Draw(img)
@ -1396,6 +1397,28 @@ def test_stroke_descender() -> None:
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76)
@skip_unless_feature("freetype2")
def test_stroke_inside_gap() -> None:
# Arrange
im = Image.new("RGB", (120, 130))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
# Act
draw.text((12, 12), "i", "#f00", font, stroke_width=20)
# Assert
for y in range(im.height):
glyph = ""
for x in range(im.width):
if im.getpixel((x, y)) == (0, 0, 0):
if glyph == "started":
glyph = "ended"
else:
assert glyph != "ended", "Gap inside stroked glyph"
glyph = "started"
@skip_unless_feature("freetype2")
def test_split_word() -> None:
# Arrange
@ -1504,7 +1527,6 @@ def test_same_color_outline(bbox: Coords) -> None:
x2, y2 = 95, 50
x3, y3 = 95, 5
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -1608,7 +1630,7 @@ def test_compute_regular_polygon_vertices(
0,
ValueError,
"bounding_circle should contain 2D coordinates "
"and a radius (e.g. (x, y, r) or ((x, y), r) )",
r"and a radius \(e.g. \(x, y, r\) or \(\(x, y\), r\) \)",
),
(
3,
@ -1622,7 +1644,7 @@ def test_compute_regular_polygon_vertices(
((50, 50, 50), 25),
0,
ValueError,
"bounding_circle centre should contain 2D coordinates (e.g. (x, y))",
r"bounding_circle centre should contain 2D coordinates \(e.g. \(x, y\)\)",
),
(
3,
@ -1647,9 +1669,8 @@ def test_compute_regular_polygon_vertices_input_error_handling(
expected_error: type[Exception],
error_message: str,
) -> None:
with pytest.raises(expected_error) as e:
with pytest.raises(expected_error, match=error_message):
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
assert str(e.value) == error_message
def test_continuous_horizontal_edges_polygon() -> None:

View File

@ -176,9 +176,8 @@ class TestImageFile:
b"0" * ImageFile.SAFEBLOCK
) # only SAFEBLOCK bytes, so that the header is truncated
)
with pytest.raises(OSError) as e:
with pytest.raises(OSError, match="Truncated File Read"):
BmpImagePlugin.BmpImageFile(b)
assert str(e.value) == "Truncated File Read"
@skip_unless_feature("zlib")
def test_truncated_with_errors(self) -> None:
@ -191,13 +190,10 @@ class TestImageFile:
im.load()
@skip_unless_feature("zlib")
def test_truncated_without_errors(self) -> None:
def test_truncated_without_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/truncated_image.png") as im:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
im.load()
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im.load()
@skip_unless_feature("zlib")
def test_broken_datastream_with_errors(self) -> None:
@ -206,13 +202,12 @@ class TestImageFile:
im.load()
@skip_unless_feature("zlib")
def test_broken_datastream_without_errors(self) -> None:
def test_broken_datastream_without_errors(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
with Image.open("Tests/images/broken_data_stream.png") as im:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
im.load()
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im.load()
class MockPyDecoder(ImageFile.PyDecoder):

View File

@ -254,7 +254,8 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
@pytest.mark.parametrize(
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
"align, ext",
(("left", ""), ("center", "_center"), ("right", "_right"), ("justify", "_justify")),
)
def test_render_multiline_text_align(
font: ImageFont.FreeTypeFont, align: str, ext: str
@ -461,6 +462,20 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
assert mask.size == (108, 13)
def test_stroke_mask() -> None:
# Arrange
text = "i"
# Act
font = ImageFont.truetype(FONT_PATH, 128)
mask = font.getmask(text, stroke_width=2)
# Assert
assert mask.getpixel((34, 5)) == 255
assert mask.getpixel((38, 5)) == 0
assert mask.getpixel((42, 5)) == 255
def test_load_when_image_not_found() -> None:
with tempfile.NamedTemporaryFile(delete=False) as tmp:
pass
@ -543,7 +558,7 @@ def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
# issue #3777
text = "A\u278A\U0001F12B"
text = "A\u278a\U0001f12b"
target = "Tests/images/unicode_extended.png"
ttf = ImageFont.truetype(
@ -1012,7 +1027,7 @@ def test_sbix(layout_engine: ImageFont.Layout) -> None:
im = Image.new("RGB", (400, 400), "white")
d = ImageDraw.Draw(im)
d.text((50, 50), "\uE901", font=font, embedded_color=True)
d.text((50, 50), "\ue901", font=font, embedded_color=True)
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1)
except OSError as e: # pragma: no cover
@ -1029,7 +1044,7 @@ def test_sbix_mask(layout_engine: ImageFont.Layout) -> None:
im = Image.new("RGB", (400, 400), "white")
d = ImageDraw.Draw(im)
d.text((50, 50), "\uE901", (100, 0, 0), font=font)
d.text((50, 50), "\ue901", (100, 0, 0), font=font)
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1)
except OSError as e: # pragma: no cover

View File

@ -229,7 +229,7 @@ def test_getlength(
@pytest.mark.parametrize("direction", ("ltr", "ttb"))
@pytest.mark.parametrize(
"text",
("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"),
("i" + ("\u030c" * 15) + "i", "i" + "\u032c" * 15 + "i", "\u035cii", "i\u0305i"),
ids=("caron-above", "caron-below", "double-breve", "overline"),
)
def test_getlength_combine(mode: str, direction: str, text: str) -> None:
@ -272,27 +272,27 @@ def test_anchor_ttb(anchor: str) -> None:
combine_tests = (
# extends above (e.g. issue #4553)
("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08),
("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08),
("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08),
("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08),
("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3),
("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3),
("caron", "a\u030c\u030c\u030c\u030c\u030cb", None, None, 0.08),
("caron_la", "a\u030c\u030c\u030c\u030c\u030cb", "la", None, 0.08),
("caron_lt", "a\u030c\u030c\u030c\u030c\u030cb", "lt", None, 0.08),
("caron_ls", "a\u030c\u030c\u030c\u030c\u030cb", "ls", None, 0.08),
("caron_ttb", "ca" + ("\u030c" * 15) + "b", None, "ttb", 0.3),
("caron_ttb_lt", "ca" + ("\u030c" * 15) + "b", "lt", "ttb", 0.3),
# extends below
("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02),
("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02),
("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02),
("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02),
("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03),
("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03),
("caron_below", "a\u032c\u032c\u032c\u032c\u032cb", None, None, 0.02),
("caron_below_ld", "a\u032c\u032c\u032c\u032c\u032cb", "ld", None, 0.02),
("caron_below_lb", "a\u032c\u032c\u032c\u032c\u032cb", "lb", None, 0.02),
("caron_below_ls", "a\u032c\u032c\u032c\u032c\u032cb", "ls", None, 0.02),
("caron_below_ttb", "a" + ("\u032c" * 15) + "b", None, "ttb", 0.03),
("caron_below_ttb_lb", "a" + ("\u032c" * 15) + "b", "lb", "ttb", 0.03),
# extends to the right (e.g. issue #3745)
("double_breve_below", "a\u035Ci", None, None, 0.02),
("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02),
("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02),
("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02),
("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02),
("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02),
("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02),
("double_breve_below", "a\u035ci", None, None, 0.02),
("double_breve_below_ma", "a\u035ci", "ma", None, 0.02),
("double_breve_below_ra", "a\u035ci", "ra", None, 0.02),
("double_breve_below_ttb", "a\u035cb", None, "ttb", 0.02),
("double_breve_below_ttb_rt", "a\u035cb", "rt", "ttb", 0.02),
("double_breve_below_ttb_mt", "a\u035cb", "mt", "ttb", 0.02),
("double_breve_below_ttb_st", "a\u035cb", "st", "ttb", 0.02),
# extends to the left (fail=0.064)
("overline", "i\u0305", None, None, 0.02),
("overline_la", "i\u0305", "la", None, 0.02),
@ -346,7 +346,7 @@ def test_combine_multiline(anchor: str, align: str) -> None:
path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word
text = "i\u0305\u035c\ntext" # i with overline and double breve, and a word
im = Image.new("RGB", (400, 400), "white")
d = ImageDraw.Draw(im)

View File

@ -80,15 +80,12 @@ def test_lut(op: str) -> None:
def test_no_operator_loaded() -> None:
im = Image.new("L", (1, 1))
mop = ImageMorph.MorphOp()
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="No operator loaded"):
mop.apply(im)
assert str(e.value) == "No operator loaded"
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="No operator loaded"):
mop.match(im)
assert str(e.value) == "No operator loaded"
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="No operator loaded"):
mop.save_lut("")
assert str(e.value) == "No operator loaded"
# Test the named patterns
@ -238,15 +235,12 @@ def test_incorrect_mode() -> None:
im = hopper("RGB")
mop = ImageMorph.MorphOp(op_name="erosion8")
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Image mode must be L"):
mop.apply(im)
assert str(e.value) == "Image mode must be L"
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Image mode must be L"):
mop.match(im)
assert str(e.value) == "Image mode must be L"
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Image mode must be L"):
mop.get_on_pixels(im)
assert str(e.value) == "Image mode must be L"
def test_add_patterns() -> None:
@ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None:
lb.add_patterns(new_patterns)
# Act / Assert
with pytest.raises(Exception) as e:
with pytest.raises(
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
):
lb.build_lut()
assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"'
def test_load_invalid_mrl() -> None:
@ -290,9 +285,8 @@ def test_load_invalid_mrl() -> None:
mop = ImageMorph.MorphOp()
# Act / Assert
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="Wrong size operator file!"):
mop.load_lut(invalid_mrl)
assert str(e.value) == "Wrong size operator file!"
def test_roundtrip_mrl(tmp_path: Path) -> None:

View File

@ -165,14 +165,10 @@ def test_pad() -> None:
def test_pad_round() -> None:
im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 1))
px = new_im.load()
assert px is not None
assert px[2, 0] == 1
assert new_im.getpixel((2, 0)) == 1
new_im = ImageOps.pad(im, (1, 4))
px = new_im.load()
assert px is not None
assert px[0, 2] == 1
assert new_im.getpixel((0, 2)) == 1
@pytest.mark.parametrize("mode", ("P", "PA"))
@ -405,7 +401,6 @@ def test_exif_transpose() -> None:
else:
original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert_image_similar(base_im, transposed_im, 17)
if orientation_im is base_im:
assert "exif" not in im.info
@ -417,7 +412,6 @@ def test_exif_transpose() -> None:
# Repeat the operation to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im)
assert transposed_im2 is not None
assert_image_equal(transposed_im2, transposed_im)
check(base_im)
@ -433,7 +427,6 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
transposed_im._reload_exif()
@ -446,17 +439,24 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
# Orientation set directly on Image.Exif
im = hopper()
im.getexif()[0x0112] = 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_with_xmp_tuple() -> None:
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
assert im.getexif()[0x0112] == 3
im.info["xmp"] = (b"test",)
transposed_im = ImageOps.exif_transpose(im)
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_xml_without_xmp() -> None:
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
assert im.getexif()[0x0112] == 3
@ -464,7 +464,6 @@ def test_exif_transpose_xml_without_xmp() -> None:
del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()

View File

@ -17,6 +17,7 @@ def test_sanity() -> None:
def test_reload() -> None:
with Image.open("Tests/images/hopper.gif") as im:
original = im.copy()
assert im.palette is not None
im.palette.dirty = 1
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
@ -111,7 +112,7 @@ def test_make_linear_lut() -> None:
assert isinstance(lut, list)
assert len(lut) == 256
# Check values
for i in range(0, len(lut)):
for i in range(len(lut)):
assert lut[i] == i
@ -189,7 +190,7 @@ def test_2bit_palette(tmp_path: Path) -> None:
rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2
img = Image.frombytes("P", (6, 1), rgb)
img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB
img.putpalette(b"\xff\x00\x00\x00\xff\x00\x00\x00\xff") # RGB
img.save(outfile, format="PNG")
assert_image_equal_tofile(img, outfile)

View File

@ -68,25 +68,10 @@ def test_path_constructors(
assert list(p) == [(0.0, 1.0)]
@pytest.mark.parametrize(
"coords",
(
("a", "b"),
([0, 1],),
[[0, 1]],
([0.0, 1.0],),
[[0.0, 1.0]],
),
)
def test_invalid_path_constructors(
coords: tuple[str, str] | Sequence[Sequence[int]]
) -> None:
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
# Assert
assert str(e.value) == "incorrect coordinate type"
def test_invalid_path_constructors() -> None:
# Arrange / Act
with pytest.raises(ValueError, match="incorrect coordinate type"):
ImagePath.Path(("a", "b"))
@pytest.mark.parametrize(
@ -99,13 +84,9 @@ def test_invalid_path_constructors(
),
)
def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None:
# Act
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="wrong number of coordinates"):
ImagePath.Path(coords)
# Assert
assert str(e.value) == "wrong number of coordinates"
@pytest.mark.parametrize(
"coords, expected",

View File

@ -32,7 +32,7 @@ def test_sanity(tmp_path: Path) -> None:
def test_iterator() -> None:
with Image.open("Tests/images/multipage.tiff") as im:
i = ImageSequence.Iterator(im)
for index in range(0, im.n_frames):
for index in range(im.n_frames):
assert i[index] == next(i)
with pytest.raises(IndexError):
i[index + 1]

View File

@ -7,36 +7,30 @@ import pytest
from PIL import Image
def test_overflow() -> None:
def test_overflow(monkeypatch: pytest.MonkeyPatch) -> None:
# There is the potential to overflow comparisons in map.c
# if there are > SIZE_MAX bytes in the image or if
# the file encodes an offset that makes
# (offset + size(bytes)) > SIZE_MAX
# Note that this image triggers the decompression bomb warning:
max_pixels = Image.MAX_IMAGE_PIXELS
Image.MAX_IMAGE_PIXELS = None
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None)
# This image hits the offset test.
with Image.open("Tests/images/l2rgb_read.bmp") as im:
with pytest.raises((ValueError, MemoryError, OSError)):
im.load()
Image.MAX_IMAGE_PIXELS = max_pixels
def test_tobytes() -> None:
def test_tobytes(monkeypatch: pytest.MonkeyPatch) -> None:
# Note that this image triggers the decompression bomb warning:
max_pixels = Image.MAX_IMAGE_PIXELS
Image.MAX_IMAGE_PIXELS = None
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None)
# Previously raised an access violation on Windows
with Image.open("Tests/images/l2rgb_read.bmp") as im:
with pytest.raises((ValueError, MemoryError, OSError)):
im.tobytes()
Image.MAX_IMAGE_PIXELS = max_pixels
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_ysize() -> None:

View File

@ -141,9 +141,7 @@ def test_save_tiff_uint16() -> None:
a.shape = TEST_IMAGE_SIZE
img = Image.fromarray(a)
img_px = img.load()
assert img_px is not None
assert img_px[0, 0] == pixel_value
assert img.getpixel((0, 0)) == pixel_value
@pytest.mark.parametrize(

View File

@ -20,10 +20,10 @@ from PIL.PdfParser import (
def test_text_encode_decode() -> None:
assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c"
assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc"
assert encode_text("abc") == b"\xfe\xff\x00a\x00b\x00c"
assert decode_text(b"\xfe\xff\x00a\x00b\x00c") == "abc"
assert decode_text(b"abc") == "abc"
assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD"
assert decode_text(b"\x1b a \x1c") == "\u02d9 a \u02dd"
def test_indirect_refs() -> None:
@ -45,8 +45,8 @@ def test_parsing() -> None:
assert PdfParser.get_value(b"false%", 0) == (False, 5)
assert PdfParser.get_value(b"null<", 0) == (None, 4)
assert PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0) == (123, 15)
assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1F\xA3", 8)
assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1F\xA0", 17)
assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1f\xa3", 8)
assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1f\xa0", 17)
assert PdfParser.get_value(b"(asd)", 0) == (b"asd", 5)
assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13)
assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14)
@ -56,9 +56,9 @@ def test_parsing() -> None:
assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12)
assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12)
assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7)
assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6)
assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5)
assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6)
assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2b", 6)
assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2b", 5)
assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2ba", 6)
assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7)
assert PdfParser.get_value(b" 123 (", 0) == (123, 4)
assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0
@ -118,7 +118,7 @@ def test_pdf_repr() -> None:
assert pdf_repr(None) == b"null"
assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)"
assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]"
assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>"
assert pdf_repr(PdfBinary(b"\x90\x1f\xa0")) == b"<901FA0>"
def test_duplicate_xref_entry() -> None:

View File

@ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
("Tests/images/itxt_chunks.png", None),
],
)
@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1))
@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_image(
tmp_path: Path, test_file: str, test_mode: str | None, protocol: int
) -> None:
@ -92,7 +92,7 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
im = im.convert("PA")
# Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
im._mode = "LA"
with open(filename, "wb") as f:
pickle.dump(im, f, protocol)
@ -133,7 +133,7 @@ def helper_assert_pickled_font_images(
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_string(protocol: int) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
@ -147,7 +147,7 @@ def test_pickle_font_string(protocol: int) -> None:
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_file(tmp_path: Path, protocol: int) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)

View File

@ -2,7 +2,7 @@
# install libimagequant
archive_name=libimagequant
archive_version=4.3.3
archive_version=4.3.4
archive=$archive_name-$archive_version

View File

@ -6,12 +6,11 @@ Goals
The fork author's goal is to foster and support active development of PIL through:
- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_
- Continuous integration testing via `GitHub Actions`_
- Publicized development activity on `GitHub`_
- Regular releases to the `Python Package Index`_
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
.. _GitHub: https://github.com/python-pillow/Pillow
.. _Python Package Index: https://pypi.org/project/pillow/

View File

@ -22,7 +22,7 @@ import PIL
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "8.1"
needs_sphinx = "8.2"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -121,7 +121,7 @@ nitpicky = True
# generating warnings in “nitpicky mode”. Note that type should include the domain name
# if present. Example entries would be ('py:func', 'int') or
# ('envvar', 'LD_LIBRARY_PATH').
nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")]
nitpick_ignore = [("py:class", "_CmsProfileCompatible")]
# -- Options for HTML output ----------------------------------------------

View File

@ -175,6 +175,24 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object.
ExifTags.IFD.Makernote
^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.1.0
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
Image.Image.get_child_images()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.2.0
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.
Removed features
----------------

View File

@ -285,7 +285,7 @@ Image.register_decoder("DXT5", DXT5Decoder)
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS "
return prefix.startswith(b"DDS ")
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)

View File

@ -454,7 +454,8 @@ The :py:meth:`~PIL.Image.open` method may set the following
Raw EXIF data from the image.
**comment**
A comment about the image.
A comment about the image, from the COM marker. This is separate from the
UserComment tag that may be stored in the EXIF data.
.. versionadded:: 7.1.0
@ -572,10 +573,19 @@ JPEG 2000
Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel.
Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``,
``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports
JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files
(``.jp2`` or ``.jpx`` files).
.. versionadded:: 8.3.0
Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with
subsampled components.
.. versionadded:: 10.4.0
Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later.
.. versionadded:: 11.1.0
Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later.
Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed
JPEG 2000 files (``.jp2`` or ``.jpx`` files).
When loading, if you set the ``mode`` on the image prior to the
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to
@ -1153,9 +1163,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
**append_images**
A list of images to append as additional frames. Each of the
images in the list can be single or multiframe images. Note however, that for
correct results, all the appended images should have the same
``encoderinfo`` and ``encoderconfig`` properties.
images in the list can be single or multiframe images.
.. versionadded:: 4.2.0
@ -1199,6 +1207,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 8.4.0
**big_tiff**
If true, the image will be saved as a BigTIFF.
.. versionadded:: 11.1.0
**compression**
A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression

View File

@ -54,7 +54,7 @@ true color.
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"SPAM"
return prefix.startswith(b"SPAM")
class SpamImageFile(ImageFile.ImageFile):

View File

@ -33,10 +33,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml
:alt: GitHub Actions build status (Test Cygwin)
.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build
:target: https://ci.appveyor.com/project/python-pillow/Pillow
:alt: AppVeyor CI build status (Windows)
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
:alt: GitHub Actions build status (Wheels)

View File

@ -51,7 +51,7 @@ Many of Pillow's features require external libraries:
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
above uses liblcms2. Tested with **1.19** and **2.7-2.17**.
* **libwebp** provides the WebP format.
@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.3.3**
* Pillow has been tested with libimagequant **2.6-4.3.4**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.

View File

@ -27,6 +27,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 10 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | x86-64 |
@ -42,18 +44,14 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+
| | 3.10 | arm64v8 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x |
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, |
| | | ppc64le, s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2019 | 3.9 | x86-64 |
| Windows Server 2019 | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+
| | 3.13 | x86 |
| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 |
| +----------------------------+---------------------+
@ -75,7 +73,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm |
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
| +----------------------------+------------------+ |
| | 3.8 | 10.4.0 | |
+----------------------------------+----------------------------+------------------+--------------+

View File

@ -387,8 +387,11 @@ Methods
the number of pixels between lines.
:param align: If the text is passed on to
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`,
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
the relative alignment of lines. Use the ``anchor`` parameter to
specify the alignment to ``xy``.
.. versionadded:: 11.2.0 ``"justify"``
:param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm.
@ -455,8 +458,11 @@ Methods
of Pillow, but implemented only in version 8.0.0.
: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``.
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
the relative alignment of lines. Use the ``anchor`` parameter to
specify the alignment to ``xy``.
.. versionadded:: 11.2.0 ``"justify"``
:param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm.
@ -599,8 +605,11 @@ Methods
the number of pixels between lines.
:param align: If the text is passed on to
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
the relative alignment of lines. Use the ``anchor`` parameter to
specify the alignment to ``xy``.
.. versionadded:: 11.2.0 ``"justify"``
:param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm.
@ -650,8 +659,11 @@ Methods
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``.
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
the relative alignment of lines. Use the ``anchor`` parameter to
specify the alignment to ``xy``.
.. versionadded:: 11.2.0 ``"justify"``
:param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm.

View File

@ -54,6 +54,7 @@ Feature version numbers are available only where stated.
Support for the following features can be checked:
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
* ``mozjpeg``: (compile time) Whether Pillow was compiled against the MozJPEG version of libjpeg. Compile-time version number is available.
* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available.
* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.

View File

@ -1,40 +1,38 @@
11.1.0
------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
TODO
^^^^
ExifTags.IFD.Makernote
^^^^^^^^^^^^^^^^^^^^^^
TODO
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
API Changes
===========
TODO
^^^^
Writing XMP bytes to JPEG and MPO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
Pillow 11.0.0 added writing XMP data to JPEG and MPO images::
im.info["xmp"] = b"test"
im.save("out.jpg")
However, this meant that XMP data was automatically kept from an opened image,
which is inconsistent with the rest of Pillow's behaviour. This functionality
has been removed. To write XMP data, the ``xmp`` argument can still be used for
JPEG files::
im.save("out.jpg", xmp=b"test")
To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now
be used::
second_im.encoderinfo = {"xmp": b"test"}
im.save("out.mpo", save_all=True, append_images=[second_im])
API Additions
=============
@ -49,9 +47,32 @@ zlib library, and what version of zlib-ng is being used::
features.check_feature("zlib_ng") # True or False
features.version_feature("zlib_ng") # "2.2.2" for example, or None
Saving TIFF as BigTIFF
^^^^^^^^^^^^^^^^^^^^^^
TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument::
im.save("out.tiff", big_tiff=True)
Other Changes
=============
Reading JPEG 2000 comments
^^^^^^^^^^^^^^^^^^^^^^^^^^
When opening a JPEG 2000 image, the comment may now be read into
:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images.
Saving JPEG 2000 CMYK images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files.
Minimum C version
^^^^^^^^^^^^^^^^^
C99 is now the minimum version of C required to compile Pillow from source.
zlib-ng in wheels
^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,75 @@
11.2.0
------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
Image.Image.get_child_images()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.2.0
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
``"justify"`` multiline text alignment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be
aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`::
from PIL import Image, ImageDraw
im = Image.new("RGB", (50, 25))
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify")
draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify")
Check for MozJPEG
^^^^^^^^^^^^^^^^^
You can check if Pillow has been built against the MozJPEG version of the
libjpeg library, and what version of MozJPEG is being used::
from PIL import features
features.check_feature("mozjpeg") # True or False
features.version_feature("mozjpeg") # "4.1.1" for example, or None
Other Changes
=============
TODO
^^^^
TODO

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
11.2.0
11.1.0
11.0.0
10.4.0

View File

@ -43,7 +43,7 @@ dynamic = [
optional-dependencies.docs = [
"furo",
"olefile",
"sphinx>=8.1",
"sphinx>=8.2",
"sphinx-copybutton",
"sphinx-inline-tabs",
"sphinxext-opengraph",
@ -104,7 +104,6 @@ test-extras = "tests"
[tool.cibuildwheel.macos.environment]
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib"
[tool.black]
exclude = "wheels/multibuild"
@ -122,6 +121,7 @@ lint.select = [
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PT", # flake8-pytest-style
"PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa)
@ -134,6 +134,7 @@ lint.ignore = [
"E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ','
"PIE790", # flake8-pie: unnecessary-placeholder
"PT001", # pytest-fixture-incorrect-parentheses-style
"PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad

View File

@ -26,17 +26,6 @@ from typing import BinaryIO
from . import FontFile, Image
bdf_slant = {
"R": "Roman",
"I": "Italic",
"O": "Oblique",
"RI": "Reverse Italic",
"RO": "Reverse Oblique",
"OT": "Other",
}
bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
def bdf_char(
f: BinaryIO,
@ -54,7 +43,7 @@ def bdf_char(
s = f.readline()
if not s:
return None
if s[:9] == b"STARTCHAR":
if s.startswith(b"STARTCHAR"):
break
id = s[9:].strip().decode("ascii")
@ -62,7 +51,7 @@ def bdf_char(
props = {}
while True:
s = f.readline()
if not s or s[:6] == b"BITMAP":
if not s or s.startswith(b"BITMAP"):
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
@ -71,7 +60,7 @@ def bdf_char(
bitmap = bytearray()
while True:
s = f.readline()
if not s or s[:7] == b"ENDCHAR":
if not s or s.startswith(b"ENDCHAR"):
break
bitmap += s[:-1]
@ -107,7 +96,7 @@ class BdfFontFile(FontFile.FontFile):
super().__init__()
s = fp.readline()
if s[:13] != b"STARTFONT 2.1":
if not s.startswith(b"STARTFONT 2.1"):
msg = "not a valid BDF file"
raise SyntaxError(msg)
@ -116,7 +105,7 @@ class BdfFontFile(FontFile.FontFile):
while True:
s = fp.readline()
if not s or s[:13] == b"ENDPROPERTIES":
if not s or s.startswith(b"ENDPROPERTIES"):
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")

Some files were not shown because too many files have changed in this diff Show More