Merge branch 'main' into xmp

This commit is contained in:
Andrew Murray 2024-06-05 15:53:46 +10:00 committed by GitHub
commit eba2694498
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 464 additions and 288 deletions

View File

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

View File

@ -46,6 +46,7 @@ jobs:
- cp310
- cp311
- cp312
- cp313
spec:
- manylinux2014
- manylinux_2_28
@ -80,6 +81,7 @@ jobs:
CIBW_ARCHS: "aarch64"
# Likewise, select only one Python version per job to speed this up.
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
CIBW_PRERELEASE_PYTHONS: True
# Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
@ -133,6 +135,7 @@ jobs:
CIBW_BUILD: ${{ matrix.build }}
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 }}
@ -204,6 +207,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
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

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.3
rev: v0.4.7
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
@ -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.4
rev: v18.1.5
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.2
rev: 0.28.4
hooks:
- id: check-github-workflows
- id: check-readthedocs
@ -67,7 +67,7 @@ repos:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.16
rev: v0.18
hooks:
- id: validate-pyproject

View File

@ -5,6 +5,15 @@ Changelog (Pillow)
10.4.0 (unreleased)
-------------------
- Added ImageDraw circle() #8085
[void4, hugovk, radarhere]
- Add mypy target to Makefile #8077
[Yay295]
- Added more modes to Image.MODES #7984
[radarhere]
- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978
[radarhere, hugovk]

View File

@ -118,3 +118,8 @@ lint-fix:
python3 -m black .
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m ruff --fix .
.PHONY: mypy
mypy:
python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
python3 -m tox -e mypy

View File

@ -174,12 +174,13 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator:
if not features.check(feature):
version = features.version(feature)
if version is None:
return pytest.mark.skip(f"{feature} not available")
if reason is None:
reason = f"{feature} is older than {required}"
version_required = parse_version(required)
version_available = parse_version(features.version(feature))
version_available = parse_version(version)
return pytest.mark.skipif(version_available < version_required, reason=reason)
@ -189,12 +190,13 @@ def mark_if_feature_version(
version_blacklist: str,
reason: str | None = None,
) -> pytest.MarkDecorator:
if not features.check(feature):
version = features.version(feature)
if version is None:
return pytest.mark.pil_noop_mark()
if reason is None:
reason = f"{feature} is {version_blacklist}"
version_required = parse_version(version_blacklist)
version_available = parse_version(features.version(feature))
version_available = parse_version(version)
if (
version_available.major == version_required.major
and version_available.minor == version_required.minor
@ -220,16 +222,11 @@ class PillowLeakTestCase:
from resource import RUSAGE_SELF, getrusage
mem = getrusage(RUSAGE_SELF).ru_maxrss
if sys.platform == "darwin":
# man 2 getrusage:
# ru_maxrss
# This is the maximum resident set size utilized (in bytes).
return mem / 1024 # Kb
# linux
# man 2 getrusage
# ru_maxrss (since Linux 2.6.32)
# This is the maximum resident set size used (in kilobytes).
return mem # Kb
# man 2 getrusage:
# ru_maxrss
# This is the maximum resident set size utilized
# in bytes on macOS, in kilobytes on Linux
return mem / 1024 if sys.platform == "darwin" else mem
def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage()

View File

@ -12,8 +12,9 @@ from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"):
pytest.skip("Fuzzer is linux only", allow_module_level=True)
if features.check("libjpeg_turbo"):
version = packaging.version.parse(features.version("libjpeg_turbo"))
libjpeg_turbo_version = features.version("libjpeg_turbo")
if libjpeg_turbo_version is not None:
version = packaging.version.parse(libjpeg_turbo_version)
if version.major == 2 and version.minor == 0:
pytestmark = pytest.mark.valgrind_known_error(
reason="Known failing with libjpeg_turbo 2.0"

View File

@ -30,7 +30,7 @@ def test_version() -> None:
# Check the correctness of the convenience function
# and the format of version numbers
def test(name: str, function: Callable[[str], bool]) -> None:
def test(name: str, function: Callable[[str], str | None]) -> None:
version = features.version(name)
if not features.check(name):
assert version is None
@ -67,12 +67,16 @@ def test_webp_anim() -> None:
@skip_unless_feature("libjpeg_turbo")
def test_libjpeg_turbo_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo"))
version = features.version("libjpeg_turbo")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@skip_unless_feature("libimagequant")
def test_libimagequant_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
version = features.version("libimagequant")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@pytest.mark.parametrize("feature", features.modules)

View File

@ -1252,10 +1252,11 @@ def test_palette_save_L(tmp_path: Path) -> None:
im = hopper("P")
im_l = Image.frombytes("L", im.size, im.tobytes())
palette = bytes(im.getpalette())
palette = im.getpalette()
assert palette is not None
out = str(tmp_path / "temp.gif")
im_l.save(out, palette=palette)
im_l.save(out, palette=bytes(palette))
with Image.open(out) as reloaded:
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))

View File

@ -70,7 +70,9 @@ class TestFileJpeg:
def test_sanity(self) -> None:
# internal version number
assert re.search(r"\d+\.\d+$", features.version_codec("jpg"))
version = features.version_codec("jpg")
assert version is not None
assert re.search(r"\d+\.\d+$", version)
with Image.open(TEST_FILE) as im:
im.load()
@ -152,7 +154,7 @@ class TestFileJpeg:
assert k > 0.9
def test_rgb(self) -> None:
def getchannels(im: Image.Image) -> tuple[int, int, int]:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]:
return tuple(v[0] for v in im.layer)
im = hopper()
@ -441,7 +443,7 @@ class TestFileJpeg:
assert_image(im1, im2.mode, im2.size)
def test_subsampling(self) -> None:
def getsampling(im: Image.Image):
def getsampling(im: JpegImagePlugin.JpegImageFile):
layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]

View File

@ -48,7 +48,9 @@ def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
def test_sanity() -> None:
# Internal version number
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000"))
version = features.version_codec("jpg_2000")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
with Image.open("Tests/images/test-card-lossless.jp2") as im:
px = im.load()

View File

@ -52,7 +52,9 @@ class LibTiffTestCase:
class TestFileLibTiff(LibTiffTestCase):
def test_version(self) -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff"))
version = features.version_codec("libtiff")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
def test_g4_tiff(self, tmp_path: Path) -> None:
"""Test the ordinary file path load path"""
@ -666,7 +668,8 @@ class TestFileLibTiff(LibTiffTestCase):
pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0)
assert_image_similar_tofile(pilim, buffer_io, 0)
with Image.open(buffer_io) as saved_im:
assert_image_similar(pilim, saved_im, 0)
save_bytesio()
save_bytesio("raw")

View File

@ -85,9 +85,9 @@ class TestFilePng:
def test_sanity(self, tmp_path: Path) -> None:
# internal version number
assert re.search(
r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib")
)
version = features.version_codec("zlib")
assert version is not None
assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version)
test_file = str(tmp_path / "temp.png")

View File

