Merge branch 'main' into init

This commit is contained in:
Andrew Murray 2024-07-06 22:03:19 +10:00 committed by GitHub
commit 972abcb757
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
178 changed files with 2227 additions and 2034 deletions

View File

@ -21,9 +21,9 @@ environment:
- PYTHON: C:/Python312
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64
- PYTHON: C:/Python39-x64
ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
install:
@ -38,7 +38,7 @@ install:
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
c:\pillow\winbuild\build\build_dep_all.cmd
$host.SetShouldExit(0)
- path C:\pillow\winbuild\build\bin;%PATH%

View File

@ -28,8 +28,6 @@ fi
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
# TODO Update condition when cffi supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
@ -39,8 +37,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
# TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
python3 -m pip install numpy
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
@ -51,7 +48,6 @@ if [[ $(uname) != CYGWIN* ]]; then
# Pyroma uses non-isolated build and fails with old setuptools
if [[
$GHA_PYTHON_VERSION == pypy3.9
|| $GHA_PYTHON_VERSION == 3.8
|| $GHA_PYTHON_VERSION == 3.9
]]; then
# To match pyproject.toml

View File

@ -1 +1 @@
cibuildwheel==2.19.1
cibuildwheel==2.19.2

View File

@ -1 +1 @@
mypy==1.10.0
mypy==1.10.1

View File

@ -19,6 +19,5 @@ exclude_also =
[run]
omit =
Tests/32bit_segfault_check.py
Tests/bench_cffi_access.py
Tests/check_*.py
Tests/createfontdatachunk.py

View File

@ -18,9 +18,6 @@ else
fi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
# TODO Update condition when cffi supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
@ -28,9 +25,7 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
# TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
python3 -m pip install numpy
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-minor-version: [8, 9]
python-minor-version: [9]
timeout-minutes: 40
@ -72,7 +72,6 @@ jobs:
make
netpbm
perl
python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-numpy

View File

@ -44,13 +44,11 @@ jobs:
amazon-2023-amd64,
arch,
centos-stream-9-amd64,
debian-11-bullseye-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-39-amd64,
fedora-40-amd64,
gentoo,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64,
]

View File

@ -64,7 +64,6 @@ jobs:
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python3-cffi \
mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python3-setuptools \

View File

@ -35,7 +35,7 @@ jobs:
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.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30

View File

@ -48,7 +48,6 @@ jobs:
"3.11",
"3.10",
"3.9",
"3.8",
]
include:
- python-version: "3.11"
@ -59,13 +58,9 @@ jobs:
# M1 only available for 3.10+
- os: "macos-13"
python-version: "3.9"
- os: "macos-13"
python-version: "3.8"
exclude:
- os: "macos-14"
python-version: "3.9"
- os: "macos-14"
python-version: "3.8"
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}

View File

@ -12,7 +12,7 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
else
yum install -y fribidi
fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
python3 -m pip install numpy
fi

View File

@ -41,12 +41,8 @@ jobs:
python-version:
- pp39
- pp310
- cp38
- cp39
- cp310
- cp311
- cp312
- cp313
- cp3{9,10,11}
- cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
@ -136,8 +132,6 @@ jobs:
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: cp38-macosx_arm64
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4
@ -208,7 +202,6 @@ jobs:
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.7
rev: v0.5.0
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
@ -11,7 +11,7 @@ repos:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.7.8
rev: 1.7.9
hooks:
- id: bandit
args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.5
rev: v18.1.8
hooks:
- id: clang-format
types: [c]
@ -50,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.4
rev: 0.28.6
hooks:
- id: check-github-workflows
- id: check-readthedocs
@ -62,7 +62,7 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.8.0
rev: 2.1.3
hooks:
- id: pyproject-fmt

View File

@ -2,9 +2,99 @@
Changelog (Pillow)
==================
10.4.0 (unreleased)
11.0.0 (unreleased)
-------------------
- Drop support for Python 3.8 #8183
[hugovk, radarhere]
- Add support for Python 3.13 #8181
[hugovk, radarhere]
- Fix incompatibility with NumPy 1.20 #8187
[neutrinoceros, radarhere]
- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
[hugovk, radarhere]
10.4.0 (2024-07-01)
-------------------
- Raise FileNotFoundError if show_file() path does not exist #8178
[radarhere]
- Improved reading 16-bit TGA images with colour #7965
[Yay295, radarhere]
- Deprecate non-image ImageCms modes #8031
[radarhere]
- Fixed processing multiple JPEG EXIF markers #8127
[radarhere]
- Do not preserve EXIFIFD tag by default when saving TIFF images #8110
[radarhere]
- Added ImageFont.load_default_imagefont() #8086
[radarhere]
- Added Image.WARN_POSSIBLE_FORMATS #8063
[radarhere]
- Remove zero-byte end padding when parsing any XMP data #8171
[radarhere]
- Do not detect Ultra HDR images as MPO #8056
[radarhere]
- Raise SyntaxError specific to JP2 #8146
[Yay295, radarhere]
- Do not use first frame duration for other frames when saving APNG images #8104
[radarhere]
- Consider I;16 pixel size when using a 1 mode mask #8112
[radarhere]
- When saving multiple PNG frames, convert to mode rather than raw mode #8087
[radarhere]
- Added byte support to FreeTypeFont #8141
[radarhere]
- Allow float center for rotate operations #8114
[radarhere]
- Do not read layers immediately when opening PSD images #8039
[radarhere]
- Restore original thread state #8065
[radarhere]
- Read IM and TIFF images as RGB, rather than RGBX #7997
[radarhere]
- Only preserve TIFF IPTC_NAA_CHUNK tag if type is BYTE or UNDEFINED #7948
[radarhere]
- Clarify ImageDraw2 error message when size is missing #8165
[radarhere]
- Support unpacking more rawmodes to RGBA palettes #7966
[radarhere]
- Removed support for Qt 5 #8159
[radarhere]
- Improve ``ImageFont.freetype`` support for XDG directories on Linux #8135
[mamg22, radarhere]
- Improved consistency of XMP handling #8069
[radarhere]
- Use pkg-config to help find libwebp and raqm #8142
[radarhere]
- Accept 't' suffix for libtiff version #8126, #8129
[radarhere]

View File

@ -1,54 +0,0 @@
from __future__ import annotations
import time
from PIL import PyAccess
from .helper import hopper
# Not running this test by default. No DOS against CI.
def iterate_get(size, access) -> None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)]
def iterate_set(size, access) -> None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)] = (x % 256, y % 256, 0)
def timer(func, label, *args) -> None:
iterations = 5000
starttime = time.time()
for x in range(iterations):
func(*args)
if time.time() - starttime > 10:
break
endtime = time.time()
print(
f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, "
f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration"
)
def test_direct() -> None:
im = hopper()
im.load()
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
caccess = im.im.pixel_access(False)
access = PyAccess.new(im, False)
assert access is not None
assert caccess[(0, 0)] == access[(0, 0)]
print(f"Size: {im.width}x{im.height}")
timer(iterate_get, "PyAccess - get", im.size, access)
timer(iterate_set, "PyAccess - set", im.size, access)
timer(iterate_get, "C-api - get", im.size, caccess)
timer(iterate_set, "C-api - set", im.size, caccess)

View File

@ -11,14 +11,15 @@ import subprocess
import sys
import sysconfig
import tempfile
from collections.abc import Sequence
from functools import lru_cache
from io import BytesIO
from typing import Any, Callable, Sequence
from typing import Any, Callable
import pytest
from packaging.version import parse as parse_version
from PIL import Image, ImageMath, features
from PIL import Image, ImageFile, ImageMath, features
logger = logging.getLogger(__name__)
@ -59,9 +60,7 @@ def convert_to_comparable(
return new_a, new_b
def assert_deep_equal(
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
) -> None:
def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:
@ -240,7 +239,7 @@ class PillowLeakTestCase:
# helpers
def fromstring(data: bytes) -> Image.Image:
def fromstring(data: bytes) -> ImageFile.ImageFile:
return Image.open(BytesIO(data))

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 414 B

BIN
Tests/images/rgba16.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

BIN
Tests/images/ultrahdr.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

View File

@ -321,6 +321,7 @@ class TestColorLut3DCoreAPI:
-1, 2, 2, 2, 2, 2,
])).load()
# fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)
@ -341,6 +342,7 @@ class TestColorLut3DCoreAPI:
-3, 5, 5, 5, 5, 5,
])).load()
# fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)
@ -354,10 +356,10 @@ class TestColorLut3DCoreAPI:
class TestColorLut3DFilter:
def test_wrong_args(self) -> None:
with pytest.raises(ValueError, match="should be either an integer"):
ImageFilter.Color3DLUT("small", [1])
ImageFilter.Color3DLUT("small", [1]) # type: ignore[arg-type]
with pytest.raises(ValueError, match="should be either an integer"):
ImageFilter.Color3DLUT((11, 11), [1])
ImageFilter.Color3DLUT((11, 11), [1]) # type: ignore[arg-type]
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
ImageFilter.Color3DLUT((11, 11, 1), [1])

