diff --git a/.ci/install.sh b/.ci/install.sh
index ba6e753e2..0131c47bc 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..77d1d1caa 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -20,7 +20,7 @@ jobs:
name: Lint
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
with:
persist-credentials: false
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 01d0775fa..860e14965 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..3450de355 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -35,7 +35,7 @@ jobs:
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"]
include:
# Test the oldest Python on 32-bit
@@ -47,19 +47,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
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 52b380429..185c5dc10 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,28 +89,26 @@ 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
+LIBPNG_VERSION=1.6.53
+JPEGTURBO_VERSION=3.1.3
OPENJPEG_VERSION=2.5.4
JPEGXL_VERSION=0.11.1
-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 {
@@ -150,18 +147,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
}
@@ -169,7 +157,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..e60c79441 100644
--- a/.github/zizmor.yml
+++ b/.github/zizmor.yml
@@ -1,6 +1,8 @@
# 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..8477729e6 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
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/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 @@
-
-
-
-
-
-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/test_bmp_reference.py b/Tests/test_bmp_reference.py
index 82cab39c6..3cd0fbb2d 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",
@@ -103,7 +103,7 @@ def test_good() -> None:
# with paletized image, since the palette might
# be differently ordered for an equivalent image.
im = im.convert("RGBA")
- compare = im.convert("RGBA")
+ compare = compare.convert("RGBA")
assert_image_similar(im, compare, 5)
except Exception as msg:
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index 12204b5b7..d918a24a7 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
@@ -718,6 +719,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_gribstub.py b/Tests/test_file_gribstub.py
index 960e5f4be..4dbed6b31 100644
--- a/Tests/test_file_gribstub.py
+++ b/Tests/test_file_gribstub.py
@@ -59,7 +59,7 @@ 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
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..1e48597d3 100644
--- a/Tests/test_file_hdf5stub.py
+++ b/Tests/test_file_hdf5stub.py
@@ -61,7 +61,7 @@ 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
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_libtiff.py b/Tests/test_file_libtiff.py
index 4908496cf..e36b5f39e 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,7 +35,7 @@ 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"
@@ -355,6 +363,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:
@@ -1117,9 +1155,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..7f163a4d6 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -338,6 +338,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 +787,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:
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..556c88647 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:
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_image.py b/Tests/test_image.py
index ac30f785c..88f55638e 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))
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..b90ce84bc 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:
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..323d31f51 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -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_imagedraw.py b/Tests/test_imagedraw.py
index 790acee2a..49765cd68 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -198,10 +198,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_imageops.py b/Tests/test_imageops.py
index 27ac6f308..63cd0e4d4 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",
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_pickle.py b/Tests/test_pickle.py
index 54cef00ad..fc76f81e9 100644
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -90,18 +90,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/installation/building-from-source.rst b/docs/installation/building-from-source.rst
index b9d0d172a..8c7cda96b 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.
@@ -120,7 +120,7 @@ Many of Pillow's features require external libraries:
.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
- Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
+ Prerequisites for **Ubuntu 16.04 LTS - 24.04 LTS** are installed with::
sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
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/releasenotes/12.1.0.rst b/docs/releasenotes/12.1.0.rst
new file mode 100644
index 000000000..b6e1810c6
--- /dev/null
+++ b/docs/releasenotes/12.1.0.rst
@@ -0,0 +1,59 @@
+12.1.0
+------
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+:cve:`YYYY-XXXXX`: TODO
+^^^^^^^^^^^^^^^^^^^^^^^
+
+TODO
+
+Backwards incompatible changes
+==============================
+
+TODO
+^^^^
+
+TODO
+
+Deprecations
+============
+
+TODO
+^^^^
+
+TODO
+
+API changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API additions
+=============
+
+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
+=============
+
+TODO
+^^^^
+
+TODO
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