Merge branch 'main' into libjpeg-turbo

This commit is contained in:
Hugo van Kemenade 2024-06-07 03:46:49 -06:00 committed by GitHub
commit 13a33dc3c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
133 changed files with 1473 additions and 1111 deletions

View File

@ -34,7 +34,7 @@ install:
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.3.0
- choco install ghostscript --version=10.3.1
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |

View File

@ -1 +1 @@
cibuildwheel==2.17.0
cibuildwheel==2.18.1

View File

@ -9,6 +9,7 @@ BinPackParameters: false
BreakBeforeBraces: Attach
ColumnLimit: 88
DerivePointerAlignment: false
IndentGotoLabels: false
IndentWidth: 4
Language: Cpp
PointerAlignment: Right

View File

@ -86,7 +86,7 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.3.0 --no-progress
choco install ghostscript --version=10.3.1 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images

View File

@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.4.0
HARFBUZZ_VERSION=8.5.0
LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.3
OPENJPEG_VERSION=2.5.2
@ -33,9 +33,9 @@ if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
else
ZLIB_VERSION=1.2.8
fi
LIBWEBP_VERSION=1.3.2
LIBWEBP_VERSION=1.4.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.16.1
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
@ -70,7 +70,7 @@ function build {
fi
build_new_zlib
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib

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,12 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.4
rev: v0.4.7
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.3.0
rev: 24.4.2
hooks:
- id: black
@ -23,13 +23,20 @@ repos:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.5
hooks:
- id: clang-format
types: [c]
exclude: ^src/thirdparty/
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
@ -43,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.1
rev: 0.28.4
hooks:
- id: check-github-workflows
- id: check-readthedocs
@ -55,12 +62,12 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.7.0
rev: 1.8.0
hooks:
- 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

@ -29,33 +29,6 @@ elif "GITHUB_ACTIONS" in os.environ:
uploader = "github_actions"
modes = (
"1",
"L",
"LA",
"La",
"P",
"PA",
"F",
"I",
"I;16",
"I;16L",
"I;16B",
"I;16N",
"RGB",
"RGBA",
"RGBa",
"RGBX",
"BGR;15",
"BGR;16",
"BGR;24",
"CMYK",
"YCbCr",
"HSV",
"LAB",
)
def upload(a: Image.Image, b: Image.Image) -> str | None:
if uploader == "show":
# local img.show for errors.
@ -201,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)
@ -216,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
@ -247,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

@ -336,9 +336,7 @@ def test_readline_psfile(tmp_path: Path) -> None:
strings = ["something", "else", "baz", "bif"]
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
ending = "Failure with line ending: %s" % (
"".join("%s" % ord(s) for s in ending)
)
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

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")

39
Tests/test_file_mpeg.py Normal file
View File