View File

@ -9,9 +9,9 @@ from PIL import _deprecate
"version, expected",
[
(
11,
"Old thing is deprecated and will be removed in Pillow 11 "
r"\(2024-10-15\)\. Use new thing instead\.",
12,
"Old thing is deprecated and will be removed in Pillow 12 "
r"\(2025-10-15\)\. Use new thing instead\.",
),
(
None,
@ -54,18 +54,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
def test_plural() -> None:
expected = (
r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Old things are deprecated and will be removed in Pillow 12 \(2025-10-15\)\. "
r"Use new thing instead\."
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old things", 11, "new thing", plural=True)
_deprecate.deprecate("Old things", 12, "new thing", plural=True)
def test_replacement_and_action() -> None:
expected = "Use only one of 'replacement' and 'action'"
with pytest.raises(ValueError, match=expected):
_deprecate.deprecate(
"Old thing", 11, replacement="new thing", action="Upgrade to new thing"
"Old thing", 12, replacement="new thing", action="Upgrade to new thing"
)
@ -78,16 +78,16 @@ def test_replacement_and_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"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)\. "
r"Upgrade to new thing\."
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 11, action=action)
_deprecate.deprecate("Old thing", 12, action=action)
def test_no_replacement_or_action() -> None:
expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)"
r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)"
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 11)
_deprecate.deprecate("Old thing", 12)

View File

@ -706,10 +706,21 @@ def test_different_modes_in_later_frames(
assert reloaded.mode == mode
def test_apng_repeated_seeks_give_correct_info() -> None:
def test_different_durations(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/apng/different_durations.png") as im:
for i in range(3):
for _ in range(3):
im.seek(0)
assert im.info["duration"] == 4000
im.seek(1)
assert im.info["duration"] == 1000
im.save(test_file, save_all=True)
with Image.open(test_file) as reloaded:
assert reloaded.info["duration"] == 4000
reloaded.seek(1)
assert reloaded.info["duration"] == 1000

View File

@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close()
return Image.new("RGB", (1, 1))
def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
BufrStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
assert not handler.loaded
assert not handler.is_loaded()
im.load()
assert handler.loaded
assert handler.is_loaded()
temp_file = str(tmp_path / "temp.bufr")
im.save(temp_file)

View File

@ -329,46 +329,6 @@ def test_read_binary_preview() -> None:
pass
def test_readline_psfile(tmp_path: Path) -> None:
# check all the freaking line endings possible from the spec
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
line_endings = ["\r\n", "\n", "\n\r", "\r"]
strings = ["something", "else", "baz", "bif"]
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
assert t.readline().strip("\r\n") == "something", ending
assert t.readline().strip("\r\n") == "else", ending
assert t.readline().strip("\r\n") == "baz", ending
assert t.readline().strip("\r\n") == "bif", ending
def _test_readline_io_psfile(test_string: str, ending: str) -> None:
f = io.BytesIO(test_string.encode("latin-1"))
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(f)
_test_readline(t, ending)
def _test_readline_file_psfile(test_string: str, ending: str) -> None:
f = str(tmp_path / "temp.txt")
with open(f, "wb") as w:
w.write(test_string.encode("latin-1"))
with open(f, "rb") as r:
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(r)
_test_readline(t, ending)
for ending in line_endings:
s = ending.join(strings)
_test_readline_io_psfile(s, ending)
_test_readline_file_psfile(s, ending)
def test_psfile_deprecation() -> None:
with pytest.warns(DeprecationWarning):
EpsImagePlugin.PSFile(None)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
@pytest.mark.parametrize(
"line_ending",
@ -425,9 +385,10 @@ def test_timeout(test_file: str) -> None:
def test_bounding_box_in_trailer() -> None:
# Check bounding boxes are parsed in the same way
# when specified in the header and the trailer
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
FILE1
) as header_image:
with (
Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image,
Image.open(FILE1) as header_image,
):
assert trailer_image.size == header_image.size

View File

@ -1,9 +1,9 @@
from __future__ import annotations
import warnings
from collections.abc import Generator
from io import BytesIO
from pathlib import Path
from typing import Generator
import pytest
@ -353,7 +353,7 @@ def test_palette_434(tmp_path: Path) -> None:
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif")
im.copy().save(out, **kwargs)
im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out)
return reloaded

View File

@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close()
return Image.new("RGB", (1, 1))
def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
GribStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
assert not handler.loaded
assert not handler.is_loaded()
im.load()
assert handler.loaded
assert handler.is_loaded()
temp_file = str(tmp_path / "temp.grib")
im.save(temp_file)

View File

@ -66,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close()
return Image.new("RGB", (1, 1))
def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
@ -73,10 +76,10 @@ def test_handler(tmp_path: Path) -> None:
Hdf5StubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
assert not handler.loaded
assert not handler.is_loaded()
im.load()
assert handler.loaded
assert handler.is_loaded()
temp_file = str(tmp_path / "temp.h5")
im.save(temp_file)

View File

@ -872,7 +872,7 @@ class TestFileJpeg:
def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im:
assert im.info["exif"] == b"Exif\x00\x00firstsecond"
assert im.getexif()[270] == "firstsecond"
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@ -948,6 +948,7 @@ class TestFileJpeg:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
@ -1032,8 +1033,10 @@ class TestFileJpeg:
def test_repr_jpeg(self) -> None:
im = hopper()
b = im._repr_jpeg_()
assert b is not None
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
with Image.open(BytesIO(b)) as repr_jpeg:
assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17)

View File

@ -335,9 +335,15 @@ def test_issue_6194() -> None:
assert im.getpixel((5, 5)) == 31
def test_unknown_j2k_mode() -> None:
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/unknown_mode.j2k"):
pass
def test_unbound_local() -> None:
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError):
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/unbound_variable.jp2"):
pass

View File

@ -92,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
with open(test_file, "rb") as f:
s.write(f.read())
s.seek(0)
r = io.BufferedReader(s)
data = f.read()
class NonBytesIO(io.RawIOBase):
def read(self, size: int = -1) -> bytes:
nonlocal data
if size == -1:
size = len(data)
result = data[:size]
data = data[size:]
return result
def readable(self) -> bool:
return True
r = io.BufferedReader(NonBytesIO())
with Image.open(r) as im:
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
@ -685,13 +696,18 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
def test_exif_ifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
def test_exif_ifd(self) -> None:
out = io.BytesIO()
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert im.tag_v2[34665] == 125456
im.save(outfile)
im.save(out, "TIFF")
with Image.open(outfile) as reloaded:
with Image.open(out) as reloaded:
assert 34665 not in reloaded.tag_v2
im.save(out, "TIFF", tiffinfo={34665: 125456})
with Image.open(out) as reloaded:
if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456
@ -1043,7 +1059,11 @@ class TestFileLibTiff(LibTiffTestCase):
],
)
def test_wrong_bits_per_sample(
self, file_name: str, mode: str, size: tuple[int, int], tile
self,
file_name: str,
mode: str,
size: tuple[int, int],
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
) -> None:
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
@ -1130,7 +1150,7 @@ class TestFileLibTiff(LibTiffTestCase):
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, **arguments)
im.save(out, "TIFF", **arguments)
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)

View File

@ -226,6 +226,11 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
def test_ultra_hdr() -> None:
with Image.open("Tests/images/ultrahdr.jpg") as im:
assert im.format == "JPEG"
@pytest.mark.parametrize("test_file", test_files)
def test_image_grab(test_file: str) -> None:
with Image.open(test_file) as im:

View File