@ -49,7 +49,9 @@ class TestFileWebp:
def test_version(self) -> None:
_webp.WebPDecoderVersion()
_webp.WebPDecoderBuggyAlpha()
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp"))
version = features.version_module("webp")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
def test_read_rgb(self) -> None:
"""

View File

@ -52,8 +52,9 @@ def test_write_animation_L(tmp_path: Path) -> None:
assert_image_similar(im, orig.convert("RGBA"), 32.9)
if is_big_endian():
webp = parse_version(features.version_module("webp"))
if webp < parse_version("1.2.2"):
version = features.version_module("webp")
assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
orig.seek(orig.n_frames - 1)
im.seek(im.n_frames - 1)
@ -78,8 +79,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
# Compare second frame to original
if is_big_endian():
webp = parse_version(features.version_module("webp"))
if webp < parse_version("1.2.2"):
version = features.version_module("webp")
assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
im.seek(1)
im.load()

View File

@ -12,7 +12,7 @@ class TestTTypeFontLeak(PillowLeakTestCase):
iterations = 10
mem_limit = 4096 # k
def _test_font(self, font: ImageFont.FreeTypeFont) -> None:
def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None:
im = Image.new("RGB", (255, 255), "white")
draw = ImageDraw.ImageDraw(im)
self._test_leak(

View File

@ -25,6 +25,7 @@ from PIL import (
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
assert_image_similar_tofile,
assert_not_all_same,
hopper,
@ -99,10 +100,18 @@ class TestImage:
JPGFILE = "Tests/images/hopper.jpg"
with pytest.raises(TypeError):
with Image.open(PNGFILE, formats=123):
with Image.open(PNGFILE, formats=123): # type: ignore[arg-type]
pass
for formats in [["JPEG"], ("JPEG",), ["jpeg"], ["Jpeg"], ["jPeG"], ["JpEg"]]:
format_list: list[list[str] | tuple[str, ...]] = [
["JPEG"],
("JPEG",),
["jpeg"],
["Jpeg"],
["jPeG"],
["JpEg"],
]
for formats in format_list:
with pytest.raises(UnidentifiedImageError):
with Image.open(PNGFILE, formats=formats):
pass
@ -138,7 +147,7 @@ class TestImage:
def test_bad_mode(self) -> None:
with pytest.raises(ValueError):
with Image.open("filename", "bad mode"):
with Image.open("filename", "bad mode"): # type: ignore[arg-type]
pass
def test_stringio(self) -> None:
@ -185,7 +194,8 @@ class TestImage:
with tempfile.TemporaryFile() as fp:
im.save(fp, "JPEG")
fp.seek(0)
assert_image_similar_tofile(im, fp, 20)
with Image.open(fp) as reloaded:
assert_image_similar(im, reloaded, 20)
def test_unknown_extension(self, tmp_path: Path) -> None:
im = hopper()
@ -497,9 +507,11 @@ class TestImage:
def test_check_size(self) -> None:
# Checking that the _check_size function throws value errors when we want it to
with pytest.raises(ValueError):
Image.new("RGB", 0) # not a tuple
# not a tuple
Image.new("RGB", 0) # type: ignore[arg-type]
with pytest.raises(ValueError):
Image.new("RGB", (0,)) # Tuple too short
# tuple too short
Image.new("RGB", (0,)) # type: ignore[arg-type]
with pytest.raises(ValueError):
Image.new("RGB", (-1, -1)) # w,h < 0

View File

@ -86,8 +86,8 @@ def test_fromarray() -> None:
assert test("RGBX") == ("RGBA", (128, 100), True)
# Test mode is None with no "typestr" in the array interface
wrapped = Wrapper(hopper("L"), {"shape": (100, 128)})
with pytest.raises(TypeError):
wrapped = Wrapper(test("L"), {"shape": (100, 128)})
Image.fromarray(wrapped)

View File

@ -18,7 +18,7 @@ def test_crop(mode: str) -> None:
def test_wide_crop() -> None:
def crop(*bbox: int) -> tuple[int, ...]:
def crop(bbox: tuple[int, int, int, int]) -> tuple[int, ...]:
i = im.crop(bbox)
h = i.histogram()
while h and not h[-1]:
@ -27,23 +27,23 @@ def test_wide_crop() -> None:
im = Image.new("L", (100, 100), 1)
assert crop(0, 0, 100, 100) == (0, 10000)
assert crop(25, 25, 75, 75) == (0, 2500)
assert crop((0, 0, 100, 100)) == (0, 10000)
assert crop((25, 25, 75, 75)) == (0, 2500)
# sides
assert crop(-25, 0, 25, 50) == (1250, 1250)
assert crop(0, -25, 50, 25) == (1250, 1250)
assert crop(75, 0, 125, 50) == (1250, 1250)
assert crop(0, 75, 50, 125) == (1250, 1250)
assert crop((-25, 0, 25, 50)) == (1250, 1250)
assert crop((0, -25, 50, 25)) == (1250, 1250)
assert crop((75, 0, 125, 50)) == (1250, 1250)
assert crop((0, 75, 50, 125)) == (1250, 1250)
assert crop(-25, 25, 125, 75) == (2500, 5000)
assert crop(25, -25, 75, 125) == (2500, 5000)
assert crop((-25, 25, 125, 75)) == (2500, 5000)
assert crop((25, -25, 75, 125)) == (2500, 5000)
# corners
assert crop(-25, -25, 25, 25) == (1875, 625)
assert crop(75, -25, 125, 25) == (1875, 625)
assert crop(75, 75, 125, 125) == (1875, 625)
assert crop(-25, 75, 25, 125) == (1875, 625)
assert crop((-25, -25, 25, 25)) == (1875, 625)
assert crop((75, -25, 125, 25)) == (1875, 625)
assert crop((75, 75, 125, 125)) == (1875, 625)
assert crop((-25, 75, 25, 125)) == (1875, 625)
@pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2)))

View File

@ -46,9 +46,9 @@ def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None:
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity_error(mode: str) -> None:
im = hopper(mode)
with pytest.raises(TypeError):
im = hopper(mode)
im.filter("hello")
im.filter("hello") # type: ignore[arg-type]
# crashes on small images

View File

@ -6,7 +6,7 @@ from .helper import hopper
def test_extrema() -> None:
def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]:
def extrema(mode: str) -> tuple[float, float] | tuple[tuple[int, int], ...]:
return hopper(mode).getextrema()
assert extrema("1") == (0, 255)

View File

@ -24,8 +24,9 @@ def test_sanity() -> None:
def test_libimagequant_quantize() -> None:
image = hopper()
if is_ppc64le():
libimagequant = parse_version(features.version_feature("libimagequant"))
if libimagequant < parse_version("4"):
version = features.version_feature("libimagequant")
assert version is not None
if parse_version(version) < parse_version("4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P"

View File

@ -102,7 +102,7 @@ def test_unsupported_modes(mode: str) -> None:
def get_image(mode: str) -> Image.Image:
mode_info = ImageMode.getmode(mode)
if mode_info.basetype == "L":
bands = [gradients_image]
bands: list[Image.Image] = [gradients_image]
for _ in mode_info.bands[1:]:
# rotate previous image
band = bands[-1].transpose(Image.Transpose.ROTATE_90)

View File

@ -7,7 +7,7 @@ import shutil
import sys
from io import BytesIO
from pathlib import Path
from typing import Any
from typing import Any, Literal, cast
import pytest
@ -60,10 +60,13 @@ def test_sanity() -> None:
assert list(map(type, v)) == [str, str, str, str]
# internal version number
assert re.search(r"\d+\.\d+(\.\d+)?$", features.version_module("littlecms2"))
version = features.version_module("littlecms2")
assert version is not None
assert re.search(r"\d+\.\d+(\.\d+)?$", version)
skip_missing()
i = ImageCms.profileToProfile(hopper(), SRGB, SRGB)
assert i is not None
assert_image(i, "RGB", (128, 128))
i = hopper()
@ -72,23 +75,27 @@ def test_sanity() -> None:
t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB")
i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert_image(i, "RGB", (128, 128))
with hopper() as i:
t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB")
ImageCms.applyTransform(hopper(), t, inPlace=True)
assert i is not None
assert_image(i, "RGB", (128, 128))
p = ImageCms.createProfile("sRGB")
o = ImageCms.getOpenProfile(SRGB)
t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB")
i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert_image(i, "RGB", (128, 128))
t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB")
assert t.inputMode == "RGB"
assert t.outputMode == "RGB"
i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert_image(i, "RGB", (128, 128))
# test PointTransform convenience API
@ -202,13 +209,13 @@ def test_exceptions() -> None:
ImageCms.buildTransform("foo", "bar", "RGB", "RGB")
with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"):
ImageCms.getProfileName(None)
ImageCms.getProfileName(None) # type: ignore[arg-type]
skip_missing()
# Python <= 3.9: "an integer is required (got type NoneType)"
# Python > 3.9: "'NoneType' object cannot be interpreted as an integer"
with pytest.raises(ImageCms.PyCMSError, match="integer"):
ImageCms.isIntentSupported(SRGB, None, None)
ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type]
def test_display_profile() -> None:
@ -232,7 +239,7 @@ def test_unsupported_color_space() -> None:
"Color space not supported for on-the-fly profile creation (unsupported)"
),
):
ImageCms.createProfile("unsupported")
ImageCms.createProfile("unsupported") # type: ignore[arg-type]
def test_invalid_color_temperature() -> None:
@ -240,7 +247,7 @@ def test_invalid_color_temperature() -> None:
ImageCms.PyCMSError,
match='Color temperature must be numeric, "invalid" not valid',
):
ImageCms.createProfile("LAB", "invalid")
ImageCms.createProfile("LAB", "invalid") # type: ignore[arg-type]
@pytest.mark.parametrize("flag", ("my string", -1))
@ -249,7 +256,7 @@ def test_invalid_flag(flag: str | int) -> None:
with pytest.raises(
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
):
ImageCms.profileToProfile(im, "foo", "bar", flags=flag)
ImageCms.profileToProfile(im, "foo", "bar", flags=flag) # type: ignore[arg-type]
def test_simple_lab() -> None:
@ -260,7 +267,7 @@ def test_simple_lab() -> None:
t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB")
i_lab = ImageCms.applyTransform(i, t)
assert i_lab is not None
assert i_lab.mode == "LAB"
k = i_lab.getpixel((0, 0))
@ -284,6 +291,7 @@ def test_lab_color() -> None:
# Need to add a type mapping for some PIL type to TYPE_Lab_8 in findLCMSType, and
# have that mapping work back to a PIL mode (likely RGB).
i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert_image(i, "LAB", (128, 128))
# i.save('temp.lab.tif') # visually verified vs PS.
@ -298,6 +306,7 @@ def test_lab_srgb() -> None:
with Image.open("Tests/images/hopper.Lab.tif") as img:
img_srgb = ImageCms.applyTransform(img, t)
assert img_srgb is not None
# img_srgb.save('temp.srgb.tif') # visually verified vs ps.
@ -317,11 +326,11 @@ def test_lab_roundtrip() -> None:
t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert i.info["icc_profile"] == ImageCmsProfile(pLab).tobytes()
out = ImageCms.applyTransform(i, t2)
assert out is not None
assert_image_similar(hopper(), out, 2)
@ -343,7 +352,7 @@ def test_extended_information() -> None:
p = o.profile
def assert_truncated_tuple_equal(
tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10
tup1: tuple[Any, ...] | None, tup2: tuple[Any, ...], digits: int = 10
) -> None:
# Helper function to reduce precision of tuples of floats
# recursively and then check equality.
@ -359,6 +368,7 @@ def test_extended_information() -> None:
for val in tuple_value
)
assert tup1 is not None
assert truncate_tuple(tup1) == truncate_tuple(tup2)
assert p.attributes == 4294967296
@ -504,22 +514,22 @@ def test_non_ascii_path(tmp_path: Path) -> None:
def test_profile_typesafety() -> None:
# does not segfault
with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(0).tobytes()
ImageCms.ImageCmsProfile(0) # type: ignore[arg-type]
with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(1).tobytes()
ImageCms.ImageCmsProfile(1) # type: ignore[arg-type]
# also check core function
with pytest.raises(TypeError):
ImageCms.core.profile_tobytes(0)
ImageCms.core.profile_tobytes(0) # type: ignore[arg-type]
with pytest.raises(TypeError):
ImageCms.core.profile_tobytes(1)
ImageCms.core.profile_tobytes(1) # type: ignore[arg-type]
if not is_pypy():
# core profile should not be directly instantiable
with pytest.raises(TypeError):
ImageCms.core.CmsProfile()
with pytest.raises(TypeError):
ImageCms.core.CmsProfile(0)
ImageCms.core.CmsProfile(0) # type: ignore[call-arg]
@pytest.mark.skipif(is_pypy(), reason="fails on PyPy")
@ -528,7 +538,7 @@ def test_transform_typesafety() -> None:
with pytest.raises(TypeError):
ImageCms.core.CmsTransform()
with pytest.raises(TypeError):
ImageCms.core.CmsTransform(0)
ImageCms.core.CmsTransform(0) # type: ignore[call-arg]
def assert_aux_channel_preserved(
@ -578,11 +588,13 @@ def assert_aux_channel_preserved(
)
# apply transform
result_image: Image.Image | None
if transform_in_place:
ImageCms.applyTransform(source_image, t, inPlace=True)
result_image = source_image
else:
result_image = ImageCms.applyTransform(source_image, t, inPlace=False)
assert result_image is not None
result_image_aux = result_image.getchannel(preserved_channel)
assert_image_equal(source_image_aux, result_image_aux)
@ -628,7 +640,8 @@ def test_auxiliary_channels_isolated() -> None:
continue
# convert with and without AUX data, test colors are equal
source_profile = ImageCms.createProfile(src_format[1])
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
source_profile = ImageCms.createProfile(src_colorSpace)
destination_profile = ImageCms.createProfile(dst_format[1])
source_image = src_format[3]
test_transform = ImageCms.buildTransform(
@ -639,6 +652,7 @@ def test_auxiliary_channels_isolated() -> None:
)
# test conversion from aux-ful source
test_image: Image.Image | None
if transform_in_place:
test_image = source_image.copy()
ImageCms.applyTransform(test_image, test_transform, inPlace=True)
@ -646,6 +660,7 @@ def test_auxiliary_channels_isolated() -> None:
test_image = ImageCms.applyTransform(
source_image, test_transform, inPlace=False
)
assert test_image is not None
# reference conversion from aux-less source
reference_transform = ImageCms.buildTransform(
@ -657,7 +672,7 @@ def test_auxiliary_channels_isolated() -> None:
reference_image = ImageCms.applyTransform(
source_image.convert(src_format[2]), reference_transform
)
assert reference_image is not None
assert_image_equal(test_image.convert(dst_format[2]), reference_image)

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import contextlib
import os.path
from typing import Sequence
import pytest
@ -265,6 +266,21 @@ def test_chord_too_fat() -> None:
assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png")
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("xy", ((W / 2, H / 2), [W / 2, H / 2]))
def test_circle(mode: str, xy: Sequence[float]) -> None:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
expected = f"Tests/images/imagedraw_ellipse_{mode}.png"
# Act
draw.circle(xy, 25, fill="green", outline="blue")
# Assert
assert_image_similar_tofile(im, expected, 1)
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(mode: str, bbox: Coords) -> None:
@ -1067,8 +1083,8 @@ def test_line_horizontal() -> None:
)
@pytest.mark.xfail(reason="failing test")
def test_line_h_s1_w2() -> None:
pytest.skip("failing")
img, draw = create_base_image_draw((20, 20))
draw.line((5, 5, 14, 6), BLACK, 2)
assert_image_equal_tofile(

View File

@ -202,6 +202,8 @@ class TestImageFile:
class MockPyDecoder(ImageFile.PyDecoder):
last: MockPyDecoder
def __init__(self, mode: str, *args: Any) -> None:
MockPyDecoder.last = self
@ -213,6 +215,8 @@ class MockPyDecoder(ImageFile.PyDecoder):
class MockPyEncoder(ImageFile.PyEncoder):
last: MockPyEncoder | None
def __init__(self, mode: str, *args: Any) -> None:
MockPyEncoder.last = self
@ -315,6 +319,7 @@ class TestPyEncoder(CodecsTest):
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
)
assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == xoff
assert MockPyEncoder.last.state.yoff == yoff
assert MockPyEncoder.last.state.xsize == xsize
@ -329,6 +334,7 @@ class TestPyEncoder(CodecsTest):
fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == 0
assert MockPyEncoder.last.state.yoff == 0
assert MockPyEncoder.last.state.xsize == 200

View File

@ -34,7 +34,9 @@ pytestmark = skip_unless_feature("freetype2")
def test_sanity() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2"))
version = features.version_module("freetype2")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@pytest.fixture(
@ -547,11 +549,10 @@ def test_find_font(
def loadable_font(
filepath: str, size: int, index: int, encoding: str, *args: Any
):
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
if filepath == path_to_fake:
return ImageFont._FreeTypeFont(
FONT_PATH, size, index, encoding, *args
)
return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args)
return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
return _freeTypeFont(filepath, size, index, encoding, *args)
m.setattr(ImageFont, "FreeTypeFont", loadable_font)
font = ImageFont.truetype(fontname)
@ -630,7 +631,9 @@ def test_complex_font_settings() -> None:
def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2"))
version = features.version_module("freetype2")
assert version is not None
freetype = parse_version(version)
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
font.get_variation_names()
@ -700,7 +703,9 @@ def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None
def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2"))
version = features.version_module("freetype2")
assert version is not None
freetype = parse_version(version)
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
font.set_variation_by_name("Bold")
@ -725,7 +730,9 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2"))
version = features.version_module("freetype2")
assert version is not None
freetype = parse_version(version)
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
font.set_variation_by_axes([100])

View File

@ -4,11 +4,11 @@ from typing import Generator
import pytest
from PIL import Image, ImageFilter
from PIL import Image, ImageFile, ImageFilter
@pytest.fixture
def test_images() -> Generator[dict[str, Image.Image], None, None]:
def test_images() -> Generator[dict[str, ImageFile.ImageFile], None, None]:
ims = {
"im": Image.open("Tests/images/hopper.ppm"),
"snakes": Image.open("Tests/images/color_snakes.png"),
@ -20,7 +20,7 @@ def test_images() -> Generator[dict[str, Image.Image], None, None]:
im.close()
def test_filter_api(test_images: dict[str, Image.Image]) -> None:
def test_filter_api(test_images: dict[str, ImageFile.ImageFile]) -> None:
im = test_images["im"]
test_filter = ImageFilter.GaussianBlur(2.0)
@ -34,7 +34,7 @@ def test_filter_api(test_images: dict[str, Image.Image]) -> None:
assert i.size == (128, 128)
def test_usm_formats(test_images: dict[str, Image.Image]) -> None:
def test_usm_formats(test_images: dict[str, ImageFile.ImageFile]) -> None:
im = test_images["im"]
usm = ImageFilter.UnsharpMask
@ -52,7 +52,7 @@ def test_usm_formats(test_images: dict[str, Image.Image]) -> None:
im.convert("YCbCr").filter(usm)
def test_blur_formats(test_images: dict[str, Image.Image]) -> None:
def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None:
im = test_images["im"]
blur = ImageFilter.GaussianBlur
@ -70,7 +70,7 @@ def test_blur_formats(test_images: dict[str, Image.Image]) -> None:
im.convert("YCbCr").filter(blur)
def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None:
def test_usm_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
snakes = test_images["snakes"]
src = snakes.convert("RGB")
@ -79,7 +79,7 @@ def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None:
assert i.tobytes() == src.tobytes()
def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None:
def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
snakes = test_images["snakes"]
i = snakes.filter(ImageFilter.GaussianBlur(0.4))

View File

@ -25,10 +25,10 @@ def test_sanity() -> None:
st.stddev
with pytest.raises(AttributeError):
st.spam()
st.spam() # type: ignore[attr-defined]
with pytest.raises(TypeError):
ImageStat.Stat(1)
ImageStat.Stat(1) # type: ignore[arg-type]
def test_hopper() -> None:

View File

@ -227,6 +227,18 @@ Methods
.. versionadded:: 5.3.0
.. py:method:: ImageDraw.circle(xy, radius, fill=None, outline=None, width=1)
Draws a circle with a given radius centering on a point.
.. versionadded:: 10.4.0
:param xy: The point for the center of the circle, e.g. ``(x, y)``.
:param radius: Radius of the circle.
:param outline: Color to use for the outline.
:param fill: Color to use for the fill.
:param width: The line width, in pixels.
.. py:method:: ImageDraw.ellipse(xy, fill=None, outline=None, width=1)
Draws an ellipse inside the given bounding box.

View File

@ -57,6 +57,10 @@ Classes
:undoc-members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.StubHandler()
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.StubImageFile()
:members:
:show-inheritance:

View File

@ -45,6 +45,13 @@ TODO
API Additions
=============
ImageDraw.circle
^^^^^^^^^^^^^^^^
Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functionality as
:py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it
takes a center point and radius.
TODO
^^^^
@ -53,7 +60,9 @@ TODO
Other Changes
=============
TODO
^^^^
Python 3.13 beta
^^^^^^^^^^^^^^^^
TODO
To help others prepare for Python 3.13, wheels have been built against the 3.13 beta as
a preview. This is not official support for Python 3.13, but simply an opportunity for
users to test how Pillow works with the beta and report any problems.

View File

@ -35,6 +35,7 @@ import os
import struct
from enum import IntEnum
from io import BytesIO
from typing import IO
from . import Image, ImageFile
@ -55,7 +56,7 @@ class AlphaEncoding(IntEnum):
DXT5 = 7
def unpack_565(i):
def unpack_565(i: int) -> tuple[int, int, int]:
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
@ -284,7 +285,8 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
raise OSError(msg) from e
return -1, 0
def _read_blp_header(self):
def _read_blp_header(self) -> None:
assert self.fd is not None
self.fd.seek(4)
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
@ -303,10 +305,10 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length):
def _safe_read(self, length: int) -> bytes:
return ImageFile._safe_read(self.fd, length)
def _read_palette(self):
def _read_palette(self) -> list[tuple[int, int, int, int]]:
ret = []
for i in range(256):
try:
@ -349,29 +351,30 @@ class BLP1Decoder(_BLPBaseDecoder):
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
raise BLPFormatError(msg)
def _decode_jpeg_stream(self):
def _decode_jpeg_stream(self) -> None:
from .JpegImagePlugin import JpegImageFile
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size)
assert self.fd is not None
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._blp_lengths[0])
data = jpeg_header + data
data = BytesIO(data)
image = JpegImageFile(data)
image = JpegImageFile(BytesIO(data))
Image._decompression_bomb_check(image.size)
if image.mode == "CMYK":
decoder_name, extents, offset, args = image.tile[0]
image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
r, g, b = image.convert("RGB").split()
image = Image.merge("RGB", (b, g, r))
self.set_as_raw(image.tobytes())
reversed_image = Image.merge("RGB", (b, g, r))
self.set_as_raw(reversed_image.tobytes())
class BLP2Decoder(_BLPBaseDecoder):
def _load(self):
def _load(self) -> None:
palette = self._read_palette()
assert self.fd is not None
self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1:
@ -446,7 +449,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data
def _save(im, fp, filename):
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if im.mode != "P":
msg = "Unsupported BLP image mode"
raise ValueError(msg)

View File

@ -15,7 +15,7 @@ from . import Image, ImageFile
_handler = None
def register_handler(handler):
def register_handler(handler: ImageFile.StubHandler) -> None:
"""
Install application-specific BUFR image handler.
@ -54,7 +54,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
if loader:
loader.open(self)
def _load(self):
def _load(self) -> ImageFile.StubHandler | None:
return _handler