@ -0,0 +1,39 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image, MpegImagePlugin
def test_identify() -> None:
# Arrange
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
# Act
with Image.open(b) as im:
# Assert
assert im.format == "MPEG"
assert im.mode == "RGB"
assert im.size == (16, 1)
def test_invalid_file() -> None:
# Arrange
invalid_file = "Tests/images/flower.jpg"
# Act / Assert
with pytest.raises(SyntaxError):
MpegImagePlugin.MpegImageFile(invalid_file)
def test_load() -> None:
# Arrange
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
with Image.open(b) as im:
# Act / Assert: cannot load
with pytest.raises(OSError):
im.load()

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,13 +25,13 @@ 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,
is_big_endian,
is_win32,
mark_if_feature_version,
modes,
skip_unless_feature,
)
@ -46,7 +46,7 @@ def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
class TestImage:
@pytest.mark.parametrize("mode", modes)
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
def test_image_modes_success(self, mode: str) -> None:
helper_image_new(mode, (1, 1))
@ -100,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
@ -139,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:
@ -186,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()
@ -498,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
@ -1027,7 +1038,7 @@ class TestImage:
class TestImageBytes:
@pytest.mark.parametrize("mode", modes)
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
def test_roundtrip_bytes_constructor(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()
@ -1039,7 +1050,7 @@ class TestImageBytes:
reloaded = Image.frombytes(mode, im.size, source_bytes)
assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize("mode", modes)
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
def test_roundtrip_bytes_method(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()
@ -1048,7 +1059,7 @@ class TestImageBytes:
reloaded.frombytes(source_bytes)
assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize("mode", modes)
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
def test_getdata_putdata(self, mode: str) -> None:
if is_big_endian() and mode == "BGR;15":
pytest.xfail("Known failure of BGR;15 on big-endian")

View File

@ -10,7 +10,7 @@ import pytest
from PIL import Image
from .helper import assert_image_equal, hopper, is_win32, modes
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
@ -205,12 +205,13 @@ class TestImageGetPixel(AccessTest):
with pytest.raises(error):
im.getpixel((-1, -1))
@pytest.mark.parametrize("mode", modes)
@pytest.mark.parametrize("mode", Image.MODES)
def test_basic(self, mode: str) -> None:
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
self.check(mode)
else:
self.check(mode)
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
def test_deprecated(self, mode: str) -> None:
with pytest.warns(DeprecationWarning):
self.check(mode)
def test_list(self) -> None:
@ -409,13 +410,14 @@ class TestEmbeddable:
from setuptools.command import build_ext
with open("embed_pil.c", "w", encoding="utf-8") as fh:
home = sys.prefix.replace("\\", "\\\\")
fh.write(
"""
f"""
#include "Python.h"
int main(int argc, char* argv[])
{
char *home = "%s";
{{
char *home = "{home}";
wchar_t *whome = Py_DecodeLocale(home, NULL);
Py_SetPythonHome(whome);
@ -430,9 +432,8 @@ int main(int argc, char* argv[])
PyMem_RawFree(whome);
return 0;
}
}}
"""
% sys.prefix.replace("\\", "\\\\")
)
compiler = getattr(build_ext, "new_compiler")()

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

@ -2,7 +2,7 @@
# install libimagequant
archive_name=libimagequant
archive_version=4.3.0
archive_version=4.3.1
archive=$archive_name-$archive_version

View File

@ -1,7 +1,7 @@
#!/bin/bash
# install webp
archive=libwebp-1.3.2
archive=libwebp-1.4.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -9,9 +9,9 @@ PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
PAPEROPT_a4 = --define latex_paper_size=a4
PAPEROPT_letter = --define latex_paper_size=letter
ALLSPHINXOPTS = --doctree-dir $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
@ -51,42 +51,42 @@ install-sphinx:
.PHONY: html
html:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
$(SPHINXBUILD) --builder html --fail-on-warning --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
$(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
$(SPHINXBUILD) --builder singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
$(SPHINXBUILD) --builder pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
$(SPHINXBUILD) --builder json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
$(SPHINXBUILD) --builder htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
@ -94,7 +94,7 @@ htmlhelp:
.PHONY: qthelp
qthelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
$(SPHINXBUILD) --builder qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@ -105,7 +105,7 @@ qthelp:
.PHONY: devhelp
devhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
$(SPHINXBUILD) --builder devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@ -116,14 +116,14 @@ devhelp:
.PHONY: epub
epub:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
$(SPHINXBUILD) --builder epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: latex
latex:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
$(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
@ -132,7 +132,7 @@ latex:
.PHONY: latexpdf
latexpdf:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
$(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
@ -140,21 +140,21 @@ latexpdf:
.PHONY: text
text:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
$(SPHINXBUILD) --builder text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
$(SPHINXBUILD) --builder man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
$(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
@ -163,7 +163,7 @@ texinfo:
.PHONY: info
info:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
$(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
@ -171,21 +171,21 @@ info:
.PHONY: gettext
gettext:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
$(SPHINXBUILD) --builder gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
$(SPHINXBUILD) --builder changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
$(SPHINXBUILD) --builder linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
@ -193,7 +193,7 @@ linkcheck:
.PHONY: doctest
doctest:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
$(SPHINXBUILD) --builder doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -68,7 +68,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.3**
* Pillow has been tested with libimagequant **2.6-4.3.1**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.

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

@ -165,9 +165,9 @@ if __name__ == "__main__":
print("Running selftest:")
status = doctest.testmod(sys.modules[__name__])
if status[0]:
print("*** %s tests of %d failed." % status)
print(f"*** {status[0]} tests of {status[1]} failed.")
exit_status = 1
else:
print("--- %s tests passed." % status[1])
print(f"--- {status[1]} tests passed.")
sys.exit(exit_status)

View File

@ -23,8 +23,7 @@ from setuptools.command.build_ext import build_ext
def get_version():
version_file = "src/PIL/_version.py"
with open(version_file, encoding="utf-8") as f:
exec(compile(f.read(), version_file, "exec"))
return locals()["__version__"]
return f.read().split('"')[1]
configuration = {}

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
@ -253,7 +254,7 @@ class BlpImageFile(ImageFile.ImageFile):
format = "BLP"
format_description = "Blizzard Mipmap Format"
def _open(self):
def _open(self) -> None:
self.magic = self.fp.read(4)
self.fp.seek(5, os.SEEK_CUR)
@ -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:
@ -333,7 +335,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
class BLP1Decoder(_BLPBaseDecoder):
def _load(self):
def _load(self) -> None:
if self._blp_compression == Format.JPEG:
self._decode_jpeg_stream()
@ -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:
@ -418,7 +421,7 @@ class BLP2Decoder(_BLPBaseDecoder):
class BLPEncoder(ImageFile.PyEncoder):
_pushes_fd = True
def _write_palette(self):
def _write_palette(self) -> bytes:
data = b""
palette = self.im.getpalette("RGBA", "RGBA")
for i in range(len(palette) // 4):
@ -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

@ -25,6 +25,7 @@
from __future__ import annotations
import os
from typing import IO
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@ -52,7 +53,7 @@ def _accept(prefix: bytes) -> bool:
return prefix[:2] == b"BM"
def _dib_accept(prefix):
def _dib_accept(prefix: bytes) -> bool:
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
@ -283,7 +284,7 @@ class BmpImageFile(ImageFile.ImageFile):
)
]
def _open(self):
def _open(self) -> None:
"""Open file, check magic number and read header"""
# read 14 bytes: magic number, filesize, reserved, header final offset
head_data = self.fp.read(14)
@ -376,7 +377,7 @@ class DibImageFile(BmpImageFile):
format = "DIB"
format_description = "Windows Bitmap"
def _open(self):
def _open(self) -> None:
self._bitmap()
@ -394,11 +395,13 @@ SAVE = {
}
def _dib_save(im, fp, filename):
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
_save(im, fp, filename, False)
def _save(im, fp, filename, bitmap_header=True):
def _save(
im: Image.Image, fp: IO[bytes], filename: str, bitmap_header: bool = True
) -> None:
try:
rawmode, bits, colors = SAVE[im.mode]
except KeyError as e:

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 BUFR image handler.
@ -37,7 +39,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
format = "BUFR"
format_description = "BUFR"
def _open(self):
def _open(self) -> None:
offset = self.fp.tell()
if not _accept(self.fp.read(4)):
@ -54,11 +56,11 @@ class BufrStubImageFile(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 = "BUFR save handler not installed"
raise OSError(msg)

View File

@ -37,7 +37,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
format = "CUR"
format_description = "Windows Cursor"
def _open(self):
def _open(self) -> None:
offset = self.fp.tell()
# check magic

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,12 +58,12 @@ 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)
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
self.frame = frame
@ -71,7 +71,7 @@ class DcxImageFile(PcxImageFile):
self.fp.seek(self._offset[frame])
PcxImageFile._open(self)
def tell(self):
def tell(self) -> int:
return self.frame

View File

@ -16,6 +16,7 @@ import io
import struct
import sys
from enum import IntEnum, IntFlag
from typing import IO
from . import Image, ImageFile, ImagePalette
from ._binary import i32le as i32
@ -331,7 +332,7 @@ class DdsImageFile(ImageFile.ImageFile):
format = "DDS"
format_description = "DirectDraw Surface"
def _open(self):
def _open(self) -> None:
if not _accept(self.fp.read(4)):
msg = "not a DDS file"
raise SyntaxError(msg)
@ -472,7 +473,7 @@ class DdsImageFile(ImageFile.ImageFile):
else:
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
pass
@ -510,7 +511,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
return -1, 0
def _save(im, fp, filename):
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if im.mode not in ("RGB", "RGBA", "L", "LA"):
msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg)

View File

@ -42,7 +42,7 @@ gs_binary: str | bool | None = None
gs_windows_binary = None
def has_ghostscript():
def has_ghostscript() -> bool:
global gs_binary, gs_windows_binary
if gs_binary is None:
if sys.platform.startswith("win"):
@ -178,7 +178,7 @@ class PSFile:
self.char = None
self.fp.seek(offset, whence)
def readline(self):
def readline(self) -> str:
s = [self.char or b""]
self.char = None
@ -212,7 +212,7 @@ class EpsImageFile(ImageFile.ImageFile):
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
def _open(self):
def _open(self) -> None:
(length, offset) = self._find_offset(self.fp)
# go to offset - start of "%!PS"
@ -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)
@ -404,7 +404,7 @@ class EpsImageFile(ImageFile.ImageFile):
self.tile = []
return Image.Image.load(self)
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
# we can't incrementally load, so force ImageFile.parser to
# use our custom load method by defining this method.
pass

View File

@ -123,7 +123,7 @@ class FliImageFile(ImageFile.ImageFile):
palette[i] = (r, g, b)
i += 1
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
if frame < self.__frame:
@ -132,7 +132,7 @@ class FliImageFile(ImageFile.ImageFile):
for f in range(self.__frame + 1, frame + 1):
self._seek(f)
def _seek(self, frame):
def _seek(self, frame: int) -> None:
if frame == 0:
self.__frame = -1
self._fp.seek(self.__rewind)
@ -162,7 +162,7 @@ class FliImageFile(ImageFile.ImageFile):
self.__offset += framesize
def tell(self):
def tell(self) -> int:
return self.__frame

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
@ -237,7 +237,7 @@ class FpxImageFile(ImageFile.ImageFile):
return ImageFile.ImageFile.load(self)
def close(self):
def close(self) -> None:
self.ole.close()
super().close()

View File

@ -71,7 +71,7 @@ class FtexImageFile(ImageFile.ImageFile):
format = "FTEX"
format_description = "Texture File Format (IW2:EOC)"
def _open(self):
def _open(self) -> None:
if not _accept(self.fp.read(4)):
msg = "not an FTEX file"
raise SyntaxError(msg)
@ -103,7 +103,7 @@ class FtexImageFile(ImageFile.ImageFile):
self.fp.close()
self.fp = BytesIO(data)
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
pass

View File

@ -41,7 +41,7 @@ class GbrImageFile(ImageFile.ImageFile):
format = "GBR"
format_description = "GIMP brush file"
def _open(self):
def _open(self) -> None:
header_size = i32(self.fp.read(4))
if header_size < 20:
msg = "not a GIMP brush"

View File

@ -30,6 +30,8 @@ import math
import os
import subprocess
from enum import IntEnum
from functools import cached_property
from typing import IO
from . import (
Image,
@ -76,19 +78,19 @@ class GifImageFile(ImageFile.ImageFile):
global_palette = None
def data(self):
def data(self) -> bytes | None:
s = self.fp.read(1)
if s and s[0]:
return self.fp.read(s[0])
return None
def _is_palette_needed(self, p):
def _is_palette_needed(self, p: bytes) -> bool:
for i in range(0, len(p), 3):
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
return True
return False
def _open(self):
def _open(self) -> None:
# Screen
s = self.fp.read(13)
if not _accept(s):
@ -112,8 +114,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,26 +129,25 @@ 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
def seek(self, frame):
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):
return
if frame < self.__frame:
@ -337,14 +337,13 @@ class GifImageFile(ImageFile.ImageFile):
self._mode = "RGB"
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
def _rgb(color):
def _rgb(color: int) -> tuple[int, int, int]:
if self._frame_palette:
if color * 3 + 3 > len(self._frame_palette.palette):
color = 0
color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
else:
color = (color, color, color)
return color
return (color, color, color)
self.dispose_extent = frame_dispose_extent
try:
@ -417,7 +416,7 @@ class GifImageFile(ImageFile.ImageFile):
elif k in self.info:
del self.info[k]
def load_prepare(self):
def load_prepare(self) -> None:
temp_mode = "P" if self._frame_palette else "L"
self._prev_im = None
if self.__frame == 0:
@ -437,7 +436,7 @@ class GifImageFile(ImageFile.ImageFile):
super().load_prepare()
def load_end(self):
def load_end(self) -> None:
if self.__frame == 0:
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
if self._frame_transparency is not None:
@ -463,7 +462,7 @@ class GifImageFile(ImageFile.ImageFile):
else:
self.im.paste(frame_im, self.dispose_extent)
def tell(self):
def tell(self) -> int:
return self.__frame
@ -474,7 +473,7 @@ class GifImageFile(ImageFile.ImageFile):
RAWMODE = {"1": "L", "L": "L", "P": "P"}
def _normalize_mode(im):
def _normalize_mode(im: Image.Image) -> Image.Image:
"""
Takes an image (or frame), returns an image in a mode that is appropriate
for saving in a Gif.
@ -710,11 +709,13 @@ def _write_multiple_frames(im, fp, palette):
return True
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)
def _save(im, fp, filename, save_all=False):
def _save(
im: Image.Image, fp: IO[bytes], filename: str, save_all: bool = False
) -> None:
# header
if "palette" in im.encoderinfo or "palette" in im.info:
palette = im.encoderinfo.get("palette", im.info.get("palette"))
@ -731,7 +732,7 @@ def _save(im, fp, filename, save_all=False):
fp.flush()
def get_interlace(im):
def get_interlace(im: Image.Image) -> int:
interlace = im.encoderinfo.get("interlace", 1)
# workaround for @PIL153
@ -887,7 +888,7 @@ def _get_optimize(im, info):
return used_palette_colors
def _get_color_table_size(palette_bytes):
def _get_color_table_size(palette_bytes: bytes) -> int:
# calculate the palette size for the header
if not palette_bytes:
return 0
@ -897,7 +898,7 @@ def _get_color_table_size(palette_bytes):
return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1
def _get_header_palette(palette_bytes):
def _get_header_palette(palette_bytes: bytes) -> bytes:
"""
Returns the palette, null padded to the next power of 2 (*3) bytes
suitable for direct inclusion in the GIF header
@ -915,7 +916,7 @@ def _get_header_palette(palette_bytes):
return palette_bytes
def _get_palette_bytes(im):
def _get_palette_bytes(im: Image.Image) -> bytes:
"""
Gets the palette for inclusion in the gif header

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):
def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode

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 GRIB image handler.
@ -37,7 +39,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
format = "GRIB"
format_description = "GRIB"
def _open(self):
def _open(self) -> None:
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
@ -54,11 +56,11 @@ class GribStubImageFile(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 = "GRIB save handler not installed"
raise OSError(msg)

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.
@ -37,7 +39,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
format = "HDF5"
format_description = "HDF5"
def _open(self):
def _open(self) -> None:
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
@ -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

@ -252,7 +252,7 @@ class IcnsImageFile(ImageFile.ImageFile):
format = "ICNS"
format_description = "Mac OS icns resource"
def _open(self):
def _open(self) -> None:
self.icns = IcnsFile(self.fp)
self._mode = "RGBA"
self.info["sizes"] = self.icns.itersizes()

View File

@ -25,6 +25,7 @@ from __future__ import annotations
import warnings
from io import BytesIO
from math import ceil, log
from typing import IO
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16
@ -39,7 +40,7 @@ from ._binary import o32le as o32
_MAGIC = b"\0\0\1\0"
def _save(im, fp, filename):
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
fp.write(_MAGIC) # (2+2)
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
sizes = im.encoderinfo.get(
@ -194,7 +195,7 @@ class IcoFile:
"""
return self.frame(self.getentryindex(size, bpp))
def frame(self, idx):
def frame(self, idx: int) -> Image.Image:
"""
Get an image from frame idx
"""
@ -205,6 +206,7 @@ class IcoFile:
data = self.buf.read(8)
self.buf.seek(header["offset"])
im: Image.Image
if data[:8] == PngImagePlugin._MAGIC:
# png frame
im = PngImagePlugin.PngImageFile(self.buf)
@ -302,7 +304,7 @@ class IcoImageFile(ImageFile.ImageFile):
format = "ICO"
format_description = "Windows Icon"
def _open(self):
def _open(self) -> None:
self.ico = IcoFile(self.fp)
self.info["sizes"] = self.ico.sizes()
self.size = self.ico.entry[0]["dim"]
@ -341,7 +343,7 @@ class IcoImageFile(ImageFile.ImageFile):
self.size = im.size
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
# Flag the ImageFile.Parser so that it
# just does all the decode at the end.
pass

View File

@ -28,6 +28,7 @@ from __future__ import annotations
import os
import re
from typing import IO, Any
from . import Image, ImageFile, ImagePalette
@ -103,7 +104,7 @@ for j in range(2, 33):
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
def number(s):
def number(s: Any) -> float:
try:
return int(s)
except ValueError:
@ -119,7 +120,7 @@ class ImImageFile(ImageFile.ImageFile):
format_description = "IFUNC Image Memory"
_close_exclusive_fp_after_loading = False
def _open(self):
def _open(self) -> None:
# Quick rejection: if there's not an LF among the first
# 100 bytes, this is (probably) not a text header.
@ -271,14 +272,14 @@ class ImImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
@property
def n_frames(self):
def n_frames(self) -> int:
return self.info[FRAMES]
@property
def is_animated(self):
def is_animated(self) -> bool:
return self.info[FRAMES] > 1
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
@ -296,7 +297,7 @@ class ImImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
def tell(self):
def tell(self) -> int:
return self.frame
@ -325,7 +326,7 @@ SAVE = {
}
def _save(im, fp, filename):
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
try:
image_type, rawmode = SAVE[im.mode]
except KeyError as e:

View File

@ -249,7 +249,28 @@ def _conv_type_shape(im):
return shape, m.typestr
MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"]
MODES = [
"1",
"CMYK",
"F",
"HSV",
"I",
"I;16",
"I;16B",
"I;16L",
"I;16N",
"L",
"LA",
"La",
"LAB",
"P",
"PA",
"RGB",
"RGBA",
"RGBa",
"RGBX",
"YCbCr",
]
# raw modes that may be memory mapped. NOTE: if you change this, you
# may have to modify the stride calculation in map.c too!
@ -1298,7 +1319,10 @@ class Image:
self.load()
return self._new(self.im.expand(xmargin, ymargin))
def filter(self, filter):
if TYPE_CHECKING:
from . import ImageFilter
def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image:
"""
Filters this image using the given filter. For a list of
available filters, see the :py:mod:`~PIL.ImageFilter` module.
@ -1310,7 +1334,7 @@ class Image:
self.load()
if isinstance(filter, Callable):
if callable(filter):
filter = filter()
if not hasattr(filter, "filter"):
msg = "filter argument should be ImageFilter.Filter instance or class"
@ -1487,7 +1511,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 = []
@ -1511,10 +1535,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)
@ -1580,7 +1601,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.
@ -1592,6 +1613,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):
@ -1924,7 +1946,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
@ -2851,7 +2875,7 @@ class Image:
self.load()
return self._new(self.im.transpose(method))
def effect_spread(self, distance):
def effect_spread(self, distance: int) -> Image:
"""
Randomly spread pixels in an image.
@ -2988,7 +3012,7 @@ def new(
return im._new(core.fill(mode, size, color))
def frombytes(mode, size, data, decoder_name="raw", *args) -> Image:
def frombytes(mode, size, data, decoder_name: str = "raw", *args) -> Image:
"""
Creates a copy of an image memory from pixel data in a buffer.
@ -3027,7 +3051,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args) -> Image:
return im
def frombuffer(mode, size, data, decoder_name="raw", *args) -> Image:
def frombuffer(mode: str, size, data, decoder_name: str = "raw", *args) -> Image:
"""
Creates an image memory referencing pixel data in a byte buffer.
@ -3529,7 +3553,7 @@ def register_save(id: str, driver) -> None:
SAVE[id.upper()] = driver
def register_save_all(id, driver) -> None:
def register_save_all(id: str, driver) -> None:
"""
Registers an image function to save all the frames
of a multiframe format. This function should not be
@ -3541,7 +3565,7 @@ def register_save_all(id, driver) -> None:
SAVE_ALL[id.upper()] = driver
def register_extension(id, extension) -> None:
def register_extension(id: str, extension: str) -> None:
"""
Registers an image extension. This function should not be
used in application code.
@ -3552,7 +3576,7 @@ def register_extension(id, extension) -> None:
EXTENSION[extension.lower()] = id.upper()
def register_extensions(id, extensions) -> None:
def register_extensions(id: str, extensions: list[str]) -> None:
"""
Registers image extensions. This function should not be
used in application code.
@ -3564,7 +3588,7 @@ def register_extensions(id, extensions) -> None:
register_extension(id, extension)
def registered_extensions():
def registered_extensions() -> dict[str, str]:
"""
Returns a dictionary containing all file extensions belonging
to registered plugins
@ -3626,7 +3650,7 @@ def effect_mandelbrot(size, extent, quality):
return Image()._new(core.effect_mandelbrot(size, extent, quality))
def effect_noise(size, sigma):
def effect_noise(size: tuple[int, int], sigma: float) -> Image:
"""
Generate Gaussian noise centered around 128.
@ -3637,7 +3661,7 @@ def effect_noise(size, sigma):
return Image()._new(core.effect_noise(size, sigma))
def linear_gradient(mode):
def linear_gradient(mode: str) -> Image:
"""
Generate 256x256 linear gradient from black to white, top to bottom.
@ -3646,7 +3670,7 @@ def linear_gradient(mode):
return Image()._new(core.linear_gradient(mode))
def radial_gradient(mode):
def radial_gradient(mode: str) -> Image:
"""
Generate 256x256 radial gradient from black to white, centre to edge.

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