@ -76,6 +76,7 @@ def test_pil184() -> None:
def test_1px_width(tmp_path: Path) -> None:
im = Image.new("L", (1, 256))
px = im.load()
assert px is not None
for y in range(256):
px[0, y] = y
_roundtrip(tmp_path, im)
@ -84,6 +85,7 @@ def test_1px_width(tmp_path: Path) -> None:
def test_large_count(tmp_path: Path) -> None:
im = Image.new("L", (256, 1))
px = im.load()
assert px is not None
for x in range(256):
px[x, 0] = x // 67 * 67
_roundtrip(tmp_path, im)
@ -101,6 +103,7 @@ def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) ->
def test_break_in_count_overflow(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@ -110,6 +113,7 @@ def test_break_in_count_overflow(tmp_path: Path) -> None:
def test_break_one_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -119,6 +123,7 @@ def test_break_one_in_loop(tmp_path: Path) -> None:
def test_break_many_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@ -130,6 +135,7 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
def test_break_one_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -140,6 +146,7 @@ def test_break_one_at_end(tmp_path: Path) -> None:
def test_break_many_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -152,6 +159,7 @@ def test_break_many_at_end(tmp_path: Path) -> None:
def test_break_padding(tmp_path: Path) -> None:
im = Image.new("L", (257, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(257):
px[x, y] = x % 128

View File

@ -5,8 +5,9 @@ import os
import os.path
import tempfile
import time
from collections.abc import Generator
from pathlib import Path
from typing import Any, Generator
from typing import Any
import pytest
@ -117,7 +118,7 @@ def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
im.save(outfile, **params)
im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp:
contents = fp.read()

View File

@ -535,8 +535,10 @@ class TestFilePng:
def test_repr_png(self) -> None:
im = hopper()
b = im._repr_png_()
assert b is not None
with Image.open(BytesIO(im._repr_png_())) as repr_png:
with Image.open(BytesIO(b)) as repr_png:
assert repr_png.format == "PNG"
assert_image_equal(im, repr_png)
@ -655,11 +657,12 @@ class TestFilePng:
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_specify_bits(self, tmp_path: Path) -> None:
@pytest.mark.parametrize("save_all", (True, False))
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
im = hopper("P")
out = str(tmp_path / "temp.png")
im.save(out, bits=4)
im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 48
@ -683,6 +686,7 @@ class TestFilePng:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
@ -767,16 +771,12 @@ class TestFilePng:
def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
if buffer:
class MyStdOut:
buffer = BytesIO()
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
sys.stdout = mystdout
sys.stdout = mystdout # type: ignore[assignment]
with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG")
@ -784,7 +784,7 @@ class TestFilePng:
# Reset stdout
sys.stdout = old_stdout
if buffer:
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)

View File

@ -368,16 +368,12 @@ def test_mimetypes(tmp_path: Path) -> None:
def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout
if buffer:
class MyStdOut:
buffer = BytesIO()
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
sys.stdout = mystdout
sys.stdout = mystdout # type: ignore[assignment]
with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM")
@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
# Reset stdout
sys.stdout = old_stdout
if buffer:
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_FILE)

View File

@ -4,7 +4,7 @@ import warnings
import pytest
from PIL import Image, PsdImagePlugin, UnidentifiedImageError
from PIL import Image, PsdImagePlugin
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
@ -150,20 +150,26 @@ def test_combined_larger_than_size() -> None:
@pytest.mark.parametrize(
"test_file,raises",
[
(
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
UnidentifiedImageError,
),
(
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
UnidentifiedImageError,
),
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
],
)
def test_crashes(test_file: str, raises) -> None:
def test_crashes(test_file: str, raises: type[Exception]) -> None:
with open(test_file, "rb") as f:
with pytest.raises(raises):
with Image.open(f):
pass
@pytest.mark.parametrize(
"test_file",
[
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
],
)
def test_layer_crashes(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(SyntaxError):
im.layers

View File

@ -105,6 +105,7 @@ def test_load_image_series() -> None:
img_list = SpiderImagePlugin.loadImageSeries(file_list)
# Assert
assert img_list is not None
assert len(img_list) == 1
assert isinstance(img_list[0], Image.Image)
assert img_list[0].size == (128, 128)

View File

@ -72,12 +72,21 @@ def test_palette_depth_8(tmp_path: Path) -> None:
def test_palette_depth_16(tmp_path: Path) -> None:
with Image.open("Tests/images/p_16.tga") as im:
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png")
assert im.palette.mode == "RGBA"
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
out = str(tmp_path / "temp.png")
im.save(out)
with Image.open(out) as reloaded:
assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png")
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
def test_rgba_16() -> None:
with Image.open("Tests/images/rgba16.tga") as im:
assert im.mode == "RGBA"
assert im.getpixel((0, 0)) == (172, 0, 255, 255)
assert im.getpixel((1, 0)) == (0, 255, 82, 0)
def test_id_field() -> None:

View File

@ -2,10 +2,10 @@ from __future__ import annotations
import os
import warnings
from collections.abc import Generator
from io import BytesIO
from pathlib import Path
from types import ModuleType
from typing import Generator
import pytest
@ -78,6 +78,7 @@ class TestFileTiff:
def test_seek_after_close(self) -> None:
im = Image.open("Tests/images/multipage.tiff")
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.close()
with pytest.raises(ValueError):
@ -120,7 +121,7 @@ class TestFileTiff:
def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e:
ifd.legacy_api = None
ifd.legacy_api = False
assert str(e.value) == "Not allowing setting of legacy api"
def test_xyres_tiff(self) -> None:
@ -424,13 +425,13 @@ class TestFileTiff:
def test_load_float(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdabcd"
ret = ifd.load_float(data, False)
ret = getattr(ifd, "load_float")(data, False)
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
def test_load_double(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdefghabcdefgh"
ret = ifd.load_double(data, False)
ret = getattr(ifd, "load_double")(data, False)
assert ret == (8.540883223036124e194, 8.540883223036124e194)
def test_ifd_tag_type(self) -> None:
@ -599,7 +600,7 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename, **kwargs)
hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im:
# legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72
@ -621,6 +622,22 @@ class TestFileTiff:
assert_image_equal_tofile(im, tmpfile)
def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/hopper.tif") as im:
im.load()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[33723] = 1
ifd.tagtype[33723] = 4
im.tag_v2 = ifd
im.save(outfile)
with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 33723 not in im.tag_v2
def test_rowsperstrip(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
im = hopper()
@ -759,6 +776,7 @@ class TestFileTiff:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]

View File

@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import assert_deep_equal, hopper
TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}
TAG_IDS: dict[str, int] = {
info.name: info.value
for info in TiffTags.TAGS_V2.values()
if info.value is not None
}
def test_rt_metadata(tmp_path: Path) -> None:
@ -411,8 +415,8 @@ def test_empty_values() -> None:
info = TiffImagePlugin.ImageFileDirectory_v2(head)
info.load(data)
# Should not raise ValueError.
info = dict(info)
assert 33432 in info
info_dict = dict(info)
assert 33432 in info_dict
def test_photoshop_info(tmp_path: Path) -> None:

View File

@ -5,6 +5,7 @@ import re
import sys
import warnings
from pathlib import Path
from typing import Any
import pytest
@ -70,7 +71,9 @@ class TestFileWebp:
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None:
def _roundtrip(
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
) -> None:
temp_file = str(tmp_path / "temp.webp")
hopper(mode).save(temp_file, **args)

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Generator
from pathlib import Path
import pytest
@ -96,7 +97,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
check(temp_file1)
# Tests appending using a generator
def im_generator(ims):
def im_generator(
ims: list[Image.Image],
) -> Generator[Image.Image, None, None]:
yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp")

View File

@ -129,6 +129,7 @@ def test_getxmp() -> None:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
assert (
im.getxmp()["xmpmeta"]["xmptk"]
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "

View File

@ -34,7 +34,7 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
def test_leak(self) -> None:
if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError)
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try:
default_font = ImageFont.load_default()
finally:

View File

@ -8,7 +8,8 @@ import sys
import tempfile
import warnings
from pathlib import Path
from typing import IO
from types import ModuleType
from typing import IO, Any
import pytest
@ -35,6 +36,12 @@ from .helper import (
skip_unless_feature,
)
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -129,6 +136,15 @@ class TestImage:
assert im.mode == "RGB"
assert im.size == (128, 128)
def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True)
im = io.BytesIO(b"")
with pytest.warns(UserWarning):
with pytest.raises(UnidentifiedImageError):
with Image.open(im):
pass
def test_width_height(self) -> None:
im = Image.new("RGB", (1, 2))
assert im.width == 1
@ -179,11 +195,19 @@ class TestImage:
def test_fp_name(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.jpg")
class FP:
class FP(io.BytesIO):
name: str
def write(self, b: bytes) -> None:
pass
if sys.version_info >= (3, 12):
from collections.abc import Buffer
def write(self, data: Buffer) -> int:
return len(data)
else:
def write(self, data: Any) -> int:
return len(data)
fp = FP()
fp.name = temp_file
@ -352,8 +376,9 @@ class TestImage:
img = Image.alpha_composite(dst, src)
# Assert
img_colors = sorted(img.getcolors())
assert img_colors == expected_colors
img_colors = img.getcolors()
assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue")
@ -397,13 +422,13 @@ class TestImage:
# errors
with pytest.raises(ValueError):
source.alpha_composite(over, "invalid source")
source.alpha_composite(over, "invalid destination") # type: ignore[arg-type]
with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), "invalid destination")
source.alpha_composite(over, (0, 0), "invalid source") # type: ignore[arg-type]
with pytest.raises(ValueError):
source.alpha_composite(over, 0)
source.alpha_composite(over, 0) # type: ignore[arg-type]
with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), 0)
source.alpha_composite(over, (0, 0), 0) # type: ignore[arg-type]
with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), (0, -1))
@ -555,6 +580,7 @@ class TestImage:
for mode in ("I", "F", "L"):
im = Image.new(mode, (100, 100), (5,))
px = im.load()
assert px is not None
assert px[0, 0] == 5
def test_linear_gradient_wrong_mode(self) -> None:
@ -649,7 +675,9 @@ class TestImage:
im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
assert len(im_remapped.getpalette()) == 6
palette = im_remapped.getpalette()
assert palette is not None
assert len(palette) == 6
# Test unused transparency
im.info["transparency"] = 2
@ -680,7 +708,7 @@ class TestImage:
else:
assert new_image.palette is None
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
_make_new(im, im_p, ImagePalette.ImagePalette("RGB"))
_make_new(im_p, im, None)
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())
@ -913,6 +941,25 @@ class TestImage:
assert tag not in exif.get_ifd(0x8769)
assert exif.get_ifd(0xA005)
def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im:
assert im.getxmp() == {}
def test_getxmp_padded(self) -> None:
im = Image.new("RGB", (1, 1))
im.info["xmp"] = (
b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00'
)
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {}
else:
assert im.getxmp() == {"xmpmeta": None}
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size)

