Compare commits

..

No commits in common. "main" and "12.0.0" have entirely different histories.
main ... 12.0.0

161 changed files with 1463 additions and 1216 deletions

View File

@ -27,13 +27,14 @@ python3 -m pip install --upgrade wheel
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install ipython python3 -m pip install ipython
python3 -m pip install numpy
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
# optional test dependencies, only install if there's a binary package. # optional test dependency, only install if there's a binary package.
python3 -m pip install --only-binary=:all: numpy || true # fails on beta 3.14 and PyPy
python3 -m pip install --only-binary=:all: pyarrow || true python3 -m pip install --only-binary=:all: pyarrow || true
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3

View File

@ -1 +1 @@
cibuildwheel==3.3.0 cibuildwheel==3.2.1

View File

@ -1,4 +1,4 @@
mypy==1.19.1 mypy==1.18.2
arro3-compute arro3-compute
arro3-core arro3-core
IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PyQt6
@ -9,6 +9,7 @@ packaging
pyarrow-stubs pyarrow-stubs
pybind11 pybind11
pytest pytest
sphinx
types-atheris types-atheris
types-defusedxml types-defusedxml
types-olefile types-olefile

View File

@ -6,7 +6,6 @@
"labels": [ "labels": [
"Dependency" "Dependency"
], ],
"minimumReleaseAge": "7 days",
"packageRules": [ "packageRules": [
{ {
"groupName": "github-actions", "groupName": "github-actions",

View File

@ -44,13 +44,13 @@ jobs:
language: python language: python
dry-run: false dry-run: false
- name: Upload New Crash - name: Upload New Crash
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success' if: failure() && steps.build.outcome == 'success'
with: with:
name: artifacts name: artifacts
path: ./out/artifacts path: ./out/artifacts
- name: Upload Legacy Crash - name: Upload Legacy Crash
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
if: steps.run.outcome == 'success' if: steps.run.outcome == 'success'
with: with:
name: crash name: crash

View File

@ -32,7 +32,7 @@ jobs:
name: Docs name: Docs
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@ -49,7 +49,7 @@ jobs:
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: Cache libimagequant - name: Cache libimagequant
uses: actions/cache@v5 uses: actions/cache@v4
id: cache-libimagequant id: cache-libimagequant
with: with:
path: ~/cache-libimagequant path: ~/cache-libimagequant

View File

@ -2,31 +2,55 @@ name: Lint
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
permissions: {}
env: env:
FORCE_COLOR: 1 FORCE_COLOR: 1
PREK_COLOR: always
RUFF_OUTPUT_FORMAT: github permissions:
contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
lint: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Lint name: Lint
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@v6
with: - name: pre-commit cache
python-version: "3.x" uses: actions/cache@v4
- name: Install uv with:
uses: astral-sh/setup-uv@v7 path: ~/.cache/pre-commit
- name: Lint key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
run: uvx --with tox-uv tox -e lint restore-keys: |
- name: Mypy lint-pre-commit-
run: uvx --with tox-uv tox -e mypy
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
cache: pip
cache-dependency-path: "setup.py"
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Install dependencies
run: |
python3 -m pip install -U pip
python3 -m pip install -U tox
- name: Lint
run: tox -e lint
env:
PRE_COMMIT_COLOR: always
- name: Mypy
run: tox -e mypy

View File

@ -26,8 +26,9 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
# optional test dependencies, only install if there's a binary package. python3 -m pip install numpy
python3 -m pip install --only-binary=:all: numpy || true # optional test dependency, only install if there's a binary package.
# fails on beta 3.14 and PyPy
python3 -m pip install --only-binary=:all: pyarrow || true python3 -m pip install --only-binary=:all: pyarrow || true
# libavif # libavif

View File

@ -49,8 +49,8 @@ jobs:
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
debian-13-trixie-x86, debian-13-trixie-x86,
debian-13-trixie-amd64, debian-13-trixie-amd64,
fedora-41-amd64,
fedora-42-amd64, fedora-42-amd64,
fedora-43-amd64,
gentoo, gentoo,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64, ubuntu-24.04-noble-amd64,
@ -68,7 +68,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@ -45,7 +45,7 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@ -41,7 +41,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@ -39,7 +39,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@ -31,16 +31,15 @@ env:
jobs: jobs:
build: build:
runs-on: ${{ matrix.os }} runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"] python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"]
architecture: ["x64"] architecture: ["x64"]
os: ["windows-latest"]
include: include:
# Test the oldest Python on 32-bit # Test the oldest Python on 32-bit
- { python-version: "3.10", architecture: "x86", os: "windows-2022" } - { python-version: "3.10", architecture: "x86" }
timeout-minutes: 45 timeout-minutes: 45
@ -48,19 +47,19 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
@ -84,7 +83,7 @@ jobs:
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
- name: Install CPython dependencies - name: Install CPython dependencies
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" if: "!contains(matrix.python-version, 'pypy') && !contains(matrix.python-version, '3.14') && matrix.architecture != 'x86'"
run: | run: |
python3 -m pip install PyQt6 python3 -m pip install PyQt6
@ -112,7 +111,7 @@ jobs:
- name: Cache build - name: Cache build
id: build-cache id: build-cache
uses: actions/cache@v5 uses: actions/cache@v4
with: with:
path: winbuild\build path: winbuild\build
key: key:
@ -188,9 +187,8 @@ jobs:
# trim ~150MB for each job # trim ~150MB for each job
- name: Optimize build cache - name: Optimize build cache
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: | run: rmdir /S /Q winbuild\build\src
rm -rf winbuild\build\src shell: cmd
shell: bash
- name: Build Pillow - name: Build Pillow
run: | run: |
@ -207,7 +205,9 @@ jobs:
- name: Test Pillow - name: Test Pillow
run: | run: |
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
.ci\test.cmd .ci\test.cmd
shell: cmd
- name: Prepare to upload errors - name: Prepare to upload errors
if: failure() if: failure()
@ -216,7 +216,7 @@ jobs:
shell: bash shell: bash
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: errors name: errors

View File

@ -42,8 +42,6 @@ jobs:
] ]
python-version: [ python-version: [
"pypy3.11", "pypy3.11",
"3.15t",
"3.15",
"3.14t", "3.14t",
"3.14", "3.14",
"3.13t", "3.13t",
@ -56,7 +54,6 @@ jobs:
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.11", PYTHONOPTIMIZE: 2 } - { python-version: "3.11", PYTHONOPTIMIZE: 2 }
# Free-threaded # Free-threaded
- { python-version: "3.15t", disable-gil: true }
- { python-version: "3.14t", disable-gil: true } - { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true } - { python-version: "3.13t", disable-gil: true }
# Intel # Intel
@ -68,7 +65,7 @@ jobs:
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@ -92,7 +89,7 @@ jobs:
- name: Cache libimagequant - name: Cache libimagequant
if: startsWith(matrix.os, 'ubuntu') if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v5 uses: actions/cache@v4
id: cache-libimagequant id: cache-libimagequant
with: with:
path: ~/cache-libimagequant path: ~/cache-libimagequant
@ -143,7 +140,7 @@ jobs:
mkdir -p Tests/errors mkdir -p Tests/errors
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: errors name: errors

View File

@ -32,6 +32,7 @@ if [[ "$CIBW_PLATFORM" == "ios" ]]; then
# or `build/deps/iphonesimulator` # or `build/deps/iphonesimulator`
WORKDIR=$(pwd)/build/$IOS_SDK WORKDIR=$(pwd)/build/$IOS_SDK
BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK
PATCH_DIR=$(pwd)/patches/iOS
# GNU tooling insists on using aarch64 rather than arm64 # GNU tooling insists on using aarch64 rather than arm64
if [[ $PLAT == "arm64" ]]; then if [[ $PLAT == "arm64" ]]; then
@ -89,25 +90,27 @@ fi
ARCHIVE_SDIR=pillow-depends-main ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds. # Package versions for fresh source builds. Version numbers with "Patched"
# annotations have a source code patch that is required for some platforms. If
# you change those versions, ensure the patch is also updated.
if [[ -n "$IOS_SDK" ]]; then if [[ -n "$IOS_SDK" ]]; then
FREETYPE_VERSION=2.13.3 FREETYPE_VERSION=2.13.3
else else
FREETYPE_VERSION=2.14.1 FREETYPE_VERSION=2.14.1
fi fi
HARFBUZZ_VERSION=12.3.0 HARFBUZZ_VERSION=12.1.0
LIBPNG_VERSION=1.6.53 LIBPNG_VERSION=1.6.50
JPEGTURBO_VERSION=3.1.3 JPEGTURBO_VERSION=3.1.2
OPENJPEG_VERSION=2.5.4 OPENJPEG_VERSION=2.5.4
XZ_VERSION=5.8.2 XZ_VERSION=5.8.1
ZSTD_VERSION=1.5.7 ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.1 TIFF_VERSION=4.7.1
LCMS2_VERSION=2.17 LCMS2_VERSION=2.17
ZLIB_NG_VERSION=2.3.2 ZLIB_NG_VERSION=2.2.5
LIBWEBP_VERSION=1.6.0 LIBWEBP_VERSION=1.6.0
BZIP2_VERSION=1.0.8 BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0 LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.2.0 BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file.
LIBAVIF_VERSION=1.3.0 LIBAVIF_VERSION=1.3.0
function build_pkg_config { function build_pkg_config {
@ -146,9 +149,18 @@ function build_zlib_ng {
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS
unset HOST_CONFIGURE_FLAGS unset HOST_CONFIGURE_FLAGS
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat
HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS
if [[ -n "$IS_MACOS" ]] && [[ -z "$IOS_SDK" ]]; 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. This isn't needed on iOS, as iOS
# only builds the static library.
install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
fi
touch zlib-stamp touch zlib-stamp
} }
@ -156,7 +168,7 @@ function build_brotli {
if [ -e brotli-stamp ]; then return; fi if [ -e brotli-stamp ]; then return; fi
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \ (cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib -DCMAKE_MACOSX_BUNDLE=OFF $HOST_CMAKE_FLAGS . \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
&& make -j4 install) && make -j4 install)
touch brotli-stamp touch brotli-stamp
} }
@ -267,7 +279,7 @@ function build {
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
else else

View File

@ -100,14 +100,14 @@ jobs:
cibw_arch: arm64_iphoneos cibw_arch: arm64_iphoneos
- name: "iOS arm64 simulator" - name: "iOS arm64 simulator"
platform: ios platform: ios
os: macos-latest os: macos-14
cibw_arch: arm64_iphonesimulator cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator" - name: "iOS x86_64 simulator"
platform: ios platform: ios
os: macos-15-intel os: macos-15-intel
cibw_arch: x86_64_iphonesimulator cibw_arch: x86_64_iphonesimulator
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
submodules: true submodules: true
@ -134,7 +134,7 @@ jobs:
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v6 - uses: actions/upload-artifact@v4
with: with:
name: dist-${{ matrix.name }} name: dist-${{ matrix.name }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
@ -154,12 +154,12 @@ jobs:
- cibw_arch: ARM64 - cibw_arch: ARM64
os: windows-11-arm os: windows-11-arm
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
@ -186,18 +186,24 @@ jobs:
- name: Build wheels - name: Build wheels
run: | run: |
for f in winbuild/build/license/*; do setlocal EnableDelayedExpansion
name=$(basename "${f%.*}") for %%f in (winbuild\build\license\*) do (
# Skip FriBiDi license, it is not included in the wheel. set x=%%~nf
[[ $name == fribidi* ]] && continue rem Skip FriBiDi license, it is not included in the wheel.
# Skip imagequant license, it is not included in the wheel. set fribidi=!x:~0,7!
[[ $name == libimagequant* ]] && continue if NOT !fribidi!==fribidi (
echo "" >> LICENSE rem Skip imagequant license, it is not included in the wheel.
echo "===== $name =====" >> LICENSE set libimagequant=!x:~0,13!
echo "" >> LICENSE if NOT !libimagequant!==libimagequant (
cat "$f" >> LICENSE echo. >> LICENSE
done echo ===== %%~nf ===== >> LICENSE
cmd //c "winbuild\\build\\build_env.cmd && $pythonLocation\\python.exe -m cibuildwheel . --output-dir wheelhouse" echo. >> LICENSE
type %%f >> LICENSE
)
)
)
call winbuild\\build\\build_env.cmd
%pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
env: env:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
@ -211,16 +217,16 @@ jobs:
-e CI -e GITHUB_ACTIONS -e CI -e GITHUB_ACTIONS
mcr.microsoft.com/windows/servercore:ltsc2022 mcr.microsoft.com/windows/servercore:ltsc2022
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test' powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
shell: bash shell: cmd
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: dist-windows-${{ matrix.cibw_arch }} name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
- name: Upload fribidi.dll - name: Upload fribidi.dll
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: fribidi-windows-${{ matrix.cibw_arch }} name: fribidi-windows-${{ matrix.cibw_arch }}
path: winbuild\build\bin\fribidi* path: winbuild\build\bin\fribidi*
@ -229,7 +235,7 @@ jobs:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@ -240,7 +246,7 @@ jobs:
- run: make sdist - run: make sdist
- uses: actions/upload-artifact@v6 - uses: actions/upload-artifact@v4
with: with:
name: dist-sdist name: dist-sdist
path: dist/*.tar.gz path: dist/*.tar.gz
@ -250,7 +256,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Count dists name: Count dists
steps: steps:
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v5
with: with:
pattern: dist-* pattern: dist-*
path: dist path: dist
@ -269,13 +275,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels name: Upload wheels to scientific-python-nightly-wheels
steps: steps:
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v5
with: with:
pattern: dist-!(sdist)* pattern: dist-!(sdist)*
path: dist path: dist
merge-multiple: true merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels - name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3 uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2
with: with:
artifacts_path: dist artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
@ -291,7 +297,7 @@ jobs:
permissions: permissions:
id-token: write id-token: write
steps: steps:
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v5
with: with:
pattern: dist-* pattern: dist-*
path: dist path: dist

1
.github/zizmor.yml vendored
View File

@ -1,3 +1,4 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://docs.zizmor.sh/configuration/ # https://docs.zizmor.sh/configuration/
rules: rules:
unpinned-uses: unpinned-uses:

View File

@ -1,17 +1,17 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10 rev: v0.13.3
hooks: hooks:
- id: ruff-check - id: ruff-check
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.12.0 rev: 25.9.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.9.2 rev: 1.8.6
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -21,10 +21,10 @@ repos:
rev: v1.5.5 rev: v1.5.5
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v21.1.8 rev: v21.1.2
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -46,29 +46,29 @@ repos:
- id: check-yaml - id: check-yaml
args: [--allow-multiple-documents] args: [--allow-multiple-documents]
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: ^Tests/images/ exclude: ^Tests/images/|\.patch$
- id: trailing-whitespace - id: trailing-whitespace
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.36.0 rev: 0.34.0
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
- id: check-renovate - id: check-renovate
- repo: https://github.com/zizmorcore/zizmor-pre-commit - repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.19.0 rev: v1.14.2
hooks: hooks:
- id: zizmor - id: zizmor
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.2 rev: v1.0.0
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.11.1 rev: v2.7.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
@ -79,7 +79,7 @@ repos:
additional_dependencies: [trove-classifiers>=2024.10.12] additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt - repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.7.1 rev: 1.6.0
hooks: hooks:
- id: tox-ini-fmt - id: tox-ini-fmt

View File

@ -15,6 +15,7 @@ include tox.ini
graft Tests graft Tests
graft Tests/images graft Tests/images
graft checks graft checks
graft patches
graft src graft src
graft depends graft depends
graft winbuild graft winbuild

View File

@ -2,7 +2,7 @@
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSans-Regular.ttf, from https://www.google.com/get/noto/ NotoSans-Regular.ttf, from https://www.google.com/get/noto/
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype. AdobeVFPrototypeDuplicates.ttf is a modified version of this AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
ter-x20b.pcf, from http://terminus-font.sourceforge.net/ ter-x20b.pcf, from http://terminus-font.sourceforge.net/

View File

@ -55,8 +55,8 @@ def convert_to_comparable(
if a.mode == "P": if a.mode == "P":
new_a = Image.new("L", a.size) new_a = Image.new("L", a.size)
new_b = Image.new("L", b.size) new_b = Image.new("L", b.size)
new_a.putdata(a.get_flattened_data()) new_a.putdata(a.getdata())
new_b.putdata(b.get_flattened_data()) new_b.putdata(b.getdata())
elif a.mode == "I;16": elif a.mode == "I;16":
new_a = a.convert("I") new_a = a.convert("I")
new_b = b.convert("I") new_b = b.convert("I")
@ -104,9 +104,10 @@ def assert_image_equal_tofile(
msg: str | None = None, msg: str | None = None,
mode: str | None = None, mode: str | None = None,
) -> None: ) -> None:
with Image.open(filename) as im: with Image.open(filename) as img:
converted_im = im.convert(mode) if mode else im if mode:
assert_image_equal(a, converted_im, msg) img = img.convert(mode)
assert_image_equal(a, img, msg)
def assert_image_similar( def assert_image_similar(

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

View File

@ -0,0 +1,578 @@
<!DOCTYPE html>
<html>
<head>
<title>BMP Suite Image List</title>
<style>
.b { background:url(bkgd.png); }
.q { background-color:#fff0e0; }
.bad { background-color:#ffa0a0; }
</style>
</head>
<body>
<h1>BMP Suite Image List</h1>
<p><i>For <a href="http://entropymine.com/jason/bmpsuite/">BMP Suite</a>
version 2.3</i></p>
<p>This document describes the images in <i>BMP Suite</i>, and shows what
I allege to be the correct way to interpret them. PNG and JPEG images are
used for reference.
</p>
<p>It also shows how your web browser displays the BMP images,
but that&rsquo;s not its main purpose.
BMP is poor image format to use on web pages, so a web browser&rsquo;s
level of support for it is arguably not important.</p>
<table border=1 cellpadding=8>
<tr>
<th>File</th>
<th>Ver.</th>
<th>Correct display</th>
<th>In your browser</th>
<th>Notes</th>
</tr>
<tr>
<td>g/pal1.bmp</td>
<td>3</td>
<td class=b><img src="pal1.png"></td>
<td class=b><img src="../g/pal1.bmp"></td>
<td>1 bit/pixel paletted image, in which black is the first color in
the palette.</td>
</tr>
<tr>
<td>g/pal1wb.bmp</td>
<td>3</td>
<td class=b><img src="pal1.png"></td>
<td class=b><img src="../g/pal1wb.bmp"></td>
<td>1 bit/pixel paletted image, in which white is the first color in
the palette.</td>
</tr>
<tr>
<td>g/pal1bg.bmp</td>
<td>3</td>
<td class=b><img src="pal1bg.png"></td>
<td class=b><img src="../g/pal1bg.bmp"></td>
<td>1 bit/pixel paletted image, with colors other than black and white.</td>
</tr>
<tr>
<td class=q>q/pal1p1.bmp</td>
<td>3</td>
<td class=b><img src="pal1p1.png"></td>
<td class=b><img src="../q/pal1p1.bmp"></td>
<td>1 bit/pixel paletted image, with only one color in the palette.
The documentation says that 1-bpp images have a palette size of 2
(not &ldquo;up to 2&rdquo;), but it would be silly for a viewer not to
support a size of 1.</td>
</tr>
<tr>
<td class=q>q/pal2.bmp</td>
<td>3</td>
<td class=b><img src="pal2.png"></td>
<td class=b><img src="../q/pal2.bmp"></td>
<td>A paletted image with 2 bits/pixel. Usually only 1, 4,
and 8 are allowed, but 2 is legal on Windows CE.</td>
</tr>
<tr>
<td>g/pal4.bmp</td>
<td>3</td>
<td class=b><img src="pal4.png"></td>
<td class=b><img src="../g/pal4.bmp"></td>
<td>Paletted image with 12 palette colors, and 4 bits/pixel.</td>
</tr>
<tr>
<td>g/pal4rle.bmp</td>
<td>3</td>
<td class=b><img src="pal4.png"></td>
<td class=b><img src="../g/pal4rle.bmp"></td>
<td>4-bit image that uses RLE compression.</td>
</tr>
<tr>
<td class=q>q/pal4rletrns.bmp</td>
<td>3</td>
<td class=b><img src="pal4rletrns.png"><br>
or<br><img src="pal4rletrns-0.png"><br>
or<br><img src="pal4rletrns-b.png"></td>
<td class=b><img src="../q/pal4rletrns.bmp"></td>
<td>An RLE-compressed image that used &ldquo;delta&rdquo;
codes to skip over some pixels, leaving them undefined. Some viewers
make undefined pixels transparent, others make them black, and
others assign them palette color 0 (purple, in this case).</td>
</tr>
<tr>
<td>g/pal8.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8.bmp"></td>
<td>Our standard paletted image, with 252 palette colors, and 8
bits/pixel.</td>
</tr>
<tr>
<td>g/pal8-0.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8-0.bmp"></td>
<td>Every field that can be set to 0 is set to 0: pixels/meter=0;
colors used=0 (meaning the default 256); size-of-image=0.</td>
</tr>
<tr>
<td>g/pal8rle.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8rle.bmp"></td>
<td>8-bit image that uses RLE compression.</td>
</tr>
<tr>
<td class=q>q/pal8rletrns.bmp</td>
<td>3</td>
<td class=b><img src="pal8rletrns.png"><br>
or<br><img src="pal8rletrns-0.png"><br>
or<br><img src="pal8rletrns-b.png"></td>
<td class=b><img src="../q/pal8rletrns.bmp"></td>
<td>8-bit version of q/pal4rletrns.bmp.</td>
</tr>
<tr>
<td>g/pal8w126.bmp</td>
<td>3</td>
<td class=b><img src="pal8w126.png"></td>
<td class=b><img src="../g/pal8w126.bmp"></td>
<td rowspan=3>Images with different widths and heights.
In BMP format, rows are padded to a multiple of four bytes, so we
test all four possibilities.</td>
</tr>
<tr>
<td>g/pal8w125.bmp</td>
<td>3</td>
<td class=b><img src="pal8w125.png"></td>
<td class=b><img src="../g/pal8w125.bmp"></td>
</tr>
<tr>
<td>g/pal8w124.bmp</td>
<td>3</td>
<td class=b><img src="pal8w124.png"></td>
<td class=b><img src="../g/pal8w124.bmp"></td>
</tr>
<tr>
<td>g/pal8topdown.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8topdown.bmp"></td>
<td>BMP images are normally stored from the bottom up, but
there is a way to store them from the top down.</td>
</tr>
<tr>
<td class=q>q/pal8offs.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8offs.bmp"></td>
<td>A file with some unused bytes between the palette and the
image. This is probably valid, but I&rsquo;m not 100% sure.</td>
</tr>
<tr>
<td class=q>q/pal8oversizepal.bmp</td>
<td>3</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8oversizepal.bmp"></td>
<td>An 8-bit image with 300 palette colors. This may be invalid,
because the documentation could
be interpreted to imply that 8-bit images aren&rsquo;t allowed
to have more than 256 colors.</td>
</tr>
<tr>
<td>g/pal8nonsquare.bmp</td>
<td>3</td>
<td class=b>
<img src="pal8nonsquare-v.png"><br>
or<br>
<img src="pal8nonsquare-e.png">
</td>
<td class=b><img src="../g/pal8nonsquare.bmp"></td>
<td>An image with non-square pixels: the X pixels/meter is twice
the Y pixels/meter. Image <i>editors</i> can be expected to
leave the image &ldquo;squashed&rdquo;; image <i>viewers</i> should
consider stretching it to its correct proportions.</td>
</tr>
<tr>
<td>g/pal8os2.bmp</td>
<td>OS/2v1</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8os2.bmp"></td>
<td>An OS/2-style bitmap.</td>
</tr>
<tr>
<td class=q>q/pal8os2sp.bmp</td>
<td>OS/2v1</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8os2sp.bmp"></td>
<td>An OS/2v1 with a less-than-full-sized palette.
Probably not valid, but such files have been seen in the wild.</td>
</tr>
<tr>
<td class=q>q/pal8os2v2.bmp</td>
<td>OS/2v2</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8os2v2.bmp"></td>
<td>My attempt to make an OS/2v2 bitmap.</td>
</tr>
<tr>
<td class=q>q/pal8os2v2-16.bmp</td>
<td>OS/2v2</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../q/pal8os2v2-16.bmp"></td>
<td>An OS/2v2 bitmap whose header has only 16 bytes, instead of the full 64.</td>
</tr>
<tr>
<td>g/pal8v4.bmp</td>
<td>4</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8v4.bmp"></td>
<td>A v4 bitmap. I&rsquo;m not sure that the gamma and chromaticity values in
this file are sensible, because I can&rsquo;t find any detailed documentation
of them.</td>
</tr>
<tr>
<td>g/pal8v5.bmp</td>
<td>5</td>
<td class=b><img src="pal8.png"></td>
<td class=b><img src="../g/pal8v5.bmp"></td>
<td>A v5 bitmap. Version 5 has additional colorspace options over v4, so it
is easier to create, and ought to be more portable.</td>
</tr>
<tr>
<td>g/rgb16.bmp</td>
<td>3</td>
<td class=b><img src="rgb16.png"></td>
<td class=b><img src="../g/rgb16.bmp"></td>
<td>A 16-bit image with the default color format: 5 bits each for red,
green, and blue, and 1 unused bit.
The whitest colors should (I assume) be displayed as pure white:
<span style="background-color:rgb(255,255,255)">(255,255,255)</span>, not
<span style="background-color:rgb(248,248,248)">(248,248,248)</span>.</td>
</tr>
<tr>
<td>g/rgb16-565.bmp</td>
<td>3</td>
<td class=b><img src="rgb16-565.png"></td>
<td class=b><img src="../g/rgb16-565.bmp"></td>
<td>A 16-bit image with a BITFIELDS segment indicating 5 red, 6 green,
and 5 blue bits. This is a standard 16-bit format, even supported by
old versions of Windows that don&rsquo;t support any other non-default 16-bit
formats.
The whitest colors should be displayed as pure white:
<span style="background-color:rgb(255,255,255)">(255,255,255)</span>, not
<span style="background-color:rgb(248,252,248)">(248,252,248)</span>.</td>
</tr>
<tr>
<td>g/rgb16-565pal.bmp</td>
<td>3</td>
<td class=b><img src="rgb16-565.png"></td>
<td class=b><img src="../g/rgb16-565pal.bmp"></td>
<td>A 16-bit image with both a BITFIELDS segment and a palette.</td>
</tr>
<tr>
<td class=q>q/rgb16-231.bmp</td>
<td>3</td>
<td class=b><img src="rgb16-231.png"></td>
<td class=b><img src="../q/rgb16-231.bmp"></td>
<td>An unusual and silly 16-bit image, with 2 red bits, 3 green bits, and 1
blue bit. Most viewers do support this image, but the colors may be darkened
with a yellow-green shadow. That&rsquo;s because they&rsquo;re doing simple
bit-shifting (possibly including one round of bit replication), instead of
proper scaling.</td>
</tr>
<tr>
<td class=q>q/rgba16-4444.bmp</td>
<td>5</td>
<td class=b><img src="rgba16-4444.png"></td>
<td class=b><img src="../q/rgba16-4444.bmp"></td>
<td>A 16-bit image with an alpha channel. There are 4 bits for each color
channel, and 4 bits for the alpha channel.
It&rsquo;s not clear if this is valid, but I can&rsquo;t find anything that
suggests it isn&rsquo;t.
</td>
</tr>
<tr>
<td>g/rgb24.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../g/rgb24.bmp"></td>
<td>A perfectly ordinary 24-bit (truecolor) image.</td>
</tr>
<tr>
<td>g/rgb24pal.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../g/rgb24pal.bmp"></td>
<td>A 24-bit image, with a palette containing 256 colors. There is little if
any reason for a truecolor image to contain a palette, but it is legal.</td>
</tr>
<tr>
<td class=q>q/rgb24largepal.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb24largepal.bmp"></td>
<td>A 24-bit image, with a palette containing 300 colors.
The fact that the palette has more than 256 colors may cause some viewers
to complain, but the documentation does not mention a size limit.</td>
</tr>
<tr>
<td class=q>q/rgb24prof.bmp</td>
<td>5</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb24prof.bmp"></td>
<td>My attempt to make a BMP file with an embedded color profile.</td>
</tr>
<tr>
<td class=q>q/rgb24lprof.bmp</td>
<td>5</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb24lprof.bmp"></td>
<td>My attempt to make a BMP file with a linked color profile.</td>
</tr>
<tr>
<td class=q>q/rgb24jpeg.bmp</td>
<td>5</td>
<td class=b><img src="rgb24.jpg"></td>
<td class=b><img src="../q/rgb24jpeg.bmp"></td>
<td rowspan=2>My attempt to make BMP files with embedded JPEG and PNG images.
These are not likely to be supported by much of anything (they&rsquo;re
intended for printers).</td>
</tr>
<tr>
<td class=q>q/rgb24png.bmp</td>
<td>5</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb24png.bmp"></td>
</tr>
<tr>
<td>g/rgb32.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../g/rgb32.bmp"></td>
<td>A 32-bit image using the default color format for 32-bit images (no
BITFIELDS segment). There are 8 bits per color channel, and 8 unused
bits. The unused bits are set to 0.</td>
</tr>
<tr>
<td>g/rgb32bf.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../g/rgb32bf.bmp"></td>
<td>A 32-bit image with a BITFIELDS segment. As usual, there are 8 bits per
color channel, and 8 unused bits. But the color channels are in an unusual
order, so the viewer must read the BITFIELDS, and not just guess.</td>
</tr>
<tr>
<td class=q>q/rgb32fakealpha.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"><br>
or<br>
<img class=b src="fakealpha.png">
</td>
<td class=b><img src="../q/rgb32fakealpha.bmp"></td>
<td>Same as g/rgb32.bmp, except that the unused bits are set to something
other than 0.
If the image becomes transparent toward the bottom, it probably means
the viewer uses heuristics to guess whether the undefined
data represents transparency.</td>
</tr>
<tr>
<td class=q>q/rgb32-111110.bmp</td>
<td>3</td>
<td class=b><img src="rgb24.png"></td>
<td class=b><img src="../q/rgb32-111110.bmp"></td>
<td>A 32 bits/pixel image, with all 32 bits used: 11 each for red and
green, and 10 for blue. As far as I know, this is perfectly valid, but it
is unusual.</td>
</tr>
<tr>
<td class=q>q/rgba32.bmp</td>
<td>5</td>
<td class=b><img src="rgba32.png"></td>
<td class=b><img src="../q/rgba32.bmp"></td>
<td>A BMP with an alpha channel. Transparency is barely documented,
so it&rsquo;s <i>possible</i> that this file is not correctly formed.
The color channels are in an unusual order, to prevent viewers from
passing this test by making a lucky guess.</td>
</tr>
<tr>
<td class=q>q/rgba32abf.bmp</td>
<td>3</td>
<td class=b><img src="rgba32.png"></td>
<td class=b><img src="../q/rgba32abf.bmp"></td>
<td>An image of type BI_ALHPABITFIELDS. Supposedly, this was used on
Windows CE. I don&rsquo;t know whether it is constructed correctly.</td>
</tr>
<tr>
<td class=bad>b/badbitcount.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badbitcount.bmp"></td>
<td>Header indicates an absurdly large number of bits/pixel.</td>
</tr>
<tr>
<td class=bad>b/badbitssize.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badbitssize.bmp"></td>
<td>Header incorrectly indicates that the bitmap is several GB in size.</td>
</tr>
<tr>
<td class=bad>b/baddens1.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/baddens1.bmp"></td>
<td rowspan=2>Density (pixels per meter) suggests the image is <i>much</i>
larger in one dimension than the other.</td>
</tr>
<tr>
<td class=bad>b/baddens2.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/baddens2.bmp"></td>
</tr>
<tr>
<td class=bad>b/badfilesize.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badfilesize.bmp"></td>
<td>Header incorrectly indicates that the file is several GB in size.</td>
</tr>
<tr>
<td class=bad>b/badheadersize.bmp</td>
<td>?</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badheadersize.bmp"></td>
<td>Header size is 66 bytes, which is not a valid size for any known BMP
version.</td>
</tr>
<tr>
<td class=bad>b/badpalettesize.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badpalettesize.bmp"></td>
<td>Header incorrectly indicates that the palette contains an absurdly large
number of colors.</td>
</tr>
<tr>
<td class=bad>b/badplanes.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badplanes.bmp"></td>
<td>The &ldquo;planes&rdquo; setting, which is required to be 1, is not 1.</td>
</tr>
<tr>
<td class=bad>b/badrle.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badrle.bmp"></td>
<td>An invalid RLE-compressed image that tries to cause buffer overruns.</td>
</tr>
<tr>
<td class=bad>b/badwidth.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/badwidth.bmp"></td>
<td>The image claims to be a negative number of pixels in width.</td>
</tr>
<tr>
<td class=bad>b/pal8badindex.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/pal8badindex.bmp"></td>
<td>Many of the palette indices used in the image are not present in the
palette.</td>
</tr>
<tr>
<td class=bad>b/reallybig.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/reallybig.bmp"></td>
<td>An image with a very large reported width and height.</td>
</tr>
<tr>
<td class=bad>b/rletopdown.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/rletopdown.bmp"></td>
<td>An RLE-compressed image that tries to use top-down orientation,
which isn&rsquo;t allowed.</td>
</tr>
<tr>
<td class=bad>b/shortfile.bmp</td>
<td>3</td>
<td class=b>N/A</td>
<td class=b><img src="../b/shortfile.bmp"></td>
<td>A file that has been truncated in the middle of the bitmap.</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

After

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

View File

@ -72,7 +72,7 @@ def test_good() -> None:
"pal8-0.bmp": "pal8.png", "pal8-0.bmp": "pal8.png",
"pal8rle.bmp": "pal8.png", "pal8rle.bmp": "pal8.png",
"pal8topdown.bmp": "pal8.png", "pal8topdown.bmp": "pal8.png",
"pal8nonsquare.bmp": "pal8nonsquare-e.png", "pal8nonsquare.bmp": "pal8nonsquare-v.png",
"pal8os2.bmp": "pal8.png", "pal8os2.bmp": "pal8.png",
"pal8os2sp.bmp": "pal8.png", "pal8os2sp.bmp": "pal8.png",
"pal8os2v2.bmp": "pal8.png", "pal8os2v2.bmp": "pal8.png",
@ -95,16 +95,16 @@ def test_good() -> None:
for f in get_files("g"): for f in get_files("g"):
try: try:
with Image.open(f) as im: with Image.open(f) as im:
im.load()
with Image.open(get_compare(f)) as compare: with Image.open(get_compare(f)) as compare:
# assert image similar doesn't really work compare.load()
# with paletized image, since the palette might if im.mode == "P":
# be differently ordered for an equivalent image. # assert image similar doesn't really work
im_converted = im.convert("RGBA") if im.mode == "P" else im # with paletized image, since the palette might
compare_converted = ( # be differently ordered for an equivalent image.
compare.convert("RGBA") if im.mode == "P" else compare im = im.convert("RGBA")
) compare = im.convert("RGBA")
assert_image_similar(im, compare, 5)
assert_image_similar(im_converted, compare_converted, 5)
except Exception as msg: except Exception as msg:
# there are three here that are unsupported: # there are three here that are unsupported:

View File

@ -28,13 +28,9 @@ def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image:
def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None:
it = iter(im.get_flattened_data()) it = iter(im.getdata())
for data_row in data: for data_row in data:
im_row = [] im_row = [next(it) for _ in range(im.size[0])]
for _ in range(im.width):
im_v = next(it)
assert isinstance(im_v, (int, float))
im_row.append(im_v)
if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)): if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)):
assert im_row == data_row assert im_row == data_row
with pytest.raises(StopIteration): with pytest.raises(StopIteration):

View File

@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -278,25 +277,25 @@ def test_apng_mode() -> None:
assert isinstance(im, PngImagePlugin.PngImageFile) assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im_rgb = im.convert("RGB") im = im.convert("RGB")
assert im_rgb.getpixel((0, 0)) == (0, 255, 0) assert im.getpixel((0, 0)) == (0, 255, 0)
assert im_rgb.getpixel((64, 32)) == (0, 255, 0) assert im.getpixel((64, 32)) == (0, 255, 0)
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile) assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im_rgba = im.convert("RGBA") im = im.convert("RGBA")
assert im_rgba.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im_rgba.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile) assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im_rgba = im.convert("RGBA") im = im.convert("RGBA")
assert im_rgba.getpixel((0, 0)) == (0, 0, 255, 128) assert im.getpixel((0, 0)) == (0, 0, 255, 128)
assert im_rgba.getpixel((64, 32)) == (0, 0, 255, 128) assert im.getpixel((64, 32)) == (0, 0, 255, 128)
def test_apng_chunk_errors() -> None: def test_apng_chunk_errors() -> None:
@ -518,24 +517,6 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
assert im.info["duration"] == 600 assert im.info["duration"] == 600
def test_apng_save_duration_float(tmp_path: Path) -> None:
test_file = tmp_path / "temp.png"
im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1)
im.save(test_file, save_all=True, append_images=[im2], duration=0.5)
with Image.open(test_file) as reloaded:
assert reloaded.info["duration"] == 0.5
def test_apng_save_large_duration(tmp_path: Path) -> None:
test_file = tmp_path / "temp.png"
im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1)
with pytest.raises(ValueError, match="cannot write duration"):
im.save(test_file, save_all=True, append_images=[im2], duration=65536000)
def test_apng_save_disposal(tmp_path: Path) -> None: def test_apng_save_disposal(tmp_path: Path) -> None:
test_file = tmp_path / "temp.png" test_file = tmp_path / "temp.png"
size = (128, 64) size = (128, 64)
@ -737,25 +718,6 @@ def test_apng_save_size(tmp_path: Path) -> None:
assert reloaded.size == (200, 200) assert reloaded.size == (200, 200)
def test_compress_level() -> None:
compress_level_sizes = {}
for compress_level in (0, 9):
out = BytesIO()
im = Image.new("L", (100, 100))
im.save(
out,
"PNG",
save_all=True,
append_images=[Image.new("L", (200, 200))],
compress_level=compress_level,
)
compress_level_sizes[compress_level] = len(out.getvalue())
assert compress_level_sizes[0] > compress_level_sizes[9]
def test_seek_after_close() -> None: def test_seek_after_close() -> None:
im = Image.open("Tests/images/apng/delay.png") im = Image.open("Tests/images/apng/delay.png")
im.seek(1) im.seek(1)

View File

@ -121,6 +121,7 @@ class TestFileAvif:
assert image.size == (128, 128) assert image.size == (128, 128)
assert image.format == "AVIF" assert image.format == "AVIF"
assert image.get_format_mimetype() == "image/avif" assert image.get_format_mimetype() == "image/avif"
image.getdata()
# generated with: # generated with:
# avifdec hopper.avif hopper_avif_write.png # avifdec hopper.avif hopper_avif_write.png
@ -142,6 +143,7 @@ class TestFileAvif:
assert reloaded.mode == "RGB" assert reloaded.mode == "RGB"
assert reloaded.size == (128, 128) assert reloaded.size == (128, 128)
assert reloaded.format == "AVIF" assert reloaded.format == "AVIF"
reloaded.getdata()
# avifdec hopper.avif avif/hopper_avif_write.png # avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile( assert_image_similar_tofile(

View File

@ -165,9 +165,9 @@ def test_rgba_bitfields() -> None:
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
# So before the comparing the image, swap the channels # So before the comparing the image, swap the channels
b, g, r = im.split()[1:] b, g, r = im.split()[1:]
im_rgb = Image.merge("RGB", (r, g, b)) im = Image.merge("RGB", (r, g, b))
assert_image_equal_tofile(im_rgb, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
# This test image has been manually hexedited # This test image has been manually hexedited
# to change the bitfield compression in the header from XBGR to ABGR # to change the bitfield compression in the header from XBGR to ABGR

View File

@ -61,7 +61,6 @@ def test_handler(tmp_path: Path) -> None:
def load(self, im: ImageFile.StubImageFile) -> Image.Image: def load(self, im: ImageFile.StubImageFile) -> Image.Image:
self.loaded = True self.loaded = True
assert im.fp is not None
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) return Image.new("RGB", (1, 1))

View File

@ -57,7 +57,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
def test_sanity_dxt1_bc1(image_path: str) -> None: def test_sanity_dxt1_bc1(image_path: str) -> None:
"""Check DXT1 and BC1 images can be opened""" """Check DXT1 and BC1 images can be opened"""
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
target_rgba = target.convert("RGBA") target = target.convert("RGBA")
with Image.open(image_path) as im: with Image.open(image_path) as im:
im.load() im.load()
@ -65,7 +65,7 @@ def test_sanity_dxt1_bc1(image_path: str) -> None:
assert im.mode == "RGBA" assert im.mode == "RGBA"
assert im.size == (256, 256) assert im.size == (256, 256)
assert_image_equal(im, target_rgba) assert_image_equal(im, target)
def test_sanity_dxt3() -> None: def test_sanity_dxt3() -> None:
@ -380,11 +380,6 @@ def test_palette() -> None:
assert_image_equal_tofile(im, "Tests/images/transparent.gif") assert_image_equal_tofile(im, "Tests/images/transparent.gif")
def test_zero_mask_totals() -> None:
with Image.open("Tests/images/zero_mask_totals.dds") as im:
im.load()
def test_unsupported_header_size() -> None: def test_unsupported_header_size() -> None:
with pytest.raises(OSError, match="Unsupported header size 0"): with pytest.raises(OSError, match="Unsupported header size 0"):
with Image.open(BytesIO(b"DDS " + b"\x00" * 4)): with Image.open(BytesIO(b"DDS " + b"\x00" * 4)):
@ -520,9 +515,9 @@ def test_save_dx10_bc5(tmp_path: Path) -> None:
im.save(out, pixel_format="BC5") im.save(out, pixel_format="BC5")
assert_image_similar_tofile(im, out, 9.56) assert_image_similar_tofile(im, out, 9.56)
im_l = hopper("L") im = hopper("L")
with pytest.raises(OSError, match="only RGB mode can be written as BC5"): with pytest.raises(OSError, match="only RGB mode can be written as BC5"):
im_l.save(out, pixel_format="BC5") im.save(out, pixel_format="BC5")
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -265,9 +265,9 @@ def test_bytesio_object() -> None:
img.load() img.load()
with Image.open(FILE1_COMPARE) as image1_scale1_compare: with Image.open(FILE1_COMPARE) as image1_scale1_compare:
image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB") image1_scale1_compare = image1_scale1_compare.convert("RGB")
image1_scale1_compare_rgb.load() image1_scale1_compare.load()
assert_image_similar(img, image1_scale1_compare_rgb, 5) assert_image_similar(img, image1_scale1_compare, 5)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@ -301,17 +301,17 @@ def test_render_scale1() -> None:
with Image.open(FILE1) as image1_scale1: with Image.open(FILE1) as image1_scale1:
image1_scale1.load() image1_scale1.load()
with Image.open(FILE1_COMPARE) as image1_scale1_compare: with Image.open(FILE1_COMPARE) as image1_scale1_compare:
image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB") image1_scale1_compare = image1_scale1_compare.convert("RGB")
image1_scale1_compare_rgb.load() image1_scale1_compare.load()
assert_image_similar(image1_scale1, image1_scale1_compare_rgb, 5) assert_image_similar(image1_scale1, image1_scale1_compare, 5)
# Non-zero bounding box # Non-zero bounding box
with Image.open(FILE2) as image2_scale1: with Image.open(FILE2) as image2_scale1:
image2_scale1.load() image2_scale1.load()
with Image.open(FILE2_COMPARE) as image2_scale1_compare: with Image.open(FILE2_COMPARE) as image2_scale1_compare:
image2_scale1_compare_rgb = image2_scale1_compare.convert("RGB") image2_scale1_compare = image2_scale1_compare.convert("RGB")
image2_scale1_compare_rgb.load() image2_scale1_compare.load()
assert_image_similar(image2_scale1, image2_scale1_compare_rgb, 10) assert_image_similar(image2_scale1, image2_scale1_compare, 10)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@ -324,16 +324,18 @@ def test_render_scale2() -> None:
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile) assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
image1_scale2.load(scale=2) image1_scale2.load(scale=2)
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
image1_scale2_compare_rgb = image1_scale2_compare.convert("RGB") image1_scale2_compare = image1_scale2_compare.convert("RGB")
assert_image_similar(image1_scale2, image1_scale2_compare_rgb, 5) image1_scale2_compare.load()
assert_image_similar(image1_scale2, image1_scale2_compare, 5)
# Non-zero bounding box # Non-zero bounding box
with Image.open(FILE2) as image2_scale2: with Image.open(FILE2) as image2_scale2:
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile) assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
image2_scale2.load(scale=2) image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
image2_scale2_compare_rgb = image2_scale2_compare.convert("RGB") image2_scale2_compare = image2_scale2_compare.convert("RGB")
assert_image_similar(image2_scale2, image2_scale2_compare_rgb, 10) image2_scale2_compare.load()
assert_image_similar(image2_scale2, image2_scale2_compare, 10)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@ -343,8 +345,8 @@ def test_render_scale2() -> None:
def test_resize(filename: str) -> None: def test_resize(filename: str) -> None:
with Image.open(filename) as im: with Image.open(filename) as im:
new_size = (100, 100) new_size = (100, 100)
im_resized = im.resize(new_size) im = im.resize(new_size)
assert im_resized.size == new_size assert im.size == new_size
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")

View File

@ -33,7 +33,7 @@ def test_multiple_load_operations() -> None:
assert_image_equal_tofile(im, "Tests/images/gbr.png") assert_image_equal_tofile(im, "Tests/images/gbr.png")
def create_gbr_image(info: dict[str, int] = {}, magic_number: bytes = b"") -> BytesIO: def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
return BytesIO( return BytesIO(
b"".join( b"".join(
_binary.o32be(i) _binary.o32be(i)

View File

@ -327,13 +327,14 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None:
im.seek(1) im.seek(1)
assert im.mode == mode assert im.mode == mode
im_rgb = im.convert("RGB") if mode == "RGBA" else im if mode == "RGBA":
im = im.convert("RGB")
# Check a color only from the old palette # Check a color only from the old palette
assert im_rgb.getpixel((0, 0)) == original_color assert im.getpixel((0, 0)) == original_color
# Check a color from the new palette # Check a color from the new palette
assert im_rgb.getpixel((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: def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
@ -353,16 +354,16 @@ def test_palette_handling(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/513 # see https://github.com/python-pillow/Pillow/issues/513
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im_rgb = im.convert("RGB") im = im.convert("RGB")
im_rgb = im_rgb.resize((100, 100), Image.Resampling.LANCZOS) im = im.resize((100, 100), Image.Resampling.LANCZOS)
im_p = im_rgb.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
f = tmp_path / "temp.gif" f = tmp_path / "temp.gif"
im_p.save(f, optimize=True) im2.save(f, optimize=True)
with Image.open(f) as reloaded: with Image.open(f) as reloaded:
assert_image_similar(im_rgb, reloaded.convert("RGB"), 10) assert_image_similar(im, reloaded.convert("RGB"), 10)
def test_palette_434(tmp_path: Path) -> None: def test_palette_434(tmp_path: Path) -> None:
@ -382,36 +383,35 @@ def test_palette_434(tmp_path: Path) -> None:
with roundtrip(im, optimize=True) as reloaded: with roundtrip(im, optimize=True) as reloaded:
assert_image_similar(im, reloaded, 1) assert_image_similar(im, reloaded, 1)
im_rgb = im.convert("RGB") im = im.convert("RGB")
# check automatic P conversion
# check automatic P conversion with roundtrip(im) as reloaded:
with roundtrip(im_rgb) as reloaded: reloaded = reloaded.convert("RGB")
reloaded = reloaded.convert("RGB") assert_image_equal(im, reloaded)
assert_image_equal(im_rgb, reloaded)
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as img: with Image.open(TEST_GIF) as img:
img_rgb = img.convert("RGB") img = img.convert("RGB")
tempfile = str(tmp_path / "temp.gif") tempfile = str(tmp_path / "temp.gif")
b = BytesIO() b = BytesIO()
GifImagePlugin._save_netpbm(img_rgb, b, tempfile) GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded: with Image.open(tempfile) as reloaded:
assert_image_similar(img_rgb, reloaded.convert("RGB"), 0) assert_image_similar(img, reloaded.convert("RGB"), 0)
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_l_mode(tmp_path: Path) -> None: def test_save_netpbm_l_mode(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as img: with Image.open(TEST_GIF) as img:
img_l = img.convert("L") img = img.convert("L")
tempfile = str(tmp_path / "temp.gif") tempfile = str(tmp_path / "temp.gif")
b = BytesIO() b = BytesIO()
GifImagePlugin._save_netpbm(img_l, b, tempfile) GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded: with Image.open(tempfile) as reloaded:
assert_image_similar(img_l, reloaded.convert("L"), 0) assert_image_similar(img, reloaded.convert("L"), 0)
def test_seek() -> None: def test_seek() -> None:
@ -1038,9 +1038,9 @@ def test_webp_background(tmp_path: Path) -> None:
im.save(out) im.save(out)
# Test non-opaque WebP background # Test non-opaque WebP background
im2 = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im2.info["background"] = (0, 0, 0, 0) im.info["background"] = (0, 0, 0, 0)
im2.save(out) im.save(out)
def test_comment(tmp_path: Path) -> None: def test_comment(tmp_path: Path) -> None:
@ -1048,16 +1048,16 @@ def test_comment(tmp_path: Path) -> None:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0"
out = tmp_path / "temp.gif" out = tmp_path / "temp.gif"
im2 = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im2.info["comment"] = b"Test comment text" im.info["comment"] = b"Test comment text"
im2.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["comment"] == im2.info["comment"] assert reread.info["comment"] == im.info["comment"]
im2.info["comment"] = "Test comment text" im.info["comment"] = "Test comment text"
im2.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["comment"] == im2.info["comment"].encode() assert reread.info["comment"] == im.info["comment"].encode()
# Test that GIF89a is used for comments # Test that GIF89a is used for comments
assert reread.info["version"] == b"GIF89a" assert reread.info["version"] == b"GIF89a"

View File

@ -59,9 +59,8 @@ def test_handler(tmp_path: Path) -> None:
def open(self, im: Image.Image) -> None: def open(self, im: Image.Image) -> None:
self.opened = True self.opened = True
def load(self, im: ImageFile.ImageFile) -> Image.Image: def load(self, im: Image.Image) -> Image.Image:
self.loaded = True self.loaded = True
assert im.fp is not None
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) return Image.new("RGB", (1, 1))

View File

@ -61,9 +61,8 @@ def test_handler(tmp_path: Path) -> None:
def open(self, im: Image.Image) -> None: def open(self, im: Image.Image) -> None:
self.opened = True self.opened = True
def load(self, im: ImageFile.ImageFile) -> Image.Image: def load(self, im: Image.Image) -> Image.Image:
self.loaded = True self.loaded = True
assert im.fp is not None
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) return Image.new("RGB", (1, 1))

View File

@ -6,13 +6,13 @@ import pytest
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
from .helper import assert_image_equal from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/iptc.jpg" TEST_FILE = "Tests/images/iptc.jpg"
def create_iptc_image(info: dict[str, int] = {}) -> BytesIO: def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
def field(tag: tuple[int, int], value: bytes) -> bytes: def field(tag, value):
return bytes((0x1C,) + tag + (0, len(value))) + value return bytes((0x1C,) + tag + (0, len(value))) + value
data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0)))) data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
@ -85,7 +85,7 @@ def test_getiptcinfo() -> None:
def test_getiptcinfo_jpg_none() -> None: def test_getiptcinfo_jpg_none() -> None:
# Arrange # Arrange
with Image.open("Tests/images/hopper.jpg") as im: with hopper() as im:
# Act # Act
iptc = IptcImagePlugin.getiptcinfo(im) iptc = IptcImagePlugin.getiptcinfo(im)
@ -143,7 +143,6 @@ def test_getiptcinfo_tiff() -> None:
# Test with LONG tag type # Test with LONG tag type
with Image.open("Tests/images/hopper.Lab.tif") as im: with Image.open("Tests/images/hopper.Lab.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2.tagtype[TiffImagePlugin.IPTC_NAA_CHUNK] = TiffTags.LONG im.tag_v2.tagtype[TiffImagePlugin.IPTC_NAA_CHUNK] = TiffTags.LONG
iptc = IptcImagePlugin.getiptcinfo(im) iptc = IptcImagePlugin.getiptcinfo(im)

View File

@ -1133,9 +1133,8 @@ class TestFileCloseW32:
im.save(tmpfile) im.save(tmpfile)
im = Image.open(tmpfile) im = Image.open(tmpfile)
assert im.fp is not None
assert not im.fp.closed
fp = im.fp fp = im.fp
assert not fp.closed
with pytest.raises(OSError): with pytest.raises(OSError):
os.remove(tmpfile) os.remove(tmpfile)
im.load() im.load()

View File

@ -164,7 +164,7 @@ def test_reduce() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im: with Image.open("Tests/images/test-card-lossless.jp2") as im:
assert callable(im.reduce) assert callable(im.reduce)
im.reduce = 2 # type: ignore[assignment, method-assign] im.reduce = 2
assert im.reduce == 2 assert im.reduce == 2
im.load() im.load()

View File

@ -11,15 +11,7 @@ from typing import Any, NamedTuple
import pytest import pytest
from PIL import ( from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
Image,
ImageFile,
ImageFilter,
ImageOps,
TiffImagePlugin,
TiffTags,
features,
)
from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
from .helper import ( from .helper import (
@ -35,13 +27,14 @@ from .helper import (
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
class LibTiffTestCase: class LibTiffTestCase:
def _assert_noerr(self, tmp_path: Path, im: ImageFile.ImageFile) -> None: def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None:
"""Helper tests that assert basic sanity about the g4 tiff reading""" """Helper tests that assert basic sanity about the g4 tiff reading"""
# 1 bit # 1 bit
assert im.mode == "1" assert im.mode == "1"
# Does the data actually load # Does the data actually load
im.load() im.load()
im.getdata()
assert isinstance(im, TiffImagePlugin.TiffImageFile) assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im._compression == "group4" assert im._compression == "group4"
@ -362,36 +355,6 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault # Should not segfault
im.save(outfile) im.save(outfile)
@pytest.mark.parametrize("tagtype", (TiffTags.SIGNED_RATIONAL, TiffTags.IFD))
def test_tag_type(
self, tagtype: int, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[37000] = 100
ifd.tagtype[37000] = tagtype
out = tmp_path / "temp.tif"
im = Image.new("L", (1, 1))
im.save(out, tiffinfo=ifd)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[37000] == 100
def test_inknames_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = tmp_path / "temp.tif"
hopper("L").save(out, tiffinfo={333: "name\x00"})
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[333] in ("name", "name\x00")
def test_whitepoint_tag( def test_whitepoint_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None: ) -> None:
@ -515,12 +478,12 @@ class TestFileLibTiff(LibTiffTestCase):
# and save to compressed tif. # and save to compressed tif.
out = tmp_path / "temp.tif" out = tmp_path / "temp.tif"
with Image.open("Tests/images/pport_g4.tif") as im: with Image.open("Tests/images/pport_g4.tif") as im:
im_l = im.convert("L") im = im.convert("L")
im_l = im_l.filter(ImageFilter.GaussianBlur(4)) im = im.filter(ImageFilter.GaussianBlur(4))
im_l.save(out, compression="tiff_adobe_deflate") im.save(out, compression="tiff_adobe_deflate")
assert_image_equal_tofile(im_l, out) assert_image_equal_tofile(im, out)
def test_compressions(self, tmp_path: Path) -> None: def test_compressions(self, tmp_path: Path) -> None:
# Test various tiff compressions and assert similar image content but reduced # Test various tiff compressions and assert similar image content but reduced
@ -609,9 +572,8 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, compression=compression) im.save(out, compression=compression)
def test_fp_leak(self) -> None: def test_fp_leak(self) -> None:
im: ImageFile.ImageFile | None = Image.open("Tests/images/hopper_g4_500.tif") im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif")
assert im is not None assert im is not None
assert im.fp is not None
fn = im.fp.fileno() fn = im.fp.fileno()
os.fstat(fn) os.fstat(fn)
@ -1087,10 +1049,8 @@ class TestFileLibTiff(LibTiffTestCase):
data = data[:102] + b"\x02" + data[103:] data = data[:102] + b"\x02" + data[103:]
with Image.open(io.BytesIO(data)) as im: with Image.open(io.BytesIO(data)) as im:
im_transposed = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
assert_image_equal_tofile( assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
im_transposed, "Tests/images/old-style-jpeg-compression.png"
)
def test_open_missing_samplesperpixel(self) -> None: def test_open_missing_samplesperpixel(self) -> None:
with Image.open( with Image.open(
@ -1157,9 +1117,9 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im: with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9): for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
im_transposed = ImageOps.exif_transpose(im) im = ImageOps.exif_transpose(im)
assert_image_similar(base_im, im_transposed, 0.7) assert_image_similar(base_im, im, 0.7)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file",

View File

@ -22,10 +22,10 @@ def test_sanity() -> None:
# Adjust for the gamma of 2.2 encoded into the file # Adjust for the gamma of 2.2 encoded into the file
lut = ImagePalette.make_gamma_lut(1 / 2.2) lut = ImagePalette.make_gamma_lut(1 / 2.2)
im1 = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()])
im2 = hopper("RGBA") im2 = hopper("RGBA")
assert_image_similar(im1, im2, 10) assert_image_similar(im, im2, 10)
def test_n_frames() -> None: def test_n_frames() -> None:

View File

@ -300,12 +300,12 @@ def test_save_all() -> None:
im_reloaded.seek(1) im_reloaded.seek(1)
assert_image_similar(im, im_reloaded, 30) assert_image_similar(im, im_reloaded, 30)
im_rgb = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
for colors in (("#f00",), ("#f00", "#0f0")): for colors in (("#f00",), ("#f00", "#0f0")):
append_images = [Image.new("RGB", (1, 1), color) for color in colors] append_images = [Image.new("RGB", (1, 1), color) for color in colors]
im_reloaded = roundtrip(im_rgb, save_all=True, append_images=append_images) im_reloaded = roundtrip(im, save_all=True, append_images=append_images)
assert_image_equal(im_rgb, im_reloaded) assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile) assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100" assert im_reloaded.mpinfo[45056] == b"0100"
@ -315,7 +315,7 @@ def test_save_all() -> None:
assert_image_similar(im_reloaded, im_expected, 1) assert_image_similar(im_reloaded, im_expected, 1)
# Test that a single frame image will not be saved as an MPO # Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im_rgb, save_all=True) jpg = roundtrip(im, save_all=True)
assert "mp" not in jpg.info assert "mp" not in jpg.info

View File

@ -101,13 +101,12 @@ class TestFilePng:
assert im.get_format_mimetype() == "image/png" assert im.get_format_mimetype() == "image/png"
for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]: for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]:
im1 = hopper(mode) im = hopper(mode)
im1.save(test_file) im.save(test_file)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
converted_reloaded = ( if mode == "I;16B":
reloaded.convert(mode) if mode == "I;16B" else reloaded reloaded = reloaded.convert(mode)
) assert_image_equal(reloaded, im)
assert_image_equal(converted_reloaded, im1)
def test_invalid_file(self) -> None: def test_invalid_file(self) -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
@ -226,11 +225,11 @@ class TestFilePng:
test_file = "Tests/images/pil123p.png" test_file = "Tests/images/pil123p.png"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert_image(im, "P", (162, 150)) assert_image(im, "P", (162, 150))
im_rgba = im.convert("RGBA") im = im.convert("RGBA")
assert_image(im_rgba, "RGBA", (162, 150)) assert_image(im, "RGBA", (162, 150))
# image has 124 unique alpha values # image has 124 unique alpha values
colors = im_rgba.getchannel("A").getcolors() colors = im.getchannel("A").getcolors()
assert colors is not None assert colors is not None
assert len(colors) == 124 assert len(colors) == 124
@ -240,11 +239,11 @@ class TestFilePng:
assert im.info["transparency"] == (0, 255, 52) assert im.info["transparency"] == (0, 255, 52)
assert_image(im, "RGB", (64, 64)) assert_image(im, "RGB", (64, 64))
im_rgba = im.convert("RGBA") im = im.convert("RGBA")
assert_image(im_rgba, "RGBA", (64, 64)) assert_image(im, "RGBA", (64, 64))
# image has 876 transparent pixels # image has 876 transparent pixels
colors = im_rgba.getchannel("A").getcolors() colors = im.getchannel("A").getcolors()
assert colors is not None assert colors is not None
assert colors[0][0] == 876 assert colors[0][0] == 876
@ -263,11 +262,11 @@ class TestFilePng:
assert len(im.info["transparency"]) == 256 assert len(im.info["transparency"]) == 256
assert_image(im, "P", (162, 150)) assert_image(im, "P", (162, 150))
im_rgba = im.convert("RGBA") im = im.convert("RGBA")
assert_image(im_rgba, "RGBA", (162, 150)) assert_image(im, "RGBA", (162, 150))
# image has 124 unique alpha values # image has 124 unique alpha values
colors = im_rgba.getchannel("A").getcolors() colors = im.getchannel("A").getcolors()
assert colors is not None assert colors is not None
assert len(colors) == 124 assert len(colors) == 124
@ -286,13 +285,13 @@ class TestFilePng:
assert im.info["transparency"] == 164 assert im.info["transparency"] == 164
assert im.getpixel((31, 31)) == 164 assert im.getpixel((31, 31)) == 164
assert_image(im, "P", (64, 64)) assert_image(im, "P", (64, 64))
im_rgba = im.convert("RGBA") im = im.convert("RGBA")
assert_image(im_rgba, "RGBA", (64, 64)) assert_image(im, "RGBA", (64, 64))
assert im_rgba.getpixel((31, 31)) == (0, 255, 52, 0) assert im.getpixel((31, 31)) == (0, 255, 52, 0)
# image has 876 transparent pixels # image has 876 transparent pixels
colors = im_rgba.getchannel("A").getcolors() colors = im.getchannel("A").getcolors()
assert colors is not None assert colors is not None
assert colors[0][0] == 876 assert colors[0][0] == 876
@ -339,15 +338,6 @@ class TestFilePng:
assert colors is not None assert colors is not None
assert colors[0][0] == num_transparent assert colors[0][0] == num_transparent
def test_save_1_transparency(self, tmp_path: Path) -> None:
out = tmp_path / "temp.png"
im = Image.new("1", (1, 1), 1)
im.save(out, transparency=1)
with Image.open(out) as reloaded:
assert reloaded.info["transparency"] == 255
def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/caption_6_33_22.png" in_file = "Tests/images/caption_6_33_22.png"
with Image.open(in_file) as im: with Image.open(in_file) as im:
@ -788,9 +778,7 @@ class TestFilePng:
im.save(test_file, exif=im.getexif()) im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
exif = reloaded._getexif() exif = reloaded._getexif()
assert exif is not None
assert exif[305] == "Adobe Photoshop CS Macintosh" assert exif[305] == "Adobe Photoshop CS Macintosh"
def test_exif_argument(self, tmp_path: Path) -> None: def test_exif_argument(self, tmp_path: Path) -> None:
@ -823,7 +811,7 @@ class TestFilePng:
monkeypatch.setattr(sys, "stdout", mystdout) monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG") # type: ignore[arg-type] im.save(sys.stdout, "PNG")
if isinstance(mystdout, MyStdOut): if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer

View File

@ -389,7 +389,7 @@ def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(sys, "stdout", mystdout) monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM") # type: ignore[arg-type] im.save(sys.stdout, "PPM")
if isinstance(mystdout, MyStdOut): if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer

View File

@ -100,7 +100,7 @@ def test_seek_tell() -> None:
im.seek(2) im.seek(2)
layer_number = im.tell() layer_number = im.tell()
assert layer_number == 2 assert layer_number == 2
def test_seek_eoferror() -> None: def test_seek_eoferror() -> None:
@ -138,7 +138,7 @@ def test_icc_profile() -> None:
assert "icc_profile" in im.info assert "icc_profile" in im.info
icc_profile = im.info["icc_profile"] icc_profile = im.info["icc_profile"]
assert len(icc_profile) == 3144 assert len(icc_profile) == 3144
def test_no_icc_profile() -> None: def test_no_icc_profile() -> None:
@ -158,16 +158,17 @@ def test_combined_larger_than_size() -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file,raises",
[ [
"Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
"Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
], ],
) )
def test_crashes(test_file: str) -> None: def test_crashes(test_file: str, raises: type[Exception]) -> None:
with pytest.raises(OSError): with open(test_file, "rb") as f:
with Image.open(test_file): with pytest.raises(raises):
pass with Image.open(f):
pass
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -178,7 +179,8 @@ def test_crashes(test_file: str) -> None:
], ],
) )
def test_layer_crashes(test_file: str) -> None: def test_layer_crashes(test_file: str) -> None:
with Image.open(test_file) as im: with open(test_file, "rb") as f:
assert isinstance(im, PsdImagePlugin.PsdImageFile) with Image.open(f) as im:
with pytest.raises(SyntaxError): assert isinstance(im, PsdImagePlugin.PsdImageFile)
im.layers with pytest.raises(SyntaxError):
im.layers

View File

@ -84,8 +84,8 @@ def test_rgbx() -> None:
with Image.open(io.BytesIO(data)) as im: with Image.open(io.BytesIO(data)) as im:
r, g, b = im.split() r, g, b = im.split()
im_rgb = Image.merge("RGB", (b, g, r)) im = Image.merge("RGB", (b, g, r))
assert_image_equal_tofile(im_rgb, os.path.join(EXTRA_DIR, "32bpp.png")) assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
@pytest.mark.skipif( @pytest.mark.skipif(

View File

@ -764,9 +764,9 @@ class TestFileTiff:
# Test appending images # Test appending images
mp = BytesIO() mp = BytesIO()
im_rgb = Image.new("RGB", (100, 100), "#f00") im = Image.new("RGB", (100, 100), "#f00")
ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]]
im_rgb.copy().save(mp, format="TIFF", save_all=True, append_images=ims) im.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
mp.seek(0, os.SEEK_SET) mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread: with Image.open(mp) as reread:
@ -778,7 +778,7 @@ class TestFileTiff:
yield from ims yield from ims
mp = BytesIO() mp = BytesIO()
im_rgb.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims)) im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
mp.seek(0, os.SEEK_SET) mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread: with Image.open(mp) as reread:
@ -971,7 +971,6 @@ class TestFileTiff:
im = Image.open(tmpfile) im = Image.open(tmpfile)
fp = im.fp fp = im.fp
assert fp is not None
assert not fp.closed assert not fp.closed
im.load() im.load()
assert fp.closed assert fp.closed
@ -985,7 +984,6 @@ class TestFileTiff:
with open(tmpfile, "rb") as f: with open(tmpfile, "rb") as f:
im = Image.open(f) im = Image.open(f)
fp = im.fp fp = im.fp
assert fp is not None
assert not fp.closed assert not fp.closed
im.load() im.load()
assert not fp.closed assert not fp.closed
@ -1036,9 +1034,8 @@ class TestFileTiffW32:
im.save(tmpfile) im.save(tmpfile)
im = Image.open(tmpfile) im = Image.open(tmpfile)
assert im.fp is not None
assert not im.fp.closed
fp = im.fp fp = im.fp
assert not fp.closed
with pytest.raises(OSError): with pytest.raises(OSError):
os.remove(tmpfile) os.remove(tmpfile)
im.load() im.load()

View File

@ -175,13 +175,13 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
del info[278] del info[278]
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
im_resized = im.resize((500, 500)) im = im.resize((500, 500))
info[TiffImagePlugin.IMAGEWIDTH] = im_resized.width info[TiffImagePlugin.IMAGEWIDTH] = im.width
# STRIPBYTECOUNTS can be a SHORT or a LONG # STRIPBYTECOUNTS can be a SHORT or a LONG
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
im_resized.save(out, tiffinfo=info) im.save(out, tiffinfo=info)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)

View File

@ -60,6 +60,7 @@ class TestFileWebp:
assert image.size == (128, 128) assert image.size == (128, 128)
assert image.format == "WEBP" assert image.format == "WEBP"
image.load() image.load()
image.getdata()
# generated with: # generated with:
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
@ -76,6 +77,7 @@ class TestFileWebp:
assert image.size == (128, 128) assert image.size == (128, 128)
assert image.format == "WEBP" assert image.format == "WEBP"
image.load() image.load()
image.getdata()
if mode == self.rgb_mode: if mode == self.rgb_mode:
# generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm

View File

@ -29,6 +29,7 @@ def test_read_rgba() -> None:
assert image.size == (200, 150) assert image.size == (200, 150)
assert image.format == "WEBP" assert image.format == "WEBP"
image.load() image.load()
image.getdata()
image.tobytes() image.tobytes()
@ -59,6 +60,7 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
assert image.size == pil_image.size assert image.size == pil_image.size
assert image.format == "WEBP" assert image.format == "WEBP"
image.load() image.load()
image.getdata()
assert_image_equal(image, pil_image) assert_image_equal(image, pil_image)
@ -81,6 +83,7 @@ def test_write_rgba(tmp_path: Path) -> None:
assert image.size == (10, 10) assert image.size == (10, 10)
assert image.format == "WEBP" assert image.format == "WEBP"
image.load() image.load()
image.getdata()
assert_image_similar(image, pil_image, 1.0) assert_image_similar(image, pil_image, 1.0)
@ -130,6 +133,7 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
assert image.format == "WEBP" assert image.format == "WEBP"
image.load() image.load()
image.getdata()
with Image.open(file_path) as im: with Image.open(file_path) as im:
target = im.convert("RGBA") target = im.convert("RGBA")

View File

@ -24,5 +24,6 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
assert image.size == (128, 128) assert image.size == (128, 128)
assert image.format == "WEBP" assert image.format == "WEBP"
image.load() image.load()
image.getdata()
assert_image_equal(image, hopper(RGB_MODE)) assert_image_equal(image, hopper(RGB_MODE))

View File

@ -13,15 +13,15 @@ def test_white() -> None:
k = i.getpixel((0, 0)) k = i.getpixel((0, 0))
L = i.get_flattened_data(0) L = i.getdata(0)
a = i.get_flattened_data(1) a = i.getdata(1)
b = i.get_flattened_data(2) b = i.getdata(2)
assert k == (255, 128, 128) assert k == (255, 128, 128)
assert L == (255,) * 100 assert list(L) == [255] * 100
assert a == (128,) * 100 assert list(a) == [128] * 100
assert b == (128,) * 100 assert list(b) == [128] * 100
def test_green() -> None: def test_green() -> None:

View File

@ -613,8 +613,8 @@ class TestImage:
assert im.getpixel((0, 0)) == 0 assert im.getpixel((0, 0)) == 0
assert im.getpixel((255, 255)) == 255 assert im.getpixel((255, 255)) == 255
with Image.open(target_file) as target: with Image.open(target_file) as target:
im_target = target.convert(mode) target = target.convert(mode)
assert_image_equal(im, im_target) assert_image_equal(im, target)
def test_radial_gradient_wrong_mode(self) -> None: def test_radial_gradient_wrong_mode(self) -> None:
# Arrange # Arrange
@ -638,8 +638,8 @@ class TestImage:
assert im.getpixel((0, 0)) == 255 assert im.getpixel((0, 0)) == 255
assert im.getpixel((128, 128)) == 0 assert im.getpixel((128, 128)) == 0
with Image.open(target_file) as target: with Image.open(target_file) as target:
im_target = target.convert(mode) target = target.convert(mode)
assert_image_equal(im, im_target) assert_image_equal(im, target)
def test_register_extensions(self) -> None: def test_register_extensions(self) -> None:
test_format = "a" test_format = "a"
@ -663,20 +663,20 @@ class TestImage:
assert_image_equal(im, im.remap_palette(list(range(256)))) assert_image_equal(im, im.remap_palette(list(range(256))))
# Test identity transform with an RGBA palette # Test identity transform with an RGBA palette
im_p = Image.new("P", (256, 1)) im = Image.new("P", (256, 1))
for x in range(256): for x in range(256):
im_p.putpixel((x, 0), x) im.putpixel((x, 0), x)
im_p.putpalette(list(range(256)) * 4, "RGBA") im.putpalette(list(range(256)) * 4, "RGBA")
im_remapped = im_p.remap_palette(list(range(256))) im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im_p, im_remapped) assert_image_equal(im, im_remapped)
assert im_p.palette is not None assert im.palette is not None
assert im_remapped.palette is not None assert im_remapped.palette is not None
assert im_p.palette.palette == im_remapped.palette.palette assert im.palette.palette == im_remapped.palette.palette
# Test illegal image mode # Test illegal image mode
with hopper() as im_hopper: with hopper() as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im_hopper.remap_palette([]) im.remap_palette([])
def test_remap_palette_transparency(self) -> None: def test_remap_palette_transparency(self) -> None:
im = Image.new("P", (1, 2), (0, 0, 0)) im = Image.new("P", (1, 2), (0, 0, 0))
@ -1181,10 +1181,10 @@ class TestImageBytes:
assert reloaded.tobytes() == source_bytes assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize("mode", Image.MODES) @pytest.mark.parametrize("mode", Image.MODES)
def test_get_flattened_data_putdata(self, mode: str) -> None: def test_getdata_putdata(self, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
reloaded = Image.new(mode, im.size) reloaded = Image.new(mode, im.size)
reloaded.putdata(im.get_flattened_data()) reloaded.putdata(im.getdata())
assert_image_equal(im, reloaded) assert_image_equal(im, reloaded)

View File

@ -78,7 +78,7 @@ def test_fromarray() -> None:
}, },
) )
out = Image.fromarray(wrapped) out = Image.fromarray(wrapped)
return out.mode, out.size, i.get_flattened_data() == out.get_flattened_data() return out.mode, out.size, list(i.getdata()) == list(out.getdata())
# assert test("1") == ("1", (128, 100), True) # assert test("1") == ("1", (128, 100), True)
assert test("L") == ("L", (128, 100), True) assert test("L") == ("L", (128, 100), True)

View File

@ -80,8 +80,8 @@ def test_16bit() -> None:
_test_float_conversion(im) _test_float_conversion(im)
for color in (65535, 65536): for color in (65535, 65536):
im_i = Image.new("I", (1, 1), color) im = Image.new("I", (1, 1), color)
im_i16 = im_i.convert("I;16") im_i16 = im.convert("I;16")
assert im_i16.getpixel((0, 0)) == 65535 assert im_i16.getpixel((0, 0)) == 65535

View File

@ -78,13 +78,13 @@ def test_crop_crash() -> None:
extents = (1, 1, 10, 10) extents = (1, 1, 10, 10)
# works prepatch # works prepatch
with Image.open(test_img) as img: with Image.open(test_img) as img:
img1 = img.crop(extents) img2 = img.crop(extents)
img1.load() img2.load()
# fail prepatch # fail prepatch
with Image.open(test_img) as img: with Image.open(test_img) as img:
img2 = img.crop(extents) img = img.crop(extents)
img2.load() img.load()
def test_crop_zero() -> None: def test_crop_zero() -> None:
@ -95,10 +95,10 @@ def test_crop_zero() -> None:
cropped = im.crop((10, 10, 20, 20)) cropped = im.crop((10, 10, 20, 20))
assert cropped.size == (10, 10) assert cropped.size == (10, 10)
assert cropped.getpixel((0, 0)) == (0, 0, 0) assert cropped.getdata()[0] == (0, 0, 0)
im = Image.new("RGB", (0, 0)) im = Image.new("RGB", (0, 0))
cropped = im.crop((10, 10, 20, 20)) cropped = im.crop((10, 10, 20, 20))
assert cropped.size == (10, 10) assert cropped.size == (10, 10)
assert cropped.getpixel((2, 0)) == (0, 0, 0) assert cropped.getdata()[2] == (0, 0, 0)

View File

@ -1,23 +1,23 @@
from __future__ import annotations from __future__ import annotations
import pytest
from PIL import Image from PIL import Image
from .helper import hopper from .helper import hopper
def test_sanity() -> None: def test_sanity() -> None:
data = hopper().get_flattened_data() data = hopper().getdata()
len(data)
list(data)
assert len(data) == 128 * 128
assert data[0] == (20, 20, 70) assert data[0] == (20, 20, 70)
def test_mode() -> None: def test_mode() -> None:
def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]: def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]:
im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
data = im.get_flattened_data() data = im.getdata()
return data[0], len(data), len(list(data)) return data[0], len(data), len(list(data))
assert getdata("1") == (0, 960, 960) assert getdata("1") == (0, 960, 960)
@ -28,13 +28,3 @@ def test_mode() -> None:
assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960) assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960)
assert getdata("CMYK") == ((244, 242, 203, 0), 960, 960) assert getdata("CMYK") == ((244, 242, 203, 0), 960, 960)
assert getdata("YCbCr") == ((16, 147, 123), 960, 960) assert getdata("YCbCr") == ((16, 147, 123), 960, 960)
def test_deprecation() -> None:
im = hopper()
with pytest.warns(DeprecationWarning, match="getdata"):
data = im.getdata()
assert len(data) == 128 * 128
assert data[0] == (20, 20, 70)
assert list(data)[0] == (20, 20, 70)

View File

@ -38,7 +38,6 @@ def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None:
def test_contextmanager() -> None: def test_contextmanager() -> None:
fn = None fn = None
with Image.open("Tests/images/hopper.gif") as im: with Image.open("Tests/images/hopper.gif") as im:
assert im.fp is not None
fn = im.fp.fileno() fn = im.fp.fileno()
os.fstat(fn) os.fstat(fn)

View File

@ -2,7 +2,6 @@ from __future__ import annotations
import sys import sys
from array import array from array import array
from typing import cast
import pytest import pytest
@ -13,19 +12,21 @@ from .helper import assert_image_equal, hopper
def test_sanity() -> None: def test_sanity() -> None:
im1 = hopper() im1 = hopper()
for data in (im1.get_flattened_data(), im1.im):
im2 = Image.new(im1.mode, im1.size, 0)
im2.putdata(data)
assert_image_equal(im1, im2) data = list(im1.getdata())
# readonly im2 = Image.new(im1.mode, im1.size, 0)
im2 = Image.new(im1.mode, im2.size, 0) im2.putdata(data)
im2.readonly = 1
im2.putdata(data)
assert not im2.readonly assert_image_equal(im1, im2)
assert_image_equal(im1, im2)
# readonly
im2 = Image.new(im1.mode, im2.size, 0)
im2.readonly = 1
im2.putdata(data)
assert not im2.readonly
assert_image_equal(im1, im2)
def test_long_integers() -> None: def test_long_integers() -> None:
@ -59,22 +60,22 @@ def test_mode_with_L_with_float() -> None:
@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
def test_mode_i(mode: str) -> None: def test_mode_i(mode: str) -> None:
src = hopper("L") src = hopper("L")
data = src.get_flattened_data() data = list(src.getdata())
im = Image.new(mode, src.size, 0) im = Image.new(mode, src.size, 0)
im.putdata(data, 2, 256) im.putdata(data, 2, 256)
target = tuple(2 * elt + 256 for elt in cast(tuple[int, ...], data)) target = [2 * elt + 256 for elt in data]
assert im.get_flattened_data() == target assert list(im.getdata()) == target
def test_mode_F() -> None: def test_mode_F() -> None:
src = hopper("L") src = hopper("L")
data = src.get_flattened_data() data = list(src.getdata())
im = Image.new("F", src.size, 0) im = Image.new("F", src.size, 0)
im.putdata(data, 2.0, 256.0) im.putdata(data, 2.0, 256.0)
target = tuple(2.0 * float(elt) + 256.0 for elt in cast(tuple[int, ...], data)) target = [2.0 * float(elt) + 256.0 for elt in data]
assert im.get_flattened_data() == target assert list(im.getdata()) == target
def test_array_B() -> None: def test_array_B() -> None:
@ -85,7 +86,7 @@ def test_array_B() -> None:
im = Image.new("L", (150, 100)) im = Image.new("L", (150, 100))
im.putdata(arr) im.putdata(arr)
assert len(im.get_flattened_data()) == len(arr) assert len(im.getdata()) == len(arr)
def test_array_F() -> None: def test_array_F() -> None:
@ -96,7 +97,7 @@ def test_array_F() -> None:
arr = array("f", [0.0]) * 15000 arr = array("f", [0.0]) * 15000
im.putdata(arr) im.putdata(arr)
assert len(im.get_flattened_data()) == len(arr) assert len(im.getdata()) == len(arr)
def test_not_flattened() -> None: def test_not_flattened() -> None:

View File

@ -58,8 +58,8 @@ def test_rgba_quantize() -> None:
def test_quantize() -> None: def test_quantize() -> None:
with Image.open("Tests/images/caption_6_33_22.png") as image: with Image.open("Tests/images/caption_6_33_22.png") as image:
converted = image.convert("RGB") image = image.convert("RGB")
converted = converted.quantize() converted = image.quantize()
assert converted.mode == "P" assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 1) assert_image_similar(converted.convert("RGB"), image, 1)
@ -67,13 +67,13 @@ def test_quantize() -> None:
def test_quantize_no_dither() -> None: def test_quantize_no_dither() -> None:
image = hopper() image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette: with Image.open("Tests/images/caption_6_33_22.png") as palette:
palette_p = palette.convert("P") palette = palette.convert("P")
converted = image.quantize(dither=Image.Dither.NONE, palette=palette_p) converted = image.quantize(dither=Image.Dither.NONE, palette=palette)
assert converted.mode == "P" assert converted.mode == "P"
assert converted.palette is not None assert converted.palette is not None
assert palette_p.palette is not None assert palette.palette is not None
assert converted.palette.palette == palette_p.palette.palette assert converted.palette.palette == palette.palette.palette
def test_quantize_no_dither2() -> None: def test_quantize_no_dither2() -> None:
@ -97,10 +97,10 @@ def test_quantize_no_dither2() -> None:
def test_quantize_dither_diff() -> None: def test_quantize_dither_diff() -> None:
image = hopper() image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette: with Image.open("Tests/images/caption_6_33_22.png") as palette:
palette_p = palette.convert("P") palette = palette.convert("P")
dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette_p) dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette)
nodither = image.quantize(dither=Image.Dither.NONE, palette=palette_p) nodither = image.quantize(dither=Image.Dither.NONE, palette=palette)
assert dither.tobytes() != nodither.tobytes() assert dither.tobytes() != nodither.tobytes()

View File

@ -160,7 +160,7 @@ class TestImagingCoreResize:
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample)
assert r.mode == "RGB" assert r.mode == "RGB"
assert r.size == (212, 195) assert r.size == (212, 195)
assert r.getpixel((0, 0)) == (0, 0, 0) assert r.getdata()[0] == (0, 0, 0)
def test_unknown_filter(self) -> None: def test_unknown_filter(self) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -314,8 +314,8 @@ class TestImageResize:
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_transposed(self) -> None: def test_transposed(self) -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
im_resized = im.resize((64, 64)) im = im.resize((64, 64))
assert im_resized.size == (64, 64) assert im.size == (64, 64)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mode", ("L", "RGB", "I", "I;16", "I;16L", "I;16B", "I;16N", "F") "mode", ("L", "RGB", "I", "I;16", "I;16L", "I;16B", "I;16N", "F")

View File

@ -43,8 +43,8 @@ def test_angle(angle: int) -> None:
with Image.open("Tests/images/test-card.png") as im: with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle) rotate(im, im.mode, angle)
im_hopper = hopper() im = hopper()
assert_image_equal(im_hopper.rotate(angle), im_hopper.rotate(angle, expand=1)) assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
@ -76,9 +76,9 @@ def test_center_0() -> None:
with Image.open("Tests/images/hopper_45.png") as target: with Image.open("Tests/images/hopper_45.png") as target:
target_origin = target.size[1] / 2 target_origin = target.size[1] / 2
im_target = target.crop((0, target_origin, 128, target_origin + 128)) target = target.crop((0, target_origin, 128, target_origin + 128))
assert_image_similar(im, im_target, 15) assert_image_similar(im, target, 15)
def test_center_14() -> None: def test_center_14() -> None:
@ -87,22 +87,22 @@ def test_center_14() -> None:
with Image.open("Tests/images/hopper_45.png") as target: with Image.open("Tests/images/hopper_45.png") as target:
target_origin = target.size[1] / 2 - 14 target_origin = target.size[1] / 2 - 14
im_target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) target = target.crop((6, target_origin, 128 + 6, target_origin + 128))
assert_image_similar(im, im_target, 10) assert_image_similar(im, target, 10)
def test_translate() -> None: def test_translate() -> None:
im = hopper() im = hopper()
with Image.open("Tests/images/hopper_45.png") as target: with Image.open("Tests/images/hopper_45.png") as target:
target_origin = (target.size[1] / 2 - 64) - 5 target_origin = (target.size[1] / 2 - 64) - 5
im_target = target.crop( target = target.crop(
(target_origin, target_origin, target_origin + 128, target_origin + 128) (target_origin, target_origin, target_origin + 128, target_origin + 128)
) )
im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC) im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC)
assert_image_similar(im, im_target, 1) assert_image_similar(im, target, 1)
def test_fastpath_center() -> None: def test_fastpath_center() -> None:

View File

@ -159,9 +159,9 @@ def test_reducing_gap_for_DCT_scaling() -> None:
with Image.open("Tests/images/hopper.jpg") as ref: with Image.open("Tests/images/hopper.jpg") as ref:
# thumbnail should call draft with reducing_gap scale # thumbnail should call draft with reducing_gap scale
ref.draft(None, (18 * 3, 18 * 3)) ref.draft(None, (18 * 3, 18 * 3))
im_ref = ref.resize((18, 18), Image.Resampling.BICUBIC) ref = ref.resize((18, 18), Image.Resampling.BICUBIC)
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0) im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
assert_image_similar(im_ref, im, 1.4) assert_image_similar(ref, im, 1.4)

View File

@ -250,14 +250,14 @@ class TestImageTransform:
def test_missing_method_data(self) -> None: def test_missing_method_data(self) -> None:
with hopper() as im: with hopper() as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.transform((100, 100), None) # type: ignore[arg-type] im.transform((100, 100), None)
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None:
with hopper() as im: with hopper() as im:
(w, h) = im.size (w, h) = im.size
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type] im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample)
class TestImageTransformAffine: class TestImageTransformAffine:

View File

@ -274,13 +274,13 @@ def test_simple_lab() -> None:
# not a linear luminance map. so L != 128: # not a linear luminance map. so L != 128:
assert k == (137, 128, 128) assert k == (137, 128, 128)
l_data = i_lab.get_flattened_data(0) l_data = i_lab.getdata(0)
a_data = i_lab.get_flattened_data(1) a_data = i_lab.getdata(1)
b_data = i_lab.get_flattened_data(2) b_data = i_lab.getdata(2)
assert l_data == (137,) * 100 assert list(l_data) == [137] * 100
assert a_data == (128,) * 100 assert list(a_data) == [128] * 100
assert b_data == (128,) * 100 assert list(b_data) == [128] * 100
def test_lab_color() -> None: def test_lab_color() -> None:

View File

@ -68,22 +68,10 @@ def test_sanity() -> None:
draw.rectangle(list(range(4))) draw.rectangle(list(range(4)))
def test_new_color() -> None: def test_valueerror() -> None:
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
assert im.palette is not None
assert len(im.palette.colors) == 249
# Test drawing a new color onto the palette
draw.line((0, 0), fill=(0, 0, 0)) draw.line((0, 0), fill=(0, 0, 0))
assert im.palette is not None
assert len(im.palette.colors) == 250
assert im.palette.dirty
# Test drawing another new color, now that the palette is dirty
draw.point((0, 0), fill=(1, 0, 0))
assert len(im.palette.colors) == 251
assert im.convert("RGB").getpixel((0, 0)) == (1, 0, 0)
def test_mode_mismatch() -> None: def test_mode_mismatch() -> None:
@ -210,10 +198,10 @@ def test_bitmap() -> None:
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
with Image.open("Tests/images/pil123rgba.png") as small: with Image.open("Tests/images/pil123rgba.png") as small:
small_resized = small.resize((50, 50), Image.Resampling.NEAREST) small = small.resize((50, 50), Image.Resampling.NEAREST)
# Act # Act
draw.bitmap((10, 10), small_resized) draw.bitmap((10, 10), small)
# Assert # Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png")

View File

@ -702,7 +702,7 @@ def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
font.get_variation_axes() font.get_variation_axes()
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf")
assert font.get_variation_names() == [ assert font.get_variation_names(), [
b"ExtraLight", b"ExtraLight",
b"Light", b"Light",
b"Regular", b"Regular",
@ -742,21 +742,6 @@ def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
] ]
def test_variation_duplicates() -> None:
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototypeDuplicates.ttf")
assert font.get_variation_names() == [
b"ExtraLight",
b"Light",
b"Regular",
b"Semibold",
b"Bold",
b"Black",
b"Black Medium Contrast",
b"Black High Contrast",
b"Default",
]
def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None:
im = Image.new("RGB", (100, 75), "white") im = Image.new("RGB", (100, 75), "white")
d = ImageDraw.Draw(im) d = ImageDraw.Draw(im)

View File

@ -15,10 +15,13 @@ def string_to_img(image_string: str) -> Image.Image:
rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)]
height = len(rows) height = len(rows)
width = len(rows[0]) width = len(rows[0])
im = Image.new("1", (width, height)) im = Image.new("L", (width, height))
for x in range(width): for i in range(width):
for y in range(height): for j in range(height):
im.putpixel((x, y), rows[y][x] in "X1") c = rows[j][i]
v = c in "X1"
im.putpixel((i, j), v)
return im return im
@ -39,10 +42,10 @@ def img_to_string(im: Image.Image) -> str:
"""Turn a (small) binary image into a string representation""" """Turn a (small) binary image into a string representation"""
chars = ".1" chars = ".1"
result = [] result = []
for y in range(im.height): for r in range(im.height):
line = "" line = ""
for x in range(im.width): for c in range(im.width):
value = im.getpixel((x, y)) value = im.getpixel((c, r))
assert not isinstance(value, tuple) assert not isinstance(value, tuple)
assert value is not None assert value is not None
line += chars[value > 0] line += chars[value > 0]
@ -162,12 +165,10 @@ def test_edge() -> None:
) )
@pytest.mark.parametrize("mode", ("1", "L")) def test_corner() -> None:
def test_corner(mode: str) -> None:
# Create a corner detector pattern # Create a corner detector pattern
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"])
image = A.convert(mode) if mode == "L" else A count, Aout = mop.apply(A)
count, Aout = mop.apply(image)
assert count == 5 assert count == 5
assert_img_equal_img_string( assert_img_equal_img_string(
Aout, Aout,
@ -183,7 +184,7 @@ def test_corner(mode: str) -> None:
) )
# Test the coordinate counting with the same operator # Test the coordinate counting with the same operator
coords = mop.match(image) coords = mop.match(A)
assert len(coords) == 4 assert len(coords) == 4
assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4))
@ -231,15 +232,15 @@ def test_negate() -> None:
def test_incorrect_mode() -> None: def test_incorrect_mode() -> None:
im = hopper("RGB")
mop = ImageMorph.MorphOp(op_name="erosion8") mop = ImageMorph.MorphOp(op_name="erosion8")
with hopper() as im: with pytest.raises(ValueError, match="Image mode must be L"):
with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.apply(im)
mop.apply(im) with pytest.raises(ValueError, match="Image mode must be L"):
with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.match(im)
mop.match(im) with pytest.raises(ValueError, match="Image mode must be L"):
with pytest.raises(ValueError, match="Image mode must be 1 or L"): mop.get_on_pixels(im)
mop.get_on_pixels(im)
def test_add_patterns() -> None: def test_add_patterns() -> None:
@ -280,11 +281,6 @@ def test_pattern_syntax_error(pattern: str) -> None:
lb.build_lut() lb.build_lut()
def test_build_default_lut() -> None:
lb = ImageMorph.LutBuilder(op_name="corner")
assert lb.build_default_lut() == lb.lut
def test_load_invalid_mrl() -> None: def test_load_invalid_mrl() -> None:
# Arrange # Arrange
invalid_mrl = "Tests/images/hopper.png" invalid_mrl = "Tests/images/hopper.png"

View File

@ -261,10 +261,10 @@ def test_colorize_2color() -> None:
# Open test image (256px by 10px, black to white) # Open test image (256px by 10px, black to white)
with Image.open("Tests/images/bw_gradient.png") as im: with Image.open("Tests/images/bw_gradient.png") as im:
im_l = im.convert("L") im = im.convert("L")
# Create image with original 2-color functionality # Create image with original 2-color functionality
im_test = ImageOps.colorize(im_l, "red", "green") im_test = ImageOps.colorize(im, "red", "green")
# Test output image (2-color) # Test output image (2-color)
left = (0, 1) left = (0, 1)
@ -301,11 +301,11 @@ def test_colorize_2color_offset() -> None:
# Open test image (256px by 10px, black to white) # Open test image (256px by 10px, black to white)
with Image.open("Tests/images/bw_gradient.png") as im: with Image.open("Tests/images/bw_gradient.png") as im:
im_l = im.convert("L") im = im.convert("L")
# Create image with original 2-color functionality with offsets # Create image with original 2-color functionality with offsets
im_test = ImageOps.colorize( im_test = ImageOps.colorize(
im_l, black="red", white="green", blackpoint=50, whitepoint=100 im, black="red", white="green", blackpoint=50, whitepoint=100
) )
# Test output image (2-color) with offsets # Test output image (2-color) with offsets
@ -343,11 +343,11 @@ def test_colorize_3color_offset() -> None:
# Open test image (256px by 10px, black to white) # Open test image (256px by 10px, black to white)
with Image.open("Tests/images/bw_gradient.png") as im: with Image.open("Tests/images/bw_gradient.png") as im:
im_l = im.convert("L") im = im.convert("L")
# Create image with new three color functionality with offsets # Create image with new three color functionality with offsets
im_test = ImageOps.colorize( im_test = ImageOps.colorize(
im_l, im,
black="red", black="red",
white="green", white="green",
mid="blue", mid="blue",
@ -457,9 +457,9 @@ def test_exif_transpose() -> None:
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()
# Orientation set directly on Image.Exif # Orientation set directly on Image.Exif
im1 = hopper() im = hopper()
im1.getexif()[0x0112] = 3 im.getexif()[0x0112] = 3
transposed_im = ImageOps.exif_transpose(im1) transposed_im = ImageOps.exif_transpose(im)
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()

View File

@ -49,12 +49,6 @@ def test_getcolor() -> None:
palette.getcolor("unknown") # type: ignore[arg-type] palette.getcolor("unknown") # type: ignore[arg-type]
def test_getcolor_rgba() -> None:
palette = ImagePalette.ImagePalette("RGBA", (1, 2, 3, 4))
palette.getcolor((5, 6, 7, 8))
assert palette.palette == b"\x01\x02\x03\x04\x05\x06\x07\x08"
def test_getcolor_rgba_color_rgb_palette() -> None: def test_getcolor_rgba_color_rgb_palette() -> None:
palette = ImagePalette.ImagePalette("RGB") palette = ImagePalette.ImagePalette("RGB")

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest import pytest
from PIL import Image, ImageDraw, ImageFont, ImageText, features from PIL import Image, ImageDraw, ImageFont, ImageText
from .helper import assert_image_similar_tofile, skip_unless_feature from .helper import assert_image_similar_tofile, skip_unless_feature
@ -20,75 +20,37 @@ def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
return request.param return request.param
@pytest.fixture( @pytest.fixture(scope="module")
scope="module", def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont:
params=[ return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine)
None,
pytest.param(ImageFont.Layout.BASIC, marks=skip_unless_feature("freetype2")),
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
],
)
def font(
request: pytest.FixtureRequest,
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont:
layout_engine = request.param
if layout_engine is None:
return ImageFont.load_default_imagefont()
else:
return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine)
def test_get_length(font: ImageFont.ImageFont | ImageFont.FreeTypeFont) -> None: def test_get_length(font: ImageFont.FreeTypeFont) -> None:
factor = 1 if isinstance(font, ImageFont.ImageFont) else 2 assert ImageText.Text("A", font).get_length() == 12
assert ImageText.Text("A", font).get_length() == 6 * factor assert ImageText.Text("AB", font).get_length() == 24
assert ImageText.Text("AB", font).get_length() == 12 * factor assert ImageText.Text("M", font).get_length() == 12
assert ImageText.Text("M", font).get_length() == 6 * factor assert ImageText.Text("y", font).get_length() == 12
assert ImageText.Text("y", font).get_length() == 6 * factor assert ImageText.Text("a", font).get_length() == 12
assert ImageText.Text("a", font).get_length() == 6 * factor
text = ImageText.Text("\n", font)
with pytest.raises(ValueError, match="can't measure length of multiline text"):
text.get_length()
@pytest.mark.parametrize( def test_get_bbox(font: ImageFont.FreeTypeFont) -> None:
"text, expected", assert ImageText.Text("A", font).get_bbox() == (0, 4, 12, 16)
( assert ImageText.Text("AB", font).get_bbox() == (0, 4, 24, 16)
("A", (0, 4, 12, 16)), assert ImageText.Text("M", font).get_bbox() == (0, 4, 12, 16)
("AB", (0, 4, 24, 16)), assert ImageText.Text("y", font).get_bbox() == (0, 7, 12, 20)
("M", (0, 4, 12, 16)), assert ImageText.Text("a", font).get_bbox() == (0, 7, 12, 16)
("y", (0, 7, 12, 20)),
("a", (0, 7, 12, 16)),
),
)
def test_get_bbox(
font: ImageFont.ImageFont | ImageFont.FreeTypeFont,
text: str,
expected: tuple[int, int, int, int],
) -> None:
if isinstance(font, ImageFont.ImageFont):
expected = (0, 0, expected[2] // 2, 11)
assert ImageText.Text(text, font).get_bbox() == expected
def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None:
if features.check_module("freetype2"): font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) text = ImageText.Text("Hello World!", font)
text = ImageText.Text("Hello World!", font) text.embed_color()
text.embed_color()
assert text.get_length() == 288
im = Image.new("RGB", (300, 64), "white") im = Image.new("RGB", (300, 64), "white")
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
draw.text((10, 10), text, "#fa6") draw.text((10, 10), text, "#fa6")
assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1)
text = ImageText.Text("", mode="1")
with pytest.raises(
ValueError, match="Embedded color supported only in RGB and RGBA modes"
):
text.embed_color()
@skip_unless_feature("freetype2") @skip_unless_feature("freetype2")

View File

@ -20,19 +20,21 @@ TEST_IMAGE_SIZE = (10, 10)
def test_numpy_to_image() -> None: def test_numpy_to_image() -> None:
def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image: def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
data = tuple(range(100))
if bands == 1: if bands == 1:
if boolean: if boolean:
data = (0, 255) * 50 data = [0, 255] * 50
else:
data = list(range(100))
a = numpy.array(data, dtype=dtype) a = numpy.array(data, dtype=dtype)
a.shape = TEST_IMAGE_SIZE a.shape = TEST_IMAGE_SIZE
i = Image.fromarray(a) i = Image.fromarray(a)
assert i.get_flattened_data() == data assert list(i.getdata()) == data
else: else:
data = list(range(100))
a = numpy.array([[x] * bands for x in data], dtype=dtype) a = numpy.array([[x] * bands for x in data], dtype=dtype)
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
i = Image.fromarray(a) i = Image.fromarray(a)
assert i.get_flattened_data(0) == tuple(range(100)) assert list(i.getchannel(0).getdata()) == list(range(100))
return i return i
# Check supported 1-bit integer formats # Check supported 1-bit integer formats
@ -189,7 +191,7 @@ def test_putdata() -> None:
arr = numpy.zeros((15000,), numpy.float32) arr = numpy.zeros((15000,), numpy.float32)
im.putdata(arr) im.putdata(arr)
assert len(im.get_flattened_data()) == len(arr) assert len(im.getdata()) == len(arr)
def test_resize() -> None: def test_resize() -> None:
@ -246,7 +248,7 @@ def test_bool() -> None:
a[0][0] = True a[0][0] = True
im2 = Image.fromarray(a) im2 = Image.fromarray(a)
assert im2.getpixel((0, 0)) == 255 assert im2.getdata()[0] == 255
def test_no_resource_warning_for_numpy_array() -> None: def test_no_resource_warning_for_numpy_array() -> None:

View File

@ -19,28 +19,30 @@ def helper_pickle_file(
# Arrange # Arrange
with Image.open(test_file) as im: with Image.open(test_file) as im:
filename = tmp_path / "temp.pkl" filename = tmp_path / "temp.pkl"
converted_im = im.convert(mode) if mode else im if mode:
im = im.convert(mode)
# Act # Act
with open(filename, "wb") as f: with open(filename, "wb") as f:
pickle.dump(converted_im, f, protocol) pickle.dump(im, f, protocol)
with open(filename, "rb") as f: with open(filename, "rb") as f:
loaded_im = pickle.load(f) loaded_im = pickle.load(f)
# Assert # Assert
assert converted_im == loaded_im assert im == loaded_im
def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
converted_im = im.convert(mode) if mode else im if mode:
im = im.convert(mode)
# Act # Act
dumped_string = pickle.dumps(converted_im, protocol) dumped_string = pickle.dumps(im, protocol)
loaded_im = pickle.loads(dumped_string) loaded_im = pickle.loads(dumped_string)
# Assert # Assert
assert converted_im == loaded_im assert im == loaded_im
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -88,18 +90,18 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange # Arrange
filename = tmp_path / "temp.pkl" filename = tmp_path / "temp.pkl"
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
im_pa = im.convert("PA") im = im.convert("PA")
# Act / Assert # Act / Assert
for protocol in range(pickle.HIGHEST_PROTOCOL + 1): for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
im_pa._mode = "LA" im._mode = "LA"
with open(filename, "wb") as f: with open(filename, "wb") as f:
pickle.dump(im_pa, f, protocol) pickle.dump(im, f, protocol)
with open(filename, "rb") as f: with open(filename, "rb") as f:
loaded_im = pickle.load(f) loaded_im = pickle.load(f)
im_pa._mode = "PA" im._mode = "PA"
assert im_pa == loaded_im assert im == loaded_im
@skip_unless_feature("webp") @skip_unless_feature("webp")

View File

@ -6,15 +6,10 @@ import pytest
from PIL import __version__ from PIL import __version__
TYPE_CHECKING = False
if TYPE_CHECKING:
from importlib.metadata import PackageMetadata
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
def map_metadata_keys(md: PackageMetadata) -> dict[str, str | list[str] | None]: def map_metadata_keys(md):
# Convert installed wheel metadata into canonical Core Metadata 2.4 format. # Convert installed wheel metadata into canonical Core Metadata 2.4 format.
# This was a utility method in pyroma 4.3.3; it was removed in 5.0. # This was a utility method in pyroma 4.3.3; it was removed in 5.0.
# This implementation is constructed from the relevant logic from # This implementation is constructed from the relevant logic from
@ -22,16 +17,16 @@ def map_metadata_keys(md: PackageMetadata) -> dict[str, str | list[str] | None]:
# upstream to Pyroma as https://github.com/regebro/pyroma/pull/116, # upstream to Pyroma as https://github.com/regebro/pyroma/pull/116,
# so it may be possible to simplify this test in future. # so it may be possible to simplify this test in future.
data = {} data = {}
for key in set(md): for key in set(md.keys()):
value = md.get_all(key) value = md.get_all(key)
key = pyroma.projectdata.normalize(key) key = pyroma.projectdata.normalize(key)
if value is not None and len(value) == 1: if len(value) == 1:
first_value = value[0] value = value[0]
if first_value.strip() != "UNKNOWN": if value.strip() == "UNKNOWN":
data[key] = first_value continue
else:
data[key] = value data[key] = value
return data return data

View File

@ -49,13 +49,11 @@ class TestShellInjection:
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im_rgb = im.convert("RGB") im = im.convert("RGB")
self.assert_save_filename_check( self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm)
tmp_path, im_rgb, GifImagePlugin._save_netpbm
)
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im_l = im.convert("L") im = im.convert("L")
self.assert_save_filename_check(tmp_path, im_l, GifImagePlugin._save_netpbm) self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm)

View File

@ -2,7 +2,7 @@
# install libimagequant # install libimagequant
archive_name=libimagequant archive_name=libimagequant
archive_version=4.4.1 archive_version=4.4.0
archive=$archive_name-$archive_version archive=$archive_name-$archive_version

View File

@ -11,7 +11,7 @@ import subprocess
TYPE_CHECKING = False TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from sphinx.application import Sphinx
DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+")
VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n")
@ -28,7 +28,7 @@ def get_date_for(git_version: str) -> str | None:
return out.split()[0] return out.split()[0]
def add_date(app: Any, doc_name: str, source: list[str]) -> None: def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None:
if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])):
old_title = m.group(1) old_title = m.group(1)
@ -43,6 +43,6 @@ def add_date(app: Any, doc_name: str, source: list[str]) -> None:
source[0] = result source[0] = result
def setup(app: Any) -> dict[str, bool]: def setup(app: Sphinx) -> dict[str, bool]:
app.connect("source-read", add_date) app.connect("source-read", add_date)
return {"parallel_read_safe": True} return {"parallel_read_safe": True}

View File

@ -73,16 +73,6 @@ Image._show
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15). ``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
Use :py:meth:`~PIL.ImageShow.show` instead. Use :py:meth:`~PIL.ImageShow.show` instead.
Image getdata()
~~~~~~~~~~~~~~~
.. deprecated:: 12.1.0
:py:meth:`~PIL.Image.Image.getdata` has been deprecated.
:py:meth:`~PIL.Image.Image.get_flattened_data` can be used instead. This new method is
identical, except that it returns a tuple of pixel values, instead of an internal
Pillow data type.
Removed features Removed features
---------------- ----------------

View File

@ -213,7 +213,6 @@ class DdsImageFile(ImageFile.ImageFile):
format_description = "DirectDraw Surface" format_description = "DirectDraw Surface"
def _open(self) -> None: def _open(self) -> None:
assert self.fp is not None
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not a DDS file" msg = "not a DDS file"
raise SyntaxError(msg) raise SyntaxError(msg)

View File

@ -999,7 +999,7 @@ where applicable:
The number of times to loop this APNG, 0 indicates infinite looping. The number of times to loop this APNG, 0 indicates infinite looping.
**duration** **duration**
The time to display this APNG frame (in milliseconds), given as a float. The time to display this APNG frame (in milliseconds).
.. note:: .. note::
@ -1041,8 +1041,9 @@ following parameters can also be set:
Defaults to 0. Defaults to 0.
**duration** **duration**
The length of time (or list or tuple of lengths of time) to display this APNG frame Integer (or list or tuple of integers) length of time to display this APNG frame
(in milliseconds). Defaults to 0. (in milliseconds).
Defaults to 0.
**disposal** **disposal**
An integer (or list or tuple of integers) specifying the APNG disposal An integer (or list or tuple of integers) specifying the APNG disposal

View File

@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.4.1** * Pillow has been tested with libimagequant **2.6-4.4.0**
* Libimagequant is licensed GPLv3, which is more restrictive than * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.
@ -116,7 +116,7 @@ Many of Pillow's features require external libraries:
.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
Prerequisites for **Ubuntu 16.04 LTS - 24.04 LTS** are installed with:: Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \

View File

@ -33,9 +33,9 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 13 Trixie | 3.13 | x86, x86-64 | | Debian 13 Trixie | 3.13 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 42 | 3.13 | x86-64 | | Fedora 41 | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 43 | 3.14 | x86-64 | | Fedora 42 | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Gentoo | 3.12 | x86-64 | | Gentoo | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
@ -53,8 +53,8 @@ These platforms are built and tested for every change.
| | | s390x | | | | s390x |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.10 | x86 | | Windows Server 2022 | 3.10 | x86 |
+----------------------------------+----------------------------+---------------------+ | +----------------------------+---------------------+
| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | | | 3.11, 3.12, 3.13, 3.14, | x86-64 |
| | PyPy3 | | | | PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 | | | 3.12 (MinGW) | x86-64 |
@ -71,102 +71,100 @@ These platforms have been reported to work at the versions mentioned.
Contributors please test Pillow on your platform then update this Contributors please test Pillow on your platform then update this
document and send a pull request. document and send a pull request.
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Operating system | | Tested Python | | Latest tested | | Tested | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | versions | | Pillow version | | processors |
+==================================+=============================+==================+==============+ +==================================+============================+==================+==============+
| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.0.0 |arm | | macOS 26 Tahoe | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm |
| +-----------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.9 | 11.3.0 | | | macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm |
+----------------------------------+-----------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13 | 11.3.0 |arm | | | 3.8 | 10.4.0 | |
| +-----------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.8 | 10.4.0 | | | macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
+----------------------------------+-----------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | | | 3.7 | 9.5.0 | |
| +-----------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.7 | 9.5.0 | | | macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
+----------------------------------+-----------------------------+------------------+--------------+ | +----------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
| +-----------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | | | 3.6 | 8.4.0 | |
| +-----------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.6 | 8.4.0 | | | macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | | | 3.5 | 7.2.0 | |
| +-----------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.5 | 7.2.0 | | | macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | | | 2.7 | 6.0.0 | |
| +-----------------------------+------------------+ | | +----------------------------+------------------+ |
| | 2.7 | 6.0.0 | | | | 3.4 | 5.4.1 | |
| +-----------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.4 | 5.4.1 | | | macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | | macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | | Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | | | 3.3 | 4.1.0 | |
| +-----------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.3 | 4.1.0 | | | Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | | Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | | Redhat Linux 6 | 2.6 | |x86 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Redhat Linux 6 | 2.6 | |x86 | | CentOS 6.3 | 2.7, 3.3 | |x86 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| CentOS 6.3 | 2.7, 3.3 | |x86 | | CentOS 8 | 3.9 | 9.0.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| CentOS 8 | 3.9 | 9.0.0 |x86-64 | | Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | | Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ | | | PyPy5.3.1, PyPy3 v2.4.0 | | |
| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | | +----------------------------+------------------+--------------+
| | | PyPy5.3.1, PyPy3 v2.4.0 | | | | | 2.7 | 4.3.0 |x86-64 |
| +-----------------------------+------------------+--------------+ | +----------------------------+------------------+--------------+
| | 2.7 | 4.3.0 |x86-64 | | | 2.7, 3.2 | 3.4.1 |ppc |
| +-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| | 2.7, 3.2 | 3.4.1 |ppc | | Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | | Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | | Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | | Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | | Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm |
+----------------------------------+-----------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | | | 2.7 | 6.2.2 | |
| +-----------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 2.7 | 6.2.2 | | | Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | | FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | | FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | | Windows 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13 | 11.0.0 |arm64 | | Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | | Windows 10 | 3.7 | 7.1.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 10 | 3.7 | 7.1.0 |x86-64 | | Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | | Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | | Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | | Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | | Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
+----------------------------------+-----------------------------+------------------+--------------+

View File

@ -191,7 +191,6 @@ This helps to get the bounding box coordinates of the input image::
.. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.getchannel
.. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getcolors
.. automethod:: PIL.Image.Image.getdata .. automethod:: PIL.Image.Image.getdata
.. automethod:: PIL.Image.Image.get_flattened_data
.. automethod:: PIL.Image.Image.getexif .. automethod:: PIL.Image.Image.getexif
.. automethod:: PIL.Image.Image.getextrema .. automethod:: PIL.Image.Image.getextrema
.. automethod:: PIL.Image.Image.getpalette .. automethod:: PIL.Image.Image.getpalette

View File

@ -44,11 +44,9 @@ or the clipboard to a PIL image memory.
.. versionadded:: 7.1.0 .. versionadded:: 7.1.0
:param window: :param window:
Capture a single window. On Windows, this is a HWND. On macOS, this is a HWND, to capture a single window. Windows only.
CGWindowID.
.. versionadded:: 11.2.1 Windows support .. versionadded:: 11.2.1
.. versionadded:: 12.1.0 macOS support
:return: An image :return: An image
.. py:function:: grabclipboard() .. py:function:: grabclipboard()

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