@ -34,7 +34,7 @@ from __future__ import annotations
import math
import numbers
import struct
from typing import Sequence, cast
from typing import TYPE_CHECKING, Sequence, cast
from . import Image, ImageColor
from ._typing import Coords
@ -92,7 +92,10 @@ class ImageDraw:
self.fontmode = "L" # aliasing is okay for other modes
self.fill = False
def getfont(self):
if TYPE_CHECKING:
from . import ImageFont
def getfont(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
"""
Get the current default font.
@ -178,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]
@ -898,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
@ -311,7 +312,7 @@ class ImageFile(Image.Image):
return Image.Image.load(self)
def load_prepare(self):
def load_prepare(self) -> None:
# create image memory if necessary
if not self.im or self.im.mode != self.mode or self.im.size != self.size:
self.im = Image.core.new(self.mode, self.size)
@ -319,16 +320,16 @@ class ImageFile(Image.Image):
if self.mode == "P":
Image.Image.load(self)
def load_end(self):
def load_end(self) -> None:
# may be overridden
pass
# may be defined for contained formats
# def load_seek(self, pos):
# def load_seek(self, pos: int) -> None:
# pass
# may be defined for blocked formats (e.g. PNG)
# def load_read(self, read_bytes):
# def load_read(self, read_bytes: int) -> bytes:
# pass
def _seek_check(self, frame):
@ -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.
@ -390,7 +400,7 @@ class Parser:
offset = 0
finished = 0
def reset(self):
def reset(self) -> None:
"""
(Consumer) Reset the parser. Note that you can only call this
method immediately after you've created a parser; parser
@ -605,7 +615,7 @@ def _safe_read(fp, size):
class PyCodecState:
def __init__(self):
def __init__(self) -> None:
self.xsize = 0
self.ysize = 0
self.xoff = 0
@ -634,7 +644,7 @@ class PyCodec:
"""
self.args = args
def cleanup(self):
def cleanup(self) -> None:
"""
Override to perform codec specific cleanup

View File

@ -16,11 +16,16 @@
#
from __future__ import annotations
import abc
import functools
from types import ModuleType
from typing import Any, Sequence
class Filter:
pass
@abc.abstractmethod
def filter(self, image):
pass
class MultibandFilter(Filter):
@ -53,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)
@ -76,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
@ -98,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
@ -113,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
@ -128,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
@ -144,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):
@ -162,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):
@ -190,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)
@ -225,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
@ -375,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)
@ -389,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
@ -436,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:
@ -541,7 +554,7 @@ class Color3DLUT(MultibandFilter):
_copy_table=False,
)
def __repr__(self):
def __repr__(self) -> str:
r = [
f"{self.__class__.__name__} from {self.table.__class__.__name__}",
"size={:d}x{:d}x{:d}".format(*self.size),

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
@ -66,7 +66,7 @@ class ImagePalette:
def colors(self, colors):
self._colors = colors
def copy(self):
def copy(self) -> ImagePalette:
new = ImagePalette()
new.mode = self.mode
@ -77,7 +77,7 @@ class ImagePalette:
return new
def getdata(self):
def getdata(self) -> tuple[str, bytes]:
"""
Get palette contents in format suitable for the low-level
``im.putpalette`` primitive.
@ -88,7 +88,7 @@ class ImagePalette:
return self.rawmode, self.palette
return self.mode, self.tobytes()
def tobytes(self):
def tobytes(self) -> bytes:
"""Convert palette to bytes.
.. warning:: This method is experimental.
@ -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

@ -128,7 +128,7 @@ class PhotoImage:
if image:
self.paste(image)
def __del__(self):
def __del__(self) -> None:
name = self.__photo.name
self.__photo.name = None
try:
@ -136,7 +136,7 @@ class PhotoImage:
except Exception:
pass # ignore internal errors
def __str__(self):
def __str__(self) -> str:
"""
Get the Tkinter photo image identifier. This method is automatically
called by Tkinter whenever a PhotoImage object is passed to a Tkinter
@ -146,7 +146,7 @@ class PhotoImage:
"""
return str(self.__photo)
def width(self):
def width(self) -> int:
"""
Get the width of the image.
@ -154,7 +154,7 @@ class PhotoImage:
"""
return self.__size[0]
def height(self):
def height(self) -> int:
"""
Get the height of the image.
@ -219,7 +219,7 @@ class BitmapImage:
kw["data"] = image.tobitmap()
self.__photo = tkinter.BitmapImage(**kw)
def __del__(self):
def __del__(self) -> None:
name = self.__photo.name
self.__photo.name = None
try:
@ -227,7 +227,7 @@ class BitmapImage:
except Exception:
pass # ignore internal errors
def width(self):
def width(self) -> int:
"""
Get the width of the image.
@ -235,7 +235,7 @@ class BitmapImage:
"""
return self.__size[0]
def height(self):
def height(self) -> int:
"""
Get the height of the image.
@ -243,7 +243,7 @@ class BitmapImage:
"""
return self.__size[1]
def __str__(self):
def __str__(self) -> str:
"""
Get the Tkinter bitmap image identifier. This method is automatically
called by Tkinter whenever a BitmapImage object is passed to a Tkinter

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
)
@ -204,7 +208,7 @@ class Window:
def ui_handle_damage(self, x0, y0, x1, y1):
pass
def ui_handle_destroy(self):
def ui_handle_destroy(self) -> None:
pass
def ui_handle_repair(self, dc, x0, y0, x1, y1):
@ -213,7 +217,7 @@ class Window:
def ui_handle_resize(self, width, height):
pass
def mainloop(self):
def mainloop(self) -> None:
Image.core.eventloop()