View File

@ -12,19 +12,6 @@ from PIL import Image
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:
try:
import cffi
from PIL import PyAccess
except ImportError:
cffi = None
numpy: ModuleType | None
try:
import numpy
@ -32,21 +19,7 @@ except ImportError:
numpy = None
class AccessTest:
# Initial value
_init_cffi_access = Image.USE_CFFI_ACCESS
_need_cffi_access = False
@classmethod
def setup_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._need_cffi_access
@classmethod
def teardown_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._init_cffi_access
class TestImagePutPixel(AccessTest):
class TestImagePutPixel:
def test_sanity(self) -> None:
im1 = hopper()
im2 = Image.new(im1.mode, im1.size, 0)
@ -54,7 +27,9 @@ class TestImagePutPixel(AccessTest):
for y in range(im1.size[1]):
for x in range(im1.size[0]):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2)
@ -64,7 +39,9 @@ class TestImagePutPixel(AccessTest):
for y in range(im1.size[1]):
for x in range(im1.size[0]):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly
assert_image_equal(im1, im2)
@ -74,10 +51,12 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load()
pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
with pytest.raises(TypeError):
pix1[0, "0"]
pix1[0, "0"] # type: ignore[index]
with pytest.raises(TypeError):
pix1["0", 0]
pix1["0", 0] # type: ignore[index]
for y in range(im1.size[1]):
for x in range(im1.size[0]):
@ -96,7 +75,9 @@ class TestImagePutPixel(AccessTest):
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2)
@ -106,7 +87,9 @@ class TestImagePutPixel(AccessTest):
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly
assert_image_equal(im1, im2)
@ -116,6 +99,8 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load()
pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pix2[x, y] = pix1[x, y]
@ -125,13 +110,14 @@ class TestImagePutPixel(AccessTest):
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
def test_numpy(self) -> None:
im = hopper()
pix = im.load()
px = im.load()
assert px is not None
assert numpy is not None
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
assert px[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
class TestImageGetPixel(AccessTest):
class TestImageGetPixel:
@staticmethod
def color(mode: str) -> int | tuple[int, ...]:
bands = Image.getmodebands(mode)
@ -144,9 +130,6 @@ class TestImageGetPixel(AccessTest):
return tuple(range(1, bands + 1))
def check(self, mode: str, expected_color_int: int | None = None) -> None:
if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes")
expected_color = (
self.color(mode) if expected_color_int is None else expected_color_int
)
@ -171,15 +154,14 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None)
assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error):
with pytest.raises(IndexError):
im.putpixel((0, 0), expected_color)
with pytest.raises(error):
with pytest.raises(IndexError):
im.getpixel((0, 0))
# Check negative index
with pytest.raises(error):
with pytest.raises(IndexError):
im.putpixel((-1, -1), expected_color)
with pytest.raises(error):
with pytest.raises(IndexError):
im.getpixel((-1, -1))
# Check initial color
@ -199,10 +181,10 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color)
with pytest.raises(error):
with pytest.raises(IndexError):
im.getpixel((0, 0))
# Check negative index
with pytest.raises(error):
with pytest.raises(IndexError):
im.getpixel((-1, -1))
@pytest.mark.parametrize("mode", Image.MODES)
@ -235,126 +217,7 @@ class TestImageGetPixel(AccessTest):
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffiPutPixel(TestImagePutPixel):
_need_cffi_access = True
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffiGetPixel(TestImageGetPixel):
_need_cffi_access = True
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffi(AccessTest):
_need_cffi_access = True
def _test_get_access(self, im: Image.Image) -> None:
"""Do we get the same thing as the old pixel access
Using private interfaces, forcing a capi access and
a pyaccess for the same image"""
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
w, h = im.size
for x in range(0, w, 10):
for y in range(0, h, 10):
assert access[(x, y)] == caccess[(x, y)]
# Access an out-of-range pixel
with pytest.raises(ValueError):
access[(access.xsize + 1, access.ysize + 1)]
def test_get_vs_c(self) -> None:
with pytest.warns(DeprecationWarning):
rgb = hopper("RGB")
rgb.load()
self._test_get_access(rgb)
for mode in ("RGBA", "L", "LA", "1", "P", "F"):
self._test_get_access(hopper(mode))
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
self._test_get_access(im)
def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image?
Using private interfaces, forcing a capi access and
a pyaccess for the same image"""
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
w, h = im.size
for x in range(0, w, 10):
for y in range(0, h, 10):
access[(x, y)] = color
assert color == caccess[(x, y)]
# Attempt to set the value on a read-only image
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, True)
assert access is not None
with pytest.raises(ValueError):
access[(0, 0)] = color
def test_set_vs_c(self) -> None:
rgb = hopper("RGB")
with pytest.warns(DeprecationWarning):
rgb.load()
self._test_set_access(rgb, (255, 128, 0))
self._test_set_access(hopper("RGBA"), (255, 192, 128, 0))
self._test_set_access(hopper("L"), 128)
self._test_set_access(hopper("LA"), (128, 128))
self._test_set_access(hopper("1"), 255)
self._test_set_access(hopper("P"), 128)
self._test_set_access(hopper("PA"), (128, 128))
self._test_set_access(hopper("F"), 1024.0)
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
self._test_set_access(im, 45000)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_not_implemented(self) -> None:
assert PyAccess.new(hopper("BGR;15")) is None
# Ref https://github.com/python-pillow/Pillow/pull/2009
def test_reference_counting(self) -> None:
size = 10
for _ in range(10):
# Do not save references to the image, only to the access object
with pytest.warns(DeprecationWarning):
px = Image.new("L", (size, 1), 0).load()
for i in range(size):
# Pixels can contain garbage if image is released
assert px[i, 0] == 0
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_p_putpixel_rgb_rgba(self, mode: str) -> None:
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1))
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
access.putpixel((0, 0), color)
if len(color) == 3:
color += (255,)
assert im.convert("RGBA").getpixel((0, 0)) == color
class TestImagePutPixelError(AccessTest):
class TestImagePutPixelError:
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"]
IMAGE_MODES2 = ["L", "I", "I;16"]
INVALID_TYPES = ["foo", 1.0, None]
@ -364,7 +227,7 @@ class TestImagePutPixelError(AccessTest):
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"):
im.putpixel((0, 0), v)
im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize(
("mode", "band_numbers", "match"),
@ -398,7 +261,7 @@ class TestImagePutPixelError(AccessTest):
with pytest.raises(
TypeError, match="color must be int or single-element tuple"
):
im.putpixel((0, 0), v)
im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
def test_putpixel_overflow_error(self, mode: str) -> None:

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING, Any
import pytest
from packaging.version import parse as parse_version
@ -13,13 +13,16 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
im = hopper().resize((128, 100))
if TYPE_CHECKING:
import numpy.typing as npt
def test_toarray() -> None:
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
ai = numpy.array(im.convert(mode))
return ai.shape, ai.dtype.str, ai.nbytes
def test_with_dtype(dtype) -> None:
def test_with_dtype(dtype: npt.DTypeLike) -> None:
ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype

View File

@ -222,8 +222,10 @@ def test_l_macro_rounding(convert_mode: str) -> None:
converted_im = im.convert(convert_mode)
px = converted_im.load()
assert px is not None
converted_color = px[0, 0]
if convert_mode == "LA":
assert isinstance(converted_color, tuple)
converted_color = converted_color[0]
assert converted_color == 1

View File

@ -16,7 +16,9 @@ def draft_roundtrip(
im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG")
im = fromstring(data)
mode, box = im.draft(req_mode, req_size)
result = im.draft(req_mode, req_size)
assert result is not None
box = result[1]
scale, _ = im.decoderconfig
assert box[:2] == (0, 0)
assert (im.width - scale) < box[2] <= im.width

View File

@ -137,7 +137,7 @@ def test_builtinfilter_p() -> None:
builtin_filter = ImageFilter.BuiltinFilter()
with pytest.raises(ValueError):
builtin_filter.filter(hopper("P"))
builtin_filter.filter(hopper("P").im)
def test_kernel_not_enough_coefficients() -> None:

View File

@ -54,17 +54,21 @@ def test_pack() -> None:
assert A is None
A = im.getcolors(maxcolors=3)
assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=4)
assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=8)
assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=16)
assert A is not None
A.sort()
assert A == expected

View File

@ -12,9 +12,10 @@ from .helper import hopper
def test_sanity() -> None:
im = hopper()
pix = im.load()
px = im.load()
assert pix[0, 0] == (20, 20, 70)
assert px is not None
assert px[0, 0] == (20, 20, 70)
def test_close() -> None:

View File

@ -14,6 +14,7 @@ class TestImagingPaste:
self, im: Image.Image, expected: list[tuple[int, int, int, int]]
) -> None:
px = im.load()
assert px is not None
actual = [
px[0, 0],
px[self.size // 2, 0],
@ -48,6 +49,7 @@ class TestImagingPaste:
def mask_1(self) -> Image.Image:
mask = Image.new("1", (self.size, self.size))
px = mask.load()
assert px is not None
for y in range(mask.height):
for x in range(mask.width):
px[y, x] = (x + y) % 2
@ -61,6 +63,7 @@ class TestImagingPaste:
def gradient_L(self) -> Image.Image:
gradient = Image.new("L", (self.size, self.size))
px = gradient.load()
assert px is not None
for y in range(gradient.height):
for x in range(gradient.width):
px[y, x] = (x + y) % 255
@ -338,3 +341,8 @@ class TestImagingPaste:
im.copy().paste(im2)
im.copy().paste(im2, (0, 0))
def test_incorrect_abbreviated_form(self) -> None:
im = Image.new("L", (1, 1))
with pytest.raises(ValueError):
im.paste(im, im, im)

View File

@ -61,4 +61,4 @@ def test_f_lut() -> None:
def test_f_mode() -> None:
im = hopper("F")
with pytest.raises(ValueError):
im.point(None)
im.point([])

View File

@ -31,7 +31,7 @@ def test_sanity() -> None:
def test_long_integers() -> None:
# see bug-200802-systemerror
def put(value: int) -> tuple[int, int, int, int]:
def put(value: int) -> float | tuple[int, ...] | None:
im = Image.new("RGBA", (1, 1))
im.putdata([value])
return im.getpixel((0, 0))
@ -113,13 +113,13 @@ def test_array_F() -> None:
def test_not_flattened() -> None:
im = Image.new("L", (1, 1))
with pytest.raises(TypeError):
im.putdata([[0]]) # type: ignore[list-item]
im.putdata([[0]])
with pytest.raises(TypeError):
im.putdata([[0]], 2) # type: ignore[list-item]
im.putdata([[0]], 2)
with pytest.raises(TypeError):
im = Image.new("I", (1, 1))
im.putdata([[0]]) # type: ignore[list-item]
im.putdata([[0]])
with pytest.raises(TypeError):
im = Image.new("F", (1, 1))
im.putdata([[0]]) # type: ignore[list-item]
im.putdata([[0]])

View File

@ -79,6 +79,7 @@ def test_putpalette_with_alpha_values() -> None:
(
("RGBA", (1, 2, 3, 4)),
("RGBAX", (1, 2, 3, 4, 0)),
("ARGB", (4, 1, 2, 3)),
),
)
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:

View File

@ -31,7 +31,9 @@ def test_libimagequant_quantize() -> None:
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15)
assert len(converted.getcolors()) == 100
colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_octree_quantize() -> None:
@ -39,7 +41,9 @@ def test_octree_quantize() -> None:
converted = image.quantize(100, Image.Quantize.FASTOCTREE)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 20)
assert len(converted.getcolors()) == 100
colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_rgba_quantize() -> None:
@ -80,6 +84,7 @@ def test_quantize_no_dither2() -> None:
assert tuple(quantized.palette.palette) == data
px = quantized.load()
assert px is not None
for x in range(9):
assert px[x, 0] == (0 if x < 5 else 1)
@ -118,10 +123,12 @@ def test_colors() -> None:
def test_transparent_colors_equal() -> None:
im = Image.new("RGBA", (1, 2), (0, 0, 0, 0))
px = im.load()
assert px is not None
px[0, 1] = (255, 255, 255, 0)
converted = im.quantize()
converted_px = converted.load()
assert converted_px is not None
assert converted_px[0, 0] == converted_px[0, 1]
@ -139,6 +146,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
converted = im.quantize(method=method)
converted_px = converted.load()
assert converted_px is not None
assert converted_px[0, 0] == converted.palette.colors[color]
@ -154,4 +162,6 @@ def test_small_palette() -> None:
im = im.quantize(palette=p)
# Assert
assert len(im.getcolors()) == 2
quantized_colors = im.getcolors()
assert quantized_colors is not None
assert len(quantized_colors) == 2

View File

@ -1,7 +1,7 @@
from __future__ import annotations
from collections.abc import Generator
from contextlib import contextmanager
from typing import Generator
import pytest
@ -74,6 +74,7 @@ class TestImagingCoreResampleAccuracy:
data = data.replace(" ", "")
sample = Image.new("L", size)
s_px = sample.load()
assert s_px is not None
w, h = size[0] // 2, size[1] // 2
for y in range(h):
for x in range(w):
@ -87,6 +88,8 @@ class TestImagingCoreResampleAccuracy:
def check_case(self, case: Image.Image, sample: Image.Image) -> None:
s_px = sample.load()
c_px = case.load()
assert s_px is not None
assert c_px is not None
for y in range(case.size[1]):
for x in range(case.size[0]):
if c_px[x, y] != s_px[x, y]:
@ -98,6 +101,7 @@ class TestImagingCoreResampleAccuracy:
def serialize_image(self, image: Image.Image) -> str:
s_px = image.load()
assert s_px is not None
return "\n".join(
" ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0]))
for y in range(image.size[1])
@ -233,13 +237,16 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency:
def make_case(
self, mode: str, fill: tuple[int, int, int] | float
) -> tuple[Image.Image, tuple[int, ...]]:
) -> tuple[Image.Image, float | tuple[int, ...]]:
im = Image.new(mode, (512, 9), fill)
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
px = im.load()
assert px is not None
return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0]
def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None:
def run_case(self, case: tuple[Image.Image, float | tuple[int, ...]]) -> None:
channel, color = case
px = channel.load()
assert px is not None
for x in range(channel.size[0]):
for y in range(channel.size[1]):
if px[x, y] != color:
@ -249,6 +256,7 @@ class TestCoreResampleConsistency:
def test_8u(self) -> None:
im, color = self.make_case("RGB", (0, 64, 255))
r, g, b = im.split()
assert isinstance(color, tuple)
self.run_case((r, color[0]))
self.run_case((g, color[1]))
self.run_case((b, color[2]))
@ -271,6 +279,7 @@ class TestCoreResampleAlphaCorrect:
def make_levels_case(self, mode: str) -> Image.Image:
i = Image.new(mode, (256, 16))
px = i.load()
assert px is not None
for y in range(i.size[1]):
for x in range(i.size[0]):
pix = [x] * len(mode)
@ -280,8 +289,13 @@ class TestCoreResampleAlphaCorrect:
def run_levels_case(self, i: Image.Image) -> None:
px = i.load()
assert px is not None
for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])}
used_colors = set()
for x in range(i.size[0]):
value = px[x, y]
assert isinstance(value, tuple)
used_colors.add(value[0])
assert 256 == len(used_colors), (
"All colors should be present in resized image. "
f"Only {len(used_colors)} on line {y}."
@ -310,6 +324,7 @@ class TestCoreResampleAlphaCorrect:
) -> Image.Image:
i = Image.new(mode, (64, 64), dirty_pixel)
px = i.load()
assert px is not None
xdiv4 = i.size[0] // 4
ydiv4 = i.size[1] // 4
for y in range(ydiv4 * 2):
@ -319,14 +334,16 @@ class TestCoreResampleAlphaCorrect:
def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None:
px = i.load()
assert px is not None
for y in range(i.size[1]):
for x in range(i.size[0]):
if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel:
value = px[x, y]
assert isinstance(value, tuple)
if value[-1] != 0 and value[:-1] != clean_pixel:
message = (
f"pixel at ({x}, {y}) is different:\n"
f"{px[x, y]}\n{clean_pixel}"
f"pixel at ({x}, {y}) is different:\n{value}\n{clean_pixel}"
)
assert px[x, y][:3] == clean_pixel, message
assert value[:3] == clean_pixel, message
def test_dirty_pixels_rgba(self) -> None:
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))
@ -406,6 +423,7 @@ class TestCoreResampleCoefficients:
draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color)
px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load()
assert px is not None
if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0]
@ -445,7 +463,7 @@ class TestCoreResampleBox:
im.resize((32, 32), resample, (20, 20, 100, 20))
with pytest.raises(TypeError, match="must be sequence of length 4"):
im.resize((32, 32), resample, (im.width, im.height))
im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
with pytest.raises(ValueError, match="can't be negative"):
im.resize((32, 32), resample, (-20, 20, 100, 100))

View File

@ -4,9 +4,9 @@ Tests for resize functionality.
from __future__ import annotations
from collections.abc import Generator
from itertools import permutations
from pathlib import Path
from typing import Generator
import pytest
@ -285,14 +285,14 @@ class TestReducingGapResize:
class TestImageResize:
def test_resize(self) -> None:
def resize(mode: str, size: tuple[int, int]) -> None:
def resize(mode: str, size: tuple[int, int] | list[int]) -> None:
out = hopper(mode).resize(size)
assert out.mode == mode
assert out.size == size
assert out.size == tuple(size)
for mode in "1", "P", "L", "RGB", "I", "F":
resize(mode, (112, 103))
resize(mode, (188, 214))
resize(mode, [188, 214])
# Test unknown resampling filter
with hopper() as im:

View File

@ -192,8 +192,9 @@ class TestImageTransform:
im = op(im, (40, 10))
colors = sorted(im.getcolors())
assert colors == sorted(
colors = im.getcolors()
assert colors is not None
assert sorted(colors) == sorted(
(
(20 * 10, opaque),
(20 * 10, transparent),

View File

@ -391,23 +391,25 @@ def test_overlay() -> None:
def test_logical() -> None:
def table(
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
) -> tuple[int, int, int, int]:
) -> list[float]:
out = []
for x in (a, b):
imx = Image.new("1", (1, 1), x)
for y in (a, b):
imy = Image.new("1", (1, 1), y)
out.append(op(imx, imy).getpixel((0, 0)))
return tuple(out)
value = op(imx, imy).getpixel((0, 0))
assert not isinstance(value, tuple) and value is not None
out.append(value)
return out
assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255)
assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255)
assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0)
assert table(ImageChops.logical_and, 0, 1) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 1) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 1) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255)
assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255)
assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0)
assert table(ImageChops.logical_and, 0, 128) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 128) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 128) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255)
assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255)
assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0)
assert table(ImageChops.logical_and, 0, 255) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 255) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 255) == [0, 255, 255, 0]

View File

@ -103,7 +103,7 @@ def test_sanity() -> None:
def test_flags() -> None:
assert ImageCms.Flags.NONE == 0
assert ImageCms.Flags.NONE.value == 0
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
@ -569,9 +569,9 @@ def assert_aux_channel_preserved(
for delta in nine_grid_deltas:
channel_data.paste(
channel_pattern,
tuple(
paste_offset[c] + delta[c] * channel_pattern.size[c]
for c in range(2)
(
paste_offset[0] + delta[0] * channel_pattern.size[0],
paste_offset[1] + delta[1] * channel_pattern.size[1],
),
)
chans.append(channel_data)
@ -642,7 +642,8 @@ def test_auxiliary_channels_isolated() -> None:
# convert with and without AUX data, test colors are equal
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
source_profile = ImageCms.createProfile(src_colorSpace)
destination_profile = ImageCms.createProfile(dst_format[1])
dst_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], dst_format[1])
destination_profile = ImageCms.createProfile(dst_colorSpace)
source_image = src_format[3]
test_transform = ImageCms.buildTransform(
source_profile,
@ -678,7 +679,8 @@ def test_auxiliary_channels_isolated() -> None:
def test_long_modes() -> None:
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
with pytest.warns(DeprecationWarning):
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
@ -689,7 +691,9 @@ def test_rgb_lab(mode: str) -> None:
im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode)
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
value = converted_im.getpixel((0, 0))
assert isinstance(value, tuple)
assert value[:3] == (0, 255, 255)
def test_deprecation() -> None:
@ -699,3 +703,9 @@ def test_deprecation() -> None:
assert ImageCms.VERSION == "1.0.0 pil"
with pytest.warns(DeprecationWarning):
assert isinstance(ImageCms.FLAGS, dict)
profile = ImageCmsProfile(ImageCms.createProfile("sRGB"))
with pytest.warns(DeprecationWarning):
ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB")
with pytest.warns(DeprecationWarning):
ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B")

View File

@ -1,8 +1,8 @@
from __future__ import annotations
import contextlib
import os.path
from typing import Sequence
from collections.abc import Sequence
from typing import Callable
import pytest
@ -448,6 +448,7 @@ def test_shape1() -> None:
x3, y3 = 95, 5
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -469,6 +470,7 @@ def test_shape2() -> None:
x3, y3 = 5, 95
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -487,6 +489,7 @@ def test_transform() -> None:
draw = ImageDraw.Draw(im)
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.line(0, 0)
s.transform((0, 0, 0, 0, 0, 0))
@ -631,6 +634,19 @@ def test_polygon(points: Coords) -> None:
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
@pytest.mark.parametrize("points", POINTS)
def test_polygon_width_I16(points: Coords) -> None:
# Arrange
im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.polygon(points, outline=0xFFFF, width=2)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon_width_I.tiff")
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("kite_points", KITE_POINTS)
def test_polygon_kite(
@ -913,7 +929,12 @@ def test_rounded_rectangle_translucent(
def test_floodfill(bbox: Coords) -> None:
red = ImageColor.getrgb("red")
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
mode_values: list[tuple[str, int | tuple[int, ...]]] = [
("L", 1),
("RGBA", (255, 0, 0, 0)),
("RGB", red),
]
for mode, value in mode_values:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@ -1401,25 +1422,44 @@ def test_default_font_size() -> None:
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def check(func: Callable[[], None]) -> None:
if freetype_support:
func()
else:
with pytest.raises(ImportError):
func()
def draw_text() -> None:
draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
check(draw_text)
def draw_textlength() -> None:
assert draw.textlength(text, font_size=16) == 216
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
check(draw_textlength)
def draw_textbbox() -> None:
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_textbbox)
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def draw_multiline_text() -> None:
draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
check(draw_multiline_text)
def draw_multiline_textbbox() -> None:
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_multiline_textbbox)
@pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox: Coords) -> None:
@ -1429,6 +1469,7 @@ def test_same_color_outline(bbox: Coords) -> None:
x2, y2 = 95, 50
x3, y3 = 95, 5
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -1467,7 +1508,7 @@ def test_same_color_outline(bbox: Coords) -> None:
(4, "square", {}),
(8, "regular_octagon", {}),
(4, "square_rotate_45", {"rotation": 45}),
(3, "triangle_width", {"width": 5, "outline": "yellow"}),
(3, "triangle_width", {"outline": "yellow", "width": 5}),
],
)
def test_draw_regular_polygon(
@ -1477,7 +1518,10 @@ def test_draw_regular_polygon(
filename = f"Tests/images/imagedraw_{polygon_name}.png"
draw = ImageDraw.Draw(im)
bounding_circle = ((W // 2, H // 2), 25)
draw.regular_polygon(bounding_circle, n_sides, fill="red", **args)
rotation = int(args.get("rotation", 0))
outline = args.get("outline")
width = int(args.get("width", 1))
draw.regular_polygon(bounding_circle, n_sides, rotation, "red", outline, width)
assert_image_equal_tofile(im, filename)
@ -1569,7 +1613,7 @@ def test_compute_regular_polygon_vertices_input_error_handling(
error_message: str,
) -> None:
with pytest.raises(expected_error) as e:
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
assert str(e.value) == error_message
@ -1630,6 +1674,6 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
draw.rounded_rectangle(xy)
def test_getdraw():
def test_getdraw() -> None:
with pytest.warns(DeprecationWarning):
ImageDraw.getdraw(None, [])

View File

@ -51,9 +51,18 @@ def test_sanity() -> None:
pen = ImageDraw2.Pen("blue", width=7)
draw.line(list(range(10)), pen)
draw, handler = ImageDraw.getdraw(im)
draw2, handler = ImageDraw.getdraw(im)
assert draw2 is not None
pen = ImageDraw2.Pen("blue", width=7)
draw.line(list(range(10)), pen)
draw2.line(list(range(10)), pen)
def test_mode() -> None:
draw = ImageDraw2.Draw("L", (1, 1))
assert draw.image.mode == "L"
with pytest.raises(ValueError):
ImageDraw2.Draw("L")
@pytest.mark.parametrize("bbox", BBOX)

View File

@ -209,7 +209,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args)
def decode(self, buffer):
def decode(self, buffer: bytes) -> tuple[int, int]:
# eof
return -1, 0
@ -222,7 +222,7 @@ class MockPyEncoder(ImageFile.PyEncoder):
super().__init__(mode, *args)
def encode(self, buffer):
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
return 1, 1, b""
def cleanup(self) -> None:
@ -305,7 +305,7 @@ class TestPyDecoder(CodecsTest):
def test_decode(self) -> None:
decoder = ImageFile.PyDecoder(None)
with pytest.raises(NotImplementedError):
decoder.decode(None)
decoder.decode(b"")
class TestPyEncoder(CodecsTest):
@ -351,7 +351,9 @@ class TestPyEncoder(CodecsTest):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
)
assert MockPyEncoder.last.cleanup_called
last: MockPyEncoder | None = MockPyEncoder.last
assert last
assert last.cleanup_called
with pytest.raises(ValueError):
ImageFile._save(
@ -381,7 +383,7 @@ class TestPyEncoder(CodecsTest):
def test_encode(self) -> None:
encoder = ImageFile.PyEncoder(None)
with pytest.raises(NotImplementedError):
encoder.encode(None)
encoder.encode(0)
bytes_consumed, errcode = encoder.encode_to_pyfd()
assert bytes_consumed == 0

View File

@ -209,7 +209,7 @@ def test_getlength(
assert length == length_raqm
def test_float_size() -> None:
def test_float_size(layout_engine: ImageFont.Layout) -> None:
lengths = []
for size in (48, 48.5, 49):
f = ImageFont.truetype(
@ -494,8 +494,8 @@ def test_default_font() -> None:
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
@pytest.mark.parametrize("mode", ("", "1", "RGBA"))
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
assert (0, 4, 12, 16) == font.getbbox("A", mode)
@ -548,7 +548,7 @@ def test_find_font(
def loadable_font(
filepath: str, size: int, index: int, encoding: str, *args: Any
):
) -> ImageFont.FreeTypeFont:
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
if filepath == path_to_fake:
return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
@ -564,6 +564,7 @@ def test_find_font(
# catching syntax like errors
monkeypatch.setattr(sys, "platform", platform)
if platform == "linux":
monkeypatch.setenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
@ -1096,6 +1097,23 @@ def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
imagefont.getmask("A" * 1_000_001)
def test_bytes(font: ImageFont.FreeTypeFont) -> None:
assert font.getlength(b"test") == font.getlength("test")
assert font.getbbox(b"test") == font.getbbox("test")
assert_image_equal(
Image.Image()._new(font.getmask(b"test")),
Image.Image()._new(font.getmask("test")),
)
assert_image_equal(
Image.Image()._new(font.getmask2(b"test")[0]),
Image.Image()._new(font.getmask2("test")[0]),
)
assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
@pytest.mark.parametrize(
"test_file",
[

View File

@ -9,51 +9,57 @@ from PIL import Image, ImageDraw, ImageFont, _util, features
from .helper import assert_image_equal_tofile
original_core = ImageFont.core
fonts = [ImageFont.load_default_imagefont()]
if not features.check_module("freetype2"):
default_font = ImageFont.load_default()
if isinstance(default_font, ImageFont.ImageFont):
fonts.append(default_font)
def setup_module() -> None:
if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError)
def teardown_module() -> None:
ImageFont.core = original_core
def test_default_font() -> None:
@pytest.mark.parametrize("font", fonts)
def test_default_font(font: ImageFont.ImageFont) -> None:
# Arrange
txt = 'This is a "better than nothing" default font.'
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
# Act
default_font = ImageFont.load_default()
draw.text((10, 10), txt, font=default_font)
draw.text((10, 10), txt, font=font)
# Assert
assert_image_equal_tofile(im, "Tests/images/default_font.png")
def test_size_without_freetype() -> None:
with pytest.raises(ImportError):
ImageFont.load_default(size=14)
def test_without_freetype() -> None:
original_core = ImageFont.core
if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try:
with pytest.raises(ImportError):
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
assert isinstance(ImageFont.load_default(), ImageFont.ImageFont)
with pytest.raises(ImportError):
ImageFont.load_default(size=14)
finally:
ImageFont.core = original_core
def test_unicode() -> None:
@pytest.mark.parametrize("font", fonts)
def test_unicode(font: ImageFont.ImageFont) -> None:
# should not segfault, should return UnicodeDecodeError
# issue #2826
font = ImageFont.load_default()
with pytest.raises(UnicodeEncodeError):
font.getbbox("")
def test_textbbox() -> None:
@pytest.mark.parametrize("font", fonts)
def test_textbbox(font: ImageFont.ImageFont) -> None:
im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im)
default_font = ImageFont.load_default()
assert d.textlength("test", font=default_font) == 24
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
assert d.textlength("test", font=font) == 24
assert d.textbbox((0, 0), "test", font=font) == (0, 0, 24, 11)
def test_decompression_bomb() -> None:

View File

@ -60,6 +60,8 @@ class TestImageGrab:
def test_grabclipboard(self) -> None:
if sys.platform == "darwin":
subprocess.call(["screencapture", "-cx"])
ImageGrab.grabclipboard()
elif sys.platform == "win32":
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
p.stdin.write(
@ -69,6 +71,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200
[Windows.Forms.Clipboard]::SetImage($bmp)"""
)
p.communicate()
ImageGrab.grabclipboard()
else:
if not shutil.which("wl-paste") and not shutil.which("xclip"):
with pytest.raises(
@ -77,9 +81,6 @@ $bmp = New-Object Drawing.Bitmap 200, 200
r" ImageGrab.grabclipboard\(\) on Linux",
):
ImageGrab.grabclipboard()
return
ImageGrab.grabclipboard()
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None:
@ -89,6 +90,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
p.communicate()
im = ImageGrab.grabclipboard()
assert isinstance(im, list)
assert len(im) == 1
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
@ -105,6 +107,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
p.communicate()
im = ImageGrab.grabclipboard()
assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, "Tests/images/hopper.png")
@pytest.mark.skipif(
@ -120,6 +123,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp)
im = ImageGrab.grabclipboard()
assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, image_path)
@pytest.mark.skipif(

View File

@ -41,11 +41,15 @@ A = string_to_img(
def img_to_string(im: Image.Image) -> str:
"""Turn a (small) binary image into a string representation"""
chars = ".1"
width, height = im.size
return "\n".join(
"".join(chars[im.getpixel((c, r)) > 0] for c in range(width))
for r in range(height)
)
result = []
for r in range(im.height):
line = ""
for c in range(im.width):
value = im.getpixel((c, r))
assert not isinstance(value, tuple) and value is not None
line += chars[value > 0]
result.append(line)
return "\n".join(result)
def img_string_normalize(im: str) -> str:

View File

@ -165,10 +165,14 @@ def test_pad() -> None:
def test_pad_round() -> None:
im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 1))
assert new_im.load()[2, 0] == 1
px = new_im.load()
assert px is not None
assert px[2, 0] == 1
new_im = ImageOps.pad(im, (1, 4))
assert new_im.load()[0, 2] == 1
px = new_im.load()
assert px is not None
assert px[0, 2] == 1
@pytest.mark.parametrize("mode", ("P", "PA"))
@ -223,6 +227,7 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
else:
left, top, right, bottom = border
px = im_expanded.convert("RGB").load()
assert px is not None
for x in range(im_expanded.width):
for b in range(top):
assert px[x, b] == (255, 0, 0)
@ -254,20 +259,26 @@ def test_colorize_2color() -> None:
left = (0, 1)
middle = (127, 1)
right = (255, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left),
value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(middle),
value,
(127, 63, 0),
threshold=1,
msg="mid test pixel incorrect",
)
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(right),
value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@ -290,20 +301,26 @@ def test_colorize_2color_offset() -> None:
left = (25, 1)
middle = (75, 1)
right = (125, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left),
value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(middle),
value,
(127, 63, 0),
threshold=1,
msg="mid test pixel incorrect",
)
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(right),
value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@ -334,29 +351,37 @@ def test_colorize_3color_offset() -> None:
middle = (100, 1)
right_middle = (150, 1)
right = (225, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left),
value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
value = im_test.getpixel(left_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left_middle),
value,
(127, 0, 127),
threshold=1,
msg="low-mid test pixel incorrect",
)
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect")
value = im_test.getpixel(right_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect"
)
assert_tuple_approx_equal(
im_test.getpixel(right_middle),
value,
(0, 63, 127),
threshold=1,
msg="high-mid test pixel incorrect",
)
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(right),
value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@ -432,6 +457,17 @@ def test_exif_transpose() -> None:
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_xml_without_xmp() -> None:
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
assert im.getexif()[0x0112] == 3
assert "XML:com.adobe.xmp" in im.info
del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_in_place() -> None:
with Image.open("Tests/images/orientation_rectangle.jpg") as im:
assert im.size == (2, 1)

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Generator
from collections.abc import Generator
import pytest
@ -101,7 +101,7 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
assert i.im.getpixel((x, y))[c] >= 250
# Fuzzy match.
def gp(x, y):
def gp(x: int, y: int) -> tuple[int, ...]:
return i.im.getpixel((x, y))
assert 236 <= gp(7, 4)[0] <= 239