View File

@ -42,7 +42,7 @@ class DcxImageFile(PcxImageFile):
format_description = "Intel DCX"
_close_exclusive_fp_after_loading = False
def _open(self):
def _open(self) -> None:
# Header
s = self.fp.read(4)
if not _accept(s):
@ -58,7 +58,7 @@ class DcxImageFile(PcxImageFile):
self._offset.append(offset)
self._fp = self.fp
self.frame = None
self.frame = -1
self.n_frames = len(self._offset)
self.is_animated = self.n_frames > 1
self.seek(0)

View File

@ -228,7 +228,7 @@ class EpsImageFile(ImageFile.ImageFile):
reading_trailer_comments = False
trailer_reached = False
def check_required_header_comments():
def check_required_header_comments() -> None:
if "PS-Adobe" not in self.info:
msg = 'EPS header missing "%!PS-Adobe" comment'
raise SyntaxError(msg)

View File

@ -70,7 +70,7 @@ class FpxImageFile(ImageFile.ImageFile):
self._open_index(1)
def _open_index(self, index=1):
def _open_index(self, index: int = 1) -> None:
#
# get the Image Contents Property Set
@ -85,7 +85,7 @@ class FpxImageFile(ImageFile.ImageFile):
size = max(self.size)
i = 1
while size > 64:
size = size / 2
size = size // 2
i += 1
self.maxid = i - 1
@ -118,7 +118,7 @@ class FpxImageFile(ImageFile.ImageFile):
self._open_subimage(1, self.maxid)
def _open_subimage(self, index=1, subimage=0):
def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
#
# setup tile descriptors for a given subimage