View File

@ -57,7 +57,7 @@ def dump(c: Sequence[int | bytes]) -> None:
""".. deprecated:: 10.2.0"""
deprecate("IptcImagePlugin.dump", 12)
for i in c:
print("%02x" % _i8(i), end=" ")
print(f"{_i8(i):02x}", end=" ")
print()

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)
@ -63,18 +63,18 @@ class BoxReader:
data = self._read_bytes(size)
return struct.unpack(field_format, data)
def read_boxes(self):
def read_boxes(self) -> BoxReader:
size = self.remaining_in_box
data = self._read_bytes(size)
return BoxReader(io.BytesIO(data), size)
def has_next_box(self):
def has_next_box(self) -> bool:
if self.has_length:
return self.fp.tell() + self.remaining_in_box < self.length
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)
@ -215,7 +215,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
format = "JPEG2000"
format_description = "JPEG 2000 (ISO 15444)"
def _open(self):
def _open(self) -> None:
sig = self.fp.read(4)
if sig == b"\xff\x4f\xff\x51":
self.codec = "j2k"
@ -267,7 +267,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
)
]
def _parse_comment(self):
def _parse_comment(self) -> None:
hdr = self.fp.read(2)
length = _binary.i16be(hdr)
self.fp.seek(length - 2, 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)
@ -191,7 +192,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
@ -202,7 +203,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
@ -250,7 +251,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.
@ -408,7 +409,7 @@ class JpegImageFile(ImageFile.ImageFile):
msg = "no marker found"
raise SyntaxError(msg)
def load_read(self, read_bytes):
def load_read(self, read_bytes: int) -> bytes:
"""
internal: read more image data
For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
@ -462,7 +463,7 @@ class JpegImageFile(ImageFile.ImageFile):
box = (0, 0, original_size[0] / scale, original_size[1] / scale)
return self.mode, box
def load_djpeg(self):
def load_djpeg(self) -> None:
# ALTERNATIVE: handle JPEGs via the IJG command line utilities
f, path = tempfile.mkstemp()
@ -493,13 +494,13 @@ 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 getxmp(self):
def getxmp(self) -> dict[str, Any]:
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
@ -515,7 +516,7 @@ class JpegImageFile(ImageFile.ImageFile):
return {}
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

@ -38,7 +38,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
format_description = "Microsoft Image Composer"
_close_exclusive_fp_after_loading = False
def _open(self):
def _open(self) -> None:
# read the OLE directory and see if this is a likely
# to be a Microsoft Image Composer file
@ -88,7 +88,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
def tell(self):
return self.frame
def close(self):
def close(self) -> None:
self.__fp.close()
self.ole.close()
super().close()

View File

@ -53,6 +53,10 @@ class BitStream:
return v
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\x00\x00\x01\xb3"
##
# Image plugin for MPEG streams. This plugin can identify a stream,
# but it cannot read it.
@ -77,7 +81,7 @@ class MpegImageFile(ImageFile.ImageFile):
# --------------------------------------------------------------------
# Registry stuff
Image.register_open(MpegImageFile.format, MpegImageFile)
Image.register_open(MpegImageFile.format, MpegImageFile, _accept)
Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"])

View File

@ -22,6 +22,7 @@ from __future__ import annotations
import itertools
import os
import struct
from typing import IO
from . import (
Image,
@ -32,7 +33,7 @@ from . import (
from ._binary import o32le
def _save(im, fp, filename):
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
JpegImagePlugin._save(im, fp, filename)
@ -100,7 +101,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
format_description = "MPO (CIPA DC-007)"
_close_exclusive_fp_after_loading = False
def _open(self):
def _open(self) -> None:
self.fp.seek(0) # prep the fp in order to pass the JPEG test
JpegImagePlugin.JpegImageFile._open(self)
self._after_jpeg_open()
@ -124,10 +125,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
# for now we can only handle reading and individual frame extraction
self.readonly = 1
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
self._fp.seek(pos)
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
self.fp = self._fp
@ -149,7 +150,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
self.__frame = frame
def tell(self):
def tell(self) -> int:
return self.__frame
@staticmethod

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,30 +53,32 @@ 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):
def end_document(self) -> None:
"""Ends printing. (Write PostScript DSC footer.)"""
self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n")
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

@ -25,6 +25,7 @@ import io
import math
import os
import time
from typing import IO
from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
@ -39,7 +40,7 @@ from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
# 5. page contents
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)

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")
@ -87,10 +87,10 @@ class IndirectReferenceTuple(NamedTuple):
class IndirectReference(IndirectReferenceTuple):
def __str__(self):
def __str__(self) -> str:
return f"{self.object_id} {self.generation} R"
def __bytes__(self):
def __bytes__(self) -> bytes:
return self.__str__().encode("us-ascii")
def __eq__(self, other):
@ -103,12 +103,12 @@ 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))
class IndirectObjectDef(IndirectReference):
def __str__(self):
def __str__(self) -> str:
return f"{self.object_id} {self.generation} obj"
@ -150,7 +150,7 @@ class XrefTable:
def __contains__(self, key):
return key in self.existing_entries or key in self.new_entries
def __len__(self):
def __len__(self) -> int:
return len(
set(self.existing_entries.keys())
| set(self.new_entries.keys())
@ -211,7 +211,7 @@ class PdfName:
else:
self.name = name.encode("us-ascii")
def name_as_str(self):
def name_as_str(self) -> str:
return self.name.decode("us-ascii")
def __eq__(self, other):
@ -219,10 +219,10 @@ 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):
def __repr__(self) -> str:
return f"{self.__class__.__name__}({repr(self.name)})"
@classmethod
@ -231,7 +231,7 @@ class PdfName:
allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
def __bytes__(self):
def __bytes__(self) -> bytes:
result = bytearray(b"/")
for b in self.name:
if b in self.allowed_chars:
@ -242,7 +242,7 @@ class PdfName:
class PdfArray(List[Any]):
def __bytes__(self):
def __bytes__(self) -> bytes:
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
@ -286,7 +286,7 @@ class PdfDict(_DictBase):
value = time.gmtime(calendar.timegm(value) + offset)
return value
def __bytes__(self):
def __bytes__(self) -> bytes:
out = bytearray(b"<<")
for key, value in self.items():
if value is None:
@ -304,7 +304,7 @@ class PdfBinary:
def __init__(self, data):
self.data = data
def __bytes__(self):
def __bytes__(self) -> bytes:
return b"<%s>" % b"".join(b"%02X" % b for b in self.data)
@ -402,41 +402,41 @@ class PdfParser:
if f:
self.seek_end()
def __enter__(self):
def __enter__(self) -> PdfParser:
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
return False # do not suppress exceptions
def start_writing(self):
def start_writing(self) -> None:
self.close_buf()
self.seek_end()
def close_buf(self):
def close_buf(self) -> None:
try:
self.buf.close()
except AttributeError:
pass
self.buf = None
def close(self):
def close(self) -> None:
if self.should_close_buf:
self.close_buf()
if self.f is not None and self.should_close_file:
self.f.close()
self.f = None
def seek_end(self):
def seek_end(self) -> None:
self.f.seek(0, os.SEEK_END)
def write_header(self):
def write_header(self) -> None:
self.f.write(b"%PDF-1.4\n")
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)
@ -450,7 +450,7 @@ class PdfParser:
)
return self.root_ref
def rewrite_pages(self):
def rewrite_pages(self) -> None:
pages_tree_nodes_to_delete = []
for i, page_ref in enumerate(self.orig_pages):
page_info = self.cached_objects[page_ref]
@ -529,7 +529,7 @@ class PdfParser:
f.write(b"endobj\n")
return ref
def del_root(self):
def del_root(self) -> None:
if self.root_ref is None:
return
del self.xref_table[self.root_ref.object_id]
@ -547,7 +547,7 @@ class PdfParser:
except ValueError: # cannot mmap an empty file
return b""
def read_pdf_info(self):
def read_pdf_info(self) -> None:
self.file_size_total = len(self.buf)
self.file_size_this = self.file_size_total - self.start_offset
self.read_trailer()
@ -823,11 +823,10 @@ class PdfParser:
m = cls.re_stream_start.match(data, offset)
if m:
try:
stream_len = int(result[b"Length"])
except (TypeError, KeyError, ValueError) as e:
msg = "bad or missing Length in stream dict (%r)" % result.get(
b"Length", None
)
stream_len_str = result.get(b"Length")
stream_len = int(stream_len_str)
except (TypeError, ValueError) as e:
msg = f"bad or missing Length in stream dict ({stream_len_str})"
raise PdfFormatError(msg) from e
stream_data = data[m.end() : m.end() + stream_len]
m = cls.re_stream_end.match(data, m.end() + stream_len)

View File

@ -39,6 +39,7 @@ import struct
import warnings
import zlib
from enum import IntEnum
from typing import IO, Any
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16
@ -149,14 +150,15 @@ def _crc32(data, seed=0):
class ChunkStream:
def __init__(self, fp):
self.fp = fp
self.queue = []
def __init__(self, fp: IO[bytes]) -> None:
self.fp: IO[bytes] | None = fp
self.queue: list[tuple[bytes, int, int]] | None = []
def read(self):
def read(self) -> tuple[bytes, int, int]:
"""Fetch a new chunk. Returns header information."""
cid = None
assert self.fp is not None
if self.queue:
cid, pos, length = self.queue.pop()
self.fp.seek(pos)
@ -173,16 +175,17 @@ class ChunkStream:
return cid, pos, length
def __enter__(self):
def __enter__(self) -> ChunkStream:
return self
def __exit__(self, *args):
self.close()
def close(self):
def close(self) -> None:
self.queue = self.fp = None
def push(self, cid, pos, length):
def push(self, cid: bytes, pos: int, length: int) -> None:
assert self.queue is not None
self.queue.append((cid, pos, length))
def call(self, cid, pos, length):
@ -191,7 +194,7 @@ class ChunkStream:
logger.debug("STREAM %r %s %s", cid, pos, length)
return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length)
def crc(self, cid, data):
def crc(self, cid: bytes, data: bytes) -> None:
"""Read and verify checksum"""
# Skip CRC checks for ancillary chunks if allowed to load truncated
@ -201,6 +204,7 @@ class ChunkStream:
self.crc_skip(cid, data)
return
assert self.fp is not None
try:
crc1 = _crc32(data, _crc32(cid))
crc2 = i32(self.fp.read(4))
@ -211,12 +215,13 @@ class ChunkStream:
msg = f"broken PNG file (incomplete checksum in {repr(cid)})"
raise SyntaxError(msg) from e
def crc_skip(self, cid, data):
def crc_skip(self, cid: bytes, data: bytes) -> None:
"""Read checksum"""
assert self.fp is not None
self.fp.read(4)
def verify(self, endchunk=b"IEND"):
def verify(self, endchunk: bytes = b"IEND") -> list[bytes]:
# Simple approach; just calculate checksum for all remaining
# blocks. Must be called directly after open.
@ -361,7 +366,7 @@ class PngStream(ChunkStream):
self.text_memory = 0
def check_text_memory(self, chunklen):
def check_text_memory(self, chunklen: int) -> None:
self.text_memory += chunklen
if self.text_memory > MAX_TEXT_MEMORY:
msg = (
@ -370,19 +375,19 @@ class PngStream(ChunkStream):
)
raise ValueError(msg)
def save_rewind(self):
def save_rewind(self) -> None:
self.rewind_state = {
"info": self.im_info.copy(),
"tile": self.im_tile,
"seq_num": self._seq_num,
}
def rewind(self):
def rewind(self) -> None:
self.im_info = self.rewind_state["info"].copy()
self.im_tile = self.rewind_state["tile"]
self._seq_num = self.rewind_state["seq_num"]
def chunk_iCCP(self, pos, length):
def chunk_iCCP(self, pos: int, length: int) -> bytes:
# ICC profile
s = ImageFile._safe_read(self.fp, length)
# according to PNG spec, the iCCP chunk contains:
@ -409,7 +414,7 @@ class PngStream(ChunkStream):
self.im_info["icc_profile"] = icc_profile
return s
def chunk_IHDR(self, pos, length):
def chunk_IHDR(self, pos: int, length: int) -> bytes:
# image header
s = ImageFile._safe_read(self.fp, length)
if length < 13:
@ -446,14 +451,14 @@ class PngStream(ChunkStream):
msg = "end of PNG image"
raise EOFError(msg)
def chunk_PLTE(self, pos, length):
def chunk_PLTE(self, pos: int, length: int) -> bytes:
# palette
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
self.im_palette = "RGB", s
return s
def chunk_tRNS(self, pos, length):
def chunk_tRNS(self, pos: int, length: int) -> bytes:
# transparency
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
@ -473,13 +478,13 @@ class PngStream(ChunkStream):
self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
return s
def chunk_gAMA(self, pos, length):
def chunk_gAMA(self, pos: int, length: int) -> bytes:
# gamma setting
s = ImageFile._safe_read(self.fp, length)
self.im_info["gamma"] = i32(s) / 100000.0
return s
def chunk_cHRM(self, pos, length):
def chunk_cHRM(self, pos: int, length: int) -> bytes:
# chromaticity, 8 unsigned ints, actual value is scaled by 100,000
# WP x,y, Red x,y, Green x,y Blue x,y
@ -488,7 +493,7 @@ class PngStream(ChunkStream):
self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
return s
def chunk_sRGB(self, pos, length):
def chunk_sRGB(self, pos: int, length: int) -> bytes:
# srgb rendering intent, 1 byte
# 0 perceptual
# 1 relative colorimetric
@ -504,7 +509,7 @@ class PngStream(ChunkStream):
self.im_info["srgb"] = s[0]
return s
def chunk_pHYs(self, pos, length):
def chunk_pHYs(self, pos: int, length: int) -> bytes:
# pixels per unit
s = ImageFile._safe_read(self.fp, length)
if length < 9:
@ -521,7 +526,7 @@ class PngStream(ChunkStream):
self.im_info["aspect"] = px, py
return s
def chunk_tEXt(self, pos, length):
def chunk_tEXt(self, pos: int, length: int) -> bytes:
# text
s = ImageFile._safe_read(self.fp, length)
try:
@ -540,7 +545,7 @@ class PngStream(ChunkStream):
return s
def chunk_zTXt(self, pos, length):
def chunk_zTXt(self, pos: int, length: int) -> bytes:
# compressed text
s = ImageFile._safe_read(self.fp, length)
try:
@ -574,7 +579,7 @@ class PngStream(ChunkStream):
return s
def chunk_iTXt(self, pos, length):
def chunk_iTXt(self, pos: int, length: int) -> bytes:
# international text
r = s = ImageFile._safe_read(self.fp, length)
try:
@ -614,13 +619,13 @@ class PngStream(ChunkStream):
return s
def chunk_eXIf(self, pos, length):
def chunk_eXIf(self, pos: int, length: int) -> bytes:
s = ImageFile._safe_read(self.fp, length)
self.im_info["exif"] = b"Exif\x00\x00" + s
return s
# APNG chunks
def chunk_acTL(self, pos, length):
def chunk_acTL(self, pos: int, length: int) -> bytes:
s = ImageFile._safe_read(self.fp, length)
if length < 8:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -640,7 +645,7 @@ class PngStream(ChunkStream):
self.im_custom_mimetype = "image/apng"
return s
def chunk_fcTL(self, pos, length):
def chunk_fcTL(self, pos: int, length: int) -> bytes:
s = ImageFile._safe_read(self.fp, length)
if length < 26:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -669,7 +674,7 @@ class PngStream(ChunkStream):
self.im_info["blend"] = s[25]
return s
def chunk_fdAT(self, pos, length):
def chunk_fdAT(self, pos: int, length: int) -> bytes:
if length < 4:
if ImageFile.LOAD_TRUNCATED_IMAGES:
s = ImageFile._safe_read(self.fp, length)
@ -701,7 +706,7 @@ class PngImageFile(ImageFile.ImageFile):
format = "PNG"
format_description = "Portable network graphics"
def _open(self):
def _open(self) -> None:
if not _accept(self.fp.read(8)):
msg = "not a PNG file"
raise SyntaxError(msg)
@ -711,8 +716,8 @@ class PngImageFile(ImageFile.ImageFile):
#
# Parse headers up to the first IDAT or fDAT chunk
self.private_chunks = []
self.png = PngStream(self.fp)
self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = []
self.png: PngStream | None = PngStream(self.fp)
while True:
#
@ -793,6 +798,7 @@ class PngImageFile(ImageFile.ImageFile):
# back up to beginning of IDAT block
self.fp.seek(self.tile[0][2] - 8)
assert self.png is not None
self.png.verify()
self.png.close()
@ -800,7 +806,7 @@ class PngImageFile(ImageFile.ImageFile):
self.fp.close()
self.fp = None
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
if frame < self.__frame:
@ -909,10 +915,10 @@ class PngImageFile(ImageFile.ImageFile):
else:
self.dispose = None
def tell(self):
def tell(self) -> int:
return self.__frame
def load_prepare(self):
def load_prepare(self) -> None:
"""internal: prepare to read PNG file"""
if self.info.get("interlace"):
@ -921,9 +927,10 @@ class PngImageFile(ImageFile.ImageFile):
self.__idat = self.__prepare_idat # used by load_read()
ImageFile.ImageFile.load_prepare(self)
def load_read(self, read_bytes):
def load_read(self, read_bytes: int) -> bytes:
"""internal: read more image data"""
assert self.png is not None
while self.__idat == 0:
# end of chunk, skip forward to next one
@ -954,8 +961,9 @@ class PngImageFile(ImageFile.ImageFile):
return self.fp.read(read_bytes)
def load_end(self):
def load_end(self) -> None:
"""internal: finished reading image data"""
assert self.png is not None
if self.__idat != 0:
self.fp.read(self.__idat)
while True:
@ -1011,7 +1019,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:
@ -1024,7 +1032,7 @@ class PngImageFile(ImageFile.ImageFile):
return super().getexif()
def getxmp(self):
def getxmp(self) -> dict[str, Any]:
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
@ -1042,22 +1050,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"),
}
@ -1079,7 +1087,7 @@ class _idat:
self.fp = fp
self.chunk = chunk
def write(self, data):
def write(self, data: bytes) -> None:
self.chunk(self.fp, b"IDAT", data)
@ -1091,7 +1099,7 @@ class _fdat:
self.chunk = chunk
self.seq_num = seq_num
def write(self, data):
def write(self, data: bytes) -> None:
self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
self.seq_num += 1
@ -1226,7 +1234,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)
@ -1286,7 +1294,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
@ -1301,7 +1309,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
@ -1436,10 +1445,10 @@ def getchunks(im, **params):
class collector:
data = []
def write(self, data):
def write(self, data: bytes) -> None:
pass
def append(self, chunk):
def append(self, chunk: bytes) -> None:
self.data.append(chunk)
def append(fp, cid, *data):

View File

@ -57,7 +57,7 @@ class PsdImageFile(ImageFile.ImageFile):
format_description = "Adobe Photoshop"
_close_exclusive_fp_after_loading = False
def _open(self):
def _open(self) -> None:
read = self.fp.read
#
@ -141,23 +141,22 @@ class PsdImageFile(ImageFile.ImageFile):
self.frame = 1
self._min_frame = 1
def seek(self, layer):
def seek(self, layer: int) -> None:
if not self._seek_check(layer):
return
# seek to given layer (1..max)
try:
name, mode, bbox, tile = self.layers[layer - 1]
_, mode, _, tile = self.layers[layer - 1]
self._mode = mode
self.tile = tile
self.frame = layer
self.fp = self._fp
return name, bbox
except IndexError as e:
msg = "no such layer"
raise EOFError(msg) from e
def tell(self):
def tell(self) -> int:
# return layer number (0=image, 1..max=layers)
return self.frame

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
@ -70,14 +74,15 @@ class PyAccess:
# logger.debug("%s", vals)
self._post_init()
def _post_init(self):
def _post_init(self) -> None:
pass
def __setitem__(self, xy, color):
"""
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