View File

@ -45,7 +45,7 @@ def test_getcolor() -> None:
# Test unknown color specifier
with pytest.raises(ValueError):
palette.getcolor("unknown")
palette.getcolor("unknown") # type: ignore[arg-type]
def test_getcolor_rgba_color_rgb_palette() -> None:
@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None:
palette.save(f)
p = ImagePalette.load(f)
lut = ImagePalette.load(f)
# load returns raw palette information
assert len(p[0]) == 768
assert p[1] == "RGB"
assert len(lut[0]) == 768
assert lut[1] == "RGB"
p = ImagePalette.raw(p[1], p[0])
p = ImagePalette.raw(lut[1], lut[0])
assert isinstance(p, ImagePalette.ImagePalette)
assert p.palette == palette.tobytes()

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import array
import math
import struct
from typing import Sequence
from collections.abc import Sequence
import pytest

View File

@ -41,18 +41,13 @@ def test_rgb() -> None:
checkrgb(0, 0, 255)
def test_image() -> None:
modes = ["1", "RGB", "RGBA", "L", "P"]
qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage
if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
modes.append("I;16")
for mode in modes:
im = hopper(mode)
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
if mode not in ("RGB", "RGBA"):
im = im.convert("RGB")
assert_image_similar(roundtripped_im, im, 1)
@pytest.mark.parametrize("mode", ("1", "RGB", "RGBA", "L", "P", "I;16"))
def test_image(mode: str) -> None:
im = hopper(mode)
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
if mode not in ("RGB", "RGBA"):
im = im.convert("RGB")
assert_image_similar(roundtripped_im, im, 1)
def test_closed_file() -> None:

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import os
from typing import Any
import pytest
@ -15,8 +16,11 @@ def test_sanity() -> None:
def test_register() -> None:
# Test registering a viewer that is not a class
ImageShow.register("not a class")
# Test registering a viewer that is an instance
class TestViewer(ImageShow.Viewer):
pass
ImageShow.register(TestViewer())
# Restore original state
ImageShow._viewers.pop()
@ -65,6 +69,27 @@ def test_show_without_viewers() -> None:
ImageShow._viewers = viewers
@pytest.mark.parametrize(
"viewer",
(
ImageShow.Viewer(),
ImageShow.WindowsViewer(),
ImageShow.MacViewer(),
ImageShow.XDGViewer(),
ImageShow.DisplayViewer(),
ImageShow.GmDisplayViewer(),
ImageShow.EogViewer(),
ImageShow.XVViewer(),
ImageShow.IPythonViewer(),
),
)
def test_show_file(viewer: ImageShow.Viewer) -> None:
assert not os.path.exists("missing.png")
with pytest.raises(FileNotFoundError):
viewer.show_file("missing.png")
def test_viewer() -> None:
viewer = ImageShow.Viewer()

