diff --git a/.appveyor.yml b/.appveyor.yml
index 4c5a7f9ee..b0740b1ac 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -6,6 +6,7 @@ init:
# Uncomment previous line to get RDP access during the build.
environment:
+ COVERAGE_CORE: sysmon
EXECUTABLE: python.exe
TEST_OPTIONS:
DEPLOY: YES
diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt
new file mode 100644
index 000000000..3673e1d81
--- /dev/null
+++ b/.ci/requirements-mypy.txt
@@ -0,0 +1 @@
+mypy==1.8.0
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index e0e6804bf..8fc6bd0ad 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1 @@
-tidelift: "pypi/Pillow"
+tidelift: "pypi/pillow"
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 4319cc8ff..92e860cb5 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -7,10 +7,12 @@ on:
paths:
- ".github/workflows/docs.yml"
- "docs/**"
+ - "src/PIL/**"
pull_request:
paths:
- ".github/workflows/docs.yml"
- "docs/**"
+ - "src/PIL/**"
workflow_dispatch:
permissions:
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 2615fb427..4526b9454 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ COVERAGE_CORE: sysmon
+
jobs:
build:
runs-on: windows-latest
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index 1c6d15b77..a07a27c46 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ COVERAGE_CORE: sysmon
+
jobs:
build:
runs-on: windows-latest
@@ -64,10 +67,10 @@ jobs:
mingw-w64-x86_64-python3-cffi \
mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \
- mingw-w64-x86_64-python3-pip \
mingw-w64-x86_64-python3-setuptools \
mingw-w64-x86_64-python-pyqt6
+ python3 -m ensurepip
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
pushd depends && ./install_extra_test_images.sh && popd
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 75fccf795..c936be559 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -26,13 +26,16 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ COVERAGE_CORE: sysmon
+
jobs:
build:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
- python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
+ python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"]
timeout-minutes: 30
@@ -66,8 +69,16 @@ jobs:
- name: Print build system information
run: python3 .github/workflows/system-info.py
- - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
- run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
+ - name: Install Python dependencies
+ run: >
+ python3 -m pip install
+ coverage>=7.4.2
+ defusedxml
+ olefile
+ pyroma
+ pytest
+ pytest-cov
+ pytest-timeout
- name: Install dependencies
id: install
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 19f4a6dae..643273e58 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -27,6 +27,7 @@ concurrency:
cancel-in-progress: true
env:
+ COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs:
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 26bf2f6d6..581c71253 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -19,7 +19,7 @@ FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.3.0
LIBPNG_VERSION=1.6.40
JPEGTURBO_VERSION=3.0.1
-OPENJPEG_VERSION=2.5.0
+OPENJPEG_VERSION=2.5.2
XZ_VERSION=5.4.5
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
@@ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
function build_openjpeg {
- local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz)
+ local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
@@ -62,7 +62,7 @@ function build_brotli {
function build {
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
- export BUILD_PREFIX="/usr/local"
+ sudo chown -R runner /usr/local
fi
build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
@@ -75,8 +75,8 @@ function build {
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
- if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
- cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
+ if [[ "$CIBW_ARCHS" == "arm64" ]]; then
+ cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig
fi
else
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
@@ -87,12 +87,10 @@ function build {
build_tiff
build_libpng
build_lcms2
- if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
- for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do
- cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib
- done
- fi
build_openjpeg
+ if [ -f /usr/local/lib64/libopenjp2.so ]; then
+ cp /usr/local/lib64/libopenjp2.so /usr/local/lib
+ fi
ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -O3 -DNDEBUG"
@@ -128,14 +126,19 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de
untar pillow-depends-main.zip
if [[ -n "$IS_MACOS" ]]; then
- # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
+ # libtiff and libxcb cause a conflict with building libtiff and libxcb
# libxau and libxdmcp cause an issue on macOS < 11
- # if php is installed, brew tries to reinstall these after installing openblas
# remove cairo to fix building harfbuzz on arm64
# remove lcms2 and libpng to fix building openjpeg on arm64
- # remove zstd to avoid inclusion on x86_64
+ # remove jpeg-turbo to avoid inclusion on arm64
+ # remove webp and zstd to avoid inclusion on x86_64
# curl from brew requires zstd, use system curl
- brew remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd
+ brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd
+ if [[ "$CIBW_ARCHS" == "arm64" ]]; then
+ brew remove --ignore-dependencies jpeg-turbo
+ else
+ brew remove --ignore-dependencies webp
+ fi
brew install pkg-config
fi
diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh
index 207ec1567..3fbf3be69 100755
--- a/.github/workflows/wheels-test.sh
+++ b/.github/workflows/wheels-test.sh
@@ -4,6 +4,9 @@ set -e
if [[ "$OSTYPE" == "darwin"* ]]; then
brew install fribidi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
+ if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then
+ sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib
+ fi
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
apk add curl fribidi
else
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index ad3ceb4f0..36bb54050 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -101,7 +101,7 @@ jobs:
cibw_arch: x86_64
macosx_deployment_target: "10.10"
- name: "macOS arm64"
- os: macos-latest
+ os: macos-14
cibw_arch: arm64
macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
@@ -134,7 +134,7 @@ jobs:
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_SKIP: pp38-*
- CIBW_TEST_SKIP: "*-macosx_arm64"
+ CIBW_TEST_SKIP: cp38-macosx_arm64
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4
diff --git a/CHANGES.rst b/CHANGES.rst
index a8404260f..44dfa65f7 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,21 @@ Changelog (Pillow)
10.3.0 (unreleased)
-------------------
+- Open 16-bit grayscale PNGs as I;16 #7849
+ [radarhere]
+
+- Handle truncated chunks at the end of PNG images #7709
+ [lajiyuan, radarhere]
+
+- Match mask size to pasted image size in GifImagePlugin #7779
+ [radarhere]
+
+- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782
+ [evanmiller, radarhere]
+
+- Fixed reading FLI/FLC images with a prefix chunk #7804
+ [twolife]
+
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
[nik012003, radarhere]
diff --git a/README.md b/README.md
index 6ca870166..f142ef563 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ As of 2019, Pillow development is
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg">
+ src="https://tidelift.com/badges/package/pypi/pillow?style=flat">
@@ -82,9 +82,6 @@ As of 2019, Pillow development is
-
float:
def _test_leak(
- min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any
+ min_iterations: int,
+ max_iterations: int,
+ fn: Callable[..., Image.Image | None],
+ *args: Any,
) -> None:
mem_limit = None
for i in range(max_iterations):
diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py
index d65ba6abc..63d6657bc 100644
--- a/Tests/check_png_dos.py
+++ b/Tests/check_png_dos.py
@@ -17,6 +17,7 @@ def test_ignore_dos_text() -> None:
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
+ assert isinstance(im, PngImagePlugin.PngImageFile)
for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
@@ -32,6 +33,7 @@ def test_dos_text() -> None:
assert msg, "Decompressed Data Too Large"
return
+ assert isinstance(im, PngImagePlugin.PngImageFile)
for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
@@ -57,6 +59,7 @@ def test_dos_total_memory() -> None:
return
total_len = 0
+ assert isinstance(im2, PngImagePlugin.PngImageFile)
for txt in im2.text.values():
total_len += len(txt)
assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M"
diff --git a/Tests/helper.py b/Tests/helper.py
index b98883946..9849bf655 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -351,7 +351,7 @@ def is_mingw() -> bool:
class CachedProperty:
- def __init__(self, func: Callable[[Any], None]) -> None:
+ def __init__(self, func: Callable[[Any], Any]) -> None:
self.func = func
def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
diff --git a/Tests/images/16_bit_binary_pgm.png b/Tests/images/16_bit_binary_pgm.png
deleted file mode 100644
index 918be1ad4..000000000
Binary files a/Tests/images/16_bit_binary_pgm.png and /dev/null differ
diff --git a/Tests/images/16_bit_binary_pgm.tiff b/Tests/images/16_bit_binary_pgm.tiff
new file mode 100644
index 000000000..1ce808bcf
Binary files /dev/null and b/Tests/images/16_bit_binary_pgm.tiff differ
diff --git a/Tests/images/2422.flc b/Tests/images/2422.flc
new file mode 100644
index 000000000..eed5fb59e
Binary files /dev/null and b/Tests/images/2422.flc differ
diff --git a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png
deleted file mode 100644
index 2b84283b7..000000000
Binary files a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png and /dev/null differ
diff --git a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff
new file mode 100644
index 000000000..b72509cc4
Binary files /dev/null and b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff differ
diff --git a/Tests/images/hopper_emboss.bmp b/Tests/images/hopper_emboss.bmp
index d8e001e2b..01d48fa3f 100644
Binary files a/Tests/images/hopper_emboss.bmp and b/Tests/images/hopper_emboss.bmp differ
diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png
deleted file mode 100644
index f4dab388f..000000000
Binary files a/Tests/images/hopper_emboss_I.png and /dev/null differ
diff --git a/Tests/images/hopper_emboss_more.bmp b/Tests/images/hopper_emboss_more.bmp
index 37a5db830..01247f97e 100644
Binary files a/Tests/images/hopper_emboss_more.bmp and b/Tests/images/hopper_emboss_more.bmp differ
diff --git a/Tests/images/hopper_emboss_more_I.png b/Tests/images/hopper_emboss_more_I.png
deleted file mode 100644
index c417c915f..000000000
Binary files a/Tests/images/hopper_emboss_more_I.png and /dev/null differ
diff --git a/Tests/images/imagedraw_rectangle_I.png b/Tests/images/imagedraw_rectangle_I.png
deleted file mode 100644
index a75f12c2e..000000000
Binary files a/Tests/images/imagedraw_rectangle_I.png and /dev/null differ
diff --git a/Tests/images/imagedraw_rectangle_I.tiff b/Tests/images/imagedraw_rectangle_I.tiff
new file mode 100644
index 000000000..9b9eda883
Binary files /dev/null and b/Tests/images/imagedraw_rectangle_I.tiff differ
diff --git a/Tests/images/truncated_end_chunk.png b/Tests/images/truncated_end_chunk.png
new file mode 100644
index 000000000..5e88c5e4f
Binary files /dev/null and b/Tests/images/truncated_end_chunk.png differ
diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py
index 6ffc8f6f5..584d8f91d 100644
--- a/Tests/test_deprecate.py
+++ b/Tests/test_deprecate.py
@@ -20,7 +20,7 @@ from PIL import _deprecate
),
],
)
-def test_version(version, expected) -> None:
+def test_version(version: int | None, expected: str) -> None:
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", version, "new thing")
@@ -46,7 +46,7 @@ def test_unknown_version() -> None:
),
],
)
-def test_old_version(deprecated, plural, expected) -> None:
+def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
expected = r""
with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural)
@@ -76,7 +76,7 @@ def test_replacement_and_action() -> None:
"Upgrade to new thing.",
],
)
-def test_action(action) -> None:
+def test_action(action: str) -> None:
expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Upgrade to new thing\."
diff --git a/Tests/test_features.py b/Tests/test_features.py
index de74e9c18..8d2d198ff 100644
--- a/Tests/test_features.py
+++ b/Tests/test_features.py
@@ -2,6 +2,7 @@ from __future__ import annotations
import io
import re
+from typing import Callable
import pytest
@@ -29,7 +30,7 @@ def test_version() -> None:
# Check the correctness of the convenience function
# and the format of version numbers
- def test(name, function) -> None:
+ def test(name: str, function: Callable[[str], bool]) -> None:
version = features.version(name)
if not features.check(name):
assert version is None
@@ -73,12 +74,12 @@ def test_libimagequant_version() -> None:
@pytest.mark.parametrize("feature", features.modules)
-def test_check_modules(feature) -> None:
+def test_check_modules(feature: str) -> None:
assert features.check_module(feature) in [True, False]
@pytest.mark.parametrize("feature", features.codecs)
-def test_check_codecs(feature) -> None:
+def test_check_codecs(feature: str) -> None:
assert features.check_codec(feature) in [True, False]
diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py
index 3904d3bc5..1e2f20c40 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None:
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
],
)
-def test_crashes(test_file) -> None:
+def test_crashes(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(OSError):
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index c36466e02..1eaff0c7d 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -16,7 +16,7 @@ from .helper import (
def test_sanity(tmp_path: Path) -> None:
- def roundtrip(im) -> None:
+ def roundtrip(im: Image.Image) -> None:
outfile = str(tmp_path / "temp.bmp")
im.save(outfile, "BMP")
@@ -194,7 +194,7 @@ def test_rle4() -> None:
("Tests/images/bmp/g/pal8rle.bmp", 1064),
),
)
-def test_rle8_eof(file_name, length) -> None:
+def test_rle8_eof(file_name: str, length: int) -> None:
with open(file_name, "rb") as fp:
data = fp.read(length)
with Image.open(io.BytesIO(data)) as im:
diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py
index 813b444db..7f76fb47a 100644
--- a/Tests/test_file_container.py
+++ b/Tests/test_file_container.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+from typing import Literal
+
import pytest
from PIL import ContainerIO, Image
@@ -21,9 +23,16 @@ def test_isatty() -> None:
assert container.isatty() is False
-def test_seek_mode_0() -> None:
+@pytest.mark.parametrize(
+ "mode, expected_position",
+ (
+ (0, 33),
+ (1, 66),
+ (2, 100),
+ ),
+)
+def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None:
# Arrange
- mode = 0
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@@ -32,35 +41,7 @@ def test_seek_mode_0() -> None:
container.seek(33, mode)
# Assert
- assert container.tell() == 33
-
-
-def test_seek_mode_1() -> None:
- # Arrange
- mode = 1
- with open(TEST_FILE, "rb") as fh:
- container = ContainerIO.ContainerIO(fh, 22, 100)
-
- # Act
- container.seek(33, mode)
- container.seek(33, mode)
-
- # Assert
- assert container.tell() == 66
-
-
-def test_seek_mode_2() -> None:
- # Arrange
- mode = 2
- with open(TEST_FILE, "rb") as fh:
- container = ContainerIO.ContainerIO(fh, 22, 100)
-
- # Act
- container.seek(33, mode)
- container.seek(33, mode)
-
- # Assert
- assert container.tell() == 100
+ assert container.tell() == expected_position
@pytest.mark.parametrize("bytesmode", (True, False))
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index fc524721c..f86fb8d09 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -4,7 +4,7 @@ import warnings
import pytest
-from PIL import FliImagePlugin, Image
+from PIL import FliImagePlugin, Image, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
@@ -12,9 +12,12 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
# save as...-> hopper.fli, default options.
static_test_file = "Tests/images/hopper.fli"
-# From https://samples.libav.org/fli-flc/
+# From https://samples.ffmpeg.org/fli-flc/
animated_test_file = "Tests/images/a.fli"
+# From https://samples.ffmpeg.org/fli-flc/
+animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
+
def test_sanity() -> None:
with Image.open(static_test_file) as im:
@@ -32,6 +35,24 @@ def test_sanity() -> None:
assert im.is_animated
+def test_prefix_chunk() -> None:
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
+ try:
+ with Image.open(animated_test_file_with_prefix_chunk) as im:
+ assert im.mode == "P"
+ assert im.size == (320, 200)
+ assert im.format == "FLI"
+ assert im.info["duration"] == 171
+ assert im.is_animated
+
+ palette = im.getpalette()
+ assert palette[3:6] == [255, 255, 255]
+ assert palette[381:384] == [204, 204, 12]
+ assert palette[765:] == [252, 0, 0]
+ finally:
+ ImageFile.LOAD_TRUNCATED_IMAGES = False
+
+
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index db9d3586c..9585e576f 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -1113,6 +1113,21 @@ def test_append_images(tmp_path: Path) -> None:
assert reread.n_frames == 10
+def test_append_different_size_image(tmp_path: Path) -> None:
+ out = str(tmp_path / "temp.gif")
+
+ im = Image.new("RGB", (100, 100))
+ bigger_im = Image.new("RGB", (200, 200), "#f00")
+
+ im.save(out, save_all=True, append_images=[bigger_im])
+
+ with Image.open(out) as reread:
+ assert reread.size == (100, 100)
+
+ reread.seek(1)
+ assert reread.size == (100, 100)
+
+
def test_transparent_optimize(tmp_path: Path) -> None:
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
# transparency.
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index 65f090931..f69a290fa 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None:
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
-def test_save_to_bytes_bmp(mode) -> None:
+def test_save_to_bytes_bmp(mode: str) -> None:
output = io.BytesIO()
im = hopper(mode)
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py
index f932069b9..036965bf5 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -82,7 +82,7 @@ def test_eoferror() -> None:
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
-def test_roundtrip(mode, tmp_path: Path) -> None:
+def test_roundtrip(mode: str, tmp_path: Path) -> None:
out = str(tmp_path / "temp.im")
im = hopper(mode)
im.save(out)
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index 9c0969437..88c30d468 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -98,7 +98,7 @@ def test_i() -> None:
assert ret == 97
-def test_dump(monkeypatch) -> None:
+def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
c = b"abc"
# Temporarily redirect stdout
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 654242148..33f845402 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -6,7 +6,7 @@ import warnings
from io import BytesIO
from pathlib import Path
from types import ModuleType
-from typing import Any
+from typing import Any, cast
import pytest
@@ -45,14 +45,20 @@ TEST_FILE = "Tests/images/hopper.jpg"
@skip_unless_feature("jpg")
class TestFileJpeg:
- def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image:
+ def roundtrip_with_bytes(
+ self, im: Image.Image, **options: Any
+ ) -> tuple[JpegImagePlugin.JpegImageFile, int]:
out = BytesIO()
im.save(out, "JPEG", **options)
test_bytes = out.tell()
out.seek(0)
- im = Image.open(out)
- im.bytes = test_bytes # for testing only
- return im
+ reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out))
+ return reloaded, test_bytes
+
+ def roundtrip(
+ self, im: Image.Image, **options: Any
+ ) -> JpegImagePlugin.JpegImageFile:
+ return self.roundtrip_with_bytes(im, **options)[0]
def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image:
"""Generates a very hard to compress file
@@ -246,13 +252,13 @@ class TestFileJpeg:
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
def test_optimize(self) -> None:
- im1 = self.roundtrip(hopper())
- im2 = self.roundtrip(hopper(), optimize=0)
- im3 = self.roundtrip(hopper(), optimize=1)
+ im1, im1_bytes = self.roundtrip_with_bytes(hopper())
+ im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0)
+ im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1)
assert_image_equal(im1, im2)
assert_image_equal(im1, im3)
- assert im1.bytes >= im2.bytes
- assert im1.bytes >= im3.bytes
+ assert im1_bytes >= im2_bytes
+ assert im1_bytes >= im3_bytes
def test_optimize_large_buffer(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148
@@ -262,15 +268,15 @@ class TestFileJpeg:
im.save(f, format="JPEG", optimize=True)
def test_progressive(self) -> None:
- im1 = self.roundtrip(hopper())
+ im1, im1_bytes = self.roundtrip_with_bytes(hopper())
im2 = self.roundtrip(hopper(), progressive=False)
- im3 = self.roundtrip(hopper(), progressive=True)
+ im3, im3_bytes = self.roundtrip_with_bytes(hopper(), progressive=True)
assert not im1.info.get("progressive")
assert not im2.info.get("progressive")
assert im3.info.get("progressive")
assert_image_equal(im1, im3)
- assert im1.bytes >= im3.bytes
+ assert im1_bytes >= im3_bytes
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
@@ -341,6 +347,7 @@ class TestFileJpeg:
assert exif.get_ifd(0x8825) == {}
transposed = ImageOps.exif_transpose(im)
+ assert transposed is not None
exif = transposed.getexif()
assert exif.get_ifd(0x8825) == {}
@@ -419,14 +426,14 @@ class TestFileJpeg:
assert im3.info.get("progression")
def test_quality(self) -> None:
- im1 = self.roundtrip(hopper())
- im2 = self.roundtrip(hopper(), quality=50)
+ im1, im1_bytes = self.roundtrip_with_bytes(hopper())
+ im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50)
assert_image(im1, im2.mode, im2.size)
- assert im1.bytes >= im2.bytes
+ assert im1_bytes >= im2_bytes
- im3 = self.roundtrip(hopper(), quality=0)
+ im3, im3_bytes = self.roundtrip_with_bytes(hopper(), quality=0)
assert_image(im1, im3.mode, im3.size)
- assert im2.bytes > im3.bytes
+ assert im2_bytes > im3_bytes
def test_smooth(self) -> None:
im1 = self.roundtrip(hopper())
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index fab19e2ea..b7f8350c7 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -40,10 +40,8 @@ test_card.load()
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO()
im.save(out, "JPEG2000", **options)
- test_bytes = out.tell()
out.seek(0)
with Image.open(out) as im:
- im.bytes = test_bytes # for testing only
im.load()
return im
@@ -77,7 +75,9 @@ def test_invalid_file() -> None:
def test_bytesio() -> None:
with open("Tests/images/test-card-lossless.jp2", "rb") as f:
data = BytesIO(f.read())
- assert_image_similar_tofile(test_card, data, 1.0e-3)
+ with Image.open(data) as im:
+ im.load()
+ assert_image_similar(im, test_card, 1.0e-3)
# These two test pre-written JPEG 2000 files that were not written with
@@ -340,6 +340,7 @@ def test_parser_feed() -> None:
p.feed(data)
# Assert
+ assert p.image is not None
assert p.image.size == (640, 480)
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 0994d9904..908464a11 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -27,7 +27,7 @@ from .helper import (
@skip_unless_feature("libtiff")
class LibTiffTestCase:
- def _assert_noerr(self, tmp_path: Path, im: Image.Image) -> None:
+ def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None:
"""Helper tests that assert basic sanity about the g4 tiff reading"""
# 1 bit
assert im.mode == "1"
@@ -524,7 +524,8 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, compression=compression)
def test_fp_leak(self) -> None:
- im = Image.open("Tests/images/hopper_g4_500.tif")
+ im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif")
+ assert im is not None
fn = im.fp.fileno()
os.fstat(fn)
@@ -716,6 +717,7 @@ class TestFileLibTiff(LibTiffTestCase):
f.write(src.read())
im = Image.open(tmpfile)
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.n_frames
im.close()
# Should not raise PermissionError.
@@ -1097,6 +1099,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as im:
# Assert that there are multiple strips
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[STRIPOFFSETS]) > 1
@pytest.mark.parametrize("argument", (True, False))
@@ -1113,6 +1116,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, **arguments)
with Image.open(out) as im:
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[STRIPOFFSETS]) == 1
finally:
TiffImagePlugin.STRIP_SIZE = 65536
diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py
index 2c94fdc39..e11e6bb52 100644
--- a/Tests/test_file_mcidas.py
+++ b/Tests/test_file_mcidas.py
@@ -19,7 +19,7 @@ def test_valid_file() -> None:
# https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8
# https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/
test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara"
- saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png"
+ saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff"
# Act
with Image.open(test_file) as im:
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index 4fb00d699..f105428ca 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -2,11 +2,11 @@ from __future__ import annotations
import warnings
from io import BytesIO
-from typing import Any
+from typing import Any, cast
import pytest
-from PIL import Image
+from PIL import Image, MpoImagePlugin
from .helper import (
assert_image_equal,
@@ -20,14 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg")
-def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
+def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile:
out = BytesIO()
im.save(out, "MPO", **options)
- test_bytes = out.tell()
out.seek(0)
- im = Image.open(out)
- im.bytes = test_bytes # for testing only
- return im
+ return cast(MpoImagePlugin.MpoImageFile, Image.open(out))
@pytest.mark.parametrize("test_file", test_files)
diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py
index f9f81d114..b0964aabe 100644
--- a/Tests/test_file_msp.py
+++ b/Tests/test_file_msp.py
@@ -52,7 +52,7 @@ def test_open_windows_v1() -> None:
assert isinstance(im, MspImagePlugin.MspImageFile)
-def _assert_file_image_equal(source_path, target_path) -> None:
+def _assert_file_image_equal(source_path: str, target_path: str) -> None:
with Image.open(source_path) as im:
assert_image_equal_tofile(im, target_path)
diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py
index a2486be40..ab9f9663e 100644
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -9,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin
from .helper import assert_image_equal, hopper
-def _roundtrip(tmp_path: Path, im) -> None:
+def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
f = str(tmp_path / "temp.pcx")
im.save(f)
with Image.open(f) as im2:
@@ -44,7 +44,7 @@ def test_invalid_file() -> None:
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
-def test_odd(tmp_path: Path, mode) -> None:
+def test_odd(tmp_path: Path, mode: str) -> None:
# See issue #523, odd sized images should have a stride that's even.
# Not that ImageMagick or GIMP write PCX that way.
# We were not handling properly.
@@ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None:
_roundtrip(tmp_path, im)
-def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None:
+def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None:
_last = ImageFile.MAXBLOCK
ImageFile.MAXBLOCK = size
try:
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index 65a93c138..d39a86565 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -6,6 +6,7 @@ import os.path
import tempfile
import time
from pathlib import Path
+from typing import Any, Generator
import pytest
@@ -14,7 +15,7 @@ from PIL import Image, PdfParser, features
from .helper import hopper, mark_if_feature_version, skip_unless_feature
-def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
+def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
# Arrange
im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
@@ -41,13 +42,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
-def test_save(tmp_path: Path, mode) -> None:
+def test_save(tmp_path: Path, mode: str) -> None:
helper_save_as_pdf(tmp_path, mode)
@skip_unless_feature("jpg_2000")
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
-def test_save_alpha(tmp_path: Path, mode) -> None:
+def test_save_alpha(tmp_path: Path, mode: str) -> None:
helper_save_as_pdf(tmp_path, mode)
@@ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None:
{"dpi": (75, 150), "resolution": 200},
),
)
-def test_dpi(params, tmp_path: Path) -> None:
+def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
@@ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None:
assert os.path.getsize(outfile) > 0
# Test appending using a generator
- def im_generator(ims):
+ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
yield from ims
im.save(outfile, save_all=True, append_images=im_generator(ims))
@@ -226,7 +227,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
-def check_pdf_pages_consistency(pdf) -> None:
+def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info
assert b"Kids" in pages_info
@@ -339,7 +340,7 @@ def test_pdf_append_to_bytesio() -> None:
@pytest.mark.timeout(1)
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
-def test_redos(newline) -> None:
+def test_redos(newline: bytes) -> None:
malicious = b" trailer<<>>" + newline * 3456
# This particular exception isn't relevant here.
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index d4a634316..1a8d58bd6 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -6,7 +6,8 @@ import warnings
import zlib
from io import BytesIO
from pathlib import Path
-from typing import Any
+from types import ModuleType
+from typing import Any, cast
import pytest
@@ -23,6 +24,7 @@ from .helper import (
skip_unless_feature,
)
+ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
@@ -57,11 +59,11 @@ def load(data: bytes) -> Image.Image:
return Image.open(BytesIO(data))
-def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
+def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile:
out = BytesIO()
im.save(out, "PNG", **options)
out.seek(0)
- return Image.open(out)
+ return cast(PngImagePlugin.PngImageFile, Image.open(out))
@skip_unless_feature("zlib")
@@ -100,7 +102,7 @@ class TestFilePng:
im = hopper(mode)
im.save(test_file)
with Image.open(test_file) as reloaded:
- if mode in ("I;16", "I;16B"):
+ if mode in ("I", "I;16B"):
reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im)
@@ -302,8 +304,8 @@ class TestFilePng:
assert im.getcolors() == [(100, (0, 0, 0, 0))]
def test_save_grayscale_transparency(self, tmp_path: Path) -> None:
- for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
- in_file = "Tests/images/" + mode.lower() + "_trns.png"
+ for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items():
+ in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png"
with Image.open(in_file) as im:
assert im.mode == mode
assert im.info["transparency"] == 255
@@ -781,6 +783,18 @@ class TestFilePng:
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
+ def test_truncated_end_chunk(self) -> None:
+ with Image.open("Tests/images/truncated_end_chunk.png") as im:
+ with pytest.raises(OSError):
+ im.load()
+
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
+ try:
+ with Image.open("Tests/images/truncated_end_chunk.png") as im:
+ assert_image_equal_tofile(im, "Tests/images/hopper.png")
+ finally:
+ ImageFile.LOAD_TRUNCATED_IMAGES = False
+
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@skip_unless_feature("zlib")
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index 6e0fa32e4..6a0a5a445 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -88,7 +88,7 @@ def test_16bit_pgm() -> None:
assert im.size == (20, 100)
assert im.get_format_mimetype() == "image/x-portable-graymap"
- assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png")
+ assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff")
def test_16bit_pgm_write(tmp_path: Path) -> None:
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index 7eca8d9b1..e60638b22 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None:
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
],
)
-def test_crashes(test_file, raises) -> None:
+def test_crashes(test_file: str, raises) -> None:
with open(test_file, "rb") as f:
with pytest.raises(raises):
with Image.open(f):
diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py
index 75fef1dc6..9b82a962a 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -9,7 +9,7 @@ import pytest
from PIL import Image, ImageSequence, SpiderImagePlugin
-from .helper import assert_image_equal_tofile, hopper, is_pypy
+from .helper import assert_image_equal, hopper, is_pypy
TEST_FILE = "Tests/images/hopper.spider"
@@ -160,4 +160,5 @@ def test_odd_size() -> None:
im.save(data, format="SPIDER")
data.seek(0)
- assert_image_equal_tofile(im, data)
+ with Image.open(data) as im2:
+ assert_image_equal(im, im2)
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index bd8e522c7..3c6da50c5 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
@pytest.mark.parametrize("mode", _MODES)
-def test_sanity(mode, tmp_path: Path) -> None:
- def roundtrip(original_im) -> None:
+def test_sanity(mode: str, tmp_path: Path) -> None:
+ def roundtrip(original_im: Image.Image) -> None:
out = str(tmp_path / "temp.tga")
original_im.save(out, rle=rle)
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index a16b76e19..21d52462e 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -4,6 +4,8 @@ import os
import warnings
from io import BytesIO
from pathlib import Path
+from types import ModuleType
+from typing import Generator
import pytest
@@ -20,6 +22,7 @@ from .helper import (
is_win32,
)
+ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
@@ -156,7 +159,7 @@ class TestFileTiff:
"resolution_unit, dpi",
[(None, 72.8), (2, 72.8), (3, 184.912)],
)
- def test_load_float_dpi(self, resolution_unit, dpi) -> None:
+ def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None:
with Image.open(
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
) as im:
@@ -284,7 +287,7 @@ class TestFileTiff:
("Tests/images/multipage.tiff", 3),
),
)
- def test_n_frames(self, path, n_frames) -> None:
+ def test_n_frames(self, path: str, n_frames: int) -> None:
with Image.open(path) as im:
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
@@ -402,7 +405,7 @@ class TestFileTiff:
assert len_before == len_after + 1
@pytest.mark.parametrize("legacy_api", (False, True))
- def test_load_byte(self, legacy_api) -> None:
+ def test_load_byte(self, legacy_api: bool) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abc"
ret = ifd.load_byte(data, legacy_api)
@@ -431,7 +434,7 @@ class TestFileTiff:
assert 0x8825 in im.tag_v2
def test_exif(self, tmp_path: Path) -> None:
- def check_exif(exif) -> None:
+ def check_exif(exif: Image.Exif) -> None:
assert sorted(exif.keys()) == [
256,
257,
@@ -511,7 +514,7 @@ class TestFileTiff:
assert im.getexif()[273] == (1408, 1907)
@pytest.mark.parametrize("mode", ("1", "L"))
- def test_photometric(self, mode, tmp_path: Path) -> None:
+ def test_photometric(self, mode: str, tmp_path: Path) -> None:
filename = str(tmp_path / "temp.tif")
im = hopper(mode)
im.save(filename, tiffinfo={262: 0})
@@ -620,6 +623,7 @@ class TestFileTiff:
im.save(outfile, tiffinfo={278: 256})
with Image.open(outfile) as im:
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[278] == 256
def test_strip_raw(self) -> None:
@@ -660,7 +664,7 @@ class TestFileTiff:
assert_image_equal_tofile(reloaded, infile)
@pytest.mark.parametrize("mode", ("P", "PA"))
- def test_palette(self, mode, tmp_path: Path) -> None:
+ def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
im = hopper(mode)
@@ -689,7 +693,7 @@ class TestFileTiff:
assert reread.n_frames == 3
# Test appending using a generator
- def im_generator(ims):
+ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
yield from ims
mp = BytesIO()
@@ -860,7 +864,7 @@ class TestFileTiff:
],
)
@pytest.mark.timeout(2)
- def test_oom(self, test_file) -> None:
+ def test_oom(self, test_file: str) -> None:
with pytest.raises(UnidentifiedImageError):
with pytest.warns(UserWarning):
with Image.open(test_file):
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index bb6225d07..d7a18c725 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None:
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
-def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
+def test_writing_other_types_to_ascii(
+ value: bytes | int, expected: str, tmp_path: Path
+) -> None:
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271]
@@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
@pytest.mark.parametrize("value", (1, IFDRational(1)))
-def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None:
+def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None:
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py
index 73aaae6e7..c07024a2c 100644
--- a/Tests/test_format_hsv.py
+++ b/Tests/test_format_hsv.py
@@ -2,21 +2,18 @@ from __future__ import annotations
import colorsys
import itertools
+from typing import Callable
from PIL import Image
from .helper import assert_image_similar, hopper
-def int_to_float(i):
+def int_to_float(i: int) -> float:
return i / 255
-def str_to_float(i):
- return ord(i) / 255
-
-
-def tuple_to_ints(tp):
+def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]:
x, y, z = tp
return int(x * 255.0), int(y * 255.0), int(z * 255.0)
@@ -25,7 +22,7 @@ def test_sanity() -> None:
Image.new("HSV", (100, 100))
-def wedge():
+def wedge() -> Image.Image:
w = Image._wedge()
w90 = w.rotate(90)
@@ -49,7 +46,11 @@ def wedge():
return img
-def to_xxx_colorsys(im, func, mode):
+def to_xxx_colorsys(
+ im: Image.Image,
+ func: Callable[[float, float, float], tuple[float, float, float]],
+ mode: str,
+) -> Image.Image:
# convert the hard way using the library colorsys routines.
(r, g, b) = im.split()
@@ -70,11 +71,11 @@ def to_xxx_colorsys(im, func, mode):
return hsv
-def to_hsv_colorsys(im):
+def to_hsv_colorsys(im: Image.Image) -> Image.Image:
return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV")
-def to_rgb_colorsys(im):
+def to_rgb_colorsys(im: Image.Image) -> Image.Image:
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 4c04e0da4..b4e8e660c 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -138,13 +138,13 @@ class TestImage:
assert im.height == 2
with pytest.raises(AttributeError):
- im.size = (3, 4)
+ im.size = (3, 4) # type: ignore[misc]
def test_set_mode(self) -> None:
im = Image.new("RGB", (1, 1))
with pytest.raises(AttributeError):
- im.mode = "P"
+ im.mode = "P" # type: ignore[misc]
def test_invalid_image(self) -> None:
im = io.BytesIO(b"")
@@ -685,15 +685,18 @@ class TestImage:
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())
- def test_p_from_rgb_rgba(self) -> None:
- for mode, color in [
+ @pytest.mark.parametrize(
+ "mode, color",
+ (
("RGB", "#DDEEFF"),
("RGB", (221, 238, 255)),
("RGBA", (221, 238, 255, 255)),
- ]:
- im = Image.new("P", (100, 100), color)
- expected = Image.new(mode, (100, 100), color)
- assert_image_equal(im.convert(mode), expected)
+ ),
+ )
+ def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None:
+ im = Image.new("P", (100, 100), color)
+ expected = Image.new(mode, (100, 100), color)
+ assert_image_equal(im.convert(mode), expected)
def test_no_resource_warning_on_save(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/835
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index 380b89de8..8c42da57a 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -14,6 +14,7 @@ from .helper import assert_image_equal, hopper, is_win32
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
+cffi: ModuleType | None
if os.environ.get("PYTHONOPTIMIZE") == "2":
cffi = None
else:
diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py
index 6a10ae453..47f9ffa3d 100644
--- a/Tests/test_image_filter.py
+++ b/Tests/test_image_filter.py
@@ -148,9 +148,7 @@ def test_kernel_not_enough_coefficients() -> None:
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_3x3(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source:
- reference_name = "hopper_emboss"
- reference_name += "_I.png" if mode == "I" else ".bmp"
- with Image.open("Tests/images/" + reference_name) as reference:
+ with Image.open("Tests/images/hopper_emboss.bmp") as reference:
kernel = ImageFilter.Kernel(
(3, 3),
# fmt: off
@@ -160,23 +158,13 @@ def test_consistency_3x3(mode: str) -> None:
# fmt: on
0.3,
)
- source = source.split() * 2
- reference = reference.split() * 2
-
- if mode == "I":
- source = source[0].convert(mode)
- else:
- source = Image.merge(mode, source[: len(mode)])
- reference = Image.merge(mode, reference[: len(mode)])
assert_image_equal(source.filter(kernel), reference)
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_5x5(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source:
- reference_name = "hopper_emboss_more"
- reference_name += "_I.png" if mode == "I" else ".bmp"
- with Image.open("Tests/images/" + reference_name) as reference:
+ with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
kernel = ImageFilter.Kernel(
(5, 5),
# fmt: off
@@ -188,14 +176,6 @@ def test_consistency_5x5(mode: str) -> None:
# fmt: on
0.3,
)
- source = source.split() * 2
- reference = reference.split() * 2
-
- if mode == "I":
- source = source[0].convert(mode)
- else:
- source = Image.merge(mode, source[: len(mode)])
- reference = Image.merge(mode, reference[: len(mode)])
assert_image_equal(source.filter(kernel), reference)
diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py
index ea31a9de9..c20123a1b 100644
--- a/Tests/test_image_fromqimage.py
+++ b/Tests/test_image_fromqimage.py
@@ -1,7 +1,6 @@
from __future__ import annotations
import warnings
-from typing import Generator
import pytest
@@ -17,19 +16,16 @@ pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
)
+ims = [
+ hopper(),
+ Image.open("Tests/images/transparent.png"),
+ Image.open("Tests/images/7x13.png"),
+]
-@pytest.fixture
-def test_images() -> Generator[Image.Image, None, None]:
- ims = [
- hopper(),
- Image.open("Tests/images/transparent.png"),
- Image.open("Tests/images/7x13.png"),
- ]
- try:
- yield ims
- finally:
- for im in ims:
- im.close()
+
+def teardown_module() -> None:
+ for im in ims:
+ im.close()
def roundtrip(expected: Image.Image) -> None:
@@ -44,26 +40,26 @@ def roundtrip(expected: Image.Image) -> None:
assert_image_equal(result, expected.convert("RGB"))
-def test_sanity_1(test_images: Generator[Image.Image, None, None]) -> None:
- for im in test_images:
+def test_sanity_1() -> None:
+ for im in ims:
roundtrip(im.convert("1"))
-def test_sanity_rgb(test_images: Generator[Image.Image, None, None]) -> None:
- for im in test_images:
+def test_sanity_rgb() -> None:
+ for im in ims:
roundtrip(im.convert("RGB"))
-def test_sanity_rgba(test_images: Generator[Image.Image, None, None]) -> None:
- for im in test_images:
+def test_sanity_rgba() -> None:
+ for im in ims:
roundtrip(im.convert("RGBA"))
-def test_sanity_l(test_images: Generator[Image.Image, None, None]) -> None:
- for im in test_images:
+def test_sanity_l() -> None:
+ for im in ims:
roundtrip(im.convert("L"))
-def test_sanity_p(test_images: Generator[Image.Image, None, None]) -> None:
- for im in test_images:
+def test_sanity_p() -> None:
+ for im in ims:
roundtrip(im.convert("P"))
diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py
index ce7345572..d8f6b65e0 100644
--- a/Tests/test_image_paste.py
+++ b/Tests/test_image_paste.py
@@ -8,7 +8,6 @@ from .helper import CachedProperty, assert_image_equal
class TestImagingPaste:
- masks = {}
size = 128
def assert_9points_image(
@@ -33,7 +32,7 @@ class TestImagingPaste:
def assert_9points_paste(
self,
im: Image.Image,
- im2: Image.Image,
+ im2: Image.Image | str | tuple[int, ...],
mask: Image.Image,
expected: list[tuple[int, int, int, int]],
) -> None:
diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py
index 7090ff9cd..dbe193808 100644
--- a/Tests/test_image_resample.py
+++ b/Tests/test_image_resample.py
@@ -237,7 +237,7 @@ class TestCoreResampleConsistency:
im = Image.new(mode, (512, 9), fill)
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
- def run_case(self, case: tuple[Image.Image, Image.Image]) -> None:
+ def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None:
channel, color = case
px = channel.load()
for x in range(channel.size[0]):
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index a64e4a846..64098f80f 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -154,7 +154,7 @@ class TestImagingCoreResize:
def test_unknown_filter(self) -> None:
with pytest.raises(ValueError):
- self.resize(hopper(), (10, 10), 9)
+ self.resize(hopper(), (10, 10), 9) # type: ignore[arg-type]
def test_cross_platform(self, tmp_path: Path) -> None:
# This test is intended for only check for consistent behaviour across
diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py
index 94f57e066..7e2290c15 100644
--- a/Tests/test_imagechops.py
+++ b/Tests/test_imagechops.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+from typing import Callable
+
from PIL import Image, ImageChops
from .helper import assert_image_equal, hopper
@@ -387,7 +389,9 @@ def test_overlay() -> None:
def test_logical() -> None:
- def table(op, a, b):
+ def table(
+ op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
+ ) -> tuple[int, int, int, int]:
out = []
for x in (a, b):
imx = Image.new("1", (1, 1), x)
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index 83fc38ed3..6be29a70f 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -6,6 +6,7 @@ import re
import shutil
from io import BytesIO
from pathlib import Path
+from typing import Any
import pytest
@@ -237,7 +238,7 @@ def test_invalid_color_temperature() -> None:
@pytest.mark.parametrize("flag", ("my string", -1))
-def test_invalid_flag(flag) -> None:
+def test_invalid_flag(flag: str | int) -> None:
with hopper() as im:
with pytest.raises(
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
@@ -335,19 +336,21 @@ def test_extended_information() -> None:
o = ImageCms.getOpenProfile(SRGB)
p = o.profile
- def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None:
+ def assert_truncated_tuple_equal(
+ tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10
+ ) -> None:
# Helper function to reduce precision of tuples of floats
# recursively and then check equality.
power = 10**digits
- def truncate_tuple(tuple_or_float):
+ def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]:
return tuple(
(
truncate_tuple(val)
if isinstance(val, tuple)
else int(val * power) / power
)
- for val in tuple_or_float
+ for val in tuple_value
)
assert truncate_tuple(tup1) == truncate_tuple(tup2)
@@ -504,8 +507,10 @@ def test_profile_typesafety() -> None:
ImageCms.ImageCmsProfile(1).tobytes()
-def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None:
- def create_test_image():
+def assert_aux_channel_preserved(
+ mode: str, transform_in_place: bool, preserved_channel: str
+) -> None:
+ def create_test_image() -> Image.Image:
# set up test image with something interesting in the tested aux channel.
# fmt: off
nine_grid_deltas = [
@@ -633,7 +638,7 @@ def test_auxiliary_channels_isolated() -> None:
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
-def test_rgb_lab(mode) -> None:
+def test_rgb_lab(mode: str) -> None:
im = Image.new(mode, (1, 1))
converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index f7aea3034..274753c6c 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -753,7 +753,7 @@ def test_rectangle_I16(bbox: Coords) -> None:
draw.rectangle(bbox, outline=0xFFFF)
# Assert
- assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
+ assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff")
@pytest.mark.parametrize("bbox", BBOX)
diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py
index 07a25b84b..3171eb9ae 100644
--- a/Tests/test_imagedraw2.py
+++ b/Tests/test_imagedraw2.py
@@ -5,6 +5,7 @@ import os.path
import pytest
from PIL import Image, ImageDraw, ImageDraw2, features
+from PIL._typing import Coords
from .helper import (
assert_image_equal,
@@ -56,7 +57,7 @@ def test_sanity() -> None:
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse(bbox) -> None:
+def test_ellipse(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -84,7 +85,7 @@ def test_ellipse_edge() -> None:
@pytest.mark.parametrize("points", POINTS)
-def test_line(points) -> None:
+def test_line(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -98,7 +99,7 @@ def test_line(points) -> None:
@pytest.mark.parametrize("points", POINTS)
-def test_line_pen_as_brush(points) -> None:
+def test_line_pen_as_brush(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -114,7 +115,7 @@ def test_line_pen_as_brush(points) -> None:
@pytest.mark.parametrize("points", POINTS)
-def test_polygon(points) -> None:
+def test_polygon(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -129,7 +130,7 @@ def test_polygon(points) -> None:
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle(bbox) -> None:
+def test_rectangle(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py
index 9ce9cda82..6ebc61e1b 100644
--- a/Tests/test_imageenhance.py
+++ b/Tests/test_imageenhance.py
@@ -22,7 +22,7 @@ def test_crash() -> None:
ImageEnhance.Sharpness(im).enhance(0.5)
-def _half_transparent_image():
+def _half_transparent_image() -> Image.Image:
# returns an image, half transparent, half solid
im = hopper("RGB")
@@ -34,7 +34,9 @@ def _half_transparent_image():
return im
-def _check_alpha(im, original, op, amount) -> None:
+def _check_alpha(
+ im: Image.Image, original: Image.Image, op: str, amount: float
+) -> None:
assert im.getbands() == original.getbands()
assert_image_equal(
im.getchannel("A"),
@@ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount) -> None:
@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
-def test_alpha(op) -> None:
+def test_alpha(op: str) -> None:
# Issue https://github.com/python-pillow/Pillow/issues/899
# Is alpha preserved through image enhancement?
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index 491409781..44521a8b3 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -31,7 +31,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK
class TestImageFile:
def test_parser(self) -> None:
- def roundtrip(format):
+ def roundtrip(format: str) -> tuple[Image.Image, Image.Image]:
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
if format in ("MSP", "XBM"):
im = im.convert("1")
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 909026dc8..05b5d4716 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -7,11 +7,13 @@ import shutil
import sys
from io import BytesIO
from pathlib import Path
+from typing import Any, BinaryIO
import pytest
from packaging.version import parse as parse_version
from PIL import Image, ImageDraw, ImageFont, features
+from PIL._typing import StrOrBytesPath
from .helper import (
assert_image_equal,
@@ -42,16 +44,16 @@ def test_sanity() -> None:
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
],
)
-def layout_engine(request):
+def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
return request.param
@pytest.fixture(scope="module")
-def font(layout_engine):
+def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont:
return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine)
-def test_font_properties(font) -> None:
+def test_font_properties(font: ImageFont.FreeTypeFont) -> None:
assert font.path == FONT_PATH
assert font.size == FONT_SIZE
@@ -67,7 +69,9 @@ def test_font_properties(font) -> None:
assert font_copy.path == second_font_path
-def _render(font, layout_engine):
+def _render(
+ font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout
+) -> Image.Image:
txt = "Hello World!"
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine)
ttf.getbbox(txt)
@@ -80,12 +84,12 @@ def _render(font, layout_engine):
@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
-def test_font_with_name(layout_engine, font) -> None:
+def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None:
_render(font, layout_engine)
-def test_font_with_filelike(layout_engine) -> None:
- def _font_as_bytes():
+def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None:
+ def _font_as_bytes() -> BytesIO:
with open(FONT_PATH, "rb") as f:
font_bytes = BytesIO(f.read())
return font_bytes
@@ -102,12 +106,12 @@ def test_font_with_filelike(layout_engine) -> None:
# _render(shared_bytes)
-def test_font_with_open_file(layout_engine) -> None:
+def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None:
with open(FONT_PATH, "rb") as f:
_render(f, layout_engine)
-def test_render_equal(layout_engine) -> None:
+def test_render_equal(layout_engine: ImageFont.Layout) -> None:
img_path = _render(FONT_PATH, layout_engine)
with open(FONT_PATH, "rb") as f:
font_filelike = BytesIO(f.read())
@@ -116,7 +120,7 @@ def test_render_equal(layout_engine) -> None:
assert_image_equal(img_path, img_filelike)
-def test_non_ascii_path(tmp_path: Path, layout_engine) -> None:
+def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None:
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
try:
shutil.copy(FONT_PATH, tempfile)
@@ -126,7 +130,7 @@ def test_non_ascii_path(tmp_path: Path, layout_engine) -> None:
ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine)
-def test_transparent_background(font) -> None:
+def test_transparent_background(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGBA", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -140,7 +144,7 @@ def test_transparent_background(font) -> None:
assert_image_similar_tofile(im.convert("L"), target, 0.01)
-def test_I16(font) -> None:
+def test_I16(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="I;16", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -153,7 +157,7 @@ def test_I16(font) -> None:
assert_image_similar_tofile(im.convert("L"), target, 0.01)
-def test_textbbox_equal(font) -> None:
+def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -181,7 +185,13 @@ def test_textbbox_equal(font) -> None:
),
)
def test_getlength(
- text, mode, fontname, size, layout_engine, length_basic, length_raqm
+ text: str,
+ mode: str,
+ fontname: str,
+ size: int,
+ layout_engine: ImageFont.Layout,
+ length_basic: int,
+ length_raqm: float,
) -> None:
f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
@@ -207,7 +217,7 @@ def test_float_size() -> None:
assert lengths[0] != lengths[1] != lengths[2]
-def test_render_multiline(font) -> None:
+def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
line_spacing = font.getbbox("A")[3] + 4
@@ -223,7 +233,7 @@ def test_render_multiline(font) -> None:
assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
-def test_render_multiline_text(font) -> None:
+def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
# Test that text() correctly connects to multiline_text()
# and that align defaults to left
im = Image.new(mode="RGB", size=(300, 100))
@@ -243,7 +253,9 @@ def test_render_multiline_text(font) -> None:
@pytest.mark.parametrize(
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
)
-def test_render_multiline_text_align(font, align, ext) -> None:
+def test_render_multiline_text_align(
+ font: ImageFont.FreeTypeFont, align: str, ext: str
+) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align)
@@ -251,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext) -> None:
assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01)
-def test_unknown_align(font) -> None:
+def test_unknown_align(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -260,14 +272,14 @@ def test_unknown_align(font) -> None:
draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown")
-def test_draw_align(font) -> None:
+def test_draw_align(font: ImageFont.FreeTypeFont) -> None:
im = Image.new("RGB", (300, 100), "white")
draw = ImageDraw.Draw(im)
line = "some text"
draw.text((100, 40), line, (0, 0, 0), font=font, align="left")
-def test_multiline_bbox(font) -> None:
+def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -285,7 +297,7 @@ def test_multiline_bbox(font) -> None:
draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4)
-def test_multiline_width(font) -> None:
+def test_multiline_width(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -295,7 +307,7 @@ def test_multiline_width(font) -> None:
)
-def test_multiline_spacing(font) -> None:
+def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10)
@@ -306,7 +318,9 @@ def test_multiline_spacing(font) -> None:
@pytest.mark.parametrize(
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
)
-def test_rotated_transposed_font(font, orientation) -> None:
+def test_rotated_transposed_font(
+ font: ImageFont.FreeTypeFont, orientation: Image.Transpose
+) -> None:
img_gray = Image.new("L", (100, 100))
draw = ImageDraw.Draw(img_gray)
word = "testing"
@@ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation) -> None:
Image.Transpose.FLIP_TOP_BOTTOM,
),
)
-def test_unrotated_transposed_font(font, orientation) -> None:
+def test_unrotated_transposed_font(
+ font: ImageFont.FreeTypeFont, orientation: Image.Transpose
+) -> None:
img_gray = Image.new("L", (100, 100))
draw = ImageDraw.Draw(img_gray)
word = "testing"
@@ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation) -> None:
@pytest.mark.parametrize(
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
)
-def test_rotated_transposed_font_get_mask(font, orientation) -> None:
+def test_rotated_transposed_font_get_mask(
+ font: ImageFont.FreeTypeFont, orientation: Image.Transpose
+) -> None:
# Arrange
text = "mask this"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -403,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation) -> None:
Image.Transpose.FLIP_TOP_BOTTOM,
),
)
-def test_unrotated_transposed_font_get_mask(font, orientation) -> None:
+def test_unrotated_transposed_font_get_mask(
+ font: ImageFont.FreeTypeFont, orientation: Image.Transpose
+) -> None:
# Arrange
text = "mask this"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -415,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation) -> None:
assert mask.size == (108, 13)
-def test_free_type_font_get_name(font) -> None:
+def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None:
assert ("FreeMono", "Regular") == font.getname()
-def test_free_type_font_get_metrics(font) -> None:
+def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None:
ascent, descent = font.getmetrics()
assert isinstance(ascent, int)
@@ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font) -> None:
assert (ascent, descent) == (16, 4)
-def test_free_type_font_get_mask(font) -> None:
+def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
# Arrange
text = "mask this"
@@ -473,16 +493,16 @@ def test_default_font() -> None:
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
-def test_getbbox(font, mode) -> None:
+def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
assert (0, 4, 12, 16) == font.getbbox("A", mode)
-def test_getbbox_empty(font) -> None:
+def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None:
# issue #2614, should not crash.
assert (0, 0, 0, 0) == font.getbbox("")
-def test_render_empty(font) -> None:
+def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
# issue 2666
im = Image.new(mode="RGB", size=(300, 100))
target = im.copy()
@@ -492,7 +512,7 @@ def test_render_empty(font) -> None:
assert_image_equal(im, target)
-def test_unicode_extended(layout_engine) -> None:
+def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
# issue #3777
text = "A\u278A\U0001F12B"
target = "Tests/images/unicode_extended.png"
@@ -515,21 +535,23 @@ def test_unicode_extended(layout_engine) -> None:
(("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
)
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
-def test_find_font(monkeypatch, platform, font_directory) -> None:
- def _test_fake_loading_font(path_to_fake, fontname) -> None:
+def test_find_font(
+ monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str
+) -> None:
+ def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None:
# Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
with monkeypatch.context() as m:
m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
- def loadable_font(filepath, size, index, encoding, *args, **kwargs):
+ def loadable_font(
+ filepath: str, size: int, index: int, encoding: str, *args: Any
+ ):
if filepath == path_to_fake:
return ImageFont._FreeTypeFont(
- FONT_PATH, size, index, encoding, *args, **kwargs
+ FONT_PATH, size, index, encoding, *args
)
- return ImageFont._FreeTypeFont(
- filepath, size, index, encoding, *args, **kwargs
- )
+ return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args)
m.setattr(ImageFont, "FreeTypeFont", loadable_font)
font = ImageFont.truetype(fontname)
@@ -543,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None:
if platform == "linux":
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
- def fake_walker(path):
+ def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
if path == font_directory:
return [
(
@@ -567,7 +589,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None:
_test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate")
-def test_imagefont_getters(font) -> None:
+def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None:
assert font.getmetrics() == (16, 4)
assert font.font.ascent == 16
assert font.font.descent == 4
@@ -588,7 +610,7 @@ def test_imagefont_getters(font) -> None:
@pytest.mark.parametrize("stroke_width", (0, 2))
-def test_getsize_stroke(font, stroke_width) -> None:
+def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None:
assert font.getbbox("A", stroke_width=stroke_width) == (
0 - stroke_width,
4 - stroke_width,
@@ -607,7 +629,7 @@ def test_complex_font_settings() -> None:
t.getmask("абвг", language="sr")
-def test_variation_get(font) -> None:
+def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
@@ -662,7 +684,7 @@ def test_variation_get(font) -> None:
]
-def _check_text(font, path, epsilon):
+def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None:
im = Image.new("RGB", (100, 75), "white")
d = ImageDraw.Draw(im)
d.text((10, 10), "Text", font=font, fill="black")
@@ -677,7 +699,7 @@ def _check_text(font, path, epsilon):
raise
-def test_variation_set_by_name(font) -> None:
+def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
@@ -702,7 +724,7 @@ def test_variation_set_by_name(font) -> None:
_check_text(font, "Tests/images/variation_tiny_name.png", 40)
-def test_variation_set_by_axes(font) -> None:
+def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
@@ -737,7 +759,9 @@ def test_variation_set_by_axes(font) -> None:
),
ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
)
-def test_anchor(layout_engine, anchor, left, top) -> None:
+def test_anchor(
+ layout_engine: ImageFont.Layout, anchor: str, left: int, top: int
+) -> None:
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
@@ -782,7 +806,9 @@ def test_anchor(layout_engine, anchor, left, top) -> None:
("md", "center"),
),
)
-def test_anchor_multiline(layout_engine, anchor, align) -> None:
+def test_anchor_multiline(
+ layout_engine: ImageFont.Layout, anchor: str, align: str
+) -> None:
target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
text = "a\nlong\ntext sample"
@@ -800,7 +826,7 @@ def test_anchor_multiline(layout_engine, anchor, align) -> None:
assert_image_similar_tofile(im, target, 4)
-def test_anchor_invalid(font) -> None:
+def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None:
im = Image.new("RGB", (100, 100), "white")
d = ImageDraw.Draw(im)
d.font = font
@@ -826,7 +852,7 @@ def test_anchor_invalid(font) -> None:
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
-def test_bitmap_font(layout_engine, bpp) -> None:
+def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None:
text = "Bitmap Font"
layout_name = ["basic", "raqm"][layout_engine]
target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
@@ -843,7 +869,7 @@ def test_bitmap_font(layout_engine, bpp) -> None:
assert_image_equal_tofile(im, target)
-def test_bitmap_font_stroke(layout_engine) -> None:
+def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None:
text = "Bitmap Font"
layout_name = ["basic", "raqm"][layout_engine]
target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
@@ -861,7 +887,7 @@ def test_bitmap_font_stroke(layout_engine) -> None:
@pytest.mark.parametrize("embedded_color", (False, True))
-def test_bitmap_blend(layout_engine, embedded_color) -> None:
+def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None:
font = ImageFont.truetype(
"Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine
)
@@ -873,7 +899,7 @@ def test_bitmap_blend(layout_engine, embedded_color) -> None:
assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png")
-def test_standard_embedded_color(layout_engine) -> None:
+def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None:
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
ttf.getbbox(txt)
@@ -886,7 +912,7 @@ def test_standard_embedded_color(layout_engine) -> None:
@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
-def test_float_coord(layout_engine, fontmode):
+def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None:
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
@@ -908,7 +934,7 @@ def test_float_coord(layout_engine, fontmode):
raise
-def test_cbdt(layout_engine) -> None:
+def test_cbdt(layout_engine: ImageFont.Layout) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
@@ -925,7 +951,7 @@ def test_cbdt(layout_engine) -> None:
pytest.skip("freetype compiled without libpng or CBDT support")
-def test_cbdt_mask(layout_engine) -> None:
+def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
@@ -942,7 +968,7 @@ def test_cbdt_mask(layout_engine) -> None:
pytest.skip("freetype compiled without libpng or CBDT support")
-def test_sbix(layout_engine) -> None:
+def test_sbix(layout_engine: ImageFont.Layout) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
@@ -959,7 +985,7 @@ def test_sbix(layout_engine) -> None:
pytest.skip("freetype compiled without libpng or SBIX support")
-def test_sbix_mask(layout_engine) -> None:
+def test_sbix_mask(layout_engine: ImageFont.Layout) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
@@ -977,7 +1003,7 @@ def test_sbix_mask(layout_engine) -> None:
@skip_unless_feature_version("freetype2", "2.10.0")
-def test_colr(layout_engine) -> None:
+def test_colr(layout_engine: ImageFont.Layout) -> None:
font = ImageFont.truetype(
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
size=64,
@@ -993,7 +1019,7 @@ def test_colr(layout_engine) -> None:
@skip_unless_feature_version("freetype2", "2.10.0")
-def test_colr_mask(layout_engine) -> None:
+def test_colr_mask(layout_engine: ImageFont.Layout) -> None:
font = ImageFont.truetype(
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
size=64,
@@ -1008,7 +1034,7 @@ def test_colr_mask(layout_engine) -> None:
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
-def test_woff2(layout_engine) -> None:
+def test_woff2(layout_engine: ImageFont.Layout) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/OpenSans.woff2",
@@ -1042,7 +1068,7 @@ def test_render_mono_size() -> None:
assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
-def test_too_many_characters(font) -> None:
+def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
with pytest.raises(ValueError):
font.getlength("A" * 1_000_001)
with pytest.raises(ValueError):
@@ -1070,14 +1096,14 @@ def test_too_many_characters(font) -> None:
"Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
],
)
-def test_oom(test_file) -> None:
+def test_oom(test_file: str) -> None:
with open(test_file, "rb") as f:
font = ImageFont.truetype(BytesIO(f.read()))
with pytest.raises(Image.DecompressionBombError):
font.getmask("Test Text")
-def test_raqm_missing_warning(monkeypatch) -> None:
+def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
with pytest.warns(UserWarning) as record:
font = ImageFont.truetype(
@@ -1091,6 +1117,8 @@ def test_raqm_missing_warning(monkeypatch) -> None:
@pytest.mark.parametrize("size", [-1, 0])
-def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None:
+def test_invalid_truetype_sizes_raise_valueerror(
+ layout_engine: ImageFont.Layout, size: int
+) -> None:
with pytest.raises(ValueError):
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index 235a2f993..e23adeb70 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -84,6 +84,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
+ assert p.stdin is not None
p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"')
p.communicate()
@@ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_png(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
+ assert p.stdin is not None
p.stdin.write(
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
$ms = new-object System.IO.MemoryStream(, $bytes)
@@ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
reason="Linux with wl-clipboard only",
)
@pytest.mark.parametrize("ext", ("gif", "png", "ico"))
- def test_grabclipboard_wl_clipboard(self, ext) -> None:
+ def test_grabclipboard_wl_clipboard(self, ext: str) -> None:
image_path = "Tests/images/hopper." + ext
with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp)
@@ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
reason="Linux with wl-clipboard only",
)
@pytest.mark.parametrize("arg", ("text", "--clear"))
- def test_grabclipboard_wl_clipboard_errors(self, arg):
+ def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None:
subprocess.call(["wl-copy", arg])
assert ImageGrab.grabclipboard() is None
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index 46b473d7a..32615cf0e 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -73,15 +73,16 @@ def test_lut(op: str) -> None:
def test_no_operator_loaded() -> None:
+ im = Image.new("L", (1, 1))
mop = ImageMorph.MorphOp()
with pytest.raises(Exception) as e:
- mop.apply(None)
+ mop.apply(im)
assert str(e.value) == "No operator loaded"
with pytest.raises(Exception) as e:
- mop.match(None)
+ mop.match(im)
assert str(e.value) == "No operator loaded"
with pytest.raises(Exception) as e:
- mop.save_lut(None)
+ mop.save_lut("")
assert str(e.value) == "No operator loaded"
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index 50bf404ae..d6bdaf450 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -13,8 +13,12 @@ from .helper import (
)
-class Deformer:
- def getmesh(self, im):
+class Deformer(ImageOps.SupportsGetMesh):
+ def getmesh(
+ self, im: Image.Image
+ ) -> list[
+ tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
+ ]:
x, y = im.size
return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))]
@@ -108,7 +112,7 @@ def test_fit_same_ratio() -> None:
@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512)))
-def test_contain(new_size) -> None:
+def test_contain(new_size: tuple[int, int]) -> None:
im = hopper()
new_im = ImageOps.contain(im, new_size)
assert new_im.size == (256, 256)
@@ -132,7 +136,7 @@ def test_contain_round() -> None:
("hopper.png", (256, 256)), # square
),
)
-def test_cover(image_name, expected_size) -> None:
+def test_cover(image_name: str, expected_size: tuple[int, int]) -> None:
with Image.open("Tests/images/" + image_name) as im:
new_im = ImageOps.cover(im, (256, 256))
assert new_im.size == expected_size
@@ -168,7 +172,7 @@ def test_pad_round() -> None:
@pytest.mark.parametrize("mode", ("P", "PA"))
-def test_palette(mode) -> None:
+def test_palette(mode: str) -> None:
im = hopper(mode)
# Expand
@@ -210,7 +214,7 @@ def test_scale() -> None:
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
-def test_expand_palette(border) -> None:
+def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
with Image.open("Tests/images/p_16.tga") as im:
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
@@ -366,7 +370,7 @@ def test_exif_transpose() -> None:
for ext in exts:
with Image.open("Tests/images/hopper" + ext) as base_im:
- def check(orientation_im) -> None:
+ def check(orientation_im: Image.Image) -> None:
for im in [
orientation_im,
orientation_im.copy(),
@@ -376,6 +380,7 @@ def test_exif_transpose() -> None:
else:
original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im)
+ assert transposed_im is not None
assert_image_similar(base_im, transposed_im, 17)
if orientation_im is base_im:
assert "exif" not in im.info
@@ -387,6 +392,7 @@ def test_exif_transpose() -> None:
# Repeat the operation to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im)
+ assert transposed_im2 is not None
assert_image_equal(transposed_im2, transposed_im)
check(base_im)
@@ -402,6 +408,7 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
+ assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
transposed_im._reload_exif()
@@ -414,12 +421,14 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
+ assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
# Orientation set directly on Image.Exif
im = hopper()
im.getexif()[0x0112] = 3
transposed_im = ImageOps.exif_transpose(im)
+ assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
@@ -445,7 +454,7 @@ def test_autocontrast_cutoff() -> None:
# Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:
- def autocontrast(cutoff):
+ def autocontrast(cutoff: int | tuple[int, int]):
return ImageOps.autocontrast(img, cutoff).histogram()
assert autocontrast(10) == autocontrast((10, 10))
@@ -486,20 +495,20 @@ def test_autocontrast_mask_real_input() -> None:
assert result_nomask != result
assert_tuple_approx_equal(
ImageStat.Stat(result, mask=rect_mask).median,
- [195, 202, 184],
+ (195, 202, 184),
threshold=2,
msg="autocontrast with mask pixel incorrect",
)
assert_tuple_approx_equal(
ImageStat.Stat(result_nomask).median,
- [119, 106, 79],
+ (119, 106, 79),
threshold=2,
msg="autocontrast without mask pixel incorrect",
)
def test_autocontrast_preserve_tone() -> None:
- def autocontrast(mode, preserve_tone):
+ def autocontrast(mode: str, preserve_tone: bool) -> list[int]:
im = hopper(mode)
return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()
@@ -533,7 +542,7 @@ def test_autocontrast_preserve_gradient() -> None:
@pytest.mark.parametrize(
"color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0))
)
-def test_autocontrast_preserve_one_color(color) -> None:
+def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None:
img = Image.new("RGB", (10, 10), color)
# single color images shouldn't change
diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py
index 03302e20f..c15907a55 100644
--- a/Tests/test_imageops_usm.py
+++ b/Tests/test_imageops_usm.py
@@ -1,12 +1,14 @@
from __future__ import annotations
+from typing import Generator
+
import pytest
from PIL import Image, ImageFilter
@pytest.fixture
-def test_images():
+def test_images() -> Generator[dict[str, Image.Image], None, None]:
ims = {
"im": Image.open("Tests/images/hopper.ppm"),
"snakes": Image.open("Tests/images/color_snakes.png"),
@@ -18,7 +20,7 @@ def test_images():
im.close()
-def test_filter_api(test_images) -> None:
+def test_filter_api(test_images: dict[str, Image.Image]) -> None:
im = test_images["im"]
test_filter = ImageFilter.GaussianBlur(2.0)
@@ -26,13 +28,13 @@ def test_filter_api(test_images) -> None:
assert i.mode == "RGB"
assert i.size == (128, 128)
- test_filter = ImageFilter.UnsharpMask(2.0, 125, 8)
- i = im.filter(test_filter)
+ test_filter2 = ImageFilter.UnsharpMask(2.0, 125, 8)
+ i = im.filter(test_filter2)
assert i.mode == "RGB"
assert i.size == (128, 128)
-def test_usm_formats(test_images) -> None:
+def test_usm_formats(test_images: dict[str, Image.Image]) -> None:
im = test_images["im"]
usm = ImageFilter.UnsharpMask
@@ -50,7 +52,7 @@ def test_usm_formats(test_images) -> None:
im.convert("YCbCr").filter(usm)
-def test_blur_formats(test_images) -> None:
+def test_blur_formats(test_images: dict[str, Image.Image]) -> None:
im = test_images["im"]
blur = ImageFilter.GaussianBlur
@@ -68,7 +70,7 @@ def test_blur_formats(test_images) -> None:
im.convert("YCbCr").filter(blur)
-def test_usm_accuracy(test_images) -> None:
+def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None:
snakes = test_images["snakes"]
src = snakes.convert("RGB")
@@ -77,7 +79,7 @@ def test_usm_accuracy(test_images) -> None:
assert i.tobytes() == src.tobytes()
-def test_blur_accuracy(test_images) -> None:
+def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None:
snakes = test_images["snakes"]
i = snakes.filter(ImageFilter.GaussianBlur(0.4))
diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py
index 8ba745f21..9487560af 100644
--- a/Tests/test_imagepath.py
+++ b/Tests/test_imagepath.py
@@ -3,6 +3,7 @@ from __future__ import annotations
import array
import math
import struct
+from typing import Sequence
import pytest
@@ -57,7 +58,9 @@ def test_path() -> None:
ImagePath.Path((0, 1)),
),
)
-def test_path_constructors(coords) -> None:
+def test_path_constructors(
+ coords: Sequence[float] | array.array[float] | ImagePath.Path,
+) -> None:
# Arrange / Act
p = ImagePath.Path(coords)
@@ -75,7 +78,9 @@ def test_path_constructors(coords) -> None:
[[0.0, 1.0]],
),
)
-def test_invalid_path_constructors(coords) -> None:
+def test_invalid_path_constructors(
+ coords: tuple[str, str] | Sequence[Sequence[int]]
+) -> None:
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
@@ -93,7 +98,7 @@ def test_invalid_path_constructors(coords) -> None:
[0, 1, 2],
),
)
-def test_path_odd_number_of_coordinates(coords) -> None:
+def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None:
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
@@ -111,7 +116,9 @@ def test_path_odd_number_of_coordinates(coords) -> None:
(1, (0.0, 0.0, 0.0, 0.0)),
],
)
-def test_getbbox(coords, expected) -> None:
+def test_getbbox(
+ coords: int | list[int], expected: tuple[float, float, float, float]
+) -> None:
# Arrange
p = ImagePath.Path(coords)
@@ -135,7 +142,7 @@ def test_getbbox_no_args() -> None:
(list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]),
],
)
-def test_map(coords, expected) -> None:
+def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None:
# Arrange
p = ImagePath.Path(coords)
@@ -201,9 +208,9 @@ class Evil:
def __init__(self) -> None:
self.corrupt = Image.core.path(0x4000000000000000)
- def __getitem__(self, i):
+ def __getitem__(self, i: int) -> bytes:
x = self.corrupt[i]
return struct.pack("dd", x[0], x[1])
- def __setitem__(self, i, x) -> None:
+ def __setitem__(self, i: int, x: bytes) -> None:
self.corrupt[i] = struct.unpack("dd", x)
diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py
index 909f97167..88ad1f9ee 100644
--- a/Tests/test_imageqt.py
+++ b/Tests/test_imageqt.py
@@ -28,7 +28,7 @@ def test_rgb() -> None:
assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255)
- def checkrgb(r, g, b) -> None:
+ def checkrgb(r: int, g: int, b: int) -> None:
val = ImageQt.rgb(r, g, b)
val = val % 2**24 # drop the alpha
assert val >> 16 == r
diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py
index 7280dded0..7f3a3d141 100644
--- a/Tests/test_imagesequence.py
+++ b/Tests/test_imagesequence.py
@@ -26,7 +26,7 @@ def test_sanity(tmp_path: Path) -> None:
assert index == 1
with pytest.raises(AttributeError):
- ImageSequence.Iterator(0)
+ ImageSequence.Iterator(0) # type: ignore[arg-type]
def test_iterator() -> None:
@@ -72,6 +72,7 @@ def test_consecutive() -> None:
for frame in ImageSequence.Iterator(im):
if first_frame is None:
first_frame = frame.copy()
+ assert first_frame is not None
for frame in ImageSequence.Iterator(im):
assert_image_equal(frame, first_frame)
break
diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py
index f7269d45b..4e9291fbb 100644
--- a/Tests/test_imageshow.py
+++ b/Tests/test_imageshow.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+from typing import Any
+
import pytest
from PIL import Image, ImageShow
@@ -24,9 +26,9 @@ def test_register() -> None:
"order",
[-1, 0],
)
-def test_viewer_show(order) -> None:
+def test_viewer_show(order: int) -> None:
class TestViewer(ImageShow.Viewer):
- def show_image(self, image, **options) -> bool:
+ def show_image(self, image: Image.Image, **options: Any) -> bool:
self.methodCalled = True
return True
@@ -48,7 +50,7 @@ def test_viewer_show(order) -> None:
reason="Only run on CIs; hangs on Windows CIs",
)
@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
-def test_show(mode) -> None:
+def test_show(mode: str) -> None:
im = hopper(mode)
assert ImageShow.show(im)
@@ -66,14 +68,15 @@ def test_show_without_viewers() -> None:
def test_viewer() -> None:
viewer = ImageShow.Viewer()
- assert viewer.get_format(None) is None
+ im = Image.new("L", (1, 1))
+ assert viewer.get_format(im) is None
with pytest.raises(NotImplementedError):
- viewer.get_command(None)
+ viewer.get_command("")
@pytest.mark.parametrize("viewer", ImageShow._viewers)
-def test_viewers(viewer) -> None:
+def test_viewers(viewer: ImageShow.Viewer) -> None:
try:
viewer.get_command("test.jpg")
except NotImplementedError:
diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py
index c7f633e62..f59ee7284 100644
--- a/Tests/test_imagewin_pointers.py
+++ b/Tests/test_imagewin_pointers.py
@@ -70,7 +70,7 @@ if is_win32():
]
CreateDIBSection.restype = ctypes.wintypes.HBITMAP
- def serialize_dib(bi, pixels):
+ def serialize_dib(bi, pixels) -> bytearray:
bf = BITMAPFILEHEADER()
bf.bfType = 0x4D42
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize
diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py
index 903f7e0c6..1b01f95ce 100644
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -78,7 +78,7 @@ def test_basic(tmp_path: Path, mode: str) -> None:
def test_tobytes() -> None:
- def tobytes(mode: str) -> Image.Image:
+ def tobytes(mode: str) -> bytes:
return Image.new(mode, (1, 1), 1).tobytes()
order = 1 if Image._ENDIAN == "<" else -1
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
index 6ba95c2d7..9f4e6534e 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -14,7 +14,7 @@ TEST_IMAGE_SIZE = (10, 10)
def test_numpy_to_image() -> None:
- def to_image(dtype, bands: int = 1, boolean: int = 0):
+ def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image:
if bands == 1:
if boolean:
data = [0, 255] * 50
@@ -99,7 +99,7 @@ def test_1d_array() -> None:
assert_image(Image.fromarray(a), "L", (1, 5))
-def _test_img_equals_nparray(img, np) -> None:
+def _test_img_equals_nparray(img: Image.Image, np) -> None:
assert len(np.shape) >= 2
np_size = np.shape[1], np.shape[0]
assert img.size == np_size
@@ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None:
("HSV", numpy.uint8),
),
)
-def test_to_array(mode, dtype) -> None:
+def test_to_array(mode: str, dtype) -> None:
img = hopper(mode)
# Resize to non-square
diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py
index 7d6c0a8cb..3cd323553 100644
--- a/Tests/test_qt_image_qapplication.py
+++ b/Tests/test_qt_image_qapplication.py
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
-from PIL import ImageQt
+from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@@ -37,7 +37,7 @@ if ImageQt.qt_is_installed:
lbl.setPixmap(pixmap1.copy())
-def roundtrip(expected) -> None:
+def roundtrip(expected: Image.Image) -> None:
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
# Qt saves all pixmaps as rgb
assert_image_similar(result, expected.convert("RGB"), 1)
diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py
index a222a7d71..6110be707 100644
--- a/Tests/test_qt_image_toqimage.py
+++ b/Tests/test_qt_image_toqimage.py
@@ -17,7 +17,7 @@ if ImageQt.qt_is_installed:
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1"))
-def test_sanity(mode, tmp_path: Path) -> None:
+def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode)
data = ImageQt.toqimage(src)
diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py
index f51e8b3a8..073e5415c 100644
--- a/Tests/test_tiff_crashes.py
+++ b/Tests/test_tiff_crashes.py
@@ -47,9 +47,8 @@ def test_tiff_crashes(test_file: str) -> None:
with Image.open(test_file) as im:
im.load()
except FileNotFoundError:
- if not on_ci():
- pytest.skip("test image not found")
- return
- raise
+ if on_ci():
+ raise
+ pytest.skip("test image not found")
except OSError:
pass
diff --git a/Tests/test_util.py b/Tests/test_util.py
index 73e4acd55..197ef79ee 100644
--- a/Tests/test_util.py
+++ b/Tests/test_util.py
@@ -10,7 +10,7 @@ from PIL import _util
@pytest.mark.parametrize(
"test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")]
)
-def test_is_path(test_path) -> None:
+def test_is_path(test_path: str | Path | PurePath) -> None:
# Act
it_is = _util.is_path(test_path)
diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh
index 4f4b81a62..8c2967bc2 100755
--- a/depends/install_openjpeg.sh
+++ b/depends/install_openjpeg.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# install openjpeg
-archive=openjpeg-2.5.0
+archive=openjpeg-2.5.2
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
diff --git a/docs/conf.py b/docs/conf.py
index 9ae7ae605..97289c91d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -326,7 +326,7 @@ linkcheck_allowed_redirects = {
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",
r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
- r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*",
+ r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*",
r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",
r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",
}
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 205fcb9ab..8877ccdeb 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -504,3 +504,27 @@ PIL.OleFileIO
the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
PyPI (eg. ``python3 -m pip install olefile``).
+
+import _imaging
+~~~~~~~~~~~~~~~
+
+.. versionremoved:: 2.1.0
+
+Pillow >= 2.1.0 no longer supports ``import _imaging``.
+Please use ``from PIL.Image import core as _imaging`` instead.
+
+Pillow and PIL
+~~~~~~~~~~~~~~
+
+.. versionremoved:: 1.0.0
+
+Pillow and PIL cannot co-exist in the same environment.
+Before installing Pillow, please uninstall PIL.
+
+import Image
+~~~~~~~~~~~~
+
+.. versionremoved:: 1.0.0
+
+Pillow >= 1.0 no longer supports ``import Image``.
+Please use ``from PIL import Image`` instead.
diff --git a/docs/index.rst b/docs/index.rst
index 558369919..1ed9266eb 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -49,7 +49,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more
- document.addEventListener('DOMContentLoaded', function() {
- activateTab(getOS());
- });
-
-
-Warnings
---------
-
-.. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL.
-
-.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead.
-
-.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead.
-
-Python Support
---------------
-
-Pillow supports these Python versions.
-
-.. csv-table:: Newer versions
- :file: newer-versions.csv
- :header-rows: 1
-
-.. csv-table:: Older versions
- :file: older-versions.csv
- :header-rows: 1
-
-.. _Linux Installation:
-.. _macOS Installation:
-.. _Windows Installation:
-.. _FreeBSD Installation:
-
Basic Installation
------------------
-.. note::
+.. Note:: This section has moved to :ref:`basic-installation`. Please update references accordingly.
- The following instructions will install Pillow with support for
- most common image formats. See :ref:`external-libraries` for a
- full list of external libraries supported.
+Python Support
+--------------
-Install Pillow with :command:`pip`::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
-
-Optionally, install :pypi:`defusedxml` for Pillow to read XMP data,
-and :pypi:`olefile` for Pillow to read FPX and MIC images::
-
- python3 -m pip install --upgrade defusedxml olefile
-
-
-.. tab:: Linux
-
- We provide binaries for Linux for each of the supported Python
- versions in the manylinux wheel format. These include support for all
- optional libraries except libimagequant. Raqm support requires
- FriBiDi to be installed separately::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
-
- Most major Linux distributions, including Fedora, Ubuntu and ArchLinux
- also include Pillow in packages that previously contained PIL e.g.
- ``python-imaging``. Debian splits it into two packages, ``python3-pil``
- and ``python3-pil.imagetk``.
-
-.. tab:: macOS
-
- We provide binaries for macOS for each of the supported Python
- versions in the wheel format. These include support for all optional
- libraries except libimagequant. Raqm support requires
- FriBiDi to be installed separately::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
-
- While we provide binaries for both x86-64 and arm64, we do not provide universal2
- binaries. However, it is simple to combine our current binaries to create one::
-
- python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow
- python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow
- python3 -m pip install delocate
-
- Then, with the names of the downloaded wheels, use Python to combine them::
-
- from delocate.fuse import fuse_wheels
- fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl')
-
-.. tab:: Windows
-
- We provide Pillow binaries for Windows compiled for the matrix of supported
- Pythons in the wheel format. These include x86, x86-64 and arm64 versions
- (with the exception of Python 3.8 on arm64). These binaries include support
- for all optional libraries except libimagequant and libxcb. Raqm support
- requires FriBiDi to be installed separately::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
-
- To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_.
-
-.. tab:: FreeBSD
-
- Pillow can be installed on FreeBSD via the official Ports or Packages systems:
-
- **Ports**::
-
- cd /usr/ports/graphics/py-pillow && make install clean
-
- **Packages**::
-
- pkg install py38-pillow
-
- .. note::
-
- The `Pillow FreeBSD port
- `_ and packages
- are tested by the ports team with all supported FreeBSD versions.
-
-
-.. _Building on Linux:
-.. _Building on macOS:
-.. _Building on Windows:
-.. _Building on Windows using MSYS2/MinGW:
-.. _Building on FreeBSD:
-.. _Building on Android:
-
-Building From Source
---------------------
-
-.. _external-libraries:
-
-External Libraries
-^^^^^^^^^^^^^^^^^^
-
-.. note::
-
- You **do not need to install all supported external libraries** to
- use Pillow's basic features. **Zlib** and **libjpeg** are required
- by default.
-
-.. note::
-
- There are Dockerfiles in our `Docker images repo
- `_ to install the
- dependencies for some operating systems.
-
-Many of Pillow's features require external libraries:
-
-* **libjpeg** provides JPEG functionality.
-
- * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and
- libjpeg-turbo version **8**.
- * Starting with Pillow 3.0.0, libjpeg is required by default. It can be
- disabled with the ``-C jpeg=disable`` flag.
-
-* **zlib** provides access to compressed PNGs
-
- * Starting with Pillow 3.0.0, zlib is required by default. It can be
- disabled with the ``-C zlib=disable`` flag.
-
-* **libtiff** provides compressed TIFF functionality
-
- * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
-
-* **libfreetype** provides type related services
-
-* **littlecms** provides color management
-
- * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
- above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
-
-* **libwebp** provides the WebP format.
-
- * Pillow has been tested with version **0.1.3**, which does not read
- transparent WebP files. Versions **0.3.0** and above support
- transparency.
-
-* **openjpeg** provides JPEG 2000 functionality.
-
- * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
- **2.4.0** and **2.5.0**.
- * Pillow does **not** support the earlier **1.5** series which ships
- with Debian Jessie.
-
-* **libimagequant** provides improved color quantization
-
- * Pillow has been tested with libimagequant **2.6-4.2.2**
- * Libimagequant is licensed GPLv3, which is more restrictive than
- the Pillow license, therefore we will not be distributing binaries
- with libimagequant support enabled.
-
-* **libraqm** provides complex text layout support.
-
- * libraqm provides bidirectional text support (using FriBiDi),
- shaping (using HarfBuzz), and proper script itemization. As a
- result, Raqm can support most writing systems covered by Unicode.
- * libraqm depends on the following libraries: FreeType, HarfBuzz,
- FriBiDi, make sure that you install them before installing libraqm
- if not available as package in your system.
- * Setting text direction or font features is not supported without libraqm.
- * Pillow wheels since version 8.2.0 include a modified version of libraqm that
- loads libfribidi at runtime if it is installed.
- On Windows this requires compiling FriBiDi and installing ``fribidi.dll``
- into a directory listed in the `Dynamic-link library search order (Microsoft Learn)
- `_
- (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected).
- See `Build Options`_ to see how to build this version.
- * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime.
-
-* **libxcb** provides X11 screengrab support.
-
-.. tab:: Linux
-
- If you didn't build Python from source, make sure you have Python's
- development libraries installed.
-
- In Debian or Ubuntu::
-
- sudo apt-get install python3-dev python3-setuptools
-
- In Fedora, the command is::
-
- sudo dnf install python3-devel redhat-rpm-config
-
- In Alpine, the command is::
-
- sudo apk add python3-dev py3-setuptools
-
- .. 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::
-
- 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 \
- libharfbuzz-dev libfribidi-dev libxcb1-dev
-
- To install libraqm, ``sudo apt-get install meson`` and then see
- ``depends/install_raqm.sh``.
-
- Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
-
- sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
- freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \
- harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel
-
- Note that the package manager may be yum or DNF, depending on the
- exact distribution.
-
- Prerequisites are installed for **Alpine** with::
-
- sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \
- libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \
- libxcb-dev libpng-dev
-
- See also the ``Dockerfile``\s in the Test Infrastructure repo
- (https://github.com/python-pillow/docker-images) for a known working
- install process for other tested distros.
-
-.. tab:: macOS
-
- The Xcode command line tools are required to compile portions of
- Pillow. The tools are installed by running ``xcode-select --install``
- from the command line. The command line tools are required even if you
- have the full Xcode package installed. It may be necessary to run
- ``sudo xcodebuild -license`` to accept the license prior to using the
- tools.
-
- The easiest way to install external libraries is via `Homebrew
- `_. After you install Homebrew, run::
-
- brew install libjpeg libtiff little-cms2 openjpeg webp
-
- To install libraqm on macOS use Homebrew to install its dependencies::
-
- brew install freetype harfbuzz fribidi
-
- Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
-
-.. tab:: Windows
-
- We recommend you use prebuilt wheels from PyPI.
- If you wish to compile Pillow manually, you can use the build scripts
- in the ``winbuild`` directory used for CI testing and development.
- These scripts require Visual Studio 2017 or newer and NASM.
-
- The scripts also install Pillow from the local copy of the source code, so the
- `Installing`_ instructions will not be necessary afterwards.
-
-.. tab:: Windows using MSYS2/MinGW
-
- To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
- **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
-
- The following instructions target the 64-bit build, for 32-bit
- replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
-
- Make sure you have Python and GCC installed::
-
- pacman -S \
- mingw-w64-x86_64-gcc \
- mingw-w64-x86_64-python3 \
- mingw-w64-x86_64-python3-pip \
- mingw-w64-x86_64-python3-setuptools
-
- Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
-
- pacman -S \
- mingw-w64-x86_64-libjpeg-turbo \
- mingw-w64-x86_64-zlib \
- mingw-w64-x86_64-libtiff \
- mingw-w64-x86_64-freetype \
- mingw-w64-x86_64-lcms2 \
- mingw-w64-x86_64-libwebp \
- mingw-w64-x86_64-openjpeg2 \
- mingw-w64-x86_64-libimagequant \
- mingw-w64-x86_64-libraqm
-
- https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with
- MSYS2. To workaround this, before installing Pillow you must run::
-
- export SETUPTOOLS_USE_DISTUTILS=stdlib
-
-.. tab:: FreeBSD
-
- .. Note:: Only FreeBSD 10 and 11 tested
-
- Make sure you have Python's development libraries installed::
-
- sudo pkg install python3
-
- Prerequisites are installed on **FreeBSD 10 or 11** with::
-
- sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
-
- Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
-
-.. tab:: Android
-
- Basic Android support has been added for compilation within the Termux
- environment. The dependencies can be installed by::
-
- pkg install -y python ndk-sysroot clang make \
- libjpeg-turbo
-
- This has been tested within the Termux app on ChromeOS, on x86.
-
-Installing
-^^^^^^^^^^
-
-Once you have installed the prerequisites, to install Pillow from the source
-code on PyPI, run::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow --no-binary :all:
-
-If the prerequisites are installed in the standard library locations
-for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
-additional configuration should be required. If they are installed in
-a non-standard location, you may need to configure setuptools to use
-those locations by editing :file:`setup.py` or
-:file:`pyproject.toml`, or by adding environment variables on the command
-line::
-
- CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
-
-If Pillow has been previously built without the required
-prerequisites, it may be necessary to manually clear the pip cache or
-build without cache using the ``--no-cache-dir`` option to force a
-build with newly installed external libraries.
-
-If you would like to install from a local copy of the source code instead, you
-can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow``
-or download and extract the `compressed archive from PyPI`_.
-
-After navigating to the Pillow directory, run::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install .
-
-.. _compressed archive from PyPI: https://pypi.org/project/pillow/#files
-
-Build Options
-"""""""""""""
-
-* Config setting: ``-C parallel=n``. Can also be given
- with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
- multiprocessing to build the extension. Setting ``-C parallel=n``
- sets the number of CPUs to use to ``n``, or can disable parallel building by
- using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
- available, as many as are present.
-
-* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
- ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
- ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``,
- ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``.
- Disable building the corresponding feature even if the development
- libraries are present on the building machine.
-
-* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``,
- ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``,
- ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``,
- ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``.
- Require that the corresponding feature is built. The build will raise
- an exception if the libraries are not found. Webpmux (WebP metadata)
- relies on WebP support. Tcl and Tk also must be used together.
-
-* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``.
- These flags are used to compile a modified version of libraqm and
- a shim that dynamically loads libfribidi at runtime. These are
- used to compile the standard Pillow wheels. Compiling libraqm requires
- a C99-compliant compiler.
-
-* Config setting: ``-C platform-guessing=disable``. Skips all of the
- platform dependent guessing of include and library directories for
- automated build systems that configure the proper paths in the
- environment variables (e.g. Buildroot).
-
-* Config setting: ``-C debug=true``. Adds a debugging flag to the include and
- library search process to dump all paths searched for and found to stdout.
-
-
-Sample usage::
-
- python3 -m pip install --upgrade Pillow -C [feature]=enable
+.. Note:: This section has moved to :ref:`python-support`. Please update references accordingly.
Platform Support
----------------
-Current platform support for Pillow. Binary distributions are
-contributed for each release on a volunteer basis, but the source
-should compile and run everywhere platform support is listed. In
-general, we aim to support all current versions of Linux, macOS, and
-Windows.
+.. Note:: This section has moved to :ref:`platform-support`. Please update references accordingly.
-Continuous Integration Targets
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Building From Source
+--------------------
-These platforms are built and tested for every change.
-
-+----------------------------------+----------------------------+---------------------+
-| Operating system | Tested Python versions | Tested architecture |
-+==================================+============================+=====================+
-| Alpine | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Amazon Linux 2 | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Amazon Linux 2023 | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Arch | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| CentOS 7 | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| CentOS Stream 8 | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| CentOS Stream 9 | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Debian 11 Bullseye | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Debian 12 Bookworm | 3.11 | x86, x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Fedora 38 | 3.11 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Fedora 39 | 3.12 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Gentoo | 3.9 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
-| | 3.12, PyPy3 | |
-+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
-| | 3.12, PyPy3 | |
-| +----------------------------+---------------------+
-| | 3.10 | arm64v8, ppc64le, |
-| | | s390x |
-+----------------------------------+----------------------------+---------------------+
-| Windows Server 2016 | 3.8 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
-| | 3.12, PyPy3 | |
-| +----------------------------+---------------------+
-| | 3.12 | x86 |
-| +----------------------------+---------------------+
-| | 3.9 (MinGW) | x86-64 |
-| +----------------------------+---------------------+
-| | 3.8, 3.9 (Cygwin) | x86-64 |
-+----------------------------------+----------------------------+---------------------+
-
-
-Other Platforms
-^^^^^^^^^^^^^^^
-
-These platforms have been reported to work at the versions mentioned.
-
-.. note::
-
- Contributors please test Pillow on your platform then update this
- document and send a pull request.
-
-+----------------------------------+----------------------------+------------------+--------------+
-| Operating system | | Tested Python | | Latest tested | | Tested |
-| | | versions | | Pillow version | | processors |
-+==================================+============================+==================+==============+
-| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm |
-+----------------------------------+----------------------------+------------------+--------------+
-| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
-| +----------------------------+------------------+ |
-| | 3.7 | 9.5.0 | |
-+----------------------------------+----------------------------+------------------+--------------+
-| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
-+----------------------------------+----------------------------+------------------+--------------+
-| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
-| +----------------------------+------------------+--------------+
-| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
-| +----------------------------+------------------+ |
-| | 3.6 | 8.4.0 | |
-+----------------------------------+----------------------------+------------------+--------------+
-| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
-| +----------------------------+------------------+ |
-| | 3.5 | 7.2.0 | |
-+----------------------------------+----------------------------+------------------+--------------+
-| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
-| +----------------------------+------------------+ |
-| | 2.7 | 6.0.0 | |
-| +----------------------------+------------------+ |
-| | 3.4 | 5.4.1 | |
-+----------------------------------+----------------------------+------------------+--------------+
-| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
-| +----------------------------+------------------+ |
-| | 3.3 | 4.1.0 | |
-+----------------------------------+----------------------------+------------------+--------------+
-| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Redhat Linux 6 | 2.6 | |x86 |
-+----------------------------------+----------------------------+------------------+--------------+
-| CentOS 6.3 | 2.7, 3.3 | |x86 |
-+----------------------------------+----------------------------+------------------+--------------+
-| CentOS 8 | 3.9 | 9.0.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
-| | | PyPy5.3.1, PyPy3 v2.4.0 | | |
-| +----------------------------+------------------+--------------+
-| | 2.7 | 4.3.0 |x86-64 |
-| +----------------------------+------------------+--------------+
-| | 2.7, 3.2 | 3.4.1 |ppc |
-+----------------------------------+----------------------------+------------------+--------------+
-| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
-+----------------------------------+----------------------------+------------------+--------------+
-| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
-+----------------------------------+----------------------------+------------------+--------------+
-| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm |
-| +----------------------------+------------------+ |
-| | 2.7 | 6.2.2 | |
-+----------------------------------+----------------------------+------------------+--------------+
-| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Windows 10 | 3.7 | 7.1.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
-| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
-+----------------------------------+----------------------------+------------------+--------------+
+.. Note:: This section has moved to :ref:`building-from-source`. Please update references accordingly.
Old Versions
------------
-You can download old distributions from the `release history at PyPI
-`_ and by direct URL access
-eg. https://pypi.org/project/pillow/1.0/.
+.. Note:: This section has moved to :ref:`old-versions`. Please update references accordingly.
diff --git a/docs/installation/basic-installation.rst b/docs/installation/basic-installation.rst
new file mode 100644
index 000000000..01981aa4f
--- /dev/null
+++ b/docs/installation/basic-installation.rst
@@ -0,0 +1,97 @@
+.. raw:: html
+
+
+
+.. _basic-installation:
+
+Basic Installation
+==================
+
+.. note::
+
+ The following instructions will install Pillow with support for
+ most common image formats. See :ref:`external-libraries` for a
+ full list of external libraries supported.
+
+Install Pillow with :command:`pip`::
+
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --upgrade Pillow
+
+Optionally, install :pypi:`defusedxml` for Pillow to read XMP data,
+and :pypi:`olefile` for Pillow to read FPX and MIC images::
+
+ python3 -m pip install --upgrade defusedxml olefile
+
+
+.. tab:: Linux
+
+ We provide binaries for Linux for each of the supported Python
+ versions in the manylinux wheel format. These include support for all
+ optional libraries except libimagequant. Raqm support requires
+ FriBiDi to be installed separately::
+
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --upgrade Pillow
+
+ Most major Linux distributions, including Fedora, Ubuntu and ArchLinux
+ also include Pillow in packages that previously contained PIL e.g.
+ ``python-imaging``. Debian splits it into two packages, ``python3-pil``
+ and ``python3-pil.imagetk``.
+
+.. tab:: macOS
+
+ We provide binaries for macOS for each of the supported Python
+ versions in the wheel format. These include support for all optional
+ libraries except libimagequant. Raqm support requires
+ FriBiDi to be installed separately::
+
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --upgrade Pillow
+
+ While we provide binaries for both x86-64 and arm64, we do not provide universal2
+ binaries. However, it is simple to combine our current binaries to create one::
+
+ python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow
+ python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow
+ python3 -m pip install delocate
+
+ Then, with the names of the downloaded wheels, use Python to combine them::
+
+ from delocate.fuse import fuse_wheels
+ fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl')
+
+.. tab:: Windows
+
+ We provide Pillow binaries for Windows compiled for the matrix of supported
+ Pythons in the wheel format. These include x86, x86-64 and arm64 versions
+ (with the exception of Python 3.8 on arm64). These binaries include support
+ for all optional libraries except libimagequant and libxcb. Raqm support
+ requires FriBiDi to be installed separately::
+
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --upgrade Pillow
+
+ To install Pillow in MSYS2, see :ref:`building-from-source`.
+
+.. tab:: FreeBSD
+
+ Pillow can be installed on FreeBSD via the official Ports or Packages systems:
+
+ **Ports**::
+
+ cd /usr/ports/graphics/py-pillow && make install clean
+
+ **Packages**::
+
+ pkg install py38-pillow
+
+ .. note::
+
+ The `Pillow FreeBSD port
+ `_ and packages
+ are tested by the ports team with all supported FreeBSD versions.
diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst
new file mode 100644
index 000000000..b0915e4da
--- /dev/null
+++ b/docs/installation/building-from-source.rst
@@ -0,0 +1,317 @@
+.. raw:: html
+
+
+
+.. _building-from-source:
+
+Building From Source
+====================
+
+.. _external-libraries:
+
+External Libraries
+------------------
+
+.. note::
+
+ You **do not need to install all supported external libraries** to
+ use Pillow's basic features. **Zlib** and **libjpeg** are required
+ by default.
+
+.. note::
+
+ There are Dockerfiles in our `Docker images repo
+ `_ to install the
+ dependencies for some operating systems.
+
+Many of Pillow's features require external libraries:
+
+* **libjpeg** provides JPEG functionality.
+
+ * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and
+ libjpeg-turbo version **8**.
+ * Starting with Pillow 3.0.0, libjpeg is required by default. It can be
+ disabled with the ``-C jpeg=disable`` flag.
+
+* **zlib** provides access to compressed PNGs
+
+ * Starting with Pillow 3.0.0, zlib is required by default. It can be
+ disabled with the ``-C zlib=disable`` flag.
+
+* **libtiff** provides compressed TIFF functionality
+
+ * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
+
+* **libfreetype** provides type related services
+
+* **littlecms** provides color management
+
+ * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
+ above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
+
+* **libwebp** provides the WebP format.
+
+ * Pillow has been tested with version **0.1.3**, which does not read
+ transparent WebP files. Versions **0.3.0** and above support
+ transparency.
+
+* **openjpeg** provides JPEG 2000 functionality.
+
+ * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
+ **2.4.0**, **2.5.0** and **2.5.2**.
+ * Pillow does **not** support the earlier **1.5** series which ships
+ with Debian Jessie.
+
+* **libimagequant** provides improved color quantization
+
+ * Pillow has been tested with libimagequant **2.6-4.2.2**
+ * Libimagequant is licensed GPLv3, which is more restrictive than
+ the Pillow license, therefore we will not be distributing binaries
+ with libimagequant support enabled.
+
+* **libraqm** provides complex text layout support.
+
+ * libraqm provides bidirectional text support (using FriBiDi),
+ shaping (using HarfBuzz), and proper script itemization. As a
+ result, Raqm can support most writing systems covered by Unicode.
+ * libraqm depends on the following libraries: FreeType, HarfBuzz,
+ FriBiDi, make sure that you install them before installing libraqm
+ if not available as package in your system.
+ * Setting text direction or font features is not supported without libraqm.
+ * Pillow wheels since version 8.2.0 include a modified version of libraqm that
+ loads libfribidi at runtime if it is installed.
+ On Windows this requires compiling FriBiDi and installing ``fribidi.dll``
+ into a directory listed in the `Dynamic-link library search order (Microsoft Learn)
+ `_
+ (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected).
+ See `Build Options`_ to see how to build this version.
+ * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime.
+
+* **libxcb** provides X11 screengrab support.
+
+.. tab:: Linux
+
+ If you didn't build Python from source, make sure you have Python's
+ development libraries installed.
+
+ In Debian or Ubuntu::
+
+ sudo apt-get install python3-dev python3-setuptools
+
+ In Fedora, the command is::
+
+ sudo dnf install python3-devel redhat-rpm-config
+
+ In Alpine, the command is::
+
+ sudo apk add python3-dev py3-setuptools
+
+ .. 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::
+
+ 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 \
+ libharfbuzz-dev libfribidi-dev libxcb1-dev
+
+ To install libraqm, ``sudo apt-get install meson`` and then see
+ ``depends/install_raqm.sh``.
+
+ Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
+
+ sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
+ freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \
+ harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel
+
+ Note that the package manager may be yum or DNF, depending on the
+ exact distribution.
+
+ Prerequisites are installed for **Alpine** with::
+
+ sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \
+ libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \
+ libxcb-dev libpng-dev
+
+ See also the ``Dockerfile``\s in the Test Infrastructure repo
+ (https://github.com/python-pillow/docker-images) for a known working
+ install process for other tested distros.
+
+.. tab:: macOS
+
+ The Xcode command line tools are required to compile portions of
+ Pillow. The tools are installed by running ``xcode-select --install``
+ from the command line. The command line tools are required even if you
+ have the full Xcode package installed. It may be necessary to run
+ ``sudo xcodebuild -license`` to accept the license prior to using the
+ tools.
+
+ The easiest way to install external libraries is via `Homebrew
+ `_. After you install Homebrew, run::
+
+ brew install libjpeg libtiff little-cms2 openjpeg webp
+
+ To install libraqm on macOS use Homebrew to install its dependencies::
+
+ brew install freetype harfbuzz fribidi
+
+ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
+
+.. tab:: Windows
+
+ We recommend you use prebuilt wheels from PyPI.
+ If you wish to compile Pillow manually, you can use the build scripts
+ in the ``winbuild`` directory used for CI testing and development.
+ These scripts require Visual Studio 2017 or newer and NASM.
+
+ The scripts also install Pillow from the local copy of the source code, so the
+ `Installing`_ instructions will not be necessary afterwards.
+
+.. tab:: Windows using MSYS2/MinGW
+
+ To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
+ **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
+
+ The following instructions target the 64-bit build, for 32-bit
+ replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
+
+ Make sure you have Python and GCC installed::
+
+ pacman -S \
+ mingw-w64-x86_64-gcc \
+ mingw-w64-x86_64-python3 \
+ mingw-w64-x86_64-python3-pip \
+ mingw-w64-x86_64-python3-setuptools
+
+ Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
+
+ pacman -S \
+ mingw-w64-x86_64-libjpeg-turbo \
+ mingw-w64-x86_64-zlib \
+ mingw-w64-x86_64-libtiff \
+ mingw-w64-x86_64-freetype \
+ mingw-w64-x86_64-lcms2 \
+ mingw-w64-x86_64-libwebp \
+ mingw-w64-x86_64-openjpeg2 \
+ mingw-w64-x86_64-libimagequant \
+ mingw-w64-x86_64-libraqm
+
+ https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with
+ MSYS2. To workaround this, before installing Pillow you must run::
+
+ export SETUPTOOLS_USE_DISTUTILS=stdlib
+
+.. tab:: FreeBSD
+
+ .. Note:: Only FreeBSD 10 and 11 tested
+
+ Make sure you have Python's development libraries installed::
+
+ sudo pkg install python3
+
+ Prerequisites are installed on **FreeBSD 10 or 11** with::
+
+ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
+
+ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
+
+.. tab:: Android
+
+ Basic Android support has been added for compilation within the Termux
+ environment. The dependencies can be installed by::
+
+ pkg install -y python ndk-sysroot clang make \
+ libjpeg-turbo
+
+ This has been tested within the Termux app on ChromeOS, on x86.
+
+Installing
+----------
+
+Once you have installed the prerequisites, to install Pillow from the source
+code on PyPI, run::
+
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --upgrade Pillow --no-binary :all:
+
+If the prerequisites are installed in the standard library locations
+for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
+additional configuration should be required. If they are installed in
+a non-standard location, you may need to configure setuptools to use
+those locations by editing :file:`setup.py` or
+:file:`pyproject.toml`, or by adding environment variables on the command
+line::
+
+ CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
+
+If Pillow has been previously built without the required
+prerequisites, it may be necessary to manually clear the pip cache or
+build without cache using the ``--no-cache-dir`` option to force a
+build with newly installed external libraries.
+
+If you would like to install from a local copy of the source code instead, you
+can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow``
+or download and extract the `compressed archive from PyPI`_.
+
+After navigating to the Pillow directory, run::
+
+ python3 -m pip install --upgrade pip
+ python3 -m pip install .
+
+.. _compressed archive from PyPI: https://pypi.org/project/pillow/#files
+
+Build Options
+^^^^^^^^^^^^^
+
+* Config setting: ``-C parallel=n``. Can also be given
+ with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
+ multiprocessing to build the extension. Setting ``-C parallel=n``
+ sets the number of CPUs to use to ``n``, or can disable parallel building by
+ using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
+ available, as many as are present.
+
+* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
+ ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
+ ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``,
+ ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``.
+ Disable building the corresponding feature even if the development
+ libraries are present on the building machine.
+
+* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``,
+ ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``,
+ ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``,
+ ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``.
+ Require that the corresponding feature is built. The build will raise
+ an exception if the libraries are not found. Webpmux (WebP metadata)
+ relies on WebP support. Tcl and Tk also must be used together.
+
+* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``.
+ These flags are used to compile a modified version of libraqm and
+ a shim that dynamically loads libfribidi at runtime. These are
+ used to compile the standard Pillow wheels. Compiling libraqm requires
+ a C99-compliant compiler.
+
+* Config setting: ``-C platform-guessing=disable``. Skips all of the
+ platform dependent guessing of include and library directories for
+ automated build systems that configure the proper paths in the
+ environment variables (e.g. Buildroot).
+
+* Config setting: ``-C debug=true``. Adds a debugging flag to the include and
+ library search process to dump all paths searched for and found to stdout.
+
+
+Sample usage::
+
+ python3 -m pip install --upgrade Pillow -C [feature]=enable
+
+.. _old-versions:
+
+Old Versions
+============
+
+You can download old distributions from the `release history at PyPI
+`_ and by direct URL access
+eg. https://pypi.org/project/pillow/1.0/.
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
new file mode 100644
index 000000000..a94204b6b
--- /dev/null
+++ b/docs/installation/index.rst
@@ -0,0 +1,10 @@
+Installation
+============
+
+.. toctree::
+ :maxdepth: 2
+
+ basic-installation
+ python-support
+ platform-support
+ building-from-source
diff --git a/docs/newer-versions.csv b/docs/installation/newer-versions.csv
similarity index 100%
rename from docs/newer-versions.csv
rename to docs/installation/newer-versions.csv
diff --git a/docs/older-versions.csv b/docs/installation/older-versions.csv
similarity index 100%
rename from docs/older-versions.csv
rename to docs/installation/older-versions.csv
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
new file mode 100644
index 000000000..9ae97d70a
--- /dev/null
+++ b/docs/installation/platform-support.rst
@@ -0,0 +1,168 @@
+.. _platform-support:
+
+Platform Support
+================
+
+Current platform support for Pillow. Binary distributions are
+contributed for each release on a volunteer basis, but the source
+should compile and run everywhere platform support is listed. In
+general, we aim to support all current versions of Linux, macOS, and
+Windows.
+
+Continuous Integration Targets
+------------------------------
+
+These platforms are built and tested for every change.
+
++----------------------------------+----------------------------+---------------------+
+| Operating system | Tested Python versions | Tested architecture |
++==================================+============================+=====================+
+| Alpine | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Amazon Linux 2 | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Amazon Linux 2023 | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Arch | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| CentOS 7 | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| CentOS Stream 8 | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| CentOS Stream 9 | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Debian 11 Bullseye | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Debian 12 Bookworm | 3.11 | x86, x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Fedora 38 | 3.11 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Fedora 39 | 3.12 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Gentoo | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| | 3.12, PyPy3 | |
++----------------------------------+----------------------------+---------------------+
+| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| | 3.12, PyPy3 | |
+| +----------------------------+---------------------+
+| | 3.10 | arm64v8, ppc64le, |
+| | | s390x |
++----------------------------------+----------------------------+---------------------+
+| Windows Server 2016 | 3.8 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| | 3.12, PyPy3 | |
+| +----------------------------+---------------------+
+| | 3.12 | x86 |
+| +----------------------------+---------------------+
+| | 3.9 (MinGW) | x86-64 |
+| +----------------------------+---------------------+
+| | 3.8, 3.9 (Cygwin) | x86-64 |
++----------------------------------+----------------------------+---------------------+
+
+
+Other Platforms
+---------------
+
+These platforms have been reported to work at the versions mentioned.
+
+.. note::
+
+ Contributors please test Pillow on your platform then update this
+ document and send a pull request.
+
++----------------------------------+----------------------------+------------------+--------------+
+| Operating system | | Tested Python | | Latest tested | | Tested |
+| | | versions | | Pillow version | | processors |
++==================================+============================+==================+==============+
+| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
+| +----------------------------+------------------+ |
+| | 3.7 | 9.5.0 | |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
+| +----------------------------+------------------+--------------+
+| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
+| +----------------------------+------------------+ |
+| | 3.6 | 8.4.0 | |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
+| +----------------------------+------------------+ |
+| | 3.5 | 7.2.0 | |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
+| +----------------------------+------------------+ |
+| | 2.7 | 6.0.0 | |
+| +----------------------------+------------------+ |
+| | 3.4 | 5.4.1 | |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
+| +----------------------------+------------------+ |
+| | 3.3 | 4.1.0 | |
++----------------------------------+----------------------------+------------------+--------------+
+| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Redhat Linux 6 | 2.6 | |x86 |
++----------------------------------+----------------------------+------------------+--------------+
+| CentOS 6.3 | 2.7, 3.3 | |x86 |
++----------------------------------+----------------------------+------------------+--------------+
+| CentOS 8 | 3.9 | 9.0.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
+| | | PyPy5.3.1, PyPy3 v2.4.0 | | |
+| +----------------------------+------------------+--------------+
+| | 2.7 | 4.3.0 |x86-64 |
+| +----------------------------+------------------+--------------+
+| | 2.7, 3.2 | 3.4.1 |ppc |
++----------------------------------+----------------------------+------------------+--------------+
+| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
++----------------------------------+----------------------------+------------------+--------------+
+| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
++----------------------------------+----------------------------+------------------+--------------+
+| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm |
+| +----------------------------+------------------+ |
+| | 2.7 | 6.2.2 | |
++----------------------------------+----------------------------+------------------+--------------+
+| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 10 | 3.7 | 7.1.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
diff --git a/docs/installation/python-support.rst b/docs/installation/python-support.rst
new file mode 100644
index 000000000..dd5765b6b
--- /dev/null
+++ b/docs/installation/python-support.rst
@@ -0,0 +1,14 @@
+.. _python-support:
+
+Python Support
+==============
+
+Pillow supports these Python versions.
+
+.. csv-table:: Newer versions
+ :file: newer-versions.csv
+ :header-rows: 1
+
+.. csv-table:: Older versions
+ :file: older-versions.csv
+ :header-rows: 1
diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst
index 475253078..051fdcfc9 100644
--- a/docs/reference/ImageOps.rst
+++ b/docs/reference/ImageOps.rst
@@ -14,6 +14,8 @@ only work on L and RGB images.
.. autofunction:: colorize
.. autofunction:: crop
.. autofunction:: scale
+.. autoclass:: SupportsGetMesh
+ :show-inheritance:
.. autofunction:: deform
.. autofunction:: equalize
.. autofunction:: expand
diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst
index 8772a382d..af31cdb74 100644
--- a/docs/releasenotes/10.3.0.rst
+++ b/docs/releasenotes/10.3.0.rst
@@ -79,3 +79,9 @@ Portable FloatMap (PFM) images
Support has been added for reading and writing grayscale (Pf format)
Portable FloatMap (PFM) files containing ``F`` data.
+
+Release GIL when fetching WebP frames
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Python's Global Interpreter Lock is now released when fetching WebP frames from
+the libwebp decoder.
diff --git a/pyproject.toml b/pyproject.toml
index 48c59f2a1..518facc34 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,7 @@ classifiers = [
"Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
"Topic :: Multimedia :: Graphics :: Viewers",
+ "Typing :: Typed",
]
dynamic = [
"version",
@@ -79,7 +80,6 @@ Homepage = "https://python-pillow.org"
Mastodon = "https://fosstodon.org/@pillow"
"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
Source = "https://github.com/python-pillow/Pillow"
-Twitter = "https://twitter.com/PythonPillow"
[tool.setuptools]
packages = ["PIL"]
@@ -140,7 +140,3 @@ follow_imports = "silent"
warn_redundant_casts = true
warn_unreachable = true
warn_unused_ignores = true
-exclude = [
- '^src/PIL/FpxImagePlugin.py$',
- '^src/PIL/MicImagePlugin.py$',
-]
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index d2e60aa07..726359c67 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -38,7 +38,7 @@ from ._deprecate import deprecate
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
-gs_binary = None
+gs_binary: str | bool | None = None
gs_windows_binary = None
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index 9769761fc..f9e4c731c 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -77,6 +77,7 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF100:
# prefix chunk; ignore it
self.__offset = self.__offset + i32(s)
+ self.fp.seek(self.__offset)
s = self.fp.read(16)
if i16(s, 4) == 0xF1FA:
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index dc842d7a3..b38b43013 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -649,9 +649,7 @@ def _write_multiple_frames(im, fp, palette):
if "transparency" in encoderinfo:
# When the delta is zero, fill the image with transparency
diff_frame = im_frame.copy()
- fill = Image.new(
- "P", diff_frame.size, encoderinfo["transparency"]
- )
+ fill = Image.new("P", delta.size, encoderinfo["transparency"])
if delta.mode == "RGBA":
r, g, b, a = delta.split()
mask = ImageMath.eval(
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index a770488b7..5f2baa69b 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -75,7 +75,7 @@ class DecompressionBombError(Exception):
# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image
-MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3)
+MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3)
try:
@@ -978,7 +978,7 @@ class Image:
delete_trns = False
# transparency handling
if has_transparency:
- if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or (
+ if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or (
self.mode == "RGB" and mode == "RGBA"
):
# Use transparent conversion to promote from transparent
@@ -1430,7 +1430,7 @@ class Image:
root = ElementTree.fromstring(xmp_tags)
return {get_name(root.tag): get_value(root)}
- def getexif(self):
+ def getexif(self) -> Exif:
"""
Gets EXIF data from the image.
@@ -1438,7 +1438,6 @@ class Image:
"""
if self._exif is None:
self._exif = Exif()
- self._exif._loaded = False
elif self._exif._loaded:
return self._exif
self._exif._loaded = True
@@ -1525,7 +1524,7 @@ class Image:
self.load()
return self.im.ptr
- def getpalette(self, rawmode="RGB"):
+ def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None:
"""
Returns the image palette as a list.
@@ -1615,7 +1614,7 @@ class Image:
x, y = self.im.getprojection()
return list(x), list(y)
- def histogram(self, mask=None, extrema=None):
+ def histogram(self, mask=None, extrema=None) -> list[int]:
"""
Returns a histogram for the image. The histogram is returned as a
list of pixel counts, one for each pixel value in the source
@@ -1804,7 +1803,7 @@ class Image:
result = alpha_composite(background, overlay)
self.paste(result, box)
- def point(self, lut, mode=None):
+ def point(self, lut, mode: str | None = None) -> Image:
"""
Maps this image through a lookup table or function.
@@ -1928,7 +1927,7 @@ class Image:
self.im.putdata(data, scale, offset)
- def putpalette(self, data, rawmode="RGB"):
+ def putpalette(self, data, rawmode="RGB") -> None:
"""
Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image.
@@ -2108,7 +2107,7 @@ class Image:
min(self.size[1], math.ceil(box[3] + support_y)),
)
- def resize(self, size, resample=None, box=None, reducing_gap=None):
+ def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image:
"""
Returns a resized copy of this image.
@@ -2200,10 +2199,11 @@ class Image:
if factor_x > 1 or factor_y > 1:
reduce_box = self._get_safe_box(size, resample, box)
factor = (factor_x, factor_y)
- if callable(self.reduce):
- self = self.reduce(factor, box=reduce_box)
- else:
- self = Image.reduce(self, factor, box=reduce_box)
+ self = (
+ self.reduce(factor, box=reduce_box)
+ if callable(self.reduce)
+ else Image.reduce(self, factor, box=reduce_box)
+ )
box = (
(box[0] - reduce_box[0]) / factor_x,
(box[1] - reduce_box[1]) / factor_y,
@@ -2818,7 +2818,7 @@ class Image:
self.im.transform2(box, image.im, method, data, resample, fill)
- def transpose(self, method):
+ def transpose(self, method: Transpose) -> Image:
"""
Transpose image (flip or rotate in 90 degree steps)
@@ -2870,7 +2870,9 @@ class ImagePointHandler:
(for use with :py:meth:`~PIL.Image.Image.point`)
"""
- pass
+ @abc.abstractmethod
+ def point(self, im: Image) -> Image:
+ pass
class ImageTransformHandler:
@@ -3690,6 +3692,7 @@ class Exif(_ExifBase):
endian = None
bigtiff = False
+ _loaded = False
def __init__(self):
self._data = {}
@@ -3805,7 +3808,7 @@ class Exif(_ExifBase):
return merged_dict
- def tobytes(self, offset=8):
+ def tobytes(self, offset: int = 8) -> bytes:
from . import TiffImagePlugin
head = self._get_head()
@@ -3960,7 +3963,7 @@ class Exif(_ExifBase):
del self._info[tag]
self._data[tag] = value
- def __delitem__(self, tag):
+ def __delitem__(self, tag: int) -> None:
if self._info is not None and tag in self._info:
del self._info[tag]
else:
diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py
index ad59b0667..5fb80b753 100644
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -124,7 +124,7 @@ def getrgb(color):
@lru_cache
-def getcolor(color, mode):
+def getcolor(color, mode: str) -> tuple[int, ...]:
"""
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 487f53efe..a654dea27 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -32,7 +32,7 @@ import io
import itertools
import struct
import sys
-from typing import Any, NamedTuple
+from typing import IO, Any, NamedTuple
from . import Image
from ._deprecate import deprecate
@@ -384,7 +384,7 @@ class Parser:
"""
incremental = None
- image = None
+ image: Image.Image | None = None
data = None
decoder = None
offset = 0
@@ -616,7 +616,7 @@ class PyCodecState:
class PyCodec:
- fd: io.BytesIO | None
+ fd: IO[bytes] | None
def __init__(self, mode, *args):
self.im = None
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index a9e626b2b..33db8fa50 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -21,6 +21,7 @@ from __future__ import annotations
import functools
import operator
import re
+from typing import Protocol, Sequence, cast
from . import ExifTags, Image, ImagePalette
@@ -28,7 +29,7 @@ from . import ExifTags, Image, ImagePalette
# helpers
-def _border(border):
+def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
if isinstance(border, tuple):
if len(border) == 2:
left, top = right, bottom = border
@@ -39,7 +40,7 @@ def _border(border):
return left, top, right, bottom
-def _color(color, mode):
+def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]:
if isinstance(color, str):
from . import ImageColor
@@ -47,7 +48,7 @@ def _color(color, mode):
return color
-def _lut(image, lut):
+def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
if image.mode == "P":
# FIXME: apply to lookup table, not image data
msg = "mode P support coming soon"
@@ -65,7 +66,13 @@ def _lut(image, lut):
# actions
-def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
+def autocontrast(
+ image: Image.Image,
+ cutoff: float | tuple[float, float] = 0,
+ ignore: int | Sequence[int] | None = None,
+ mask: Image.Image | None = None,
+ preserve_tone: bool = False,
+) -> Image.Image:
"""
Maximize (normalize) image contrast. This function calculates a
histogram of the input image (or mask region), removes ``cutoff`` percent of the
@@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
h = histogram[layer : layer + 256]
if ignore is not None:
# get rid of outliers
- try:
+ if isinstance(ignore, int):
h[ignore] = 0
- except TypeError:
- # assume sequence
+ else:
for ix in ignore:
h[ix] = 0
if cutoff:
@@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
for ix in range(256):
n = n + h[ix]
# remove cutoff% pixels from the low end
- cut = n * cutoff[0] // 100
+ cut = int(n * cutoff[0] // 100)
for lo in range(256):
if cut > h[lo]:
cut = cut - h[lo]
@@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
if cut <= 0:
break
# remove cutoff% samples from the high end
- cut = n * cutoff[1] // 100
+ cut = int(n * cutoff[1] // 100)
for hi in range(255, -1, -1):
if cut > h[hi]:
cut = cut - h[hi]
@@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
return _lut(image, lut)
-def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127):
+def colorize(
+ image: Image.Image,
+ black: str | tuple[int, ...],
+ white: str | tuple[int, ...],
+ mid: str | int | tuple[int, ...] | None = None,
+ blackpoint: int = 0,
+ whitepoint: int = 255,
+ midpoint: int = 127,
+) -> Image.Image:
"""
Colorize grayscale image.
This function calculates a color wedge which maps all black pixels in
@@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
# Define colors from arguments
- black = _color(black, "RGB")
- white = _color(white, "RGB")
- if mid is not None:
- mid = _color(mid, "RGB")
+ rgb_black = cast(Sequence[int], _color(black, "RGB"))
+ rgb_white = cast(Sequence[int], _color(white, "RGB"))
+ rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None
# Empty lists for the mapping
red = []
@@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
# Create the low-end values
for i in range(0, blackpoint):
- red.append(black[0])
- green.append(black[1])
- blue.append(black[2])
+ red.append(rgb_black[0])
+ green.append(rgb_black[1])
+ blue.append(rgb_black[2])
# Create the mapping (2-color)
- if mid is None:
+ if rgb_mid is None:
range_map = range(0, whitepoint - blackpoint)
for i in range_map:
- red.append(black[0] + i * (white[0] - black[0]) // len(range_map))
- green.append(black[1] + i * (white[1] - black[1]) // len(range_map))
- blue.append(black[2] + i * (white[2] - black[2]) // len(range_map))
+ red.append(
+ rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
+ )
+ green.append(
+ rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
+ )
+ blue.append(
+ rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
+ )
# Create the mapping (3-color)
else:
@@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
range_map2 = range(0, whitepoint - midpoint)
for i in range_map1:
- red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1))
- green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1))
- blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1))
+ red.append(
+ rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
+ )
+ green.append(
+ rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
+ )
+ blue.append(
+ rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
+ )
for i in range_map2:
- red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2))
- green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2))
- blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2))
+ red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
+ green.append(
+ rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
+ )
+ blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
# Create the high-end values
for i in range(0, 256 - whitepoint):
- red.append(white[0])
- green.append(white[1])
- blue.append(white[2])
+ red.append(rgb_white[0])
+ green.append(rgb_white[1])
+ blue.append(rgb_white[2])
# Return converted image
image = image.convert("RGB")
return _lut(image, red + green + blue)
-def contain(image, size, method=Image.Resampling.BICUBIC):
+def contain(
+ image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
+) -> Image.Image:
"""
Returns a resized version of the image, set to the maximum width and height
within the requested size, while maintaining the original aspect ratio.
@@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
return image.resize(size, resample=method)
-def cover(image, size, method=Image.Resampling.BICUBIC):
+def cover(
+ image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
+) -> Image.Image:
"""
Returns a resized version of the image, so that the requested size is
covered, while maintaining the original aspect ratio.
@@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC):
return image.resize(size, resample=method)
-def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
+def pad(
+ image: Image.Image,
+ size: tuple[int, int],
+ method: int = Image.Resampling.BICUBIC,
+ color: str | int | tuple[int, ...] | None = None,
+ centering: tuple[float, float] = (0.5, 0.5),
+) -> Image.Image:
"""
Returns a resized and padded version of the image, expanded to fill the
requested aspect ratio and size.
@@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5
return out
-def crop(image, border=0):
+def crop(image: Image.Image, border: int = 0) -> Image.Image:
"""
Remove border from image. The same amount of pixels are removed
from all four sides. This function works on all image modes.
@@ -349,7 +386,9 @@ def crop(image, border=0):
return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
-def scale(image, factor, resample=Image.Resampling.BICUBIC):
+def scale(
+ image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
+) -> Image.Image:
"""
Returns a rescaled image by a specific factor given in parameter.
A factor greater than 1 expands the image, between 0 and 1 contracts the
@@ -372,7 +411,27 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC):
return image.resize(size, resample)
-def deform(image, deformer, resample=Image.Resampling.BILINEAR):
+class SupportsGetMesh(Protocol):
+ """
+ An object that supports the ``getmesh`` method, taking an image as an
+ argument, and returning a list of tuples. Each tuple contains two tuples,
+ the source box as a tuple of 4 integers, and a tuple of 8 integers for the
+ final quadrilateral, in order of top left, bottom left, bottom right, top
+ right.
+ """
+
+ def getmesh(
+ self, image: Image.Image
+ ) -> list[
+ tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
+ ]: ...
+
+
+def deform(
+ image: Image.Image,
+ deformer: SupportsGetMesh,
+ resample: int = Image.Resampling.BILINEAR,
+) -> Image.Image:
"""
Deform the image.
@@ -388,7 +447,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR):
)
-def equalize(image, mask=None):
+def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
"""
Equalize the image histogram. This function applies a non-linear
mapping to the input image, in order to create a uniform
@@ -419,7 +478,11 @@ def equalize(image, mask=None):
return _lut(image, lut)
-def expand(image, border=0, fill=0):
+def expand(
+ image: Image.Image,
+ border: int | tuple[int, ...] = 0,
+ fill: str | int | tuple[int, ...] = 0,
+) -> Image.Image:
"""
Add border to the image
@@ -445,7 +508,13 @@ def expand(image, border=0, fill=0):
return out
-def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
+def fit(
+ image: Image.Image,
+ size: tuple[int, int],
+ method: int = Image.Resampling.BICUBIC,
+ bleed: float = 0.0,
+ centering: tuple[float, float] = (0.5, 0.5),
+) -> Image.Image:
"""
Returns a resized and cropped version of the image, cropped to the
requested aspect ratio and size.
@@ -479,13 +548,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
# kevin@cazabon.com
# https://www.cazabon.com
- # ensure centering is mutable
- centering = list(centering)
+ centering_x, centering_y = centering
- if not 0.0 <= centering[0] <= 1.0:
- centering[0] = 0.5
- if not 0.0 <= centering[1] <= 1.0:
- centering[1] = 0.5
+ if not 0.0 <= centering_x <= 1.0:
+ centering_x = 0.5
+ if not 0.0 <= centering_y <= 1.0:
+ centering_y = 0.5
if not 0.0 <= bleed < 0.5:
bleed = 0.0
@@ -522,8 +590,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
crop_height = live_size[0] / output_ratio
# make the crop
- crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0]
- crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1]
+ crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
+ crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
@@ -531,7 +599,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
return image.resize(size, method, box=crop)
-def flip(image):
+def flip(image: Image.Image) -> Image.Image:
"""
Flip the image vertically (top to bottom).
@@ -541,7 +609,7 @@ def flip(image):
return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
-def grayscale(image):
+def grayscale(image: Image.Image) -> Image.Image:
"""
Convert the image to grayscale.
@@ -551,7 +619,7 @@ def grayscale(image):
return image.convert("L")
-def invert(image):
+def invert(image: Image.Image) -> Image.Image:
"""
Invert (negate) the image.
@@ -562,7 +630,7 @@ def invert(image):
return image.point(lut) if image.mode == "1" else _lut(image, lut)
-def mirror(image):
+def mirror(image: Image.Image) -> Image.Image:
"""
Flip image horizontally (left to right).
@@ -572,7 +640,7 @@ def mirror(image):
return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
-def posterize(image, bits):
+def posterize(image: Image.Image, bits: int) -> Image.Image:
"""
Reduce the number of bits for each color channel.
@@ -585,7 +653,7 @@ def posterize(image, bits):
return _lut(image, lut)
-def solarize(image, threshold=128):
+def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
"""
Invert all pixel values above a threshold.
@@ -602,7 +670,7 @@ def solarize(image, threshold=128):
return _lut(image, lut)
-def exif_transpose(image, *, in_place=False):
+def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
"""
If an image has an EXIF Orientation tag, other than 1, transpose the image
accordingly, and remove the orientation data.
@@ -616,7 +684,7 @@ def exif_transpose(image, *, in_place=False):
"""
image.load()
image_exif = image.getexif()
- orientation = image_exif.get(ExifTags.Base.Orientation)
+ orientation = image_exif.get(ExifTags.Base.Orientation, 1)
method = {
2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.Transpose.ROTATE_180,
@@ -653,3 +721,4 @@ def exif_transpose(image, *, in_place=False):
return transposed_image
elif not in_place:
return image.copy()
+ return None
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index 2b6cecc61..770d10025 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import array
+from typing import Sequence
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
@@ -34,11 +35,11 @@ class ImagePalette:
Defaults to an empty palette.
"""
- def __init__(self, mode="RGB", palette=None):
+ def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None:
self.mode = mode
self.rawmode = None # if set, palette contains raw data
self.palette = palette or bytearray()
- self.dirty = None
+ self.dirty: int | None = None
@property
def palette(self):
@@ -127,7 +128,7 @@ class ImagePalette:
raise ValueError(msg) from e
return index
- def getcolor(self, color, image=None):
+ def getcolor(self, color, image=None) -> int:
"""Given an rgb tuple, allocate palette entry.
.. warning:: This method is experimental.
diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py
index bb7e466a7..65cc70624 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -26,6 +26,7 @@ from __future__ import annotations
import io
import struct
+from typing import IO
from . import Image, ImageFile
from ._binary import i16le as i16
@@ -163,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder)
# write MSP files (uncompressed only)
-def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None:
+def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if im.mode != "1":
msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg)
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 3e0968a83..026bfd9a0 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -28,6 +28,7 @@ from __future__ import annotations
import io
import logging
+from typing import IO
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@@ -143,7 +144,7 @@ SAVE = {
}
-def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None:
+def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 823f12492..35f38d67c 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -62,7 +62,7 @@ _MODES = {
(2, 0): ("L", "L;2"),
(4, 0): ("L", "L;4"),
(8, 0): ("L", "L"),
- (16, 0): ("I", "I;16B"),
+ (16, 0): ("I;16", "I;16B"),
# Truecolour
(8, 2): ("RGB", "RGB"),
(16, 2): ("RGB", "RGB;16B"),
@@ -467,7 +467,7 @@ 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"):
+ elif self.im_mode in ("1", "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)
@@ -981,7 +981,13 @@ class PngImageFile(ImageFile.ImageFile):
except EOFError:
if cid == b"fdAT":
length -= 4
- ImageFile._safe_read(self.fp, length)
+ try:
+ ImageFile._safe_read(self.fp, length)
+ except OSError as e:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ break
+ else:
+ raise e
except AttributeError:
logger.debug("%r %s %s (unknown)", cid, pos, length)
s = ImageFile._safe_read(self.fp, length)
@@ -1350,7 +1356,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
transparency = max(0, min(255, transparency))
alpha = b"\xFF" * transparency + b"\0"
chunk(fp, b"tRNS", alpha[:alpha_bytes])
- elif im.mode in ("1", "L", "I"):
+ elif im.mode in ("1", "L", "I", "I;16"):
transparency = max(0, min(65535, transparency))
chunk(fp, b"tRNS", o16(transparency))
elif im.mode == "RGB":
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 3e45ba95c..6ac7a9bbc 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -16,7 +16,7 @@
from __future__ import annotations
import math
-from io import BytesIO
+from typing import IO
from . import Image, ImageFile
from ._binary import i16be as i16
@@ -324,7 +324,7 @@ class PpmDecoder(ImageFile.PyDecoder):
# --------------------------------------------------------------------
-def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
+def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if im.mode == "1":
rawmode, head = "1;I", b"P4"
elif im.mode == "L":
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index ccf661ff1..7bd84ebd4 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -24,7 +24,7 @@ from __future__ import annotations
import os
import struct
-from io import BytesIO
+from typing import IO
from . import Image, ImageFile
from ._binary import i16be as i16
@@ -125,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile):
]
-def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
+def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode"
raise ValueError(msg)
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 584932d2c..828701342 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -18,7 +18,7 @@
from __future__ import annotations
import warnings
-from io import BytesIO
+from typing import IO
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@@ -175,7 +175,7 @@ SAVE = {
}
-def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
+def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e:
diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py
index 0291e2858..eee727436 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -21,7 +21,7 @@
from __future__ import annotations
import re
-from io import BytesIO
+from typing import IO
from . import Image, ImageFile
@@ -70,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile):
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)]
-def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
+def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if im.mode != "1":
msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg)
diff --git a/src/PIL/py.typed b/src/PIL/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/_webp.c b/src/_webp.c
index a1b4dbc1a..47592547c 100644
--- a/src/_webp.c
+++ b/src/_webp.c
@@ -448,11 +448,16 @@ PyObject *
_anim_decoder_get_next(PyObject *self) {
uint8_t *buf;
int timestamp;
+ int ok;
PyObject *bytes;
PyObject *ret;
+ ImagingSectionCookie cookie;
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
- if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) {
+ ImagingSectionEnter(&cookie);
+ ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp);
+ ImagingSectionLeave(&cookie);
+ if (!ok) {
PyErr_SetString(PyExc_OSError, "failed to read next frame");
return NULL;
}
diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c
index 99d2a4ada..2654fd40d 100644
--- a/src/libImaging/Convert.c
+++ b/src/libImaging/Convert.c
@@ -878,6 +878,18 @@ I16B_L(UINT8 *out, const UINT8 *in, int xsize) {
}
}
+static void
+I16_RGB(UINT8 *out, const UINT8 *in, int xsize) {
+ int x;
+ for (x = 0; x < xsize; x++, in += 2) {
+ UINT8 v = in[1] == 0 ? in[0] : 255;
+ *out++ = v;
+ *out++ = v;
+ *out++ = v;
+ *out++ = 255;
+ }
+}
+
static struct {
const char *from;
const char *to;
@@ -978,6 +990,7 @@ static struct {
{"I", "I;16", I_I16L},
{"I;16", "I", I16L_I},
+ {"I;16", "RGB", I16_RGB},
{"L", "I;16", L_I16L},
{"I;16", "L", I16L_L},
@@ -1678,6 +1691,7 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) {
convert = rgb2rgba;
} else if ((strcmp(imIn->mode, "1") == 0 ||
strcmp(imIn->mode, "I") == 0 ||
+ strcmp(imIn->mode, "I;16") == 0 ||
strcmp(imIn->mode, "L") == 0
) && (
strcmp(mode, "RGBA") == 0 ||
@@ -1687,6 +1701,8 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) {
convert = bit2rgb;
} else if (strcmp(imIn->mode, "I") == 0) {
convert = i2rgb;
+ } else if (strcmp(imIn->mode, "I;16") == 0) {
+ convert = I16_RGB;
} else {
convert = l2rgb;
}
diff --git a/tox.ini b/tox.ini
index 8c818df7a..85a2020d6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -33,14 +33,15 @@ commands =
[testenv:mypy]
skip_install = true
deps =
+ -r .ci/requirements-mypy.txt
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
- mypy==1.7.1
numpy
packaging
types-cffi
types-defusedxml
+ types-olefile
extras =
typing
commands =
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index df33ea493..2ee9872e6 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -308,21 +308,16 @@ DEPS = {
"libs": [r"Lib\MS\*.lib"],
},
"openjpeg": {
- "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz",
- "filename": "openjpeg-2.5.0.tar.gz",
- "dir": "openjpeg-2.5.0",
+ "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.2.tar.gz",
+ "filename": "openjpeg-2.5.2.tar.gz",
+ "dir": "openjpeg-2.5.2",
"license": "LICENSE",
- "patch": {
- r"src\lib\openjp2\ht_dec.c": {
- "#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501
- }
- },
"build": [
*cmds_cmake(
"openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF"
),
- cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"),
- cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"),
+ cmd_mkdir(r"{inc_dir}\openjpeg-2.5.2"),
+ cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.2"),
],
"libs": [r"bin\*.lib"],
},