@ -21,7 +21,7 @@ class QoiImageFile(ImageFile.ImageFile):
format = "QOI"
format_description = "Quite OK Image"
def _open(self):
def _open(self) -> None:
if not _accept(self.fp.read(4)):
msg = "not a QOI file"
raise SyntaxError(msg)
@ -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

@ -37,6 +37,7 @@ from __future__ import annotations
import os
import struct
import sys
from typing import IO, TYPE_CHECKING
from . import Image, ImageFile
@ -97,7 +98,7 @@ class SpiderImageFile(ImageFile.ImageFile):
format_description = "Spider 2D image"
_close_exclusive_fp_after_loading = False
def _open(self):
def _open(self) -> None:
# check header
n = 27 * 4 # read 27 float values
f = self.fp.read(n)
@ -157,21 +158,21 @@ class SpiderImageFile(ImageFile.ImageFile):
self._fp = self.fp # FIXME: hack
@property
def n_frames(self):
def n_frames(self) -> int:
return self._nimages
@property
def is_animated(self):
def is_animated(self) -> bool:
return self._nimages > 1
# 1st image index is zero (although SPIDER imgnumber starts at 1)
def tell(self):
def tell(self) -> int:
if self.imgnumber < 1:
return 0
else:
return self.imgnumber - 1
def seek(self, frame):
def seek(self, frame: int) -> None:
if self.istack == 0:
msg = "attempt to seek in a non-stack file"
raise EOFError(msg)
@ -191,8 +192,11 @@ class SpiderImageFile(ImageFile.ImageFile):
b = -m * minimum
return self.point(lambda i, m=m, b=b: i * m + b).convert("L")
if TYPE_CHECKING:
from . import ImageTk
# returns a ImageTk.PhotoImage object, after rescaling to 0..255
def tkPhotoImage(self):
def tkPhotoImage(self) -> ImageTk.PhotoImage:
from . import ImageTk
return ImageTk.PhotoImage(self.convert2byte(), palette=256)
@ -229,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)
@ -259,7 +263,7 @@ def makeSpiderHeader(im):
return [struct.pack("f", v) for v in hdr]
def _save(im, fp, filename):
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if im.mode[0] != "F":
im = im.convert("F")
@ -275,7 +279,7 @@ def _save(im, fp, filename):
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
def _save_spider(im, fp, filename):
def _save_spider(im: Image.Image, fp: IO[bytes], filename: str) -> None:
# get the filename extension and register it with Image
ext = os.path.splitext(filename)[1]
Image.register_extension(SpiderImageFile.format, ext)