View File

@ -45,10 +45,12 @@ def test_kw() -> None:
# Test "file"
im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im1)
# Test "data"
im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im2)
# Test no relevant entry
@ -70,6 +72,11 @@ def test_photoimage(mode: str) -> None:
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA"))
with pytest.raises(ValueError):
ImageTk.PhotoImage()
with pytest.raises(ValueError):
ImageTk.PhotoImage(mode)
def test_photoimage_apply_transparency() -> None:
with Image.open("Tests/images/pil123p.png") as im:

View File

@ -45,21 +45,22 @@ if is_win32():
memcpy = ctypes.cdll.msvcrt.memcpy
memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC
windll = getattr(ctypes, "windll")
CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC]
CreateCompatibleDC.restype = ctypes.wintypes.HDC
DeleteDC = ctypes.windll.gdi32.DeleteDC
DeleteDC = windll.gdi32.DeleteDC
DeleteDC.argtypes = [ctypes.wintypes.HDC]
SelectObject = ctypes.windll.gdi32.SelectObject
SelectObject = windll.gdi32.SelectObject
SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ]
SelectObject.restype = ctypes.wintypes.HGDIOBJ
DeleteObject = ctypes.windll.gdi32.DeleteObject
DeleteObject = windll.gdi32.DeleteObject
DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ]
CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection
CreateDIBSection = windll.gdi32.CreateDIBSection
CreateDIBSection.argtypes = [
ctypes.wintypes.HDC,
ctypes.c_void_p,

View File

@ -16,6 +16,8 @@ def verify(im1: Image.Image) -> None:
assert im1.size == im2.size
pix1 = im1.load()
pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
for y in range(im1.size[1]):
for x in range(im1.size[0]):
xy = x, y

View File

@ -1,17 +1,17 @@
from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
import pytest
from PIL import Image
from PIL import Image, _typing
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
if TYPE_CHECKING:
import numpy
import numpy.typing
import numpy.typing as npt
else:
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
@ -19,9 +19,7 @@ TEST_IMAGE_SIZE = (10, 10)
def test_numpy_to_image() -> None:
def to_image(
dtype: numpy.typing.DTypeLike, bands: int = 1, boolean: int = 0
) -> Image.Image:
def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
if bands == 1:
if boolean:
data = [0, 255] * 50
@ -106,13 +104,12 @@ def test_1d_array() -> None:
assert_image(Image.fromarray(a), "L", (1, 5))
def _test_img_equals_nparray(
img: Image.Image, np_img: numpy.typing.NDArray[Any]
) -> None:
def _test_img_equals_nparray(img: Image.Image, np_img: _typing.NumpyArray) -> None:
assert len(np_img.shape) >= 2
np_size = np_img.shape[1], np_img.shape[0]
assert img.size == np_size
px = img.load()
assert px is not None
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
assert_deep_equal(px[x, y], np_img[y, x])
@ -145,6 +142,7 @@ def test_save_tiff_uint16() -> None:
img = Image.fromarray(a)
img_px = img.load()
assert img_px is not None
assert img_px[0, 0] == pixel_value
@ -166,7 +164,7 @@ def test_save_tiff_uint16() -> None:
("HSV", numpy.uint8),
),
)
def test_to_array(mode: str, dtype: numpy.typing.DTypeLike) -> None:
def test_to_array(mode: str, dtype: npt.DTypeLike) -> None:
img = hopper(mode)
# Resize to non-square
@ -200,6 +198,15 @@ def test_putdata() -> None:
assert len(im.getdata()) == len(arr)
def test_resize() -> None:
im = hopper()
size = (64, 64)
im_resized = im.resize(numpy.array(size))
assert im_resized.size == size
@pytest.mark.parametrize(
"dtype",
(
@ -216,7 +223,7 @@ def test_putdata() -> None:
numpy.float64,
),
)
def test_roundtrip_eye(dtype: numpy.typing.DTypeLike) -> None:
def test_roundtrip_eye(dtype: npt.DTypeLike) -> None:
arr = numpy.eye(10, dtype=dtype)
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))