View File

@ -30,6 +30,7 @@ import math
import os
import subprocess
from enum import IntEnum
from functools import cached_property
from . import (
Image,
@ -112,8 +113,7 @@ class GifImageFile(ImageFile.ImageFile):
self._fp = self.fp # FIXME: hack
self.__rewind = self.fp.tell()
self._n_frames = None
self._is_animated = None
self._n_frames: int | None = None
self._seek(0) # get ready to read first frame
@property
@ -128,24 +128,23 @@ class GifImageFile(ImageFile.ImageFile):
self.seek(current)
return self._n_frames
@property
def is_animated(self):
if self._is_animated is None:
if self._n_frames is not None:
self._is_animated = self._n_frames != 1
else:
current = self.tell()
if current:
self._is_animated = True
else:
try:
self._seek(1, False)
self._is_animated = True
except EOFError:
self._is_animated = False
@cached_property
def is_animated(self) -> bool:
if self._n_frames is not None:
return self._n_frames != 1
self.seek(current)
return self._is_animated
current = self.tell()
if current:
return True
try:
self._seek(1, False)
is_animated = True
except EOFError:
is_animated = False
self.seek(current)
return is_animated
def seek(self, frame: int) -> None:
if not self._seek_check(frame):

View File

@ -16,6 +16,7 @@
from __future__ import annotations
import re
from typing import IO
from ._binary import o8
@ -25,8 +26,8 @@ class GimpPaletteFile:
rawmode = "RGB"
def __init__(self, fp):
self.palette = [o8(i) * 3 for i in range(256)]
def __init__(self, fp: IO[bytes]) -> None:
palette = [o8(i) * 3 for i in range(256)]
if fp.readline()[:12] != b"GIMP Palette":
msg = "not a GIMP palette file"
@ -49,9 +50,9 @@ class GimpPaletteFile:
msg = "bad palette entry"
raise ValueError(msg)
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
self.palette = b"".join(self.palette)
self.palette = b"".join(palette)
def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode

View File

@ -15,7 +15,7 @@ from . import Image, ImageFile
_handler = None
def register_handler(handler):
def register_handler(handler: ImageFile.StubHandler) -> None:
"""
Install application-specific GRIB image handler.
@ -54,7 +54,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
if loader:
loader.open(self)
def _load(self):
def _load(self) -> ImageFile.StubHandler | None:
return _handler

View File

@ -10,12 +10,14 @@
#
from __future__ import annotations
from typing import IO
from . import Image, ImageFile
_handler = None
def register_handler(handler):
def register_handler(handler: ImageFile.StubHandler) -> None:
"""
Install application-specific HDF5 image handler.
@ -54,11 +56,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
if loader:
loader.open(self)
def _load(self):
def _load(self) -> ImageFile.StubHandler | None:
return _handler
def _save(im, fp, filename):
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if _handler is None or not hasattr(_handler, "save"):
msg = "HDF5 save handler not installed"
raise OSError(msg)

View File

@ -1519,7 +1519,7 @@ class Image:
self._exif._loaded = False
self.getexif()
def get_child_images(self):
def get_child_images(self) -> list[ImageFile.ImageFile]:
child_images = []
exif = self.getexif()
ifds = []
@ -1543,10 +1543,7 @@ class Image:
fp = self.fp
thumbnail_offset = ifd.get(513)
if thumbnail_offset is not None:
try:
thumbnail_offset += self._exif_offset
except AttributeError:
pass
thumbnail_offset += getattr(self, "_exif_offset", 0)
self.fp.seek(thumbnail_offset)
data = self.fp.read(ifd.get(514))
fp = io.BytesIO(data)
@ -1612,7 +1609,7 @@ class Image:
or "transparency" in self.info
)
def apply_transparency(self):
def apply_transparency(self) -> None:
"""
If a P mode image has a "transparency" key in the info dictionary,
remove the key and instead apply the transparency to the palette.
@ -1624,6 +1621,7 @@ class Image:
from . import ImagePalette
palette = self.getpalette("RGBA")
assert palette is not None
transparency = self.info["transparency"]
if isinstance(transparency, bytes):
for i, alpha in enumerate(transparency):
@ -1956,7 +1954,9 @@ class Image:
self.im.putband(alpha.im, band)
def putdata(self, data, scale=1.0, offset=0.0):
def putdata(
self, data: Sequence[float], scale: float = 1.0, offset: float = 0.0
) -> None:
"""
Copies pixel data from a flattened sequence object into the image. The
values should start at the upper left corner (0, 0), continue to the