View File

@ -381,7 +381,7 @@ class IFDRational(Rational):
f = self._val.limit_denominator(max_denominator)
return f.numerator, f.denominator
def __repr__(self):
def __repr__(self) -> str:
return str(float(self._val))
def __hash__(self):
@ -603,7 +603,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._next = None
self._offset = None
def __str__(self):
def __str__(self) -> str:
return str(dict(self))
def named(self):
@ -617,7 +617,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
for code, value in self.items()
}
def __len__(self):
def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v2))
def __getitem__(self, tag):
@ -1041,7 +1041,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
ifd.next = original.next # an indicator for multipage tiffs
return ifd
def to_v2(self):
def to_v2(self) -> ImageFileDirectory_v2:
"""Returns an
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
instance with the same data as is contained in the original
@ -1061,7 +1061,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
def __contains__(self, tag):
return tag in self._tags_v1 or tag in self._tagdata
def __len__(self):
def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v1))
def __iter__(self):
@ -1143,7 +1143,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.seek(current)
return self._n_frames
def seek(self, frame):
def seek(self, frame: int) -> None:
"""Select a given frame as current image"""
if not self._seek_check(frame):
return
@ -1154,7 +1154,7 @@ class TiffImageFile(ImageFile.ImageFile):
Image._decompression_bomb_check(self.size)
self.im = Image.core.new(self.mode, self.size)
def _seek(self, frame):
def _seek(self, frame: int) -> None:
self.fp = self._fp
# reset buffered io handle in case fp
@ -1198,11 +1198,11 @@ class TiffImageFile(ImageFile.ImageFile):
self.__frame = frame
self._setup()
def tell(self):
def tell(self) -> int:
"""Return the current frame number"""
return self.__frame
def getxmp(self):
def getxmp(self) -> dict[str, Any]:
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
@ -1237,7 +1237,7 @@ class TiffImageFile(ImageFile.ImageFile):
return self._load_libtiff()
return super().load()
def load_end(self):
def load_end(self) -> None:
# allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below.
if not self.is_animated:
@ -1942,7 +1942,7 @@ class AppendingTiffWriter:
self.beginning = self.f.tell()
self.setup()
def setup(self):
def setup(self) -> None:
# Reset everything.
self.f.seek(self.beginning, os.SEEK_SET)
@ -1967,7 +1967,7 @@ class AppendingTiffWriter:
self.skipIFDs()
self.goToEnd()
def finalize(self):
def finalize(self) -> None:
if self.isFirst:
return
@ -1990,12 +1990,12 @@ class AppendingTiffWriter:
self.f.seek(ifd_offset)
self.fixIFD()
def newFrame(self):
def newFrame(self) -> None:
# Call this to finish a frame.
self.finalize()
self.setup()
def __enter__(self):
def __enter__(self) -> AppendingTiffWriter:
return self
def __exit__(self, exc_type, exc_value, traceback):
@ -2003,7 +2003,7 @@ class AppendingTiffWriter:
self.close()
return False
def tell(self):
def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage
def seek(self, offset, whence=io.SEEK_SET):
@ -2013,7 +2013,7 @@ class AppendingTiffWriter:
self.f.seek(offset, whence)
return self.tell()
def goToEnd(self):
def goToEnd(self) -> None:
self.f.seek(0, os.SEEK_END)
pos = self.f.tell()
@ -2023,13 +2023,13 @@ class AppendingTiffWriter:
self.f.write(bytes(pad_bytes))
self.offsetOfNewPage = self.f.tell()
def setEndian(self, endian):
def setEndian(self, endian: str) -> None:
self.endian = endian
self.longFmt = f"{self.endian}L"
self.shortFmt = f"{self.endian}H"
self.tagFormat = f"{self.endian}HHL"
def skipIFDs(self):
def skipIFDs(self) -> None:
while True:
ifd_offset = self.readLong()
if ifd_offset == 0:
@ -2084,11 +2084,11 @@ class AppendingTiffWriter:
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
def close(self):
def close(self) -> None:
self.finalize()
self.f.close()
def fixIFD(self):
def fixIFD(self) -> None:
num_tags = self.readShort()
for i in range(num_tags):

View File

@ -32,7 +32,7 @@ class WalImageFile(ImageFile.ImageFile):
format = "WAL"
format_description = "Quake2 Texture"
def _open(self):
def _open(self) -> None:
self._mode = "P"
# read header fields

View File

@ -1,6 +1,7 @@
from __future__ import annotations
from io import BytesIO
from typing import Any
from . import Image, ImageFile
@ -43,7 +44,7 @@ class WebPImageFile(ImageFile.ImageFile):
__loaded = 0
__logical_frame = 0
def _open(self):
def _open(self) -> None:
if not _webp.HAVE_WEBPANIM:
# Legacy mode
data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode(
@ -95,12 +96,12 @@ 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()
def getxmp(self):
def getxmp(self) -> dict[str, Any]:
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
@ -109,14 +110,14 @@ class WebPImageFile(ImageFile.ImageFile):
"""
return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {}
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
# 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
@ -144,7 +145,7 @@ class WebPImageFile(ImageFile.ImageFile):
timestamp -= duration
return data, timestamp, duration
def _seek(self, frame):
def _seek(self, frame: int) -> None:
if self.__physical_frame == frame:
return # Nothing to do
if frame < self.__physical_frame:
@ -171,10 +172,10 @@ class WebPImageFile(ImageFile.ImageFile):
return super().load()
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
pass
def tell(self):
def tell(self) -> int:
if not _webp.HAVE_WEBPANIM:
return super().tell()