View File

@ -54,16 +54,12 @@ def test_stdout(buffer: bool) -> None:
# Temporarily redirect stdout
old_stdout = sys.stdout
if buffer:
class MyStdOut:
buffer = BytesIO()
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
sys.stdout = mystdout
sys.stdout = mystdout # type: ignore[assignment]
ps = PSDraw.PSDraw()
_create_document(ps)
@ -71,6 +67,6 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout
sys.stdout = old_stdout
if buffer:
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""

View File

@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import hopper, skip_unless_feature
def _test_equal(num, denom, target) -> None:
def _test_equal(
num: float | Fraction | IFDRational,
denom: int,
target: float | Fraction | IFDRational,
) -> None:
t = IFDRational(num, denom)
assert target == t

View File

@ -17,6 +17,5 @@ coverage:
# Matches 'omit:' in .coveragerc
ignore:
- "Tests/32bit_segfault_check.py"
- "Tests/bench_cffi_access.py"
- "Tests/check_*.py"
- "Tests/createfontdatachunk.py"

View File

@ -338,7 +338,7 @@ linkcheck_allowed_redirects = {
# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
_repo = "https://github.com/python-pillow/Pillow/"
extlinks = {
"cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"),
"cve": ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s"),
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
"issue": (_repo + "issues/%s", "#%s"),
"pr": (_repo + "pull/%s", "#%s"),

View File

@ -12,28 +12,6 @@ Deprecated features
Below are features which are considered deprecated. Where appropriate,
a :py:exc:`DeprecationWarning` is issued.
PSFile
~~~~~~
.. deprecated:: 9.5.0
The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will
be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
be used internally, so there is no replacement. If you need this functionality
though, it is a very short class that can easily be recreated in your own code.
PyAccess and Image.USE_CFFI_ACCESS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 10.0.0
Since Pillow's C API is now faster than PyAccess on PyPy,
:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow
11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
similarly deprecated.
ImageFile.raise_oserror
~~~~~~~~~~~~~~~~~~~~~~~
@ -107,6 +85,15 @@ BGR;15, BGR 16 and BGR;24
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
Non-image modes in ImageCms
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0
The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow
image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped
is also deprecated.
Support for LibTIFF earlier than 4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -128,6 +115,29 @@ Removed features
Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed.
PSFile
~~~~~~
.. deprecated:: 9.5.0
.. versionremoved:: 11.0.0
The :py:class:`!PSFile` class was removed in Pillow 11 (2024-10-15).
This class was only made as a helper to be used internally,
so there is no replacement. If you need this functionality though,
it is a very short class that can easily be recreated in your own code.
PyAccess and Image.USE_CFFI_ACCESS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 10.0.0
.. versionremoved:: 11.0.0
Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been
removed. Pillow's C API will now be used on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was
similarly removed.
Tk/Tcl 8.4
~~~~~~~~~~
@ -286,7 +296,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
offset.
.. image:: ./example/size_vs_bbox.png
.. image:: ./example/size_vs_bbox.webp
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
:align: center

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -26,5 +26,5 @@ if __name__ == "__main__":
d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
if y != 0:
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
im.save("docs/example/anchors.png")
im.save("docs/example/anchors.webp")
im.show()

BIN
docs/example/anchors.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

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