View File

@ -754,7 +754,7 @@ def applyTransform(
def createProfile(
colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = -1
colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0
) -> core.CmsProfile:
"""
(pyCMS) Creates a profile.
@ -777,7 +777,7 @@ def createProfile(
:param colorSpace: String, the color space of the profile you wish to
create.
Currently only "LAB", "XYZ", and "sRGB" are supported.
:param colorTemp: Positive integer for the white point for the profile, in
:param colorTemp: Positive number for the white point for the profile, in
degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50
illuminant if omitted (5000k). colorTemp is ONLY applied to LAB
profiles, and is ignored for XYZ and sRGB.
@ -1089,7 +1089,7 @@ def isIntentSupported(
raise PyCMSError(v) from v
def versions() -> tuple[str, str, str, str]:
def versions() -> tuple[str, str | None, str, str]:
"""
(pyCMS) Fetches versions.
"""

View File

@ -181,6 +181,13 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0:
self.draw.draw_ellipse(xy, ink, 0, width)
def circle(
self, xy: Sequence[float], radius: float, fill=None, outline=None, width=1
) -> None:
"""Draw a circle given center coordinates and a radius."""
ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius)
self.ellipse(ellipse_xy, fill, outline, width)
def line(self, xy: Coords, fill=None, width=0, joint=None) -> None:
"""Draw a line, or a connected sequence of line segments."""
ink = self._getink(fill)[0]
@ -901,7 +908,13 @@ def getdraw(im=None, hints=None):
return im, handler
def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
def floodfill(
image: Image.Image,
xy: tuple[int, int],
value: float | tuple[int, ...],
border: float | tuple[int, ...] | None = None,
thresh: float = 0,
) -> None:
"""
(experimental) Fills a bounded region with a given color.

View File

@ -23,7 +23,10 @@ from . import Image, ImageFilter, ImageStat
class _Enhance:
def enhance(self, factor):
image: Image.Image
degenerate: Image.Image
def enhance(self, factor: float) -> Image.Image:
"""
Returns an enhanced image.
@ -46,7 +49,7 @@ class Color(_Enhance):
the original image.
"""
def __init__(self, image):
def __init__(self, image: Image.Image) -> None:
self.image = image
self.intermediate_mode = "L"
if "A" in image.getbands():
@ -63,7 +66,7 @@ class Contrast(_Enhance):
gives a solid gray image. A factor of 1.0 gives the original image.
"""
def __init__(self, image):
def __init__(self, image: Image.Image) -> None:
self.image = image
mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5)
self.degenerate = Image.new("L", image.size, mean).convert(image.mode)
@ -80,7 +83,7 @@ class Brightness(_Enhance):
original image.
"""
def __init__(self, image):
def __init__(self, image: Image.Image) -> None:
self.image = image
self.degenerate = Image.new(image.mode, image.size, 0)
@ -96,7 +99,7 @@ class Sharpness(_Enhance):
original image, and a factor of 2.0 gives a sharpened image.
"""
def __init__(self, image):
def __init__(self, image: Image.Image) -> None:
self.image = image
self.degenerate = image.filter(ImageFilter.SMOOTH)

View File

@ -28,6 +28,7 @@
#
from __future__ import annotations
import abc
import io
import itertools
import struct
@ -347,6 +348,15 @@ class ImageFile(Image.Image):
return self.tell() != frame
class StubHandler:
def open(self, im: StubImageFile) -> None:
pass
@abc.abstractmethod
def load(self, im: StubImageFile) -> Image.Image:
pass
class StubImageFile(ImageFile):
"""
Base class for stub image loaders.

View File

@ -18,6 +18,8 @@ from __future__ import annotations
import abc
import functools
from types import ModuleType
from typing import Any, Sequence
class Filter:
@ -56,7 +58,13 @@ class Kernel(BuiltinFilter):
name = "Kernel"
def __init__(self, size, kernel, scale=None, offset=0):
def __init__(
self,
size: tuple[int, int],
kernel: Sequence[float],
scale: float | None = None,
offset: float = 0,
) -> None:
if scale is None:
# default scale is sum of kernel
scale = functools.reduce(lambda a, b: a + b, kernel)
@ -79,7 +87,7 @@ class RankFilter(Filter):
name = "Rank"
def __init__(self, size, rank):
def __init__(self, size: int, rank: int) -> None:
self.size = size
self.rank = rank
@ -101,7 +109,7 @@ class MedianFilter(RankFilter):
name = "Median"
def __init__(self, size=3):
def __init__(self, size: int = 3) -> None:
self.size = size
self.rank = size * size // 2
@ -116,7 +124,7 @@ class MinFilter(RankFilter):
name = "Min"
def __init__(self, size=3):
def __init__(self, size: int = 3) -> None:
self.size = size
self.rank = 0
@ -131,7 +139,7 @@ class MaxFilter(RankFilter):
name = "Max"
def __init__(self, size=3):
def __init__(self, size: int = 3) -> None:
self.size = size
self.rank = size * size - 1
@ -147,7 +155,7 @@ class ModeFilter(Filter):
name = "Mode"
def __init__(self, size=3):
def __init__(self, size: int = 3) -> None:
self.size = size
def filter(self, image):
@ -165,7 +173,7 @@ class GaussianBlur(MultibandFilter):
name = "GaussianBlur"
def __init__(self, radius=2):
def __init__(self, radius: float | Sequence[float] = 2) -> None:
self.radius = radius
def filter(self, image):
@ -193,10 +201,8 @@ class BoxBlur(MultibandFilter):
name = "BoxBlur"
def __init__(self, radius):
xy = radius
if not isinstance(xy, (tuple, list)):
xy = (xy, xy)
def __init__(self, radius: float | Sequence[float]) -> None:
xy = radius if isinstance(radius, (tuple, list)) else (radius, radius)
if xy[0] < 0 or xy[1] < 0:
msg = "radius must be >= 0"
raise ValueError(msg)
@ -228,7 +234,9 @@ class UnsharpMask(MultibandFilter):
name = "UnsharpMask"
def __init__(self, radius=2, percent=150, threshold=3):
def __init__(
self, radius: float = 2, percent: int = 150, threshold: int = 3
) -> None:
self.radius = radius
self.percent = percent
self.threshold = threshold
@ -378,7 +386,9 @@ class Color3DLUT(MultibandFilter):
name = "Color 3D LUT"
def __init__(self, size, table, channels=3, target_mode=None, **kwargs):
def __init__(
self, size, table, channels: int = 3, target_mode: str | None = None, **kwargs
):
if channels not in (3, 4):
msg = "Only 3 or 4 output channels are supported"
raise ValueError(msg)
@ -392,7 +402,7 @@ class Color3DLUT(MultibandFilter):
items = size[0] * size[1] * size[2]
wrong_size = False
numpy = None
numpy: ModuleType | None = None
if hasattr(table, "shape"):
try:
import numpy
@ -439,7 +449,7 @@ class Color3DLUT(MultibandFilter):
self.table = table
@staticmethod
def _check_size(size):
def _check_size(size: Any) -> list[int]:
try:
_, _, _ = size
except ValueError as e:

View File

@ -160,10 +160,6 @@ class ImageFont:
.. versionadded:: 9.2.0
:param text: Text to render.
:param mode: Used by some graphics drivers to indicate what mode the
driver prefers; if empty, the renderer may return either
mode. Note that the mode is always a string, to simplify
C-level implementations.
:return: ``(left, top, right, bottom)`` bounding box
"""
@ -261,7 +257,7 @@ class FreeTypeFont:
"""
return self.font.family, self.font.style
def getmetrics(self):
def getmetrics(self) -> tuple[int, int]:
"""
:return: A tuple of the font ascent (the distance from the baseline to
the highest outline point) and descent (the distance from the
@ -628,7 +624,7 @@ class FreeTypeFont:
layout_engine=layout_engine or self.layout_engine,
)
def get_variation_names(self):
def get_variation_names(self) -> list[bytes]:
"""
:returns: A list of the named styles in a variation font.
:exception OSError: If the font is not a variation font.

View File

@ -200,7 +200,7 @@ class MorphOp:
elif patterns is not None:
self.lut = LutBuilder(patterns=patterns).build_lut()
def apply(self, image: Image.Image):
def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
"""Run a single morphological operation on an image
Returns a tuple of the number of changed pixels and the
@ -216,7 +216,7 @@ class MorphOp:
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage
def match(self, image: Image.Image):
def match(self, image: Image.Image) -> list[tuple[int, int]]:
"""Get a list of coordinates matching the morphological operation on
an image.
@ -231,7 +231,7 @@ class MorphOp:
raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id)
def get_on_pixels(self, image: Image.Image):
def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
"""Get a list of all turned on pixels in a binary image
Returns a list of tuples of (x,y) coordinates

View File

@ -18,7 +18,7 @@
from __future__ import annotations
import array
from typing import Sequence
from typing import IO, Sequence
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
@ -166,7 +166,7 @@ class ImagePalette:
msg = f"unknown color specifier: {repr(color)}"
raise ValueError(msg)
def save(self, fp):
def save(self, fp: str | IO[str]) -> None:
"""Save palette to text file.
.. warning:: This method is experimental.
@ -213,29 +213,29 @@ def make_linear_lut(black, white):
raise NotImplementedError(msg) # FIXME
def make_gamma_lut(exp):
def make_gamma_lut(exp: float) -> list[int]:
return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
def negative(mode="RGB"):
def negative(mode: str = "RGB") -> ImagePalette:
palette = list(range(256 * len(mode)))
palette.reverse()
return ImagePalette(mode, [i // len(mode) for i in palette])
def random(mode="RGB"):
def random(mode: str = "RGB") -> ImagePalette:
from random import randint
palette = [randint(0, 255) for _ in range(256 * len(mode))]
return ImagePalette(mode, palette)
def sepia(white="#fff0c0"):
def sepia(white: str = "#fff0c0") -> ImagePalette:
bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
def wedge(mode="RGB"):
def wedge(mode: str = "RGB") -> ImagePalette:
palette = list(range(256 * len(mode)))
return ImagePalette(mode, [i // len(mode) for i in palette])

View File

@ -28,10 +28,10 @@ class HDC:
methods.
"""
def __init__(self, dc):
def __init__(self, dc: int) -> None:
self.dc = dc
def __int__(self):
def __int__(self) -> int:
return self.dc
@ -42,10 +42,10 @@ class HWND:
methods, instead of a DC.
"""
def __init__(self, wnd):
def __init__(self, wnd: int) -> None:
self.wnd = wnd
def __int__(self):
def __int__(self) -> int:
return self.wnd
@ -149,7 +149,9 @@ class Dib:
result = self.image.query_palette(handle)
return result
def paste(self, im, box=None):
def paste(
self, im: Image.Image, box: tuple[int, int, int, int] | None = None
) -> None:
"""
Paste a PIL image into the bitmap image.
@ -169,16 +171,16 @@ class Dib:
else:
self.image.paste(im.im)
def frombytes(self, buffer):
def frombytes(self, buffer: bytes) -> None:
"""
Load display memory contents from byte data.
:param buffer: A buffer containing display data (usually
data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`)
"""
return self.image.frombytes(buffer)
self.image.frombytes(buffer)
def tobytes(self):
def tobytes(self) -> bytes:
"""
Copy display memory contents to bytes object.
@ -190,7 +192,9 @@ class Dib:
class Window:
"""Create a Window with the given title size."""
def __init__(self, title="PIL", width=None, height=None):
def __init__(
self, title: str = "PIL", width: int | None = None, height: int | None = None
) -> None:
self.hwnd = Image.core.createwindow(
title, self.__dispatcher, width or 0, height or 0
)

View File

@ -34,7 +34,7 @@ class BoxReader:
self.length = length
self.remaining_in_box = -1
def _can_read(self, num_bytes):
def _can_read(self, num_bytes: int) -> bool:
if self.has_length and self.fp.tell() + num_bytes > self.length:
# Outside box: ensure we don't read past the known file length
return False
@ -44,7 +44,7 @@ class BoxReader:
else:
return True # No length known, just read
def _read_bytes(self, num_bytes):
def _read_bytes(self, num_bytes: int) -> bytes:
if not self._can_read(num_bytes):
msg = "Not enough data in header"
raise SyntaxError(msg)
@ -74,7 +74,7 @@ class BoxReader:
else:
return True
def next_box_type(self):
def next_box_type(self) -> bytes:
# Skip the rest of the box if it has not been read
if self.remaining_in_box > 0:
self.fp.seek(self.remaining_in_box, os.SEEK_CUR)

View File

@ -42,6 +42,7 @@ import subprocess
import sys
import tempfile
import warnings
from typing import Any
from . import Image, ImageFile
from ._binary import i16be as i16
@ -54,7 +55,7 @@ from .JpegPresets import presets
# Parser
def Skip(self, marker):
def Skip(self: JpegImageFile, marker: int) -> None:
n = i16(self.fp.read(2)) - 2
ImageFile._safe_read(self.fp, n)
@ -193,7 +194,7 @@ def APP(self, marker):
self.info["dpi"] = 72, 72
def COM(self, marker):
def COM(self: JpegImageFile, marker: int) -> None:
#
# Comment marker. Store these in the APP dictionary.
n = i16(self.fp.read(2)) - 2
@ -204,7 +205,7 @@ def COM(self, marker):
self.applist.append(("COM", s))
def SOF(self, marker):
def SOF(self: JpegImageFile, marker: int) -> None:
#
# Start of frame marker. Defines the size and mode of the
# image. JPEG is colour blind, so we use some simple
@ -252,7 +253,7 @@ def SOF(self, marker):
self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2]))
def DQT(self, marker):
def DQT(self: JpegImageFile, marker: int) -> None:
#
# Define quantization table. Note that there might be more
# than one table in each marker.
@ -495,14 +496,14 @@ class JpegImageFile(ImageFile.ImageFile):
self.tile = []
def _getexif(self):
def _getexif(self) -> dict[str, Any] | None:
return _getexif(self)
def _getmp(self):
return _getmp(self)
def _getexif(self):
def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info:
return None
return self.getexif()._get_merged_dict()

View File

@ -17,6 +17,7 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
from . import EpsImagePlugin
@ -38,7 +39,7 @@ class PSDraw:
fp = sys.stdout
self.fp = fp
def begin_document(self, id=None):
def begin_document(self, id: str | None = None) -> None:
"""Set up printing of a document. (Write PostScript DSC header.)"""
# FIXME: incomplete
self.fp.write(
@ -52,7 +53,7 @@ class PSDraw:
self.fp.write(EDROFF_PS)
self.fp.write(VDI_PS)
self.fp.write(b"%%EndProlog\n")
self.isofont = {}
self.isofont: dict[bytes, int] = {}
def end_document(self) -> None:
"""Ends printing. (Write PostScript DSC footer.)"""
@ -60,22 +61,24 @@ class PSDraw:
if hasattr(self.fp, "flush"):
self.fp.flush()
def setfont(self, font, size):
def setfont(self, font: str, size: int) -> None:
"""
Selects which font to use.
:param font: A PostScript font name
:param size: Size in points.
"""
font = bytes(font, "UTF-8")
if font not in self.isofont:
font_bytes = bytes(font, "UTF-8")
if font_bytes not in self.isofont:
# reencode font
self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font))
self.isofont[font] = 1
self.fp.write(
b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes)
)
self.isofont[font_bytes] = 1
# rough
self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font))
self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes))
def line(self, xy0, xy1):
def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None:
"""
Draws a line between the two points. Coordinates are given in
PostScript point coordinates (72 points per inch, (0, 0) is the lower
@ -83,7 +86,7 @@ class PSDraw:
"""
self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1))
def rectangle(self, box):
def rectangle(self, box: tuple[int, int, int, int]) -> None:
"""
Draws a rectangle.
@ -92,18 +95,22 @@ class PSDraw:
"""
self.fp.write(b"%d %d M 0 %d %d Vr\n" % box)
def text(self, xy, text):
def text(self, xy: tuple[int, int], text: str) -> None:
"""
Draws text at the given position. You must use
:py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
"""
text = bytes(text, "UTF-8")
text = b"\\(".join(text.split(b"("))
text = b"\\)".join(text.split(b")"))
xy += (text,)
self.fp.write(b"%d %d M (%s) S\n" % xy)
text_bytes = bytes(text, "UTF-8")
text_bytes = b"\\(".join(text_bytes.split(b"("))
text_bytes = b"\\)".join(text_bytes.split(b")"))
self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,)))
def image(self, box, im, dpi=None):
if TYPE_CHECKING:
from . import Image
def image(
self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None
) -> None:
"""Draw a PIL image, centered in the given box."""
# default resolution depends on mode
if not dpi:

View File

@ -48,5 +48,5 @@ class PaletteFile:
self.palette = b"".join(self.palette)
def getpalette(self):
def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode

View File

@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any, List, NamedTuple, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
# on page 656
def encode_text(s):
def encode_text(s: str) -> bytes:
return codecs.BOM_UTF16_BE + s.encode("utf_16_be")
@ -103,7 +103,7 @@ class IndirectReference(IndirectReferenceTuple):
def __ne__(self, other):
return not (self == other)
def __hash__(self):
def __hash__(self) -> int:
return hash((self.object_id, self.generation))
@ -219,7 +219,7 @@ class PdfName:
isinstance(other, PdfName) and other.name == self.name
) or other == self.name
def __hash__(self):
def __hash__(self) -> int:
return hash(self.name)
def __repr__(self) -> str:
@ -402,7 +402,7 @@ class PdfParser:
if f:
self.seek_end()
def __enter__(self):
def __enter__(self) -> PdfParser:
return self
def __exit__(self, exc_type, exc_value, traceback):
@ -436,7 +436,7 @@ class PdfParser:
def write_comment(self, s):
self.f.write(f"% {s}\n".encode())
def write_catalog(self):
def write_catalog(self) -> IndirectReference:
self.del_root()
self.root_ref = self.next_object_id(self.f.tell())
self.pages_ref = self.next_object_id(0)

View File

@ -39,7 +39,7 @@ import struct
import warnings
import zlib
from enum import IntEnum
from typing import IO
from typing import IO, Any
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16
@ -1021,7 +1021,7 @@ class PngImageFile(ImageFile.ImageFile):
if self.pyaccess:
self.pyaccess = None
def _getexif(self):
def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info:
self.load()
if "exif" not in self.info and "Raw profile type exif" not in self.info:
@ -1039,22 +1039,22 @@ class PngImageFile(ImageFile.ImageFile):
# PNG writer
_OUTMODES = {
# supported PIL modes, and corresponding rawmodes/bits/color combinations
"1": ("1", b"\x01\x00"),
"L;1": ("L;1", b"\x01\x00"),
"L;2": ("L;2", b"\x02\x00"),
"L;4": ("L;4", b"\x04\x00"),
"L": ("L", b"\x08\x00"),
"LA": ("LA", b"\x08\x04"),
"I": ("I;16B", b"\x10\x00"),
"I;16": ("I;16B", b"\x10\x00"),
"I;16B": ("I;16B", b"\x10\x00"),
"P;1": ("P;1", b"\x01\x03"),
"P;2": ("P;2", b"\x02\x03"),
"P;4": ("P;4", b"\x04\x03"),
"P": ("P", b"\x08\x03"),
"RGB": ("RGB", b"\x08\x02"),
"RGBA": ("RGBA", b"\x08\x06"),
# supported PIL modes, and corresponding rawmode, bit depth and color type
"1": ("1", b"\x01", b"\x00"),
"L;1": ("L;1", b"\x01", b"\x00"),
"L;2": ("L;2", b"\x02", b"\x00"),
"L;4": ("L;4", b"\x04", b"\x00"),
"L": ("L", b"\x08", b"\x00"),
"LA": ("LA", b"\x08", b"\x04"),
"I": ("I;16B", b"\x10", b"\x00"),
"I;16": ("I;16B", b"\x10", b"\x00"),
"I;16B": ("I;16B", b"\x10", b"\x00"),
"P;1": ("P;1", b"\x01", b"\x03"),
"P;2": ("P;2", b"\x02", b"\x03"),
"P;4": ("P;4", b"\x04", b"\x03"),
"P": ("P", b"\x08", b"\x03"),
"RGB": ("RGB", b"\x08", b"\x02"),
"RGBA": ("RGBA", b"\x08", b"\x06"),
}
@ -1223,7 +1223,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
seq_num = fdat_chunks.seq_num
def _save_all(im, fp, filename):
def _save_all(im: Image.Image, fp: IO[bytes], filename: str) -> None:
_save(im, fp, filename, save_all=True)
@ -1283,7 +1283,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
# get the corresponding PNG mode
try:
rawmode, mode = _OUTMODES[mode]
rawmode, bit_depth, color_type = _OUTMODES[mode]
except KeyError as e:
msg = f"cannot write mode {mode} as PNG"
raise OSError(msg) from e
@ -1298,7 +1298,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
b"IHDR",
o32(size[0]), # 0: size
o32(size[1]),
mode, # 8: depth/type
bit_depth,
color_type,
b"\0", # 10: compression
b"\0", # 11: filter category
b"\0", # 12: interlace flag

View File

@ -22,6 +22,7 @@ from __future__ import annotations
import logging
import sys
from typing import TYPE_CHECKING
from ._deprecate import deprecate
@ -48,9 +49,12 @@ except ImportError as ex:
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from . import Image
class PyAccess:
def __init__(self, img, readonly=False):
def __init__(self, img: Image.Image, readonly: bool = False) -> None:
deprecate("PyAccess", 11)
vals = dict(img.im.unsafe_ptrs)
self.readonly = readonly
@ -77,7 +81,8 @@ class PyAccess:
"""
Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for
multi-band images
multi-band images. In addition to this, RGB and RGBA tuples
are accepted for P and PA images.
:param xy: The pixel coordinate, given as (x, y). See
:ref:`coordinate-system`.
@ -108,7 +113,7 @@ class PyAccess:
return self.set_pixel(x, y, color)
def __getitem__(self, xy):
def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]:
"""
Returns the pixel at x,y. The pixel is returned as a single
value for single band images or a tuple for multiple band
@ -130,13 +135,19 @@ class PyAccess:
putpixel = __setitem__
getpixel = __getitem__
def check_xy(self, xy):
def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]:
(x, y) = xy
if not (0 <= x < self.xsize and 0 <= y < self.ysize):
msg = "pixel location out of range"
raise ValueError(msg)
return xy
def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]:
raise NotImplementedError()
def set_pixel(self, x: int, y: int, color: float | tuple[int, ...]) -> None:
raise NotImplementedError()
class _PyAccess32_2(PyAccess):
"""PA, LA, stored in first and last bytes of a 32 bit word"""
@ -144,7 +155,7 @@ class _PyAccess32_2(PyAccess):
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> tuple[int, int]:
pixel = self.pixels[y][x]
return pixel.r, pixel.a
@ -161,7 +172,7 @@ class _PyAccess32_3(PyAccess):
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> tuple[int, int, int]:
pixel = self.pixels[y][x]
return pixel.r, pixel.g, pixel.b
@ -180,7 +191,7 @@ class _PyAccess32_4(PyAccess):
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]:
pixel = self.pixels[y][x]
return pixel.r, pixel.g, pixel.b, pixel.a
@ -199,7 +210,7 @@ class _PyAccess8(PyAccess):
def _post_init(self, *args, **kwargs):
self.pixels = self.image8
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x]
def set_pixel(self, x, y, color):
@ -217,7 +228,7 @@ class _PyAccessI16_N(PyAccess):
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("unsigned short **", self.image)
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x]
def set_pixel(self, x, y, color):
@ -235,7 +246,7 @@ class _PyAccessI16_L(PyAccess):
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> int:
pixel = self.pixels[y][x]
return pixel.l + pixel.r * 256
@ -256,7 +267,7 @@ class _PyAccessI16_B(PyAccess):
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> int:
pixel = self.pixels[y][x]
return pixel.l * 256 + pixel.r
@ -277,7 +288,7 @@ class _PyAccessI32_N(PyAccess):
def _post_init(self, *args, **kwargs):
self.pixels = self.image32
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x]
def set_pixel(self, x, y, color):
@ -296,7 +307,7 @@ class _PyAccessI32_Swap(PyAccess):
chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0]
return ffi.cast("int *", chars)[0]
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> int:
return self.reverse(self.pixels[y][x])
def set_pixel(self, x, y, color):
@ -309,7 +320,7 @@ class _PyAccessF(PyAccess):
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("float **", self.image32)
def get_pixel(self, x, y):
def get_pixel(self, x: int, y: int) -> float:
return self.pixels[y][x]
def set_pixel(self, x, y, color):
@ -357,7 +368,7 @@ else:
mode_map["I;32B"] = _PyAccessI32_N
def new(img, readonly=False):
def new(img: Image.Image, readonly: bool = False) -> PyAccess | None:
access_type = mode_map.get(img.mode, None)
if not access_type:
logger.debug("PyAccess Not Implemented: %s", img.mode)

View File

@ -38,7 +38,7 @@ class QoiImageFile(ImageFile.ImageFile):
class QoiDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def _add_to_previous_pixels(self, value):
def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
self._previous_pixel = value
r, g, b, a = value

View File

@ -233,7 +233,7 @@ def loadImageSeries(filelist=None):
# For saving images in Spider format
def makeSpiderHeader(im):
def makeSpiderHeader(im: Image.Image) -> list[bytes]:
nsam, nrow = im.size
lenbyt = nsam * 4 # There are labrec records in the header
labrec = int(1024 / lenbyt)

View File

@ -1,6 +1,7 @@
from __future__ import annotations
from io import BytesIO
from typing import Any
from . import Image, ImageFile
@ -95,7 +96,7 @@ class WebPImageFile(ImageFile.ImageFile):
# Initialize seek state
self._reset(reset=False)
def _getexif(self):
def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info:
return None
return self.getexif()._get_merged_dict()
@ -107,7 +108,7 @@ class WebPImageFile(ImageFile.ImageFile):
# Set logical frame to requested position
self.__logical_frame = frame
def _reset(self, reset=True):
def _reset(self, reset: bool = True) -> None:
if reset:
self._decoder.reset()
self.__physical_frame = 0

View File

@ -28,7 +28,7 @@ from ._binary import si32le as _long
_handler = None
def register_handler(handler):
def register_handler(handler: ImageFile.StubHandler) -> None:
"""
Install application-specific WMF image handler.
@ -41,12 +41,12 @@ def register_handler(handler):
if hasattr(Image.core, "drawwmf"):
# install default handler (windows only)
class WmfHandler:
def open(self, im):
class WmfHandler(ImageFile.StubHandler):
def open(self, im: ImageFile.StubImageFile) -> None:
im._mode = "RGB"
self.bbox = im.info["wmf_bbox"]
def load(self, im):
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
im.fp.seek(0) # rewind
return Image.frombytes(
"RGB",
@ -147,7 +147,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
if loader:
loader.open(self)
def _load(self):
def _load(self) -> ImageFile.StubHandler | None:
return _handler
def load(self, dpi=None):

View File

@ -2,7 +2,7 @@ import datetime
import sys
from typing import Literal, SupportsFloat, TypedDict
littlecms_version: str
littlecms_version: str | None
_Tuple3f = tuple[float, float, float]
_Tuple2x3f = tuple[_Tuple3f, _Tuple3f]