View File

@ -20,6 +20,8 @@
# http://wvware.sourceforge.net/caolan/ora-wmf.html
from __future__ import annotations
from typing import IO
from . import Image, ImageFile
from ._binary import i16le as word
from ._binary import si16le as short
@ -28,7 +30,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 +43,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",
@ -79,7 +81,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
format = "WMF"
format_description = "Windows Metafile"
def _open(self):
def _open(self) -> None:
self._inch = None
# check placable header
@ -147,7 +149,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):
@ -161,7 +163,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
return super().load()
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 = "WMF save handler not installed"
raise OSError(msg)

View File

@ -36,7 +36,7 @@ class XpmImageFile(ImageFile.ImageFile):
format = "XPM"
format_description = "X11 Pixel Map"
def _open(self):
def _open(self) -> None:
if not _accept(self.fp.read(9)):
msg = "not an XPM file"
raise SyntaxError(msg)
@ -103,16 +103,13 @@ class XpmImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))]
def load_read(self, read_bytes):
def load_read(self, read_bytes: int) -> bytes:
#
# load all image data in one chunk
xsize, ysize = self.size
s = [None] * ysize
for i in range(ysize):
s[i] = self.fp.readline()[1 : xsize + 1].ljust(xsize)
s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)]
return b"".join(s)

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]

View File

@ -18,7 +18,7 @@ modules = {
}
def check_module(feature):
def check_module(feature: str) -> bool:
"""
Checks if a module is available.
@ -42,7 +42,7 @@ def check_module(feature):
return False
def version_module(feature):
def version_module(feature: str) -> str | None:
"""
:param feature: The module to check for.
:returns:
@ -54,13 +54,10 @@ def version_module(feature):
module, ver = modules[feature]
if ver is None:
return None
return getattr(__import__(module, fromlist=[ver]), ver)
def get_supported_modules():
def get_supported_modules() -> list[str]:
"""
:returns: A list of all supported modules.
"""
@ -75,7 +72,7 @@ codecs = {
}
def check_codec(feature):
def check_codec(feature: str) -> bool:
"""
Checks if a codec is available.
@ -92,7 +89,7 @@ def check_codec(feature):
return f"{codec}_encoder" in dir(Image.core)
def version_codec(feature):
def version_codec(feature: str) -> str | None:
"""
:param feature: The codec to check for.
:returns:
@ -113,7 +110,7 @@ def version_codec(feature):
return version
def get_supported_codecs():
def get_supported_codecs() -> list[str]:
"""
:returns: A list of all supported codecs.
"""
@ -133,7 +130,7 @@ features = {
}
def check_feature(feature):
def check_feature(feature: str) -> bool | None:
"""
Checks if a feature is available.
@ -157,7 +154,7 @@ def check_feature(feature):
return None
def version_feature(feature):
def version_feature(feature: str) -> str | None:
"""
:param feature: The feature to check for.
:returns: The version number as a string, or ``None`` if not available.
@ -174,14 +171,14 @@ def version_feature(feature):
return getattr(__import__(module, fromlist=[ver]), ver)
def get_supported_features():
def get_supported_features() -> list[str]:
"""
:returns: A list of all supported features.
"""
return [f for f in features if check_feature(f)]
def check(feature):
def check(feature: str) -> bool | None:
"""
:param feature: A module, codec, or feature name.
:returns:
@ -199,7 +196,7 @@ def check(feature):
return False
def version(feature):
def version(feature: str) -> str | None:
"""
:param feature:
The module, codec, or feature to check for.
@ -215,7 +212,7 @@ def version(feature):
return None
def get_supported():
def get_supported() -> list[str]:
"""
:returns: A list of all supported modules, features, and codecs.
"""

