diff --git a/.ci/install.sh b/.ci/install.sh
index 52b821417..aeb5e6514 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -27,14 +27,13 @@ python3 -m pip install --upgrade wheel
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
-python3 -m pip install numpy
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-# optional test dependency, only install if there's a binary package.
-# fails on beta 3.14 and PyPy
+# optional test dependencies, only install if there's a binary package.
+python3 -m pip install --only-binary=:all: numpy || true
python3 -m pip install --only-binary=:all: pyarrow || true
# PyQt6 doesn't support PyPy3
diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt
index 6ca35d286..5b0e2eaf8 100644
--- a/.ci/requirements-mypy.txt
+++ b/.ci/requirements-mypy.txt
@@ -1,4 +1,4 @@
-mypy==1.18.2
+mypy==1.19.0
arro3-compute
arro3-core
IceSpringPySideStubs-PyQt6
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index cf917407c..e88abf16f 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -32,7 +32,7 @@ jobs:
name: Docs
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
persist-credentials: false
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 2addbaf67..4f67be6f7 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,55 +2,31 @@ name: Lint
on: [push, pull_request, workflow_dispatch]
+permissions: {}
+
env:
FORCE_COLOR: 1
-
-permissions:
- contents: read
+ PREK_COLOR: always
+ RUFF_OUTPUT_FORMAT: github
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
- build:
-
+ lint:
runs-on: ubuntu-latest
-
name: Lint
-
steps:
- - uses: actions/checkout@v5
- with:
- persist-credentials: false
-
- - name: pre-commit cache
- uses: actions/cache@v4
- with:
- path: ~/.cache/pre-commit
- key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
- restore-keys: |
- lint-pre-commit-
-
- - 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
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+ - uses: actions/setup-python@v6
+ with:
+ python-version: "3.10"
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
+ - name: Lint
+ run: uvx --with tox-uv tox -e lint
+ - name: Mypy
+ run: uvx --with tox-uv tox -e mypy
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index b114d4a23..7c768af48 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -26,9 +26,8 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-python3 -m pip install numpy
-# optional test dependency, only install if there's a binary package.
-# fails on beta 3.14 and PyPy
+# optional test dependencies, only install if there's a binary package.
+python3 -m pip install --only-binary=:all: numpy || true
python3 -m pip install --only-binary=:all: pyarrow || true
# libavif
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 213062ee2..091edb222 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -68,7 +68,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
persist-credentials: false
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index 6c4206083..e247414c8 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -45,7 +45,7 @@ jobs:
steps:
- name: Checkout Pillow
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
persist-credentials: false
diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml
index 0f36fe30d..bd244aa5a 100644
--- a/.github/workflows/test-valgrind-memory.yml
+++ b/.github/workflows/test-valgrind-memory.yml
@@ -41,7 +41,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
persist-credentials: false
diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml
index 30caa0d4e..81cfb8456 100644
--- a/.github/workflows/test-valgrind.yml
+++ b/.github/workflows/test-valgrind.yml
@@ -39,7 +39,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
persist-credentials: false
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 02d4da999..e864763da 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -31,15 +31,16 @@ env:
jobs:
build:
- runs-on: windows-latest
+ runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
- python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"]
+ python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"]
architecture: ["x64"]
+ os: ["windows-latest"]
include:
# Test the oldest Python on 32-bit
- - { python-version: "3.10", architecture: "x86" }
+ - { python-version: "3.10", architecture: "x86", os: "windows-2022" }
timeout-minutes: 45
@@ -47,19 +48,19 @@ jobs:
steps:
- name: Checkout Pillow
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
persist-credentials: false
- name: Checkout cached dependencies
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
persist-credentials: false
repository: python-pillow/pillow-depends
path: winbuild\depends
- name: Checkout extra test images
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
persist-credentials: false
repository: python-pillow/test-images
@@ -83,7 +84,7 @@ jobs:
python3 -m pip install --upgrade pip
- name: Install CPython dependencies
- if: "!contains(matrix.python-version, 'pypy') && !contains(matrix.python-version, '3.14') && matrix.architecture != 'x86'"
+ if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
run: |
python3 -m pip install PyQt6
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ef7b34b8d..da3eea066 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -42,6 +42,8 @@ jobs:
]
python-version: [
"pypy3.11",
+ "3.15t",
+ "3.15",
"3.14t",
"3.14",
"3.13t",
@@ -54,6 +56,7 @@ jobs:
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.11", PYTHONOPTIMIZE: 2 }
# Free-threaded
+ - { python-version: "3.15t", disable-gil: true }
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
# Intel
@@ -65,7 +68,7 @@ jobs:
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
persist-credentials: false
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 226fcdb6a..e1586b7c5 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -32,7 +32,6 @@ if [[ "$CIBW_PLATFORM" == "ios" ]]; then
# or `build/deps/iphonesimulator`
WORKDIR=$(pwd)/build/$IOS_SDK
BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK
- PATCH_DIR=$(pwd)/patches/iOS
# GNU tooling insists on using aarch64 rather than arm64
if [[ $PLAT == "arm64" ]]; then
@@ -90,27 +89,25 @@ fi
ARCHIVE_SDIR=pillow-depends-main
-# 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.
+# Package versions for fresh source builds.
if [[ -n "$IOS_SDK" ]]; then
FREETYPE_VERSION=2.13.3
else
FREETYPE_VERSION=2.14.1
fi
-HARFBUZZ_VERSION=12.2.0
-LIBPNG_VERSION=1.6.50
-JPEGTURBO_VERSION=3.1.2
+HARFBUZZ_VERSION=12.3.0
+LIBPNG_VERSION=1.6.53
+JPEGTURBO_VERSION=3.1.3
OPENJPEG_VERSION=2.5.4
-XZ_VERSION=5.8.1
+XZ_VERSION=5.8.2
ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.1
LCMS2_VERSION=2.17
-ZLIB_NG_VERSION=2.2.5
+ZLIB_NG_VERSION=2.3.2
LIBWEBP_VERSION=1.6.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
-BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file.
+BROTLI_VERSION=1.2.0
LIBAVIF_VERSION=1.3.0
function build_pkg_config {
@@ -149,18 +146,9 @@ function build_zlib_ng {
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS
unset HOST_CONFIGURE_FLAGS
- build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat
+ build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat
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
}
@@ -168,7 +156,7 @@ function build_brotli {
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)
(cd $out_dir \
- && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
+ && 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 . \
&& make -j4 install)
touch brotli-stamp
}
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index e33d74a81..fb71ead37 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -107,7 +107,7 @@ jobs:
os: macos-15-intel
cibw_arch: x86_64_iphonesimulator
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
persist-credentials: false
submodules: true
@@ -154,12 +154,12 @@ jobs:
- cibw_arch: ARM64
os: windows-11-arm
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
persist-credentials: false
- name: Checkout extra test images
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
persist-credentials: false
repository: python-pillow/test-images
@@ -235,7 +235,7 @@ jobs:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
persist-credentials: false
diff --git a/.github/zizmor.yml b/.github/zizmor.yml
index b56709781..f4949c30c 100644
--- a/.github/zizmor.yml
+++ b/.github/zizmor.yml
@@ -1,6 +1,7 @@
-# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://docs.zizmor.sh/configuration/
rules:
+ obfuscation:
+ disable: true
unpinned-uses:
config:
policies:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a7dbc9a78..10343f91a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,17 +1,17 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.14.3
+ rev: v0.14.7
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
- rev: 25.9.0
+ rev: 25.11.0
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
- rev: 1.8.6
+ rev: 1.9.2
hooks:
- id: bandit
args: [--severity-level=high]
@@ -21,10 +21,10 @@ repos:
rev: v1.5.5
hooks:
- id: remove-tabs
- exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
+ exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
- rev: v21.1.2
+ rev: v21.1.6
hooks:
- id: clang-format
types: [c]
@@ -46,29 +46,29 @@ repos:
- id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer
- exclude: ^Tests/images/|\.patch$
+ exclude: ^Tests/images/
- id: trailing-whitespace
- exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
+ exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.34.1
+ rev: 0.35.0
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/zizmorcore/zizmor-pre-commit
- rev: v1.16.2
+ rev: v1.18.0
hooks:
- id: zizmor
- repo: https://github.com/sphinx-contrib/sphinx-lint
- rev: v1.0.1
+ rev: v1.0.2
hooks:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: v2.11.0
+ rev: v2.11.1
hooks:
- id: pyproject-fmt
@@ -76,7 +76,7 @@ repos:
rev: v0.24.1
hooks:
- id: validate-pyproject
- additional_dependencies: [trove-classifiers>=2024.10.12]
+ additional_dependencies: [tomli, trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.7.0
diff --git a/MANIFEST.in b/MANIFEST.in
index 6623f227d..d4623a4a8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -15,7 +15,6 @@ include tox.ini
graft Tests
graft Tests/images
graft checks
-graft patches
graft src
graft depends
graft winbuild
diff --git a/Tests/fonts/AdobeVFPrototypeDuplicates.ttf b/Tests/fonts/AdobeVFPrototypeDuplicates.ttf
new file mode 100644
index 000000000..acf0bc156
Binary files /dev/null and b/Tests/fonts/AdobeVFPrototypeDuplicates.ttf differ
diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt
index 3c8a23197..94989af90 100644
--- a/Tests/fonts/LICENSE.txt
+++ b/Tests/fonts/LICENSE.txt
@@ -2,7 +2,7 @@
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
-AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
+AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype. AdobeVFPrototypeDuplicates.ttf is a modified version of this
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
ter-x20b.pcf, from http://terminus-font.sourceforge.net/
diff --git a/Tests/helper.py b/Tests/helper.py
index dbdd30b42..d77b4b807 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -55,8 +55,8 @@ def convert_to_comparable(
if a.mode == "P":
new_a = Image.new("L", a.size)
new_b = Image.new("L", b.size)
- new_a.putdata(a.getdata())
- new_b.putdata(b.getdata())
+ new_a.putdata(a.get_flattened_data())
+ new_b.putdata(b.get_flattened_data())
elif a.mode == "I;16":
new_a = a.convert("I")
new_b = b.convert("I")
@@ -104,10 +104,9 @@ def assert_image_equal_tofile(
msg: str | None = None,
mode: str | None = None,
) -> None:
- with Image.open(filename) as img:
- if mode:
- img = img.convert(mode)
- assert_image_equal(a, img, msg)
+ with Image.open(filename) as im:
+ converted_im = im.convert(mode) if mode else im
+ assert_image_equal(a, converted_im, msg)
def assert_image_similar(
diff --git a/Tests/images/bmp/html/bkgd.png b/Tests/images/bmp/html/bkgd.png
deleted file mode 100644
index d66ca9d65..000000000
Binary files a/Tests/images/bmp/html/bkgd.png and /dev/null differ
diff --git a/Tests/images/bmp/html/bmpsuite.html b/Tests/images/bmp/html/bmpsuite.html
deleted file mode 100644
index b8e327ed9..000000000
--- a/Tests/images/bmp/html/bmpsuite.html
+++ /dev/null
@@ -1,578 +0,0 @@
-
-
-
-
-
-BMP Suite Image List
-
-
-
-
-
-
-
-BMP Suite Image List
-
-For BMP Suite
-version 2.3
-
-This document describes the images in BMP Suite, and shows what
-I allege to be the correct way to interpret them. PNG and JPEG images are
-used for reference.
-
-
-It also shows how your web browser displays the BMP images,
-but that’s not its main purpose.
-BMP is poor image format to use on web pages, so a web browser’s
-level of support for it is arguably not important.
-
-
-
-
- | File |
- Ver. |
- Correct display |
- In your browser |
- Notes |
-
-
-
- | g/pal1.bmp |
- 3 |
-  |
-  |
- 1 bit/pixel paletted image, in which black is the first color in
- the palette. |
-
-
-
- | g/pal1wb.bmp |
- 3 |
-  |
-  |
- 1 bit/pixel paletted image, in which white is the first color in
- the palette. |
-
-
-
- | g/pal1bg.bmp |
- 3 |
-  |
-  |
- 1 bit/pixel paletted image, with colors other than black and white. |
-
-
-
- | q/pal1p1.bmp |
- 3 |
-  |
-  |
- 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 “up to 2”), but it would be silly for a viewer not to
- support a size of 1. |
-
-
-
- | q/pal2.bmp |
- 3 |
-  |
-  |
- A paletted image with 2 bits/pixel. Usually only 1, 4,
- and 8 are allowed, but 2 is legal on Windows CE. |
-
-
-
- | g/pal4.bmp |
- 3 |
-  |
-  |
- Paletted image with 12 palette colors, and 4 bits/pixel. |
-
-
-
- | g/pal4rle.bmp |
- 3 |
-  |
-  |
- 4-bit image that uses RLE compression. |
-
-
-
- | q/pal4rletrns.bmp |
- 3 |
- 
- or

- or
 |
-  |
- An RLE-compressed image that used “delta”
- 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). |
-
-
-
- | g/pal8.bmp |
- 3 |
-  |
-  |
- Our standard paletted image, with 252 palette colors, and 8
- bits/pixel. |
-
-
-
- | g/pal8-0.bmp |
- 3 |
-  |
-  |
- 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. |
-
-
-
- | g/pal8rle.bmp |
- 3 |
-  |
-  |
- 8-bit image that uses RLE compression. |
-
-
-
- | q/pal8rletrns.bmp |
- 3 |
- 
- or

- or
 |
-  |
- 8-bit version of q/pal4rletrns.bmp. |
-
-
-
- | g/pal8w126.bmp |
- 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. |
-
-
-
- | g/pal8w125.bmp |
- 3 |
-  |
-  |
-
-
-
- | g/pal8w124.bmp |
- 3 |
-  |
-  |
-
-
-
- | g/pal8topdown.bmp |
- 3 |
-  |
-  |
- BMP images are normally stored from the bottom up, but
- there is a way to store them from the top down. |
-
-
-
- | q/pal8offs.bmp |
- 3 |
-  |
-  |
- A file with some unused bytes between the palette and the
- image. This is probably valid, but I’m not 100% sure. |
-
-
-
- | q/pal8oversizepal.bmp |
- 3 |
-  |
-  |
- 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’t allowed
- to have more than 256 colors. |
-
-
-
- | g/pal8nonsquare.bmp |
- 3 |
-
- 
- or
-
- |
-  |
- An image with non-square pixels: the X pixels/meter is twice
- the Y pixels/meter. Image editors can be expected to
- leave the image “squashed”; image viewers should
- consider stretching it to its correct proportions. |
-
-
-
- | g/pal8os2.bmp |
- OS/2v1 |
-  |
-  |
- An OS/2-style bitmap. |
-
-
-
- | q/pal8os2sp.bmp |
- OS/2v1 |
-  |
-  |
- An OS/2v1 with a less-than-full-sized palette.
- Probably not valid, but such files have been seen in the wild. |
-
-
-
- | q/pal8os2v2.bmp |
- OS/2v2 |
-  |
-  |
- My attempt to make an OS/2v2 bitmap. |
-
-
-
- | q/pal8os2v2-16.bmp |
- OS/2v2 |
-  |
-  |
- An OS/2v2 bitmap whose header has only 16 bytes, instead of the full 64. |
-
-
-
- | g/pal8v4.bmp |
- 4 |
-  |
-  |
- A v4 bitmap. I’m not sure that the gamma and chromaticity values in
- this file are sensible, because I can’t find any detailed documentation
- of them. |
-
-
-
- | g/pal8v5.bmp |
- 5 |
-  |
-  |
- A v5 bitmap. Version 5 has additional colorspace options over v4, so it
- is easier to create, and ought to be more portable. |
-
-
-
- | g/rgb16.bmp |
- 3 |
-  |
-  |
- 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:
- (255,255,255), not
- (248,248,248). |
-
-
-
- | g/rgb16-565.bmp |
- 3 |
-  |
-  |
- 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’t support any other non-default 16-bit
- formats.
- The whitest colors should be displayed as pure white:
- (255,255,255), not
- (248,252,248). |
-
-
-
- | g/rgb16-565pal.bmp |
- 3 |
-  |
-  |
- A 16-bit image with both a BITFIELDS segment and a palette. |
-
-
-
- | q/rgb16-231.bmp |
- 3 |
-  |
-  |
- 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’s because they’re doing simple
- bit-shifting (possibly including one round of bit replication), instead of
- proper scaling. |
-
-
-
- | q/rgba16-4444.bmp |
- 5 |
-  |
-  |
- A 16-bit image with an alpha channel. There are 4 bits for each color
- channel, and 4 bits for the alpha channel.
- It’s not clear if this is valid, but I can’t find anything that
- suggests it isn’t.
- |
-
-
-
- | g/rgb24.bmp |
- 3 |
-  |
-  |
- A perfectly ordinary 24-bit (truecolor) image. |
-
-
-
- | g/rgb24pal.bmp |
- 3 |
-  |
-  |
- 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. |
-
-
-
- | q/rgb24largepal.bmp |
- 3 |
-  |
-  |
- 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. |
-
-
-
- | q/rgb24prof.bmp |
- 5 |
-  |
-  |
- My attempt to make a BMP file with an embedded color profile. |
-
-
-
- | q/rgb24lprof.bmp |
- 5 |
-  |
-  |
- My attempt to make a BMP file with a linked color profile. |
-
-
-
- | q/rgb24jpeg.bmp |
- 5 |
-  |
-  |
- My attempt to make BMP files with embedded JPEG and PNG images.
- These are not likely to be supported by much of anything (they’re
- intended for printers). |
-
-
-
- | q/rgb24png.bmp |
- 5 |
-  |
-  |
-
-
-
- | g/rgb32.bmp |
- 3 |
-  |
-  |
- 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. |
-
-
-
- | g/rgb32bf.bmp |
- 3 |
-  |
-  |
- 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. |
-
-
-
- | q/rgb32fakealpha.bmp |
- 3 |
- 
- or
-
- |
-  |
- 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. |
-
-
-
- | q/rgb32-111110.bmp |
- 3 |
-  |
-  |
- 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. |
-
-
-
- | q/rgba32.bmp |
- 5 |
-  |
-  |
- A BMP with an alpha channel. Transparency is barely documented,
- so it’s possible 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. |
-
-
-
- | q/rgba32abf.bmp |
- 3 |
-  |
-  |
- An image of type BI_ALHPABITFIELDS. Supposedly, this was used on
- Windows CE. I don’t know whether it is constructed correctly. |
-
-
-
-
- | b/badbitcount.bmp |
- 3 |
- N/A |
-  |
- Header indicates an absurdly large number of bits/pixel. |
-
-
-
- | b/badbitssize.bmp |
- 3 |
- N/A |
-  |
- Header incorrectly indicates that the bitmap is several GB in size. |
-
-
-
- | b/baddens1.bmp |
- 3 |
- N/A |
-  |
- Density (pixels per meter) suggests the image is much
- larger in one dimension than the other. |
-
-
-
- | b/baddens2.bmp |
- 3 |
- N/A |
-  |
-
-
-
- | b/badfilesize.bmp |
- 3 |
- N/A |
-  |
- Header incorrectly indicates that the file is several GB in size. |
-
-
-
- | b/badheadersize.bmp |
- ? |
- N/A |
-  |
- Header size is 66 bytes, which is not a valid size for any known BMP
- version. |
-
-
-
- | b/badpalettesize.bmp |
- 3 |
- N/A |
-  |
- Header incorrectly indicates that the palette contains an absurdly large
- number of colors. |
-
-
-
- | b/badplanes.bmp |
- 3 |
- N/A |
-  |
- The “planes” setting, which is required to be 1, is not 1. |
-
-
-
- | b/badrle.bmp |
- 3 |
- N/A |
-  |
- An invalid RLE-compressed image that tries to cause buffer overruns. |
-
-
-
- | b/badwidth.bmp |
- 3 |
- N/A |
-  |
- The image claims to be a negative number of pixels in width. |
-
-
-
- | b/pal8badindex.bmp |
- 3 |
- N/A |
-  |
- Many of the palette indices used in the image are not present in the
- palette. |
-
-
-
- | b/reallybig.bmp |
- 3 |
- N/A |
-  |
- An image with a very large reported width and height. |
-
-
-
- | b/rletopdown.bmp |
- 3 |
- N/A |
-  |
- An RLE-compressed image that tries to use top-down orientation,
- which isn’t allowed. |
-
-
-
- | b/shortfile.bmp |
- 3 |
- N/A |
-  |
- A file that has been truncated in the middle of the bitmap. |
-
-
-
-
-
-
-
diff --git a/Tests/images/bmp/html/fakealpha.png b/Tests/images/bmp/html/fakealpha.png
deleted file mode 100644
index 89292bcbb..000000000
Binary files a/Tests/images/bmp/html/fakealpha.png and /dev/null differ
diff --git a/Tests/images/bmp/html/pal1p1.png b/Tests/images/bmp/html/pal1p1.png
deleted file mode 100644
index 92fc0f945..000000000
Binary files a/Tests/images/bmp/html/pal1p1.png and /dev/null differ
diff --git a/Tests/images/bmp/html/pal2.png b/Tests/images/bmp/html/pal2.png
deleted file mode 100644
index 1bbfe175f..000000000
Binary files a/Tests/images/bmp/html/pal2.png and /dev/null differ
diff --git a/Tests/images/bmp/html/pal4rletrns-0.png b/Tests/images/bmp/html/pal4rletrns-0.png
deleted file mode 100644
index b689c842a..000000000
Binary files a/Tests/images/bmp/html/pal4rletrns-0.png and /dev/null differ
diff --git a/Tests/images/bmp/html/pal4rletrns-b.png b/Tests/images/bmp/html/pal4rletrns-b.png
deleted file mode 100644
index 9befa575f..000000000
Binary files a/Tests/images/bmp/html/pal4rletrns-b.png and /dev/null differ
diff --git a/Tests/images/bmp/html/pal4rletrns.png b/Tests/images/bmp/html/pal4rletrns.png
deleted file mode 100644
index 9b0c04436..000000000
Binary files a/Tests/images/bmp/html/pal4rletrns.png and /dev/null differ
diff --git a/Tests/images/bmp/html/pal8nonsquare-v.png b/Tests/images/bmp/html/pal8nonsquare-v.png
deleted file mode 100644
index a1cd1ab18..000000000
Binary files a/Tests/images/bmp/html/pal8nonsquare-v.png and /dev/null differ
diff --git a/Tests/images/bmp/html/pal8rletrns-0.png b/Tests/images/bmp/html/pal8rletrns-0.png
deleted file mode 100644
index a1c1fda50..000000000
Binary files a/Tests/images/bmp/html/pal8rletrns-0.png and /dev/null differ
diff --git a/Tests/images/bmp/html/pal8rletrns-b.png b/Tests/images/bmp/html/pal8rletrns-b.png
deleted file mode 100644
index 1ede504d4..000000000
Binary files a/Tests/images/bmp/html/pal8rletrns-b.png and /dev/null differ
diff --git a/Tests/images/bmp/html/pal8rletrns.png b/Tests/images/bmp/html/pal8rletrns.png
deleted file mode 100644
index 2d8e957f1..000000000
Binary files a/Tests/images/bmp/html/pal8rletrns.png and /dev/null differ
diff --git a/Tests/images/bmp/html/rgb16-231.png b/Tests/images/bmp/html/rgb16-231.png
deleted file mode 100644
index 76efe526e..000000000
Binary files a/Tests/images/bmp/html/rgb16-231.png and /dev/null differ
diff --git a/Tests/images/bmp/html/rgb24.jpg b/Tests/images/bmp/html/rgb24.jpg
deleted file mode 100644
index c43698c9b..000000000
Binary files a/Tests/images/bmp/html/rgb24.jpg and /dev/null differ
diff --git a/Tests/images/bmp/html/rgba16-4444.png b/Tests/images/bmp/html/rgba16-4444.png
deleted file mode 100644
index bfeda6fae..000000000
Binary files a/Tests/images/bmp/html/rgba16-4444.png and /dev/null differ
diff --git a/Tests/images/bmp/html/rgba32.png b/Tests/images/bmp/html/rgba32.png
deleted file mode 100644
index 25e542a65..000000000
Binary files a/Tests/images/bmp/html/rgba32.png and /dev/null differ
diff --git a/Tests/images/morph_a.png b/Tests/images/morph_a.png
index 19f6b777f..035fbc4bb 100644
Binary files a/Tests/images/morph_a.png and b/Tests/images/morph_a.png differ
diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py
index 82cab39c6..8fbd73748 100644
--- a/Tests/test_bmp_reference.py
+++ b/Tests/test_bmp_reference.py
@@ -72,7 +72,7 @@ def test_good() -> None:
"pal8-0.bmp": "pal8.png",
"pal8rle.bmp": "pal8.png",
"pal8topdown.bmp": "pal8.png",
- "pal8nonsquare.bmp": "pal8nonsquare-v.png",
+ "pal8nonsquare.bmp": "pal8nonsquare-e.png",
"pal8os2.bmp": "pal8.png",
"pal8os2sp.bmp": "pal8.png",
"pal8os2v2.bmp": "pal8.png",
@@ -95,16 +95,16 @@ def test_good() -> None:
for f in get_files("g"):
try:
with Image.open(f) as im:
- im.load()
with Image.open(get_compare(f)) as compare:
- compare.load()
- if im.mode == "P":
- # assert image similar doesn't really work
- # with paletized image, since the palette might
- # be differently ordered for an equivalent image.
- im = im.convert("RGBA")
- compare = im.convert("RGBA")
- assert_image_similar(im, compare, 5)
+ # assert image similar doesn't really work
+ # with paletized image, since the palette might
+ # be differently ordered for an equivalent image.
+ im_converted = im.convert("RGBA") if im.mode == "P" else im
+ compare_converted = (
+ compare.convert("RGBA") if im.mode == "P" else compare
+ )
+
+ assert_image_similar(im_converted, compare_converted, 5)
except Exception as msg:
# there are three here that are unsupported:
diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py
index cb267b204..07e62db8c 100644
--- a/Tests/test_box_blur.py
+++ b/Tests/test_box_blur.py
@@ -28,9 +28,13 @@ 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:
- it = iter(im.getdata())
+ it = iter(im.get_flattened_data())
for data_row in data:
- im_row = [next(it) for _ in range(im.size[0])]
+ im_row = []
+ 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)):
assert im_row == data_row
with pytest.raises(StopIteration):
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index 12204b5b7..b57a1d1ad 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+from io import BytesIO
from pathlib import Path
import pytest
@@ -277,25 +278,25 @@ def test_apng_mode() -> None:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
- im = im.convert("RGB")
- assert im.getpixel((0, 0)) == (0, 255, 0)
- assert im.getpixel((64, 32)) == (0, 255, 0)
+ im_rgb = im.convert("RGB")
+ assert im_rgb.getpixel((0, 0)) == (0, 255, 0)
+ assert im_rgb.getpixel((64, 32)) == (0, 255, 0)
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
- im = im.convert("RGBA")
- assert im.getpixel((0, 0)) == (0, 255, 0, 255)
- assert im.getpixel((64, 32)) == (0, 255, 0, 255)
+ im_rgba = im.convert("RGBA")
+ assert im_rgba.getpixel((0, 0)) == (0, 255, 0, 255)
+ assert im_rgba.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
- im = im.convert("RGBA")
- assert im.getpixel((0, 0)) == (0, 0, 255, 128)
- assert im.getpixel((64, 32)) == (0, 0, 255, 128)
+ im_rgba = im.convert("RGBA")
+ assert im_rgba.getpixel((0, 0)) == (0, 0, 255, 128)
+ assert im_rgba.getpixel((64, 32)) == (0, 0, 255, 128)
def test_apng_chunk_errors() -> None:
@@ -517,6 +518,24 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
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:
test_file = tmp_path / "temp.png"
size = (128, 64)
@@ -718,6 +737,25 @@ def test_apng_save_size(tmp_path: Path) -> None:
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:
im = Image.open("Tests/images/apng/delay.png")
im.seek(1)
diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py
index 727191153..ffc4ce021 100644
--- a/Tests/test_file_avif.py
+++ b/Tests/test_file_avif.py
@@ -121,7 +121,6 @@ class TestFileAvif:
assert image.size == (128, 128)
assert image.format == "AVIF"
assert image.get_format_mimetype() == "image/avif"
- image.getdata()
# generated with:
# avifdec hopper.avif hopper_avif_write.png
@@ -143,7 +142,6 @@ class TestFileAvif:
assert reloaded.mode == "RGB"
assert reloaded.size == (128, 128)
assert reloaded.format == "AVIF"
- reloaded.getdata()
# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index c1c430aa5..28e863459 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -165,9 +165,9 @@ def test_rgba_bitfields() -> None:
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
# So before the comparing the image, swap the channels
b, g, r = im.split()[1:]
- im = Image.merge("RGB", (r, g, b))
+ im_rgb = Image.merge("RGB", (r, g, b))
- assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
+ assert_image_equal_tofile(im_rgb, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
# This test image has been manually hexedited
# to change the bitfield compression in the header from XBGR to ABGR
diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py
index 362578c56..8c6bb1a69 100644
--- a/Tests/test_file_bufrstub.py
+++ b/Tests/test_file_bufrstub.py
@@ -61,6 +61,7 @@ def test_handler(tmp_path: Path) -> None:
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
self.loaded = True
+ assert im.fp is not None
im.fp.close()
return Image.new("RGB", (1, 1))
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index 60d0c09bc..931ff02f1 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -57,7 +57,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
def test_sanity_dxt1_bc1(image_path: str) -> None:
"""Check DXT1 and BC1 images can be opened"""
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
- target = target.convert("RGBA")
+ target_rgba = target.convert("RGBA")
with Image.open(image_path) as im:
im.load()
@@ -65,7 +65,7 @@ def test_sanity_dxt1_bc1(image_path: str) -> None:
assert im.mode == "RGBA"
assert im.size == (256, 256)
- assert_image_equal(im, target)
+ assert_image_equal(im, target_rgba)
def test_sanity_dxt3() -> None:
@@ -520,9 +520,9 @@ def test_save_dx10_bc5(tmp_path: Path) -> None:
im.save(out, pixel_format="BC5")
assert_image_similar_tofile(im, out, 9.56)
- im = hopper("L")
+ im_l = hopper("L")
with pytest.raises(OSError, match="only RGB mode can be written as BC5"):
- im.save(out, pixel_format="BC5")
+ im_l.save(out, pixel_format="BC5")
@pytest.mark.parametrize(
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index b50915f28..d4e8db4f4 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -265,9 +265,9 @@ def test_bytesio_object() -> None:
img.load()
with Image.open(FILE1_COMPARE) as image1_scale1_compare:
- image1_scale1_compare = image1_scale1_compare.convert("RGB")
- image1_scale1_compare.load()
- assert_image_similar(img, image1_scale1_compare, 5)
+ image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB")
+ image1_scale1_compare_rgb.load()
+ assert_image_similar(img, image1_scale1_compare_rgb, 5)
@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:
image1_scale1.load()
with Image.open(FILE1_COMPARE) as image1_scale1_compare:
- image1_scale1_compare = image1_scale1_compare.convert("RGB")
- image1_scale1_compare.load()
- assert_image_similar(image1_scale1, image1_scale1_compare, 5)
+ image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB")
+ image1_scale1_compare_rgb.load()
+ assert_image_similar(image1_scale1, image1_scale1_compare_rgb, 5)
# Non-zero bounding box
with Image.open(FILE2) as image2_scale1:
image2_scale1.load()
with Image.open(FILE2_COMPARE) as image2_scale1_compare:
- image2_scale1_compare = image2_scale1_compare.convert("RGB")
- image2_scale1_compare.load()
- assert_image_similar(image2_scale1, image2_scale1_compare, 10)
+ image2_scale1_compare_rgb = image2_scale1_compare.convert("RGB")
+ image2_scale1_compare_rgb.load()
+ assert_image_similar(image2_scale1, image2_scale1_compare_rgb, 10)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@@ -324,18 +324,16 @@ def test_render_scale2() -> None:
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
image1_scale2.load(scale=2)
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
- image1_scale2_compare = image1_scale2_compare.convert("RGB")
- image1_scale2_compare.load()
- assert_image_similar(image1_scale2, image1_scale2_compare, 5)
+ image1_scale2_compare_rgb = image1_scale2_compare.convert("RGB")
+ assert_image_similar(image1_scale2, image1_scale2_compare_rgb, 5)
# Non-zero bounding box
with Image.open(FILE2) as image2_scale2:
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
- image2_scale2_compare = image2_scale2_compare.convert("RGB")
- image2_scale2_compare.load()
- assert_image_similar(image2_scale2, image2_scale2_compare, 10)
+ image2_scale2_compare_rgb = image2_scale2_compare.convert("RGB")
+ assert_image_similar(image2_scale2, image2_scale2_compare_rgb, 10)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@@ -345,8 +343,8 @@ def test_render_scale2() -> None:
def test_resize(filename: str) -> None:
with Image.open(filename) as im:
new_size = (100, 100)
- im = im.resize(new_size)
- assert im.size == new_size
+ im_resized = im.resize(new_size)
+ assert im_resized.size == new_size
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index acf79374e..2615f5a60 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -327,14 +327,13 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None:
im.seek(1)
assert im.mode == mode
- if mode == "RGBA":
- im = im.convert("RGB")
+ im_rgb = im.convert("RGB") if mode == "RGBA" else im
# Check a color only from the old palette
- assert im.getpixel((0, 0)) == original_color
+ assert im_rgb.getpixel((0, 0)) == original_color
# Check a color from the new palette
- assert im.getpixel((24, 24)) not in first_frame_colors
+ assert im_rgb.getpixel((24, 24)) not in first_frame_colors
def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
@@ -354,16 +353,16 @@ def test_palette_handling(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/513
with Image.open(TEST_GIF) as im:
- im = im.convert("RGB")
+ im_rgb = im.convert("RGB")
- im = im.resize((100, 100), Image.Resampling.LANCZOS)
- im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
+ im_rgb = im_rgb.resize((100, 100), Image.Resampling.LANCZOS)
+ im_p = im_rgb.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
- f = tmp_path / "temp.gif"
- im2.save(f, optimize=True)
+ f = tmp_path / "temp.gif"
+ im_p.save(f, optimize=True)
with Image.open(f) as reloaded:
- assert_image_similar(im, reloaded.convert("RGB"), 10)
+ assert_image_similar(im_rgb, reloaded.convert("RGB"), 10)
def test_palette_434(tmp_path: Path) -> None:
@@ -383,35 +382,36 @@ def test_palette_434(tmp_path: Path) -> None:
with roundtrip(im, optimize=True) as reloaded:
assert_image_similar(im, reloaded, 1)
- im = im.convert("RGB")
- # check automatic P conversion
- with roundtrip(im) as reloaded:
- reloaded = reloaded.convert("RGB")
- assert_image_equal(im, reloaded)
+ im_rgb = im.convert("RGB")
+
+ # check automatic P conversion
+ with roundtrip(im_rgb) as reloaded:
+ reloaded = reloaded.convert("RGB")
+ assert_image_equal(im_rgb, reloaded)
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as img:
- img = img.convert("RGB")
+ img_rgb = img.convert("RGB")
- tempfile = str(tmp_path / "temp.gif")
- b = BytesIO()
- GifImagePlugin._save_netpbm(img, b, tempfile)
- with Image.open(tempfile) as reloaded:
- assert_image_similar(img, reloaded.convert("RGB"), 0)
+ tempfile = str(tmp_path / "temp.gif")
+ b = BytesIO()
+ GifImagePlugin._save_netpbm(img_rgb, b, tempfile)
+ with Image.open(tempfile) as reloaded:
+ assert_image_similar(img_rgb, reloaded.convert("RGB"), 0)
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_l_mode(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as img:
- img = img.convert("L")
+ img_l = img.convert("L")
tempfile = str(tmp_path / "temp.gif")
b = BytesIO()
- GifImagePlugin._save_netpbm(img, b, tempfile)
+ GifImagePlugin._save_netpbm(img_l, b, tempfile)
with Image.open(tempfile) as reloaded:
- assert_image_similar(img, reloaded.convert("L"), 0)
+ assert_image_similar(img_l, reloaded.convert("L"), 0)
def test_seek() -> None:
@@ -1038,9 +1038,9 @@ def test_webp_background(tmp_path: Path) -> None:
im.save(out)
# Test non-opaque WebP background
- im = Image.new("L", (100, 100), "#000")
- im.info["background"] = (0, 0, 0, 0)
- im.save(out)
+ im2 = Image.new("L", (100, 100), "#000")
+ im2.info["background"] = (0, 0, 0, 0)
+ im2.save(out)
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"
out = tmp_path / "temp.gif"
- im = Image.new("L", (100, 100), "#000")
- im.info["comment"] = b"Test comment text"
- im.save(out)
+ im2 = Image.new("L", (100, 100), "#000")
+ im2.info["comment"] = b"Test comment text"
+ im2.save(out)
with Image.open(out) as reread:
- assert reread.info["comment"] == im.info["comment"]
+ assert reread.info["comment"] == im2.info["comment"]
- im.info["comment"] = "Test comment text"
- im.save(out)
+ im2.info["comment"] = "Test comment text"
+ im2.save(out)
with Image.open(out) as reread:
- assert reread.info["comment"] == im.info["comment"].encode()
+ assert reread.info["comment"] == im2.info["comment"].encode()
# Test that GIF89a is used for comments
assert reread.info["version"] == b"GIF89a"
diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py
index 960e5f4be..05925d502 100644
--- a/Tests/test_file_gribstub.py
+++ b/Tests/test_file_gribstub.py
@@ -59,8 +59,9 @@ def test_handler(tmp_path: Path) -> None:
def open(self, im: Image.Image) -> None:
self.opened = True
- def load(self, im: Image.Image) -> Image.Image:
+ def load(self, im: ImageFile.ImageFile) -> Image.Image:
self.loaded = True
+ assert im.fp is not None
im.fp.close()
return Image.new("RGB", (1, 1))
diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py
index e4f09a09c..e1a56309b 100644
--- a/Tests/test_file_hdf5stub.py
+++ b/Tests/test_file_hdf5stub.py
@@ -61,8 +61,9 @@ def test_handler(tmp_path: Path) -> None:
def open(self, im: Image.Image) -> None:
self.opened = True
- def load(self, im: Image.Image) -> Image.Image:
+ def load(self, im: ImageFile.ImageFile) -> Image.Image:
self.loaded = True
+ assert im.fp is not None
im.fp.close()
return Image.new("RGB", (1, 1))
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index 0376b9997..3eb5cde8e 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -6,7 +6,7 @@ import pytest
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
-from .helper import assert_image_equal, hopper
+from .helper import assert_image_equal
TEST_FILE = "Tests/images/iptc.jpg"
@@ -85,7 +85,7 @@ def test_getiptcinfo() -> None:
def test_getiptcinfo_jpg_none() -> None:
# Arrange
- with hopper() as im:
+ with Image.open("Tests/images/hopper.jpg") as im:
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
@@ -143,6 +143,7 @@ def test_getiptcinfo_tiff() -> None:
# Test with LONG tag type
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
iptc = IptcImagePlugin.getiptcinfo(im)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 96e7f4239..f818927f6 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -1133,8 +1133,9 @@ class TestFileCloseW32:
im.save(tmpfile)
im = Image.open(tmpfile)
+ assert im.fp is not None
+ assert not im.fp.closed
fp = im.fp
- assert not fp.closed
with pytest.raises(OSError):
os.remove(tmpfile)
im.load()
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index a5365a90d..575d911de 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -164,7 +164,7 @@ def test_reduce() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
assert callable(im.reduce)
- im.reduce = 2
+ im.reduce = 2 # type: ignore[assignment, method-assign]
assert im.reduce == 2
im.load()
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 4908496cf..c2336c058 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -11,7 +11,15 @@ from typing import Any, NamedTuple
import pytest
-from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
+from PIL import (
+ Image,
+ ImageFile,
+ ImageFilter,
+ ImageOps,
+ TiffImagePlugin,
+ TiffTags,
+ features,
+)
from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
from .helper import (
@@ -27,14 +35,13 @@ from .helper import (
@skip_unless_feature("libtiff")
class LibTiffTestCase:
- def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None:
+ def _assert_noerr(self, tmp_path: Path, im: ImageFile.ImageFile) -> None:
"""Helper tests that assert basic sanity about the g4 tiff reading"""
# 1 bit
assert im.mode == "1"
# Does the data actually load
im.load()
- im.getdata()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im._compression == "group4"
@@ -355,6 +362,36 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault
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(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
@@ -478,12 +515,12 @@ class TestFileLibTiff(LibTiffTestCase):
# and save to compressed tif.
out = tmp_path / "temp.tif"
with Image.open("Tests/images/pport_g4.tif") as im:
- im = im.convert("L")
+ im_l = im.convert("L")
- im = im.filter(ImageFilter.GaussianBlur(4))
- im.save(out, compression="tiff_adobe_deflate")
+ im_l = im_l.filter(ImageFilter.GaussianBlur(4))
+ im_l.save(out, compression="tiff_adobe_deflate")
- assert_image_equal_tofile(im, out)
+ assert_image_equal_tofile(im_l, out)
def test_compressions(self, tmp_path: Path) -> None:
# Test various tiff compressions and assert similar image content but reduced
@@ -572,8 +609,9 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, compression=compression)
def test_fp_leak(self) -> None:
- im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif")
+ im: ImageFile.ImageFile | None = Image.open("Tests/images/hopper_g4_500.tif")
assert im is not None
+ assert im.fp is not None
fn = im.fp.fileno()
os.fstat(fn)
@@ -1049,8 +1087,10 @@ class TestFileLibTiff(LibTiffTestCase):
data = data[:102] + b"\x02" + data[103:]
with Image.open(io.BytesIO(data)) as im:
- im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
- assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
+ im_transposed = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
+ assert_image_equal_tofile(
+ im_transposed, "Tests/images/old-style-jpeg-compression.png"
+ )
def test_open_missing_samplesperpixel(self) -> None:
with Image.open(
@@ -1117,9 +1157,9 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
- im = ImageOps.exif_transpose(im)
+ im_transposed = ImageOps.exif_transpose(im)
- assert_image_similar(base_im, im, 0.7)
+ assert_image_similar(base_im, im_transposed, 0.7)
@pytest.mark.parametrize(
"test_file",
diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py
index 9aeb306e4..0706af4c0 100644
--- a/Tests/test_file_mic.py
+++ b/Tests/test_file_mic.py
@@ -22,10 +22,10 @@ def test_sanity() -> None:
# Adjust for the gamma of 2.2 encoded into the file
lut = ImagePalette.make_gamma_lut(1 / 2.2)
- im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()])
+ im1 = Image.merge("RGBA", [chan.point(lut) for chan in im.split()])
im2 = hopper("RGBA")
- assert_image_similar(im, im2, 10)
+ assert_image_similar(im1, im2, 10)
def test_n_frames() -> None:
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index f947d1419..4db62bd6d 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -300,12 +300,12 @@ def test_save_all() -> None:
im_reloaded.seek(1)
assert_image_similar(im, im_reloaded, 30)
- im = Image.new("RGB", (1, 1))
+ im_rgb = Image.new("RGB", (1, 1))
for colors in (("#f00",), ("#f00", "#0f0")):
append_images = [Image.new("RGB", (1, 1), color) for color in colors]
- im_reloaded = roundtrip(im, save_all=True, append_images=append_images)
+ im_reloaded = roundtrip(im_rgb, save_all=True, append_images=append_images)
- assert_image_equal(im, im_reloaded)
+ assert_image_equal(im_rgb, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100"
@@ -315,7 +315,7 @@ def test_save_all() -> None:
assert_image_similar(im_reloaded, im_expected, 1)
# Test that a single frame image will not be saved as an MPO
- jpg = roundtrip(im, save_all=True)
+ jpg = roundtrip(im_rgb, save_all=True)
assert "mp" not in jpg.info
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index dc1077fed..ed3a91285 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -101,12 +101,13 @@ class TestFilePng:
assert im.get_format_mimetype() == "image/png"
for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]:
- im = hopper(mode)
- im.save(test_file)
+ im1 = hopper(mode)
+ im1.save(test_file)
with Image.open(test_file) as reloaded:
- if mode == "I;16B":
- reloaded = reloaded.convert(mode)
- assert_image_equal(reloaded, im)
+ converted_reloaded = (
+ reloaded.convert(mode) if mode == "I;16B" else reloaded
+ )
+ assert_image_equal(converted_reloaded, im1)
def test_invalid_file(self) -> None:
invalid_file = "Tests/images/flower.jpg"
@@ -225,11 +226,11 @@ class TestFilePng:
test_file = "Tests/images/pil123p.png"
with Image.open(test_file) as im:
assert_image(im, "P", (162, 150))
- im = im.convert("RGBA")
- assert_image(im, "RGBA", (162, 150))
+ im_rgba = im.convert("RGBA")
+ assert_image(im_rgba, "RGBA", (162, 150))
# image has 124 unique alpha values
- colors = im.getchannel("A").getcolors()
+ colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert len(colors) == 124
@@ -239,11 +240,11 @@ class TestFilePng:
assert im.info["transparency"] == (0, 255, 52)
assert_image(im, "RGB", (64, 64))
- im = im.convert("RGBA")
- assert_image(im, "RGBA", (64, 64))
+ im_rgba = im.convert("RGBA")
+ assert_image(im_rgba, "RGBA", (64, 64))
# image has 876 transparent pixels
- colors = im.getchannel("A").getcolors()
+ colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == 876
@@ -262,11 +263,11 @@ class TestFilePng:
assert len(im.info["transparency"]) == 256
assert_image(im, "P", (162, 150))
- im = im.convert("RGBA")
- assert_image(im, "RGBA", (162, 150))
+ im_rgba = im.convert("RGBA")
+ assert_image(im_rgba, "RGBA", (162, 150))
# image has 124 unique alpha values
- colors = im.getchannel("A").getcolors()
+ colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert len(colors) == 124
@@ -285,13 +286,13 @@ class TestFilePng:
assert im.info["transparency"] == 164
assert im.getpixel((31, 31)) == 164
assert_image(im, "P", (64, 64))
- im = im.convert("RGBA")
- assert_image(im, "RGBA", (64, 64))
+ im_rgba = im.convert("RGBA")
+ assert_image(im_rgba, "RGBA", (64, 64))
- assert im.getpixel((31, 31)) == (0, 255, 52, 0)
+ assert im_rgba.getpixel((31, 31)) == (0, 255, 52, 0)
# image has 876 transparent pixels
- colors = im.getchannel("A").getcolors()
+ colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == 876
@@ -338,6 +339,15 @@ class TestFilePng:
assert colors is not None
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:
in_file = "Tests/images/caption_6_33_22.png"
with Image.open(in_file) as im:
@@ -778,7 +788,9 @@ class TestFilePng:
im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded:
+ assert isinstance(reloaded, PngImagePlugin.PngImageFile)
exif = reloaded._getexif()
+ assert exif is not None
assert exif[305] == "Adobe Photoshop CS Macintosh"
def test_exif_argument(self, tmp_path: Path) -> None:
@@ -811,7 +823,7 @@ class TestFilePng:
monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_PNG_FILE) as im:
- im.save(sys.stdout, "PNG")
+ im.save(sys.stdout, "PNG") # type: ignore[arg-type]
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index 598e9a445..fbca46be5 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -389,7 +389,7 @@ def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_FILE) as im:
- im.save(sys.stdout, "PPM")
+ im.save(sys.stdout, "PPM") # type: ignore[arg-type]
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py
index c2f162cf9..78534e154 100644
--- a/Tests/test_file_sun.py
+++ b/Tests/test_file_sun.py
@@ -84,8 +84,8 @@ def test_rgbx() -> None:
with Image.open(io.BytesIO(data)) as im:
r, g, b = im.split()
- im = Image.merge("RGB", (b, g, r))
- assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
+ im_rgb = Image.merge("RGB", (b, g, r))
+ assert_image_equal_tofile(im_rgb, os.path.join(EXTRA_DIR, "32bpp.png"))
@pytest.mark.skipif(
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index bd364377b..c6c8467d6 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -764,9 +764,9 @@ class TestFileTiff:
# Test appending images
mp = BytesIO()
- im = Image.new("RGB", (100, 100), "#f00")
+ im_rgb = Image.new("RGB", (100, 100), "#f00")
ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]]
- im.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
+ im_rgb.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
@@ -778,7 +778,7 @@ class TestFileTiff:
yield from ims
mp = BytesIO()
- im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
+ im_rgb.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
@@ -971,6 +971,7 @@ class TestFileTiff:
im = Image.open(tmpfile)
fp = im.fp
+ assert fp is not None
assert not fp.closed
im.load()
assert fp.closed
@@ -984,6 +985,7 @@ class TestFileTiff:
with open(tmpfile, "rb") as f:
im = Image.open(f)
fp = im.fp
+ assert fp is not None
assert not fp.closed
im.load()
assert not fp.closed
@@ -1034,8 +1036,9 @@ class TestFileTiffW32:
im.save(tmpfile)
im = Image.open(tmpfile)
+ assert im.fp is not None
+ assert not im.fp.closed
fp = im.fp
- assert not fp.closed
with pytest.raises(OSError):
os.remove(tmpfile)
im.load()
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index 36ad8cee9..322ef5abc 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -175,13 +175,13 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
del info[278]
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
- im = im.resize((500, 500))
- info[TiffImagePlugin.IMAGEWIDTH] = im.width
+ im_resized = im.resize((500, 500))
+ info[TiffImagePlugin.IMAGEWIDTH] = im_resized.width
# STRIPBYTECOUNTS can be a SHORT or a LONG
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
- im.save(out, tiffinfo=info)
+ im_resized.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
index 5456adf59..f996cce67 100644
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -60,7 +60,6 @@ class TestFileWebp:
assert image.size == (128, 128)
assert image.format == "WEBP"
image.load()
- image.getdata()
# generated with:
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
@@ -77,7 +76,6 @@ class TestFileWebp:
assert image.size == (128, 128)
assert image.format == "WEBP"
image.load()
- image.getdata()
if mode == self.rgb_mode:
# generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm
diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py
index c573390c4..b1aa45f6b 100644
--- a/Tests/test_file_webp_alpha.py
+++ b/Tests/test_file_webp_alpha.py
@@ -29,7 +29,6 @@ def test_read_rgba() -> None:
assert image.size == (200, 150)
assert image.format == "WEBP"
image.load()
- image.getdata()
image.tobytes()
@@ -60,7 +59,6 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
assert image.size == pil_image.size
assert image.format == "WEBP"
image.load()
- image.getdata()
assert_image_equal(image, pil_image)
@@ -83,7 +81,6 @@ def test_write_rgba(tmp_path: Path) -> None:
assert image.size == (10, 10)
assert image.format == "WEBP"
image.load()
- image.getdata()
assert_image_similar(image, pil_image, 1.0)
@@ -133,7 +130,6 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
assert image.format == "WEBP"
image.load()
- image.getdata()
with Image.open(file_path) as im:
target = im.convert("RGBA")
diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py
index 5eaa4f599..b4c0448ac 100644
--- a/Tests/test_file_webp_lossless.py
+++ b/Tests/test_file_webp_lossless.py
@@ -24,6 +24,5 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
assert image.size == (128, 128)
assert image.format == "WEBP"
image.load()
- image.getdata()
assert_image_equal(image, hopper(RGB_MODE))
diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py
index 4fcc37e88..5f4a704f2 100644
--- a/Tests/test_format_lab.py
+++ b/Tests/test_format_lab.py
@@ -13,15 +13,15 @@ def test_white() -> None:
k = i.getpixel((0, 0))
- L = i.getdata(0)
- a = i.getdata(1)
- b = i.getdata(2)
+ L = i.get_flattened_data(0)
+ a = i.get_flattened_data(1)
+ b = i.get_flattened_data(2)
assert k == (255, 128, 128)
- assert list(L) == [255] * 100
- assert list(a) == [128] * 100
- assert list(b) == [128] * 100
+ assert L == (255,) * 100
+ assert a == (128,) * 100
+ assert b == (128,) * 100
def test_green() -> None:
diff --git a/Tests/test_image.py b/Tests/test_image.py
index ac30f785c..afc6e8e16 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -613,8 +613,8 @@ class TestImage:
assert im.getpixel((0, 0)) == 0
assert im.getpixel((255, 255)) == 255
with Image.open(target_file) as target:
- target = target.convert(mode)
- assert_image_equal(im, target)
+ im_target = target.convert(mode)
+ assert_image_equal(im, im_target)
def test_radial_gradient_wrong_mode(self) -> None:
# Arrange
@@ -638,8 +638,8 @@ class TestImage:
assert im.getpixel((0, 0)) == 255
assert im.getpixel((128, 128)) == 0
with Image.open(target_file) as target:
- target = target.convert(mode)
- assert_image_equal(im, target)
+ im_target = target.convert(mode)
+ assert_image_equal(im, im_target)
def test_register_extensions(self) -> None:
test_format = "a"
@@ -663,20 +663,20 @@ class TestImage:
assert_image_equal(im, im.remap_palette(list(range(256))))
# Test identity transform with an RGBA palette
- im = Image.new("P", (256, 1))
+ im_p = Image.new("P", (256, 1))
for x in range(256):
- im.putpixel((x, 0), x)
- im.putpalette(list(range(256)) * 4, "RGBA")
- im_remapped = im.remap_palette(list(range(256)))
- assert_image_equal(im, im_remapped)
- assert im.palette is not None
+ im_p.putpixel((x, 0), x)
+ im_p.putpalette(list(range(256)) * 4, "RGBA")
+ im_remapped = im_p.remap_palette(list(range(256)))
+ assert_image_equal(im_p, im_remapped)
+ assert im_p.palette is not None
assert im_remapped.palette is not None
- assert im.palette.palette == im_remapped.palette.palette
+ assert im_p.palette.palette == im_remapped.palette.palette
# Test illegal image mode
- with hopper() as im:
+ with hopper() as im_hopper:
with pytest.raises(ValueError):
- im.remap_palette([])
+ im_hopper.remap_palette([])
def test_remap_palette_transparency(self) -> None:
im = Image.new("P", (1, 2), (0, 0, 0))
@@ -1181,10 +1181,10 @@ class TestImageBytes:
assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize("mode", Image.MODES)
- def test_getdata_putdata(self, mode: str) -> None:
+ def test_get_flattened_data_putdata(self, mode: str) -> None:
im = hopper(mode)
reloaded = Image.new(mode, im.size)
- reloaded.putdata(im.getdata())
+ reloaded.putdata(im.get_flattened_data())
assert_image_equal(im, reloaded)
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
index abb22f949..220e128d1 100644
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -78,7 +78,7 @@ def test_fromarray() -> None:
},
)
out = Image.fromarray(wrapped)
- return out.mode, out.size, list(i.getdata()) == list(out.getdata())
+ return out.mode, out.size, i.get_flattened_data() == out.get_flattened_data()
# assert test("1") == ("1", (128, 100), True)
assert test("L") == ("L", (128, 100), True)
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 8d0ef4b22..547a6c2c6 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -80,8 +80,8 @@ def test_16bit() -> None:
_test_float_conversion(im)
for color in (65535, 65536):
- im = Image.new("I", (1, 1), color)
- im_i16 = im.convert("I;16")
+ im_i = Image.new("I", (1, 1), color)
+ im_i16 = im_i.convert("I;16")
assert im_i16.getpixel((0, 0)) == 65535
diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py
index 07fec2e64..9df8883a4 100644
--- a/Tests/test_image_crop.py
+++ b/Tests/test_image_crop.py
@@ -78,13 +78,13 @@ def test_crop_crash() -> None:
extents = (1, 1, 10, 10)
# works prepatch
with Image.open(test_img) as img:
- img2 = img.crop(extents)
- img2.load()
+ img1 = img.crop(extents)
+ img1.load()
# fail prepatch
with Image.open(test_img) as img:
- img = img.crop(extents)
- img.load()
+ img2 = img.crop(extents)
+ img2.load()
def test_crop_zero() -> None:
@@ -95,10 +95,10 @@ def test_crop_zero() -> None:
cropped = im.crop((10, 10, 20, 20))
assert cropped.size == (10, 10)
- assert cropped.getdata()[0] == (0, 0, 0)
+ assert cropped.getpixel((0, 0)) == (0, 0, 0)
im = Image.new("RGB", (0, 0))
cropped = im.crop((10, 10, 20, 20))
assert cropped.size == (10, 10)
- assert cropped.getdata()[2] == (0, 0, 0)
+ assert cropped.getpixel((2, 0)) == (0, 0, 0)
diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py
index c8b213d84..94d6cbaa2 100644
--- a/Tests/test_image_getdata.py
+++ b/Tests/test_image_getdata.py
@@ -1,23 +1,23 @@
from __future__ import annotations
+import pytest
+
from PIL import Image
from .helper import hopper
def test_sanity() -> None:
- data = hopper().getdata()
-
- len(data)
- list(data)
+ data = hopper().get_flattened_data()
+ assert len(data) == 128 * 128
assert data[0] == (20, 20, 70)
def test_mode() -> None:
def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]:
im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
- data = im.getdata()
+ data = im.get_flattened_data()
return data[0], len(data), len(list(data))
assert getdata("1") == (0, 960, 960)
@@ -28,3 +28,13 @@ def test_mode() -> None:
assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960)
assert getdata("CMYK") == ((244, 242, 203, 0), 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)
diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py
index 4f1d63b8f..1d5f0d17c 100644
--- a/Tests/test_image_load.py
+++ b/Tests/test_image_load.py
@@ -38,6 +38,7 @@ def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None:
def test_contextmanager() -> None:
fn = None
with Image.open("Tests/images/hopper.gif") as im:
+ assert im.fp is not None
fn = im.fp.fileno()
os.fstat(fn)
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
index bf8e89b53..226cb4c14 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -2,6 +2,7 @@ from __future__ import annotations
import sys
from array import array
+from typing import cast
import pytest
@@ -12,21 +13,19 @@ from .helper import assert_image_equal, hopper
def test_sanity() -> None:
im1 = hopper()
+ for data in (im1.get_flattened_data(), im1.im):
+ im2 = Image.new(im1.mode, im1.size, 0)
+ im2.putdata(data)
- data = list(im1.getdata())
+ assert_image_equal(im1, im2)
- im2 = Image.new(im1.mode, im1.size, 0)
- im2.putdata(data)
+ # readonly
+ im2 = Image.new(im1.mode, im2.size, 0)
+ im2.readonly = 1
+ im2.putdata(data)
- 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)
+ assert not im2.readonly
+ assert_image_equal(im1, im2)
def test_long_integers() -> None:
@@ -60,22 +59,22 @@ def test_mode_with_L_with_float() -> None:
@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
def test_mode_i(mode: str) -> None:
src = hopper("L")
- data = list(src.getdata())
+ data = src.get_flattened_data()
im = Image.new(mode, src.size, 0)
im.putdata(data, 2, 256)
- target = [2 * elt + 256 for elt in data]
- assert list(im.getdata()) == target
+ target = tuple(2 * elt + 256 for elt in cast(tuple[int, ...], data))
+ assert im.get_flattened_data() == target
def test_mode_F() -> None:
src = hopper("L")
- data = list(src.getdata())
+ data = src.get_flattened_data()
im = Image.new("F", src.size, 0)
im.putdata(data, 2.0, 256.0)
- target = [2.0 * float(elt) + 256.0 for elt in data]
- assert list(im.getdata()) == target
+ target = tuple(2.0 * float(elt) + 256.0 for elt in cast(tuple[int, ...], data))
+ assert im.get_flattened_data() == target
def test_array_B() -> None:
@@ -86,7 +85,7 @@ def test_array_B() -> None:
im = Image.new("L", (150, 100))
im.putdata(arr)
- assert len(im.getdata()) == len(arr)
+ assert len(im.get_flattened_data()) == len(arr)
def test_array_F() -> None:
@@ -97,7 +96,7 @@ def test_array_F() -> None:
arr = array("f", [0.0]) * 15000
im.putdata(arr)
- assert len(im.getdata()) == len(arr)
+ assert len(im.get_flattened_data()) == len(arr)
def test_not_flattened() -> None:
diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py
index e8b783ff3..887628560 100644
--- a/Tests/test_image_quantize.py
+++ b/Tests/test_image_quantize.py
@@ -58,8 +58,8 @@ def test_rgba_quantize() -> None:
def test_quantize() -> None:
with Image.open("Tests/images/caption_6_33_22.png") as image:
- image = image.convert("RGB")
- converted = image.quantize()
+ converted = image.convert("RGB")
+ converted = converted.quantize()
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 1)
@@ -67,13 +67,13 @@ def test_quantize() -> None:
def test_quantize_no_dither() -> None:
image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette:
- palette = palette.convert("P")
+ palette_p = palette.convert("P")
- converted = image.quantize(dither=Image.Dither.NONE, palette=palette)
+ converted = image.quantize(dither=Image.Dither.NONE, palette=palette_p)
assert converted.mode == "P"
assert converted.palette is not None
- assert palette.palette is not None
- assert converted.palette.palette == palette.palette.palette
+ assert palette_p.palette is not None
+ assert converted.palette.palette == palette_p.palette.palette
def test_quantize_no_dither2() -> None:
@@ -97,10 +97,10 @@ def test_quantize_no_dither2() -> None:
def test_quantize_dither_diff() -> None:
image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette:
- palette = palette.convert("P")
+ palette_p = palette.convert("P")
- dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette)
- nodither = image.quantize(dither=Image.Dither.NONE, palette=palette)
+ dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette_p)
+ nodither = image.quantize(dither=Image.Dither.NONE, palette=palette_p)
assert dither.tobytes() != nodither.tobytes()
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index 270500a44..3e8979a5b 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -160,7 +160,7 @@ class TestImagingCoreResize:
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample)
assert r.mode == "RGB"
assert r.size == (212, 195)
- assert r.getdata()[0] == (0, 0, 0)
+ assert r.getpixel((0, 0)) == (0, 0, 0)
def test_unknown_filter(self) -> None:
with pytest.raises(ValueError):
@@ -314,8 +314,8 @@ class TestImageResize:
@skip_unless_feature("libtiff")
def test_transposed(self) -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im:
- im = im.resize((64, 64))
- assert im.size == (64, 64)
+ im_resized = im.resize((64, 64))
+ assert im_resized.size == (64, 64)
@pytest.mark.parametrize(
"mode", ("L", "RGB", "I", "I;16", "I;16L", "I;16B", "I;16N", "F")
diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py
index 252a15db7..c3ff52f57 100644
--- a/Tests/test_image_rotate.py
+++ b/Tests/test_image_rotate.py
@@ -43,8 +43,8 @@ def test_angle(angle: int) -> None:
with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle)
- im = hopper()
- assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
+ im_hopper = hopper()
+ assert_image_equal(im_hopper.rotate(angle), im_hopper.rotate(angle, expand=1))
@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:
target_origin = target.size[1] / 2
- target = target.crop((0, target_origin, 128, target_origin + 128))
+ im_target = target.crop((0, target_origin, 128, target_origin + 128))
- assert_image_similar(im, target, 15)
+ assert_image_similar(im, im_target, 15)
def test_center_14() -> None:
@@ -87,22 +87,22 @@ def test_center_14() -> None:
with Image.open("Tests/images/hopper_45.png") as target:
target_origin = target.size[1] / 2 - 14
- target = target.crop((6, target_origin, 128 + 6, target_origin + 128))
+ im_target = target.crop((6, target_origin, 128 + 6, target_origin + 128))
- assert_image_similar(im, target, 10)
+ assert_image_similar(im, im_target, 10)
def test_translate() -> None:
im = hopper()
with Image.open("Tests/images/hopper_45.png") as target:
target_origin = (target.size[1] / 2 - 64) - 5
- target = target.crop(
+ im_target = target.crop(
(target_origin, target_origin, target_origin + 128, target_origin + 128)
)
im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC)
- assert_image_similar(im, target, 1)
+ assert_image_similar(im, im_target, 1)
def test_fastpath_center() -> None:
diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py
index 1181f6fca..2ae230f3d 100644
--- a/Tests/test_image_thumbnail.py
+++ b/Tests/test_image_thumbnail.py
@@ -159,9 +159,9 @@ def test_reducing_gap_for_DCT_scaling() -> None:
with Image.open("Tests/images/hopper.jpg") as ref:
# thumbnail should call draft with reducing_gap scale
ref.draft(None, (18 * 3, 18 * 3))
- ref = ref.resize((18, 18), Image.Resampling.BICUBIC)
+ im_ref = ref.resize((18, 18), Image.Resampling.BICUBIC)
with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
- assert_image_similar(ref, im, 1.4)
+ assert_image_similar(im_ref, im, 1.4)
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index 7cf52ddba..3e2b9fee8 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -250,14 +250,14 @@ class TestImageTransform:
def test_missing_method_data(self) -> None:
with hopper() as im:
with pytest.raises(ValueError):
- im.transform((100, 100), None)
+ im.transform((100, 100), None) # type: ignore[arg-type]
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None:
with hopper() as im:
(w, h) = im.size
with pytest.raises(ValueError):
- im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample)
+ im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type]
class TestImageTransformAffine:
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index 5fd7caa7c..a30fb18b8 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -274,13 +274,13 @@ def test_simple_lab() -> None:
# not a linear luminance map. so L != 128:
assert k == (137, 128, 128)
- l_data = i_lab.getdata(0)
- a_data = i_lab.getdata(1)
- b_data = i_lab.getdata(2)
+ l_data = i_lab.get_flattened_data(0)
+ a_data = i_lab.get_flattened_data(1)
+ b_data = i_lab.get_flattened_data(2)
- assert list(l_data) == [137] * 100
- assert list(a_data) == [128] * 100
- assert list(b_data) == [128] * 100
+ assert l_data == (137,) * 100
+ assert a_data == (128,) * 100
+ assert b_data == (128,) * 100
def test_lab_color() -> None:
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 1e0dedef3..44aec4a9e 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -208,10 +208,10 @@ def test_bitmap() -> None:
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
with Image.open("Tests/images/pil123rgba.png") as small:
- small = small.resize((50, 50), Image.Resampling.NEAREST)
+ small_resized = small.resize((50, 50), Image.Resampling.NEAREST)
# Act
- draw.bitmap((10, 10), small)
+ draw.bitmap((10, 10), small_resized)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png")
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 39ee9b9c9..d0b458d6b 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -702,7 +702,7 @@ def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
font.get_variation_axes()
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf")
- assert font.get_variation_names(), [
+ assert font.get_variation_names() == [
b"ExtraLight",
b"Light",
b"Regular",
@@ -742,6 +742,21 @@ 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:
im = Image.new("RGB", (100, 75), "white")
d = ImageDraw.Draw(im)
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index ca192a809..daba30015 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -15,13 +15,10 @@ def string_to_img(image_string: str) -> Image.Image:
rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)]
height = len(rows)
width = len(rows[0])
- im = Image.new("L", (width, height))
- for i in range(width):
- for j in range(height):
- c = rows[j][i]
- v = c in "X1"
- im.putpixel((i, j), v)
-
+ im = Image.new("1", (width, height))
+ for x in range(width):
+ for y in range(height):
+ im.putpixel((x, y), rows[y][x] in "X1")
return im
@@ -42,10 +39,10 @@ def img_to_string(im: Image.Image) -> str:
"""Turn a (small) binary image into a string representation"""
chars = ".1"
result = []
- for r in range(im.height):
+ for y in range(im.height):
line = ""
- for c in range(im.width):
- value = im.getpixel((c, r))
+ for x in range(im.width):
+ value = im.getpixel((x, y))
assert not isinstance(value, tuple)
assert value is not None
line += chars[value > 0]
@@ -165,10 +162,12 @@ def test_edge() -> None:
)
-def test_corner() -> None:
+@pytest.mark.parametrize("mode", ("1", "L"))
+def test_corner(mode: str) -> None:
# Create a corner detector pattern
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"])
- count, Aout = mop.apply(A)
+ image = A.convert(mode) if mode == "L" else A
+ count, Aout = mop.apply(image)
assert count == 5
assert_img_equal_img_string(
Aout,
@@ -184,7 +183,7 @@ def test_corner() -> None:
)
# Test the coordinate counting with the same operator
- coords = mop.match(A)
+ coords = mop.match(image)
assert len(coords) == 4
assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4))
@@ -232,14 +231,14 @@ def test_negate() -> None:
def test_incorrect_mode() -> None:
- im = hopper("RGB")
+ im = hopper()
mop = ImageMorph.MorphOp(op_name="erosion8")
- 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)
- 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)
- 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)
@@ -281,6 +280,11 @@ def test_pattern_syntax_error(pattern: str) -> None:
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:
# Arrange
invalid_mrl = "Tests/images/hopper.png"
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index 27ac6f308..35fe3bb8a 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -261,10 +261,10 @@ def test_colorize_2color() -> None:
# Open test image (256px by 10px, black to white)
with Image.open("Tests/images/bw_gradient.png") as im:
- im = im.convert("L")
+ im_l = im.convert("L")
# Create image with original 2-color functionality
- im_test = ImageOps.colorize(im, "red", "green")
+ im_test = ImageOps.colorize(im_l, "red", "green")
# Test output image (2-color)
left = (0, 1)
@@ -301,11 +301,11 @@ def test_colorize_2color_offset() -> None:
# Open test image (256px by 10px, black to white)
with Image.open("Tests/images/bw_gradient.png") as im:
- im = im.convert("L")
+ im_l = im.convert("L")
# Create image with original 2-color functionality with offsets
im_test = ImageOps.colorize(
- im, black="red", white="green", blackpoint=50, whitepoint=100
+ im_l, black="red", white="green", blackpoint=50, whitepoint=100
)
# 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)
with Image.open("Tests/images/bw_gradient.png") as im:
- im = im.convert("L")
+ im_l = im.convert("L")
# Create image with new three color functionality with offsets
im_test = ImageOps.colorize(
- im,
+ im_l,
black="red",
white="green",
mid="blue",
@@ -457,9 +457,9 @@ def test_exif_transpose() -> None:
assert 0x0112 not in transposed_im.getexif()
# Orientation set directly on Image.Exif
- im = hopper()
- im.getexif()[0x0112] = 3
- transposed_im = ImageOps.exif_transpose(im)
+ im1 = hopper()
+ im1.getexif()[0x0112] = 3
+ transposed_im = ImageOps.exif_transpose(im1)
assert 0x0112 not in transposed_im.getexif()
diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py
index 782022f51..6ad21502f 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -49,6 +49,12 @@ def test_getcolor() -> None:
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:
palette = ImagePalette.ImagePalette("RGB")
diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py
index 46afea064..2b424629d 100644
--- a/Tests/test_imagetext.py
+++ b/Tests/test_imagetext.py
@@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
-from PIL import Image, ImageDraw, ImageFont, ImageText
+from PIL import Image, ImageDraw, ImageFont, ImageText, features
from .helper import assert_image_similar_tofile, skip_unless_feature
@@ -20,42 +20,69 @@ def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
return request.param
-@pytest.fixture(scope="module")
-def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont:
- return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine)
+@pytest.fixture(
+ scope="module",
+ params=[
+ 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.FreeTypeFont) -> None:
- assert ImageText.Text("A", font).get_length() == 12
- assert ImageText.Text("AB", font).get_length() == 24
- assert ImageText.Text("M", font).get_length() == 12
- assert ImageText.Text("y", font).get_length() == 12
- assert ImageText.Text("a", font).get_length() == 12
+def test_get_length(font: ImageFont.ImageFont | ImageFont.FreeTypeFont) -> None:
+ factor = 1 if isinstance(font, ImageFont.ImageFont) else 2
+ assert ImageText.Text("A", font).get_length() == 6 * factor
+ assert ImageText.Text("AB", font).get_length() == 12 * factor
+ assert ImageText.Text("M", font).get_length() == 6 * factor
+ assert ImageText.Text("y", font).get_length() == 6 * factor
+ 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()
-def test_get_bbox(font: ImageFont.FreeTypeFont) -> None:
- assert ImageText.Text("A", font).get_bbox() == (0, 4, 12, 16)
- assert ImageText.Text("AB", font).get_bbox() == (0, 4, 24, 16)
- assert ImageText.Text("M", font).get_bbox() == (0, 4, 12, 16)
- assert ImageText.Text("y", font).get_bbox() == (0, 7, 12, 20)
- assert ImageText.Text("a", font).get_bbox() == (0, 7, 12, 16)
+@pytest.mark.parametrize(
+ "text, expected",
+ (
+ ("A", (0, 4, 12, 16)),
+ ("AB", (0, 4, 24, 16)),
+ ("M", (0, 4, 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:
- font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
- text = ImageText.Text("Hello World!", font)
- text.embed_color()
- assert text.get_length() == 288
+ if features.check_module("freetype2"):
+ font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
+ text = ImageText.Text("Hello World!", font)
+ text.embed_color()
+ assert text.get_length() == 288
- im = Image.new("RGB", (300, 64), "white")
- draw = ImageDraw.Draw(im)
- draw.text((10, 10), text, "#fa6")
+ im = Image.new("RGB", (300, 64), "white")
+ draw = ImageDraw.Draw(im)
+ 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(
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
index f6acb3aff..113d30755 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -20,21 +20,19 @@ TEST_IMAGE_SIZE = (10, 10)
def test_numpy_to_image() -> None:
def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
+ data = tuple(range(100))
if bands == 1:
if boolean:
- data = [0, 255] * 50
- else:
- data = list(range(100))
+ data = (0, 255) * 50
a = numpy.array(data, dtype=dtype)
a.shape = TEST_IMAGE_SIZE
i = Image.fromarray(a)
- assert list(i.getdata()) == data
+ assert i.get_flattened_data() == data
else:
- data = list(range(100))
a = numpy.array([[x] * bands for x in data], dtype=dtype)
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
i = Image.fromarray(a)
- assert list(i.getchannel(0).getdata()) == list(range(100))
+ assert i.get_flattened_data(0) == tuple(range(100))
return i
# Check supported 1-bit integer formats
@@ -191,7 +189,7 @@ def test_putdata() -> None:
arr = numpy.zeros((15000,), numpy.float32)
im.putdata(arr)
- assert len(im.getdata()) == len(arr)
+ assert len(im.get_flattened_data()) == len(arr)
def test_resize() -> None:
@@ -248,7 +246,7 @@ def test_bool() -> None:
a[0][0] = True
im2 = Image.fromarray(a)
- assert im2.getdata()[0] == 255
+ assert im2.getpixel((0, 0)) == 255
def test_no_resource_warning_for_numpy_array() -> None:
diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py
index 54cef00ad..2447ae67a 100644
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -19,30 +19,28 @@ def helper_pickle_file(
# Arrange
with Image.open(test_file) as im:
filename = tmp_path / "temp.pkl"
- if mode:
- im = im.convert(mode)
+ converted_im = im.convert(mode) if mode else im
# Act
with open(filename, "wb") as f:
- pickle.dump(im, f, protocol)
+ pickle.dump(converted_im, f, protocol)
with open(filename, "rb") as f:
loaded_im = pickle.load(f)
# Assert
- assert im == loaded_im
+ assert converted_im == loaded_im
def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None:
with Image.open(test_file) as im:
- if mode:
- im = im.convert(mode)
+ converted_im = im.convert(mode) if mode else im
# Act
- dumped_string = pickle.dumps(im, protocol)
+ dumped_string = pickle.dumps(converted_im, protocol)
loaded_im = pickle.loads(dumped_string)
# Assert
- assert im == loaded_im
+ assert converted_im == loaded_im
@pytest.mark.parametrize(
@@ -90,18 +88,18 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange
filename = tmp_path / "temp.pkl"
with Image.open("Tests/images/hopper.jpg") as im:
- im = im.convert("PA")
+ im_pa = im.convert("PA")
# Act / Assert
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
- im._mode = "LA"
+ im_pa._mode = "LA"
with open(filename, "wb") as f:
- pickle.dump(im, f, protocol)
+ pickle.dump(im_pa, f, protocol)
with open(filename, "rb") as f:
loaded_im = pickle.load(f)
- im._mode = "PA"
- assert im == loaded_im
+ im_pa._mode = "PA"
+ assert im_pa == loaded_im
@skip_unless_feature("webp")
diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py
index 465517bb6..a7e95ed83 100644
--- a/Tests/test_shell_injection.py
+++ b/Tests/test_shell_injection.py
@@ -49,11 +49,13 @@ class TestShellInjection:
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im:
- im = im.convert("RGB")
- self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm)
+ im_rgb = im.convert("RGB")
+ self.assert_save_filename_check(
+ tmp_path, im_rgb, GifImagePlugin._save_netpbm
+ )
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im:
- im = im.convert("L")
- self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm)
+ im_l = im.convert("L")
+ self.assert_save_filename_check(tmp_path, im_l, GifImagePlugin._save_netpbm)
diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh
index 357214f1f..de63abdec 100755
--- a/depends/install_imagequant.sh
+++ b/depends/install_imagequant.sh
@@ -2,7 +2,7 @@
# install libimagequant
archive_name=libimagequant
-archive_version=4.4.0
+archive_version=4.4.1
archive=$archive_name-$archive_version
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index cc5ac283f..b6a7af0a8 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -73,6 +73,16 @@ Image._show
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
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
----------------
diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py
index e4b6b9c01..e0557976c 100644
--- a/docs/example/DdsImagePlugin.py
+++ b/docs/example/DdsImagePlugin.py
@@ -213,6 +213,7 @@ class DdsImageFile(ImageFile.ImageFile):
format_description = "DirectDraw Surface"
def _open(self) -> None:
+ assert self.fp is not None
if not _accept(self.fp.read(4)):
msg = "not a DDS file"
raise SyntaxError(msg)
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 03ee96c0f..35ec99ece 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -999,7 +999,7 @@ where applicable:
The number of times to loop this APNG, 0 indicates infinite looping.
**duration**
- The time to display this APNG frame (in milliseconds).
+ The time to display this APNG frame (in milliseconds), given as a float.
.. note::
@@ -1041,9 +1041,8 @@ following parameters can also be set:
Defaults to 0.
**duration**
- Integer (or list or tuple of integers) length of time to display this APNG frame
- (in milliseconds).
- Defaults to 0.
+ The length of time (or list or tuple of lengths of time) to display this APNG frame
+ (in milliseconds). Defaults to 0.
**disposal**
An integer (or list or tuple of integers) specifying the APNG disposal
diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst
index 40e2a1938..c86ebe896 100644
--- a/docs/installation/building-from-source.rst
+++ b/docs/installation/building-from-source.rst
@@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
- * Pillow has been tested with libimagequant **2.6-4.4.0**
+ * Pillow has been tested with libimagequant **2.6-4.4.1**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index 17e38719a..ee70d8401 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -53,8 +53,8 @@ These platforms are built and tested for every change.
| | | s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.10 | x86 |
-| +----------------------------+---------------------+
-| | 3.11, 3.12, 3.13, 3.14, | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 |
| | PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 |
diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst
index e68722900..adee49228 100644
--- a/docs/reference/Image.rst
+++ b/docs/reference/Image.rst
@@ -191,6 +191,7 @@ This helps to get the bounding box coordinates of the input image::
.. automethod:: PIL.Image.Image.getchannel
.. automethod:: PIL.Image.Image.getcolors
.. automethod:: PIL.Image.Image.getdata
+.. automethod:: PIL.Image.Image.get_flattened_data
.. automethod:: PIL.Image.Image.getexif
.. automethod:: PIL.Image.Image.getextrema
.. automethod:: PIL.Image.Image.getpalette
diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst
index 5c3a73fad..413866785 100644
--- a/docs/reference/ImageGrab.rst
+++ b/docs/reference/ImageGrab.rst
@@ -44,9 +44,11 @@ or the clipboard to a PIL image memory.
.. versionadded:: 7.1.0
:param window:
- HWND, to capture a single window. Windows only.
+ Capture a single window. On Windows, this is a HWND. On macOS, this is a
+ CGWindowID.
- .. versionadded:: 11.2.1
+ .. versionadded:: 11.2.1 Windows support
+ .. versionadded:: 12.1.0 macOS support
:return: An image
.. py:function:: grabclipboard()
diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst
index 30b89a54d..f7a302713 100644
--- a/docs/reference/ImageMorph.rst
+++ b/docs/reference/ImageMorph.rst
@@ -4,10 +4,50 @@
:py:mod:`~PIL.ImageMorph` module
================================
-The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images.
+The :py:mod:`~PIL.ImageMorph` module allows `morphology`_ operators ("MorphOp") to be
+applied to 1 or L mode images::
-.. automodule:: PIL.ImageMorph
+ from PIL import Image, ImageMorph
+ img = Image.open("Tests/images/hopper.bw")
+ mop = ImageMorph.MorphOp(op_name="erosion4")
+ count, imgOut = mop.apply(img)
+ imgOut.show()
+
+.. _morphology: https://en.wikipedia.org/wiki/Mathematical_morphology
+
+In addition to applying operators, you can also analyse images.
+
+You can inspect an image in isolation to determine which pixels are non-empty::
+
+ print(mop.get_on_pixels(img)) # [(0, 0), (1, 0), (2, 0), ...]
+
+Or you can retrieve a list of pixels that match the operator. This is the number of
+pixels that will be non-empty after the operator is applied::
+
+ coords = mop.match(img)
+ print(coords) # [(17, 1), (18, 1), (34, 1), ...]
+ print(len(coords)) # 550
+
+ imgOut = mop.apply(img)[1]
+ print(len(mop.get_on_pixels(imgOut))) # 550
+
+If you would like more customized operators, you can pass patterns to the MorphOp
+class::
+
+ mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"])
+
+Or you can pass lookup table ("LUT") data directly. This LUT data can be constructed
+with the :py:class:`~PIL.ImageMorph.LutBuilder`::
+
+ builder = ImageMorph.LutBuilder()
+ mop = ImageMorph.MorphOp(lut=builder.build_lut())
+
+.. autoclass:: LutBuilder
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: MorphOp
:members:
:undoc-members:
:show-inheritance:
- :noindex:
diff --git a/docs/releasenotes/12.1.0.rst b/docs/releasenotes/12.1.0.rst
new file mode 100644
index 000000000..9740b7008
--- /dev/null
+++ b/docs/releasenotes/12.1.0.rst
@@ -0,0 +1,49 @@
+12.1.0
+------
+
+Deprecations
+============
+
+Image getdata()
+^^^^^^^^^^^^^^^
+
+: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.
+
+API changes
+===========
+
+ImageMorph build_default_lut()
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To match the behaviour of :py:meth:`~PIL.ImageMorph.LutBuilder.build_lut`,
+:py:meth:`~PIL.ImageMorph.LutBuilder.build_default_lut()` now returns the new LUT.
+
+API additions
+=============
+
+Image get_flattened_data()
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:meth:`~PIL.Image.Image.get_flattened_data` is identical to the deprecated
+:py:meth:`~PIL.Image.Image.getdata`, except that the new method returns a tuple of
+pixel values, instead of an internal Pillow data type.
+
+Specify window in ImageGrab on macOS
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can now be selected on
+macOS in addition to Windows. On macOS, this is a CGWindowID::
+
+ from PIL import ImageGrab
+ ImageGrab.grab(window=cgwindowid)
+
+Other changes
+=============
+
+Added MorphOp support for 1 mode images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:class:`~PIL.ImageMorph.MorphOp` now supports both 1 mode and L mode images.
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index f66240c89..4b25bb6a2 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,8 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ versioning
+ 12.1.0
12.0.0
11.3.0
11.2.1
@@ -79,4 +81,3 @@ expected to be backported to earlier versions.
2.5.2
2.3.2
2.3.1
- versioning
diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst
index 2a0af9e59..884102d16 100644
--- a/docs/releasenotes/versioning.rst
+++ b/docs/releasenotes/versioning.rst
@@ -17,8 +17,8 @@ prior three months.
A quarterly release bumps the MAJOR version when incompatible API changes are
made, such as removing deprecated APIs or dropping an EOL Python version. In practice,
-these occur every 12-18 months, guided by
-`Python's EOL schedule `_, and
+these occur every October, guided by
+`Python's EOL schedule `__, and
any APIs that have been deprecated for at least a year are removed at the same time.
PATCH versions ("`Point Release `_"
diff --git a/patches/README.md b/patches/README.md
deleted file mode 100644
index ff4a8f099..000000000
--- a/patches/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-Although we try to use official sources for dependencies, sometimes the official
-sources don't support a platform (especially mobile platforms), or there's a bug
-fix/feature that is required to support Pillow's usage.
-
-This folder contains patches that must be applied to official sources, organized
-by the platforms that need those patches.
-
-Each patch is against the root of the unpacked official tarball, and is named by
-appending `.patch` to the end of the tarball that is to be patched. This
-includes the full version number; so if the version is bumped, the patch will
-at a minimum require a filename change.
-
-Wherever possible, these patches should be contributed upstream, in the hope that
-future Pillow versions won't need to maintain these patches.
diff --git a/patches/iOS/brotli-1.1.0.tar.gz.patch b/patches/iOS/brotli-1.1.0.tar.gz.patch
deleted file mode 100644
index f165a9ac1..000000000
--- a/patches/iOS/brotli-1.1.0.tar.gz.patch
+++ /dev/null
@@ -1,46 +0,0 @@
-# Brotli 1.1.0 doesn't have explicit support for iOS as a CMAKE_SYSTEM_NAME.
-# That release was from 2023; there have been subsequent changes that allow
-# Brotli to build on iOS without any patches, as long as -DBROTLI_BUILD_TOOLS=NO
-# is specified on the command line.
-#
-diff -ru brotli-1.1.0-orig/CMakeLists.txt brotli-1.1.0/CMakeLists.txt
---- brotli-1.1.0-orig/CMakeLists.txt 2023-08-29 19:00:29
-+++ brotli-1.1.0/CMakeLists.txt 2024-11-07 10:46:26
-@@ -114,6 +114,8 @@
- add_definitions(-DOS_MACOSX)
- set(CMAKE_MACOS_RPATH TRUE)
- set(CMAKE_INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/lib")
-+elseif(${CMAKE_SYSTEM_NAME} MATCHES "iOS")
-+ add_definitions(-DOS_IOS)
- endif()
-
- if(BROTLI_EMSCRIPTEN)
-@@ -174,10 +176,12 @@
-
- # Installation
- if(NOT BROTLI_BUNDLED_MODE)
-- install(
-- TARGETS brotli
-- RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
-- )
-+ if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "iOS")
-+ install(
-+ TARGETS brotli
-+ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
-+ )
-+ endif()
-
- install(
- TARGETS ${BROTLI_LIBRARIES_CORE}
-diff -ru brotli-1.1.0-orig/c/common/platform.h brotli-1.1.0/c/common/platform.h
---- brotli-1.1.0-orig/c/common/platform.h 2023-08-29 19:00:29
-+++ brotli-1.1.0/c/common/platform.h 2024-11-07 10:47:28
-@@ -33,7 +33,7 @@
- #include
- #elif defined(OS_FREEBSD)
- #include
--#elif defined(OS_MACOSX)
-+#elif defined(OS_MACOSX) || defined(OS_IOS)
- #include
- /* Let's try and follow the Linux convention */
- #define BROTLI_X_BYTE_ORDER BYTE_ORDER
diff --git a/selftest.py b/selftest.py
index e9b5689a0..c484d4e2d 100755
--- a/selftest.py
+++ b/selftest.py
@@ -40,12 +40,14 @@ def testimage() -> None:
>>> with Image.open("Tests/images/hopper.gif") as im:
... _info(im)
('GIF', 'P', (128, 128))
- >>> _info(Image.open("Tests/images/hopper.ppm"))
+ >>> with Image.open("Tests/images/hopper.ppm") as im:
+ ... _info(im)
('PPM', 'RGB', (128, 128))
>>> try:
- ... _info(Image.open("Tests/images/hopper.jpg"))
+ ... with Image.open("Tests/images/hopper.jpg") as im:
+ ... _info(im)
... except OSError as v:
- ... print(v)
+ ... print(v)
('JPEG', 'RGB', (128, 128))
PIL doesn't actually load the image data until it's needed,
diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py
index 366e0c864..43c39a9fb 100644
--- a/src/PIL/AvifImagePlugin.py
+++ b/src/PIL/AvifImagePlugin.py
@@ -77,6 +77,8 @@ class AvifImageFile(ImageFile.ImageFile):
):
msg = "Invalid opening codec"
raise ValueError(msg)
+
+ assert self.fp is not None
self._decoder = _avif.AvifDecoder(
self.fp.read(),
DECODE_CODEC_CHOICE,
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index f7be7746d..6bb92edf8 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -258,6 +258,7 @@ class BlpImageFile(ImageFile.ImageFile):
format_description = "Blizzard Mipmap Format"
def _open(self) -> None:
+ assert self.fp is not None
self.magic = self.fp.read(4)
if not _accept(self.magic):
msg = f"Bad BLP magic {repr(self.magic)}"
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index 54fc69ab4..a12271370 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -76,6 +76,7 @@ class BmpImageFile(ImageFile.ImageFile):
def _bitmap(self, header: int = 0, offset: int = 0) -> None:
"""Read relevant info about the BMP"""
+ assert self.fp is not None
read, seek = self.fp.read, self.fp.seek
if header:
seek(header)
@@ -311,6 +312,7 @@ class BmpImageFile(ImageFile.ImageFile):
def _open(self) -> None:
"""Open file, check magic number and read header"""
# read 14 bytes: magic number, filesize, reserved, header final offset
+ assert self.fp is not None
head_data = self.fp.read(14)
# choke if the file does not have the required magic bytes
if not _accept(head_data):
diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py
index 8c5da14f5..264564d2b 100644
--- a/src/PIL/BufrStubImagePlugin.py
+++ b/src/PIL/BufrStubImagePlugin.py
@@ -41,6 +41,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
format_description = "BUFR"
def _open(self) -> None:
+ assert self.fp is not None
if not _accept(self.fp.read(4)):
msg = "Not a BUFR file"
raise SyntaxError(msg)
diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py
index aea661b9c..d3f456ddc 100644
--- a/src/PIL/DcxImagePlugin.py
+++ b/src/PIL/DcxImagePlugin.py
@@ -45,6 +45,7 @@ class DcxImageFile(PcxImageFile):
def _open(self) -> None:
# Header
+ assert self.fp is not None
s = self.fp.read(4)
if not _accept(s):
msg = "not a DCX file"
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 69f3062b4..2effb816c 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -189,6 +189,7 @@ class EpsImageFile(ImageFile.ImageFile):
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
def _open(self) -> None:
+ assert self.fp is not None
(length, offset) = self._find_offset(self.fp)
# go to offset - start of "%!PS"
@@ -403,6 +404,7 @@ class EpsImageFile(ImageFile.ImageFile):
) -> Image.core.PixelAccess | None:
# Load EPS via Ghostscript
if self.tile:
+ assert self.fp is not None
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
self._mode = self.im.mode
self._size = self.im.size
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index fd992cd9e..297971234 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -58,6 +58,7 @@ class FpxImageFile(ImageFile.ImageFile):
# read the OLE directory and see if this is a likely
# to be a FlashPix file
+ assert self.fp is not None
try:
self.ole = olefile.OleFileIO(self.fp)
except OSError as e:
@@ -229,6 +230,7 @@ class FpxImageFile(ImageFile.ImageFile):
if y >= ysize:
break # isn't really required
+ assert self.fp is not None
self.stream = stream
self._fp = self.fp
self.fp = None
diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py
index d60e75bb6..e4d836cbd 100644
--- a/src/PIL/FtexImagePlugin.py
+++ b/src/PIL/FtexImagePlugin.py
@@ -72,6 +72,7 @@ class FtexImageFile(ImageFile.ImageFile):
format_description = "Texture File Format (IW2:EOC)"
def _open(self) -> None:
+ assert self.fp is not None
if not _accept(self.fp.read(4)):
msg = "not an FTEX file"
raise SyntaxError(msg)
diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py
index d69295363..ec666c81c 100644
--- a/src/PIL/GbrImagePlugin.py
+++ b/src/PIL/GbrImagePlugin.py
@@ -42,6 +42,7 @@ class GbrImageFile(ImageFile.ImageFile):
format_description = "GIMP brush file"
def _open(self) -> None:
+ assert self.fp is not None
header_size = i32(self.fp.read(4))
if header_size < 20:
msg = "not a GIMP brush"
@@ -88,6 +89,7 @@ class GbrImageFile(ImageFile.ImageFile):
def load(self) -> Image.core.PixelAccess | None:
if self._im is None:
+ assert self.fp is not None
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self._data_size))
return Image.Image.load(self)
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 58c460ef3..76a0d4ab9 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -87,6 +87,7 @@ class GifImageFile(ImageFile.ImageFile):
global_palette = None
def data(self) -> bytes | None:
+ assert self.fp is not None
s = self.fp.read(1)
if s and s[0]:
return self.fp.read(s[0])
@@ -100,6 +101,7 @@ class GifImageFile(ImageFile.ImageFile):
def _open(self) -> None:
# Screen
+ assert self.fp is not None
s = self.fp.read(13)
if not _accept(s):
msg = "not a GIF file"
@@ -116,8 +118,8 @@ class GifImageFile(ImageFile.ImageFile):
# check if palette contains colour indices
p = self.fp.read(3 << bits)
if self._is_palette_needed(p):
- p = ImagePalette.raw("RGB", p)
- self.global_palette = self.palette = p
+ palette = ImagePalette.raw("RGB", p)
+ self.global_palette = self.palette = palette
self._fp = self.fp # FIXME: hack
self.__rewind = self.fp.tell()
@@ -256,7 +258,7 @@ class GifImageFile(ImageFile.ImageFile):
info["comment"] += b"\n" + comment
else:
info["comment"] = comment
- s = None
+ s = b""
continue
elif s[0] == 255 and frame == 0 and block is not None:
#
@@ -299,7 +301,7 @@ class GifImageFile(ImageFile.ImageFile):
bits = self.fp.read(1)[0]
self.__offset = self.fp.tell()
break
- s = None
+ s = b""
if interlace is None:
msg = "image not found in GIF frame"
@@ -751,7 +753,7 @@ def _write_multiple_frames(
if delta.mode == "P":
# Convert to L without considering palette
delta_l = Image.new("L", delta.size)
- delta_l.putdata(delta.getdata())
+ delta_l.putdata(delta.get_flattened_data())
delta = delta_l
mask = ImageMath.lambda_eval(
lambda args: args["convert"](args["im"] * 255, "1"),
diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py
index dfa798893..146a6fa0d 100644
--- a/src/PIL/GribStubImagePlugin.py
+++ b/src/PIL/GribStubImagePlugin.py
@@ -41,6 +41,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
format_description = "GRIB"
def _open(self) -> None:
+ assert self.fp is not None
if not _accept(self.fp.read(8)):
msg = "Not a GRIB file"
raise SyntaxError(msg)
diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py
index 76e640f15..1523e95d5 100644
--- a/src/PIL/Hdf5StubImagePlugin.py
+++ b/src/PIL/Hdf5StubImagePlugin.py
@@ -41,6 +41,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
format_description = "HDF5"
def _open(self) -> None:
+ assert self.fp is not None
if not _accept(self.fp.read(8)):
msg = "Not an HDF file"
raise SyntaxError(msg)
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index 197ea7a2b..058861d67 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -265,6 +265,7 @@ class IcnsImageFile(ImageFile.ImageFile):
format_description = "Mac OS icns resource"
def _open(self) -> None:
+ assert self.fp is not None
self.icns = IcnsFile(self.fp)
self._mode = "RGBA"
self.info["sizes"] = self.icns.itersizes()
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index d5da07d47..8dd57ff85 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -340,6 +340,7 @@ class IcoImageFile(ImageFile.ImageFile):
format_description = "Windows Icon"
def _open(self) -> None:
+ assert self.fp is not None
self.ico = IcoFile(self.fp)
self.info["sizes"] = self.ico.sizes()
self.size = self.ico.entry[0].dim
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 71b999678..ef54f16e9 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -125,6 +125,7 @@ class ImImageFile(ImageFile.ImageFile):
# Quick rejection: if there's not an LF among the first
# 100 bytes, this is (probably) not a text header.
+ assert self.fp is not None
if b"\n" not in self.fp.read(100):
msg = "not an IM file"
raise SyntaxError(msg)
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index dc51860a0..eb5616117 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -590,16 +590,11 @@ class Image:
return new
# Context manager support
- def __enter__(self):
+ def __enter__(self) -> Image:
return self
- def __exit__(self, *args):
- from . import ImageFile
-
- if isinstance(self, ImageFile.ImageFile):
- if getattr(self, "_exclusive_fp", False):
- self._close_fp()
- self.fp = None
+ def __exit__(self, *args: object) -> None:
+ pass
def close(self) -> None:
"""
@@ -1442,12 +1437,31 @@ class Image:
value (e.g. 0 to get the "R" band from an "RGB" image).
:returns: A sequence-like object.
"""
+ deprecate("Image.Image.getdata", 14, "get_flattened_data")
self.load()
if band is not None:
return self.im.getband(band)
return self.im # could be abused
+ def get_flattened_data(
+ self, band: int | None = None
+ ) -> tuple[tuple[int, ...], ...] | tuple[float, ...]:
+ """
+ Returns the contents of this image as a tuple containing pixel values.
+ The sequence object is flattened, so that values for line one follow
+ directly after the values of line zero, and so on.
+
+ :param band: What band to return. The default is to return
+ all bands. To return a single band, pass in the index
+ value (e.g. 0 to get the "R" band from an "RGB" image).
+ :returns: A tuple containing pixel values.
+ """
+ self.load()
+ if band is not None:
+ return tuple(self.im.getband(band))
+ return tuple(self.im)
+
def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]:
"""
Gets the minimum and maximum pixel values for each band in
@@ -1521,8 +1535,13 @@ class Image:
"".join(self.info["Raw profile type exif"].split("\n")[3:])
)
elif hasattr(self, "tag_v2"):
+ from . import TiffImagePlugin
+
+ assert isinstance(self, TiffImagePlugin.TiffImageFile)
self._exif.bigtiff = self.tag_v2._bigtiff
self._exif.endian = self.tag_v2._endian
+
+ assert self.fp is not None
self._exif.load_from_fp(self.fp, self.tag_v2._offset)
if exif_info is not None:
self._exif.load(exif_info)
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index a1d98bd51..3390dfa97 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -131,6 +131,8 @@ class ImageFile(Image.Image):
self.decoderconfig: tuple[Any, ...] = ()
self.decodermaxblock = MAXBLOCK
+ self.fp: IO[bytes] | None
+ self._fp: IO[bytes] | DeferredError
if is_path(fp):
# filename
self.fp = open(fp, "rb")
@@ -167,7 +169,11 @@ class ImageFile(Image.Image):
def _open(self) -> None:
pass
- def _close_fp(self):
+ # Context manager support
+ def __enter__(self) -> ImageFile:
+ return self
+
+ def _close_fp(self) -> None:
if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError):
if self._fp != self.fp:
self._fp.close()
@@ -175,6 +181,11 @@ class ImageFile(Image.Image):
if self.fp:
self.fp.close()
+ def __exit__(self, *args: object) -> None:
+ if getattr(self, "_exclusive_fp", False):
+ self._close_fp()
+ self.fp = None
+
def close(self) -> None:
"""
Closes the file pointer, if possible.
@@ -267,7 +278,7 @@ class ImageFile(Image.Image):
# raise exception if something's wrong. must be called
# directly after open, and closes file when finished.
- if self._exclusive_fp:
+ if self._exclusive_fp and self.fp:
self.fp.close()
self.fp = None
@@ -285,6 +296,7 @@ class ImageFile(Image.Image):
self.map: mmap.mmap | None = None
use_mmap = self.filename and len(self.tile) == 1
+ assert self.fp is not None
readonly = 0
# look for read/seek overrides
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 92eb763a5..d11f7bf01 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -127,11 +127,15 @@ class ImageFont:
def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
# check image
if image.mode not in ("1", "L"):
+ image.close()
+
msg = "invalid font image mode"
raise TypeError(msg)
# read PILfont header
if file.read(8) != b"PILfont\n":
+ image.close()
+
msg = "Not a PILfont file"
raise SyntaxError(msg)
file.readline()
@@ -671,8 +675,12 @@ class FreeTypeFont:
:returns: A list of the named styles in a variation font.
:exception OSError: If the font is not a variation font.
"""
- names = self.font.getvarnames()
- return [name.replace(b"\x00", b"") for name in names]
+ names = []
+ for name in self.font.getvarnames():
+ name = name.replace(b"\x00", b"")
+ if name not in names:
+ names.append(name)
+ return names
def set_variation_by_name(self, name: str | bytes) -> None:
"""
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index 1eb450734..4228078b1 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -43,7 +43,9 @@ def grab(
fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
args = ["screencapture"]
- if bbox:
+ if window:
+ args += ["-l", str(window)]
+ elif bbox:
left, top, right, bottom = bbox
args += ["-R", f"{left},{top},{right-left},{bottom-top}"]
subprocess.call(args + ["-x", filepath])
@@ -51,9 +53,35 @@ def grab(
im.load()
os.unlink(filepath)
if bbox:
- im_resized = im.resize((right - left, bottom - top))
- im.close()
- return im_resized
+ if window:
+ # Determine if the window was in Retina mode or not
+ # by capturing it without the shadow,
+ # and checking how different the width is
+ fh, filepath = tempfile.mkstemp(".png")
+ os.close(fh)
+ subprocess.call(
+ ["screencapture", "-l", str(window), "-o", "-x", filepath]
+ )
+ with Image.open(filepath) as im_no_shadow:
+ retina = im.width - im_no_shadow.width > 100
+ os.unlink(filepath)
+
+ # Since screencapture's -R does not work with -l,
+ # crop the image manually
+ if retina:
+ left, top, right, bottom = bbox
+ im_cropped = im.resize(
+ (right - left, bottom - top),
+ box=tuple(coord * 2 for coord in bbox),
+ )
+ else:
+ im_cropped = im.crop(bbox)
+ im.close()
+ return im_cropped
+ else:
+ im_resized = im.resize((right - left, bottom - top))
+ im.close()
+ return im_resized
return im
elif sys.platform == "win32":
if window is not None:
diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py
index bd70aff7b..9fcd8d78d 100644
--- a/src/PIL/ImageMorph.py
+++ b/src/PIL/ImageMorph.py
@@ -65,10 +65,12 @@ class LutBuilder:
def __init__(
self, patterns: list[str] | None = None, op_name: str | None = None
) -> None:
- if patterns is not None:
- self.patterns = patterns
- else:
- self.patterns = []
+ """
+ :param patterns: A list of input patterns, or None.
+ :param op_name: The name of a known pattern. One of "corner", "dilation4",
+ "dilation8", "erosion4", "erosion8" or "edge".
+ :exception Exception: If the op_name is not recognized.
+ """
self.lut: bytearray | None = None
if op_name is not None:
known_patterns = {
@@ -88,20 +90,38 @@ class LutBuilder:
raise Exception(msg)
self.patterns = known_patterns[op_name]
+ elif patterns is not None:
+ self.patterns = patterns
+ else:
+ self.patterns = []
def add_patterns(self, patterns: list[str]) -> None:
+ """
+ Append to list of patterns.
+
+ :param patterns: Additional patterns.
+ """
self.patterns += patterns
- def build_default_lut(self) -> None:
+ def build_default_lut(self) -> bytearray:
+ """
+ Set the current LUT, and return it.
+
+ This is the default LUT that patterns will be applied against when building.
+ """
symbols = [0, 1]
m = 1 << 4 # pos of current pixel
self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE))
+ return self.lut
def get_lut(self) -> bytearray | None:
+ """
+ Returns the current LUT
+ """
return self.lut
def _string_permute(self, pattern: str, permutation: list[int]) -> str:
- """string_permute takes a pattern and a permutation and returns the
+ """Takes a pattern and a permutation and returns the
string permuted according to the permutation list.
"""
assert len(permutation) == 9
@@ -110,7 +130,7 @@ class LutBuilder:
def _pattern_permute(
self, basic_pattern: str, options: str, basic_result: int
) -> list[tuple[str, int]]:
- """pattern_permute takes a basic pattern and its result and clones
+ """Takes a basic pattern and its result and clones
the pattern according to the modifications described in the $options
parameter. It returns a list of all cloned patterns."""
patterns = [(basic_pattern, basic_result)]
@@ -140,10 +160,9 @@ class LutBuilder:
return patterns
def build_lut(self) -> bytearray:
- """Compile all patterns into a morphology lut.
+ """Compile all patterns into a morphology LUT, and return it.
- TBD :Build based on (file) morphlut:modify_lut
- """
+ This is the data to be passed into MorphOp."""
self.build_default_lut()
assert self.lut is not None
patterns = []
@@ -163,15 +182,14 @@ class LutBuilder:
patterns += self._pattern_permute(pattern, options, result)
- # compile the patterns into regular expressions for speed
+ # Compile the patterns into regular expressions for speed
compiled_patterns = []
for pattern in patterns:
p = pattern[0].replace(".", "X").replace("X", "[01]")
compiled_patterns.append((re.compile(p), pattern[1]))
# Step through table and find patterns that match.
- # Note that all the patterns are searched. The last one
- # caught overrides
+ # Note that all the patterns are searched. The last one found takes priority
for i in range(LUT_SIZE):
# Build the bit pattern
bitpattern = bin(i)[2:]
@@ -193,26 +211,39 @@ class MorphOp:
op_name: str | None = None,
patterns: list[str] | None = None,
) -> None:
- """Create a binary morphological operator"""
- self.lut = lut
- if op_name is not None:
- self.lut = LutBuilder(op_name=op_name).build_lut()
- elif patterns is not None:
- self.lut = LutBuilder(patterns=patterns).build_lut()
+ """Create a binary morphological operator.
+
+ If the LUT is not provided, then it is built using LutBuilder from the op_name
+ or the patterns.
+
+ :param lut: The LUT data.
+ :param patterns: A list of input patterns, or None.
+ :param op_name: The name of a known pattern. One of "corner", "dilation4",
+ "dilation8", "erosion4", "erosion8", "edge".
+ :exception Exception: If the op_name is not recognized.
+ """
+ if patterns is None and op_name is None:
+ self.lut = lut
+ else:
+ self.lut = LutBuilder(patterns, op_name).build_lut()
def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
- """Run a single morphological operation on an image
+ """Run a single morphological operation on an image.
Returns a tuple of the number of changed pixels and the
- morphed image"""
+ morphed image.
+
+ :param image: A 1-mode or L-mode image.
+ :exception Exception: If the current operator is None.
+ :exception ValueError: If the image is not 1 or L mode."""
if self.lut is None:
msg = "No operator loaded"
raise Exception(msg)
- if image.mode != "L":
- msg = "Image mode must be L"
+ if image.mode not in ("1", "L"):
+ msg = "Image mode must be 1 or L"
raise ValueError(msg)
- outimage = Image.new(image.mode, image.size, None)
+ outimage = Image.new(image.mode, image.size)
count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim())
return count, outimage
@@ -220,30 +251,42 @@ class MorphOp:
"""Get a list of coordinates matching the morphological operation on
an image.
- Returns a list of tuples of (x,y) coordinates
- of all matching pixels. See :ref:`coordinate-system`."""
+ Returns a list of tuples of (x,y) coordinates of all matching pixels. See
+ :ref:`coordinate-system`.
+
+ :param image: A 1-mode or L-mode image.
+ :exception Exception: If the current operator is None.
+ :exception ValueError: If the image is not 1 or L mode."""
if self.lut is None:
msg = "No operator loaded"
raise Exception(msg)
- if image.mode != "L":
- msg = "Image mode must be L"
+ if image.mode not in ("1", "L"):
+ msg = "Image mode must be 1 or L"
raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.getim())
def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
- """Get a list of all turned on pixels in a binary image
+ """Get a list of all turned on pixels in a 1 or L mode image.
- Returns a list of tuples of (x,y) coordinates
- of all matching pixels. See :ref:`coordinate-system`."""
+ Returns a list of tuples of (x,y) coordinates of all non-empty pixels. See
+ :ref:`coordinate-system`.
- if image.mode != "L":
- msg = "Image mode must be L"
+ :param image: A 1-mode or L-mode image.
+ :exception ValueError: If the image is not 1 or L mode."""
+
+ if image.mode not in ("1", "L"):
+ msg = "Image mode must be 1 or L"
raise ValueError(msg)
return _imagingmorph.get_on_pixels(image.getim())
def load_lut(self, filename: str) -> None:
- """Load an operator from an mrl file"""
+ """
+ Load an operator from an mrl file
+
+ :param filename: The file to read from.
+ :exception Exception: If the length of the file data is not 512.
+ """
with open(filename, "rb") as f:
self.lut = bytearray(f.read())
@@ -253,7 +296,12 @@ class MorphOp:
raise Exception(msg)
def save_lut(self, filename: str) -> None:
- """Save an operator to an mrl file"""
+ """
+ Save an operator to an mrl file.
+
+ :param filename: The destination file.
+ :exception Exception: If the current operator is None.
+ """
if self.lut is None:
msg = "No operator loaded"
raise Exception(msg)
@@ -261,5 +309,9 @@ class MorphOp:
f.write(self.lut)
def set_lut(self, lut: bytearray | None) -> None:
- """Set the lut from an external source"""
+ """
+ Set the LUT from an external source
+
+ :param lut: A new LUT.
+ """
self.lut = lut
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index 103697117..eae7aea8f 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -118,7 +118,7 @@ class ImagePalette:
) -> int:
if not isinstance(self.palette, bytearray):
self._palette = bytearray(self.palette)
- index = len(self.palette) // 3
+ index = len(self.palette) // len(self.mode)
special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
if image:
special_colors = (
@@ -168,11 +168,12 @@ class ImagePalette:
index = self._new_color_index(image, e)
assert isinstance(self._palette, bytearray)
self.colors[color] = index
- if index * 3 < len(self.palette):
+ mode_len = len(self.mode)
+ if index * mode_len < len(self.palette):
self._palette = (
- self._palette[: index * 3]
+ self._palette[: index * mode_len]
+ bytes(color)
- + self._palette[index * 3 + 3 :]
+ + self._palette[index * mode_len + mode_len :]
)
else:
self._palette += bytes(color)
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index c28f4dcc7..6fc824e4c 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -49,6 +49,7 @@ class IptcImageFile(ImageFile.ImageFile):
def field(self) -> tuple[tuple[int, int] | None, int]:
#
# get a IPTC field header
+ assert self.fp is not None
s = self.fp.read(5)
if not s.strip(b"\x00"):
return None, 0
@@ -76,6 +77,7 @@ class IptcImageFile(ImageFile.ImageFile):
def _open(self) -> None:
# load descriptive fields
+ assert self.fp is not None
while True:
offset = self.fp.tell()
tag, size = self.field()
@@ -131,6 +133,7 @@ class IptcImageFile(ImageFile.ImageFile):
assert isinstance(args, tuple)
compression, band = args
+ assert self.fp is not None
self.fp.seek(self.tile[0].offset)
# Copy image data to temporary file
@@ -154,10 +157,11 @@ class IptcImageFile(ImageFile.ImageFile):
if band is not None:
bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
bands[band] = _im
- _im = Image.merge(self.mode, bands)
+ im = Image.merge(self.mode, bands)
else:
- _im.load()
- self.im = _im.im
+ im = _im
+ im.load()
+ self.im = im.im
self.tile = []
return ImageFile.ImageFile.load(self)
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 4c85dd4e2..d6ec38d43 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -252,6 +252,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
format_description = "JPEG 2000 (ISO 15444)"
def _open(self) -> None:
+ assert self.fp is not None
sig = self.fp.read(4)
if sig == b"\xff\x4f\xff\x51":
self.codec = "j2k"
@@ -304,6 +305,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
]
def _parse_comment(self) -> None:
+ assert self.fp is not None
while True:
marker = self.fp.read(2)
if not marker:
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 755ca648e..894c1547d 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -61,6 +61,7 @@ if TYPE_CHECKING:
def Skip(self: JpegImageFile, marker: int) -> None:
+ assert self.fp is not None
n = i16(self.fp.read(2)) - 2
ImageFile._safe_read(self.fp, n)
@@ -70,6 +71,7 @@ def APP(self: JpegImageFile, marker: int) -> None:
# Application marker. Store these in the APP dictionary.
# Also look for well-known application markers.
+ assert self.fp is not None
n = i16(self.fp.read(2)) - 2
s = ImageFile._safe_read(self.fp, n)
@@ -174,6 +176,7 @@ def APP(self: JpegImageFile, marker: int) -> None:
def COM(self: JpegImageFile, marker: int) -> None:
#
# Comment marker. Store these in the APP dictionary.
+ assert self.fp is not None
n = i16(self.fp.read(2)) - 2
s = ImageFile._safe_read(self.fp, n)
@@ -190,6 +193,7 @@ def SOF(self: JpegImageFile, marker: int) -> None:
# mode. Note that this could be made a bit brighter, by
# looking for JFIF and Adobe APP markers.
+ assert self.fp is not None
n = i16(self.fp.read(2)) - 2
s = ImageFile._safe_read(self.fp, n)
self._size = i16(s, 3), i16(s, 1)
@@ -240,6 +244,7 @@ def DQT(self: JpegImageFile, marker: int) -> None:
# FIXME: The quantization tables can be used to estimate the
# compression quality.
+ assert self.fp is not None
n = i16(self.fp.read(2)) - 2
s = ImageFile._safe_read(self.fp, n)
while len(s):
@@ -340,6 +345,7 @@ class JpegImageFile(ImageFile.ImageFile):
format_description = "JPEG (ISO 10918)"
def _open(self) -> None:
+ assert self.fp is not None
s = self.fp.read(3)
if not _accept(s):
@@ -408,6 +414,7 @@ class JpegImageFile(ImageFile.ImageFile):
For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
so libjpeg can finish decoding
"""
+ assert self.fp is not None
s = self.fp.read(read_bytes)
if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"):
diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py
index 9ce38c427..99a07bae0 100644
--- a/src/PIL/MicImagePlugin.py
+++ b/src/PIL/MicImagePlugin.py
@@ -67,6 +67,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1
+ assert self.fp is not None
self.__fp = self.fp
self.seek(0)
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index b1ae07873..9360061ba 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -106,6 +106,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
_close_exclusive_fp_after_loading = False
def _open(self) -> None:
+ assert self.fp is not None
self.fp.seek(0) # prep the fp in order to pass the JPEG test
JpegImagePlugin.JpegImageFile._open(self)
self._after_jpeg_open()
@@ -125,6 +126,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
assert self.n_frames == len(self.__mpoffsets)
del self.info["mpoffset"] # no longer needed
self.is_animated = self.n_frames > 1
+ assert self.fp is not None
self._fp = self.fp # FIXME: hack
self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
self.__frame = 0
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index d0f22f812..9826a4cd1 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -39,6 +39,7 @@ import struct
import warnings
import zlib
from enum import IntEnum
+from fractions import Fraction
from typing import IO, NamedTuple, cast
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
@@ -509,7 +510,9 @@ class PngStream(ChunkStream):
# otherwise, we have a byte string with one alpha value
# for each palette entry
self.im_info["transparency"] = s
- elif self.im_mode in ("1", "L", "I;16"):
+ elif self.im_mode == "1":
+ self.im_info["transparency"] = 255 if i16(s) else 0
+ elif self.im_mode in ("L", "I;16"):
self.im_info["transparency"] = i16(s)
elif self.im_mode == "RGB":
self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
@@ -757,6 +760,7 @@ class PngImageFile(ImageFile.ImageFile):
format_description = "Portable network graphics"
def _open(self) -> None:
+ assert self.fp is not None
if not _accept(self.fp.read(8)):
msg = "not a PNG file"
raise SyntaxError(msg)
@@ -853,9 +857,7 @@ class PngImageFile(ImageFile.ImageFile):
self.png.verify()
self.png.close()
- if self._exclusive_fp:
- self.fp.close()
- self.fp = None
+ super().verify()
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
@@ -988,6 +990,7 @@ class PngImageFile(ImageFile.ImageFile):
"""internal: read more image data"""
assert self.png is not None
+ assert self.fp is not None
while self.__idat == 0:
# end of chunk, skip forward to next one
@@ -1021,6 +1024,7 @@ class PngImageFile(ImageFile.ImageFile):
def load_end(self) -> None:
"""internal: finished reading image data"""
assert self.png is not None
+ assert self.fp is not None
if self.__idat != 0:
self.fp.read(self.__idat)
while True:
@@ -1152,6 +1156,15 @@ class _fdat:
self.seq_num += 1
+def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None:
+ im.encoderconfig = (
+ encoderinfo.get("optimize", False),
+ encoderinfo.get("compress_level", -1),
+ encoderinfo.get("compress_type", -1),
+ encoderinfo.get("dictionary", b""),
+ )
+
+
class _Frame(NamedTuple):
im: Image.Image
bbox: tuple[int, int, int, int] | None
@@ -1245,10 +1258,10 @@ def _write_multiple_frames(
# default image IDAT (if it exists)
if default_image:
- if im.mode != mode:
- im = im.convert(mode)
+ default_im = im if im.mode == mode else im.convert(mode)
+ _apply_encoderinfo(default_im, im.encoderinfo)
ImageFile._save(
- im,
+ default_im,
cast(IO[bytes], _idat(fp, chunk)),
[ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)],
)
@@ -1263,7 +1276,11 @@ def _write_multiple_frames(
im_frame = im_frame.crop(bbox)
size = im_frame.size
encoderinfo = frame_data.encoderinfo
- frame_duration = int(round(encoderinfo.get("duration", 0)))
+ frame_duration = encoderinfo.get("duration", 0)
+ delay = Fraction(frame_duration / 1000).limit_denominator(65535)
+ if delay.numerator > 65535:
+ msg = "cannot write duration"
+ raise ValueError(msg)
frame_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend)
# frame control
@@ -1275,13 +1292,14 @@ def _write_multiple_frames(
o32(size[1]), # height
o32(bbox[0]), # x_offset
o32(bbox[1]), # y_offset
- o16(frame_duration), # delay_numerator
- o16(1000), # delay_denominator
+ o16(delay.numerator), # delay_numerator
+ o16(delay.denominator), # delay_denominator
o8(frame_disposal), # dispose_op
o8(frame_blend), # blend_op
)
seq_num += 1
# frame data
+ _apply_encoderinfo(im_frame, im.encoderinfo)
if frame == 0 and not default_image:
# first frame must be in IDAT chunks for backwards compatibility
ImageFile._save(
@@ -1357,14 +1375,6 @@ def _save(
bits = 4
outmode += f";{bits}"
- # encoder options
- im.encoderconfig = (
- im.encoderinfo.get("optimize", False),
- im.encoderinfo.get("compress_level", -1),
- im.encoderinfo.get("compress_type", -1),
- im.encoderinfo.get("dictionary", b""),
- )
-
# get the corresponding PNG mode
try:
rawmode, bit_depth, color_type = _OUTMODES[outmode]
@@ -1494,6 +1504,7 @@ def _save(
im, fp, chunk, mode, rawmode, default_image, append_images
)
if single_im:
+ _apply_encoderinfo(single_im, im.encoderinfo)
ImageFile._save(
single_im,
cast(IO[bytes], _idat(fp, chunk)),
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index f49aaeeb1..69a8703dd 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -61,6 +61,7 @@ class PsdImageFile(ImageFile.ImageFile):
_close_exclusive_fp_after_loading = False
def _open(self) -> None:
+ assert self.fp is not None
read = self.fp.read
#
diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py
index dba5d809f..d0709b119 100644
--- a/src/PIL/QoiImagePlugin.py
+++ b/src/PIL/QoiImagePlugin.py
@@ -25,6 +25,7 @@ class QoiImageFile(ImageFile.ImageFile):
format_description = "Quite OK Image"
def _open(self) -> None:
+ assert self.fp is not None
if not _accept(self.fp.read(4)):
msg = "not a QOI file"
raise SyntaxError(msg)
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 868019e80..866292243 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -104,6 +104,7 @@ class SpiderImageFile(ImageFile.ImageFile):
def _open(self) -> None:
# check header
n = 27 * 4 # read 27 float values
+ assert self.fp is not None
f = self.fp.read(n)
try:
@@ -323,9 +324,9 @@ if __name__ == "__main__":
outfile = sys.argv[2]
# perform some image operation
- im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
+ transposed_im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
print(
f"saving a flipped version of {os.path.basename(filename)} "
f"as {outfile} "
)
- im.save(outfile, SpiderImageFile.format)
+ transposed_im.save(outfile, SpiderImageFile.format)
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 761aa3f6b..613a3b7de 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -558,7 +558,6 @@ LIBTIFF_CORE = {
LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes
LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff
LIBTIFF_CORE.remove(323) # Tiled images
-LIBTIFF_CORE.remove(333) # Ink Names either
# Note to advanced users: There may be combinations of these
# parameters and values that when added properly, will work and
diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py
index 5494f62e8..fb3e1c06a 100644
--- a/src/PIL/WalImageFile.py
+++ b/src/PIL/WalImageFile.py
@@ -39,6 +39,7 @@ class WalImageFile(ImageFile.ImageFile):
self._mode = "P"
# read header fields
+ assert self.fp is not None
header = self.fp.read(32 + 24 + 32 + 12)
self._size = i32(header, 32), i32(header, 36)
Image._decompression_bomb_check(self.size)
@@ -54,6 +55,7 @@ class WalImageFile(ImageFile.ImageFile):
def load(self) -> Image.core.PixelAccess | None:
if self._im is None:
+ assert self.fp is not None
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self.size[0] * self.size[1]))
self.putpalette(quake2palette)
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index 2847fed20..e20e40d91 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -45,33 +45,30 @@ class WebPImageFile(ImageFile.ImageFile):
def _open(self) -> None:
# Use the newer AnimDecoder API to parse the (possibly) animated file,
# and access muxed chunks like ICC/EXIF/XMP.
+ assert self.fp is not None
self._decoder = _webp.WebPAnimDecoder(self.fp.read())
# Get info from decoder
- self._size, loop_count, bgcolor, frame_count, mode = self._decoder.get_info()
- self.info["loop"] = loop_count
- bg_a, bg_r, bg_g, bg_b = (
- (bgcolor >> 24) & 0xFF,
- (bgcolor >> 16) & 0xFF,
- (bgcolor >> 8) & 0xFF,
- bgcolor & 0xFF,
+ self._size, self.info["loop"], bgcolor, self.n_frames, self.rawmode = (
+ self._decoder.get_info()
+ )
+ self.info["background"] = (
+ (bgcolor >> 16) & 0xFF, # R
+ (bgcolor >> 8) & 0xFF, # G
+ bgcolor & 0xFF, # B
+ (bgcolor >> 24) & 0xFF, # A
)
- self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
- self.n_frames = frame_count
self.is_animated = self.n_frames > 1
- self._mode = "RGB" if mode == "RGBX" else mode
- self.rawmode = mode
+ self._mode = "RGB" if self.rawmode == "RGBX" else self.rawmode
# Attempt to read ICC / EXIF / XMP chunks from file
- icc_profile = self._decoder.get_chunk("ICCP")
- exif = self._decoder.get_chunk("EXIF")
- xmp = self._decoder.get_chunk("XMP ")
- if icc_profile:
- self.info["icc_profile"] = icc_profile
- if exif:
- self.info["exif"] = exif
- if xmp:
- self.info["xmp"] = xmp
+ for key, chunk_name in {
+ "icc_profile": "ICCP",
+ "exif": "EXIF",
+ "xmp": "XMP ",
+ }.items():
+ if value := self._decoder.get_chunk(chunk_name):
+ self.info[key] = value
# Initialize seek state
self._reset(reset=False)
@@ -129,9 +126,7 @@ class WebPImageFile(ImageFile.ImageFile):
self._seek(self.__logical_frame)
# We need to load the image data for this frame
- data, timestamp, duration = self._get_next()
- self.info["timestamp"] = timestamp
- self.info["duration"] = duration
+ data, self.info["timestamp"], self.info["duration"] = self._get_next()
self.__loaded = self.__logical_frame
# Set tile
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index de714d337..3ae86242a 100644
--- a/src/PIL/WmfImagePlugin.py
+++ b/src/PIL/WmfImagePlugin.py
@@ -49,6 +49,7 @@ if hasattr(Image.core, "drawwmf"):
self.bbox = im.info["wmf_bbox"]
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
+ assert im.fp is not None
im.fp.seek(0) # rewind
return Image.frombytes(
"RGB",
@@ -81,6 +82,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
def _open(self) -> None:
# check placeable header
+ assert self.fp is not None
s = self.fp.read(44)
if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"):
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index cde28388f..192c041d9 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -66,10 +66,10 @@ class XVThumbImageFile(ImageFile.ImageFile):
break
# parse header line (already read)
- s = s.strip().split()
+ w, h = s.strip().split(maxsplit=2)[:2]
self._mode = "P"
- self._size = int(s[0]), int(s[1])
+ self._size = int(w), int(h)
self.palette = ImagePalette.raw("RGB", PALETTE)
diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py
index 616a9aace..711c62ab2 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -48,6 +48,8 @@ def deprecate(
raise RuntimeError(msg)
elif when == 13:
removed = "Pillow 13 (2026-10-15)"
+ elif when == 14:
+ removed = "Pillow 14 (2027-10-15)"
else:
msg = f"Unknown removal version: {when}. Update {__name__}?"
raise ValueError(msg)
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 41cb17a36..96363e9f1 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
-__version__ = "12.1.0.dev0"
+__version__ = "12.2.0.dev0"
diff --git a/src/_imaging.c b/src/_imaging.c
index f6be4a901..d2a195887 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -2459,7 +2459,6 @@ _merge(PyObject *self, PyObject *args) {
static PyObject *
_split(ImagingObject *self) {
- int fails = 0;
Py_ssize_t i;
PyObject *list;
PyObject *imaging_object;
@@ -2473,14 +2472,12 @@ _split(ImagingObject *self) {
for (i = 0; i < self->image->bands; i++) {
imaging_object = PyImagingNew(bands[i]);
if (!imaging_object) {
- fails += 1;
+ Py_DECREF(list);
+ list = NULL;
+ break;
}
PyTuple_SET_ITEM(list, i, imaging_object);
}
- if (fails) {
- Py_DECREF(list);
- list = NULL;
- }
return list;
}
diff --git a/src/_imagingft.c b/src/_imagingft.c
index d0af25b30..a371173d6 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -1287,7 +1287,6 @@ font_getvarnames(FontObject *self) {
}
PyList_SetItem(list_names, j, list_name);
list_names_filled[j] = 1;
- break;
}
}
}
diff --git a/src/encode.c b/src/encode.c
index b1d0181e0..513309c8d 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -668,10 +668,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
int key_int, status, is_core_tag, is_var_length, num_core_tags, i;
TIFFDataType type = TIFF_NOTYPE;
// This list also exists in TiffTags.py
- const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274,
- 277, 278, 280, 281, 340, 341, 282, 283, 284,
- 286, 287, 296, 297, 320, 321, 338, 32995, 32998,
- 32996, 339, 32997, 330, 531, 530, 65537, 301, 532};
+ const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, 277,
+ 278, 280, 281, 282, 283, 284, 286, 287, 296, 297,
+ 301, 320, 321, 330, 333, 338, 339, 340, 341, 530,
+ 531, 532, 32995, 32996, 32997, 32998, 65537};
Py_ssize_t tags_size;
PyObject *item;
@@ -821,7 +821,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
}
}
- if (type == TIFF_BYTE || type == TIFF_UNDEFINED) {
+ if (type == TIFF_BYTE || type == TIFF_UNDEFINED ||
+ key_int == TIFFTAG_INKNAMES) {
status = ImagingLibTiffSetField(
&encoder->state,
(ttag_t)key_int,
@@ -973,7 +974,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value)
);
- } else if (type == TIFF_LONG) {
+ } else if (type == TIFF_LONG || type == TIFF_IFD) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)
);
@@ -989,10 +990,6 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (FLOAT32)PyFloat_AsDouble(value)
);
- } else if (type == TIFF_DOUBLE) {
- status = ImagingLibTiffSetField(
- &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)
- );
} else if (type == TIFF_SBYTE) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (INT8)PyLong_AsLong(value)
@@ -1001,7 +998,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, PyBytes_AsString(value)
);
- } else if (type == TIFF_RATIONAL) {
+ } else if (type == TIFF_DOUBLE || type == TIFF_SRATIONAL ||
+ type == TIFF_RATIONAL) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)
);
diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c
index ac81ed6df..d99b0e28e 100644
--- a/src/libImaging/BcnDecode.c
+++ b/src/libImaging/BcnDecode.c
@@ -663,7 +663,7 @@ half_to_float(UINT16 h) {
if (o.f >= m.f) {
o.u |= 255 << 23;
}
- o.u |= (h & 0x8000) << 16;
+ o.u |= (UINT32)(h & 0x8000) << 16;
return o.f;
}
diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c
index 44994823e..9b494dfa2 100644
--- a/src/libImaging/FliDecode.c
+++ b/src/libImaging/FliDecode.c
@@ -18,9 +18,9 @@
#define I16(ptr) ((ptr)[0] + ((int)(ptr)[1] << 8))
-#define I32(ptr) \
- ((ptr)[0] + ((INT32)(ptr)[1] << 8) + ((INT32)(ptr)[2] << 16) + \
- ((INT32)(ptr)[3] << 24))
+#define I32(ptr) \
+ ((ptr)[0] + ((unsigned long)(ptr)[1] << 8) + ((unsigned long)(ptr)[2] << 16) + \
+ ((unsigned long)(ptr)[3] << 24))
#define ERR_IF_DATA_OOB(offset) \
if ((data + (offset)) > ptr + bytes) { \
@@ -31,8 +31,8 @@
int
ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) {
UINT8 *ptr;
- int framesize;
- int c, chunks, advance;
+ unsigned long framesize, advance;
+ int c, chunks;
int l, lines;
int i, j, x = 0, y, ymax;
diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c
index 7521f4cda..2e459c48f 100644
--- a/src/libImaging/Mode.c
+++ b/src/libImaging/Mode.c
@@ -20,7 +20,6 @@ const ModeData MODES[] = {
[IMAGING_MODE_I_16] = {"I;16"}, [IMAGING_MODE_I_16L] = {"I;16L"},
[IMAGING_MODE_I_16B] = {"I;16B"}, [IMAGING_MODE_I_16N] = {"I;16N"},
- [IMAGING_MODE_I_32L] = {"I;32L"}, [IMAGING_MODE_I_32B] = {"I;32B"},
};
const ModeID
@@ -76,7 +75,6 @@ const RawModeData RAWMODES[] = {
[IMAGING_RAWMODE_I_16L] = {"I;16L"},
[IMAGING_RAWMODE_I_16B] = {"I;16B"},
[IMAGING_RAWMODE_I_16N] = {"I;16N"},
- [IMAGING_RAWMODE_I_32L] = {"I;32L"},
[IMAGING_RAWMODE_I_32B] = {"I;32B"},
[IMAGING_RAWMODE_1_8] = {"1;8"},
diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h
index a3eb3d86d..39c0eb919 100644
--- a/src/libImaging/Mode.h
+++ b/src/libImaging/Mode.h
@@ -25,8 +25,6 @@ typedef enum {
IMAGING_MODE_I_16L,
IMAGING_MODE_I_16B,
IMAGING_MODE_I_16N,
- IMAGING_MODE_I_32L,
- IMAGING_MODE_I_32B,
} ModeID;
typedef struct {
@@ -64,8 +62,6 @@ typedef enum {
IMAGING_RAWMODE_I_16L,
IMAGING_RAWMODE_I_16B,
IMAGING_RAWMODE_I_16N,
- IMAGING_RAWMODE_I_32L,
- IMAGING_RAWMODE_I_32B,
// Rawmodes
IMAGING_RAWMODE_1_8,
@@ -106,6 +102,7 @@ typedef enum {
IMAGING_RAWMODE_C_I,
IMAGING_RAWMODE_Cb,
IMAGING_RAWMODE_Cr,
+ IMAGING_RAWMODE_I_32B,
IMAGING_RAWMODE_F_16,
IMAGING_RAWMODE_F_16B,
IMAGING_RAWMODE_F_16BS,
diff --git a/tox.ini b/tox.ini
index d58fd67b6..de18946ef 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,8 @@ requires =
tox>=4.2
env_list =
lint
- py{py3, 314, 313, 312, 311, 310}
+ mypy
+ py{py3, 315, 314, 313, 312, 311, 310}
[testenv]
deps =
@@ -18,11 +19,11 @@ commands =
skip_install = true
deps =
check-manifest
- pre-commit
+ prek
pass_env =
- PRE_COMMIT_COLOR
+ PREK_COLOR
commands =
- pre-commit run --all-files --show-diff-on-failure
+ prek run --all-files --show-diff-on-failure
check-manifest
[testenv:mypy]
diff --git a/winbuild/README.md b/winbuild/README.md
index db71f094e..b1c9262c2 100644
--- a/winbuild/README.md
+++ b/winbuild/README.md
@@ -11,7 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst).
* Requires Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script.
* Requires CMake 3.15 or newer (available as Visual Studio component).
-* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
+* Tested on Windows Server 2025 and 2022 with Visual Studio 2022 Enterprise (GitHub
+ Actions).
Here's an example script to build on Windows:
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 2401dd4ec..3377d952c 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -113,20 +113,20 @@ ARCHITECTURES = {
}
V = {
- "BROTLI": "1.1.0",
+ "BROTLI": "1.2.0",
"FREETYPE": "2.14.1",
"FRIBIDI": "1.0.16",
- "HARFBUZZ": "12.2.0",
- "JPEGTURBO": "3.1.2",
+ "HARFBUZZ": "12.3.0",
+ "JPEGTURBO": "3.1.3",
"LCMS2": "2.17",
"LIBAVIF": "1.3.0",
- "LIBIMAGEQUANT": "4.4.0",
- "LIBPNG": "1.6.50",
+ "LIBIMAGEQUANT": "4.4.1",
+ "LIBPNG": "1.6.53",
"LIBWEBP": "1.6.0",
"OPENJPEG": "2.5.4",
"TIFF": "4.7.1",
- "XZ": "5.8.1",
- "ZLIBNG": "2.2.5",
+ "XZ": "5.8.2",
+ "ZLIBNG": "2.3.2",
}
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
@@ -167,12 +167,12 @@ DEPS: dict[str, dict[str, Any]] = {
"license": "LICENSE.md",
"patch": {
r"CMakeLists.txt": {
- "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlib)", # noqa: E501
+ "set_target_properties(zlib-ng PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib-ng PROPERTIES OUTPUT_NAME zlib)", # noqa: E501
},
},
"build": [
*cmds_cmake(
- "zlib", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON"
+ "zlib-ng", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON"
),
],
"headers": [r"z*.h"],
@@ -183,11 +183,7 @@ DEPS: dict[str, dict[str, Any]] = {
"filename": f"xz-{V['XZ']}.tar.gz",
"license": "COPYING",
"build": [
- *cmds_cmake(
- "liblzma",
- "-DBUILD_SHARED_LIBS:BOOL=OFF"
- + (" -DXZ_CLMUL_CRC:BOOL=OFF" if struct.calcsize("P") == 4 else ""),
- ),
+ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),
cmd_mkdir(r"{inc_dir}\lzma"),
cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"),
],