View File

@ -128,14 +128,7 @@ PyImagingPhotoPut(
block.pixelPtr = (unsigned char *)im->block;
TK_PHOTO_PUT_BLOCK(
interp,
photo,
&block,
0,
0,
block.width,
block.height,
TK_PHOTO_COMPOSITE_SET);
interp, photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET);
return TCL_OK;
}
@ -287,7 +280,7 @@ load_tkinter_funcs(void) {
* Return 0 for success, non-zero for failure.
*/
HMODULE* hMods = NULL;
HMODULE *hMods = NULL;
HANDLE hProcess;
DWORD cbNeeded;
unsigned int i;
@ -313,7 +306,7 @@ load_tkinter_funcs(void) {
#endif
return 1;
}
if (!(hMods = (HMODULE*) malloc(cbNeeded))) {
if (!(hMods = (HMODULE *)malloc(cbNeeded))) {
PyErr_NoMemory();
return 1;
}
@ -345,7 +338,7 @@ load_tkinter_funcs(void) {
} else if (found_tk == 0) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines");
}
return (int) ((found_tcl != 1) || (found_tk != 1));
return (int)((found_tcl != 1) || (found_tk != 1));
}
#else /* not Windows */
@ -400,8 +393,8 @@ _func_loader(void *lib) {
return 1;
}
return (
(TK_PHOTO_PUT_BLOCK =
(Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL);
(TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) ==
NULL);
}
int

View File

@ -110,7 +110,7 @@
#define B16(p, i) ((((int)p[(i)]) << 8) + p[(i) + 1])
#define L16(p, i) ((((int)p[(i) + 1]) << 8) + p[(i)])
#define S16(v) ((v) < 32768 ? (v) : ((v)-65536))
#define S16(v) ((v) < 32768 ? (v) : ((v) - 65536))
/* -------------------------------------------------------------------- */
/* OBJECT ADMINISTRATION */
@ -533,7 +533,9 @@ getink(PyObject *color, Imaging im, char *ink) {
/* unsigned integer, single layer */
if (rIsInt != 1) {
if (tupleSize != 1) {
PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple");
PyErr_SetString(
PyExc_TypeError,
"color must be int or single-element tuple");
return NULL;
} else if (!PyArg_ParseTuple(color, "L", &r)) {
return NULL;
@ -552,7 +554,9 @@ getink(PyObject *color, Imaging im, char *ink) {
a = 255;
if (im->bands == 2) {
if (tupleSize != 1 && tupleSize != 2) {
PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements");
PyErr_SetString(
PyExc_TypeError,
"color must be int, or tuple of one or two elements");
return NULL;
} else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) {
return NULL;
@ -560,7 +564,10 @@ getink(PyObject *color, Imaging im, char *ink) {
g = b = r;
} else {
if (tupleSize != 3 && tupleSize != 4) {
PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one, three or four elements");
PyErr_SetString(
PyExc_TypeError,
"color must be int, or tuple of one, three or four "
"elements");
return NULL;
} else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) {
return NULL;
@ -599,7 +606,9 @@ getink(PyObject *color, Imaging im, char *ink) {
g = (UINT8)(r >> 8);
r = (UINT8)r;
} else if (tupleSize != 3) {
PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or three elements");
PyErr_SetString(
PyExc_TypeError,
"color must be int, or tuple of one or three elements");
return NULL;
} else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) {
return NULL;
@ -1537,14 +1546,14 @@ _putdata(ImagingObject *self, PyObject *args) {
return NULL;
}
#define set_value_to_item(seq, i) \
op = PySequence_Fast_GET_ITEM(seq, i); \
if (PySequence_Check(op)) { \
PyErr_SetString(PyExc_TypeError, "sequence must be flattened"); \
return NULL; \
} else { \
value = PyFloat_AsDouble(op); \
}
#define set_value_to_item(seq, i) \
op = PySequence_Fast_GET_ITEM(seq, i); \
if (PySequence_Check(op)) { \
PyErr_SetString(PyExc_TypeError, "sequence must be flattened"); \
return NULL; \
} else { \
value = PyFloat_AsDouble(op); \
}
if (image->image8) {
if (PyBytes_Check(data)) {
unsigned char *p;
@ -1596,8 +1605,10 @@ if (PySequence_Check(op)) { \
value = value * scale + offset;
}
if (image->type == IMAGING_TYPE_SPECIAL) {
image->image8[y][x * 2 + (bigendian ? 1 : 0)] = CLIP8((int)value % 256);
image->image8[y][x * 2 + (bigendian ? 0 : 1)] = CLIP8((int)value >> 8);
image->image8[y][x * 2 + (bigendian ? 1 : 0)] =
CLIP8((int)value % 256);
image->image8[y][x * 2 + (bigendian ? 0 : 1)] =
CLIP8((int)value >> 8);
} else {
image->image8[y][x] = (UINT8)CLIP8(value);
}
@ -1639,8 +1650,7 @@ if (PySequence_Check(op)) { \
for (i = x = y = 0; i < n; i++) {
double value;
set_value_to_item(seq, i);
IMAGING_PIXEL_INT32(image, x, y) =
(INT32)(value * scale + offset);
IMAGING_PIXEL_INT32(image, x, y) = (INT32)(value * scale + offset);
if (++x >= (int)image->xsize) {
x = 0, y++;
}
@ -2785,8 +2795,8 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
glyph = &self->glyphs[text[i]];
if (i == 0 || text[i] != text[i - 1]) {
ImagingDelete(bitmap);
bitmap =
ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
bitmap = ImagingCrop(
self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
if (!bitmap) {
goto failed;
}
@ -3315,7 +3325,8 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
free(xy);
if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < 0) {
if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) <
0) {
free(ixy);
return NULL;
}
@ -4411,7 +4422,8 @@ setup_module(PyObject *m) {
PyModule_AddObject(m, "HAVE_XCB", have_xcb);
PyObject *pillow_version = PyUnicode_FromString(version);
PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None);
PyDict_SetItemString(
d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None);
Py_XDECREF(pillow_version);
return 0;

View File

@ -213,11 +213,8 @@ cms_transform_dealloc(CmsTransformObject *self) {
static cmsUInt32Number
findLCMStype(char *PILmode) {
if (
strcmp(PILmode, "RGB") == 0 ||
strcmp(PILmode, "RGBA") == 0 ||
strcmp(PILmode, "RGBX") == 0
) {
if (strcmp(PILmode, "RGB") == 0 || strcmp(PILmode, "RGBA") == 0 ||
strcmp(PILmode, "RGBX") == 0) {
return TYPE_RGBA_8;
}
if (strcmp(PILmode, "RGBA;16B") == 0) {
@ -232,10 +229,7 @@ findLCMStype(char *PILmode) {
if (strcmp(PILmode, "L;16B") == 0) {
return TYPE_GRAY_16_SE;
}
if (
strcmp(PILmode, "YCCA") == 0 ||
strcmp(PILmode, "YCC") == 0
) {
if (strcmp(PILmode, "YCCA") == 0 || strcmp(PILmode, "YCC") == 0) {
return TYPE_YCbCr_8;
}
if (strcmp(PILmode, "LAB") == 0) {
@ -391,7 +385,7 @@ _buildTransform(
iRenderingIntent,
cmsFLAGS);
Py_END_ALLOW_THREADS
Py_END_ALLOW_THREADS;
if (!hTransform) {
PyErr_SetString(PyExc_ValueError, "cannot build transform");
@ -425,7 +419,7 @@ _buildProofTransform(
iProofIntent,
cmsFLAGS);
Py_END_ALLOW_THREADS
Py_END_ALLOW_THREADS;
if (!hTransform) {
PyErr_SetString(PyExc_ValueError, "cannot build proof transform");
@ -622,7 +616,7 @@ cms_profile_is_intent_supported(CmsProfileObject *self, PyObject *args) {
static PyObject *
cms_get_display_profile_win32(PyObject *self, PyObject *args) {
char filename[MAX_PATH];
cmsUInt32Number filename_size;
DWORD filename_size;
BOOL ok;
HANDLE handle = 0;

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