Merge branch 'main' into type_hint_image

This commit is contained in:
Andrew Murray 2024-06-19 08:06:38 +10:00 committed by GitHub
commit 66ad49774d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 159 additions and 96 deletions

View File

@ -12,7 +12,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb: class TestDecompressionBomb:
def teardown_method(self, method) -> None: def teardown_method(self) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def test_no_warning_small_file(self) -> None: def test_no_warning_small_file(self) -> None:

View File

@ -443,7 +443,9 @@ class TestFileJpeg:
assert_image(im1, im2.mode, im2.size) assert_image(im1, im2.mode, im2.size)
def test_subsampling(self) -> None: def test_subsampling(self) -> None:
def getsampling(im: JpegImagePlugin.JpegImageFile): def getsampling(
im: JpegImagePlugin.JpegImageFile,
) -> tuple[int, int, int, int, int, int]:
layer = im.layer layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
@ -699,7 +701,7 @@ class TestFileJpeg:
def test_save_cjpeg(self, tmp_path: Path) -> None: def test_save_cjpeg(self, tmp_path: Path) -> None:
with Image.open(TEST_FILE) as img: with Image.open(TEST_FILE) as img:
tempfile = str(tmp_path / "temp.jpg") tempfile = str(tmp_path / "temp.jpg")
JpegImagePlugin._save_cjpeg(img, 0, tempfile) JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright # Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17) assert_image_similar_tofile(img, tempfile, 17)
@ -917,24 +919,25 @@ class TestFileJpeg:
with Image.open("Tests/images/icc-after-SOF.jpg") as im: with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile" assert im.info["icc_profile"] == b"profile"
def test_jpeg_magic_number(self) -> None: def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
size = 4097 size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer.max_pos = 0 max_pos = 0
orig_read = buffer.read orig_read = buffer.read
def read(n=-1): def read(n: int | None = -1) -> bytes:
nonlocal max_pos
res = orig_read(n) res = orig_read(n)
buffer.max_pos = max(buffer.max_pos, buffer.tell()) max_pos = max(max_pos, buffer.tell())
return res return res
buffer.read = read monkeypatch.setattr(buffer, "read", read)
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
with Image.open(buffer): with Image.open(buffer):
pass pass
# Assert the entire file has not been read # Assert the entire file has not been read
assert 0 < buffer.max_pos < size assert 0 < max_pos < size
def test_getxmp(self) -> None: def test_getxmp(self) -> None:
with Image.open("Tests/images/xmp_test.jpg") as im: with Image.open("Tests/images/xmp_test.jpg") as im:

View File

@ -460,7 +460,7 @@ def test_plt_marker() -> None:
out.seek(length - 2, os.SEEK_CUR) out.seek(length - 2, os.SEEK_CUR)
def test_9bit(): def test_9bit() -> None:
with Image.open("Tests/images/9bit.j2k") as im: with Image.open("Tests/images/9bit.j2k") as im:
assert im.mode == "I;16" assert im.mode == "I;16"
assert im.size == (128, 128) assert im.size == (128, 128)

View File

@ -113,7 +113,7 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_seek_too_large(self): def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"): with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif") Image.open("Tests/images/seek_too_large.tif")

View File

@ -152,7 +152,7 @@ class TestImage:
def test_stringio(self) -> None: def test_stringio(self) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
with Image.open(io.StringIO()): with Image.open(io.StringIO()): # type: ignore[arg-type]
pass pass
def test_pathlib(self, tmp_path: Path) -> None: def test_pathlib(self, tmp_path: Path) -> None:

View File

@ -68,7 +68,11 @@ def test_sanity() -> None:
), ),
) )
def test_properties( def test_properties(
mode, expected_base, expected_type, expected_bands, expected_band_names mode: str,
expected_base: str,
expected_type: str,
expected_bands: int,
expected_band_names: tuple[str, ...],
) -> None: ) -> None:
assert Image.getmodebase(mode) == expected_base assert Image.getmodebase(mode) == expected_base
assert Image.getmodetype(mode) == expected_type assert Image.getmodetype(mode) == expected_type

View File

@ -113,13 +113,13 @@ def test_array_F() -> None:
def test_not_flattened() -> None: def test_not_flattened() -> None:
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.putdata([[0]]) im.putdata([[0]]) # type: ignore[list-item]
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.putdata([[0]], 2) im.putdata([[0]], 2) # type: ignore[list-item]
with pytest.raises(TypeError): with pytest.raises(TypeError):
im = Image.new("I", (1, 1)) im = Image.new("I", (1, 1))
im.putdata([[0]]) im.putdata([[0]]) # type: ignore[list-item]
with pytest.raises(TypeError): with pytest.raises(TypeError):
im = Image.new("F", (1, 1)) im = Image.new("F", (1, 1))
im.putdata([[0]]) im.putdata([[0]]) # type: ignore[list-item]

View File

@ -98,7 +98,7 @@ def test_quantize_dither_diff() -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE) "method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
) )
def test_quantize_kmeans(method) -> None: def test_quantize_kmeans(method: Image.Quantize) -> None:
im = hopper() im = hopper()
no_kmeans = im.quantize(kmeans=0, method=method) no_kmeans = im.quantize(kmeans=0, method=method)
kmeans = im.quantize(kmeans=1, method=method) kmeans = im.quantize(kmeans=1, method=method)

View File

@ -56,10 +56,12 @@ def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) ->
@pytest.mark.parametrize( @pytest.mark.parametrize(
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
) )
def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None: def test_args_factor_error(
size: float | tuple[int, int], expected_error: type[Exception]
) -> None:
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
with pytest.raises(expected_error): with pytest.raises(expected_error):
im.reduce(size) im.reduce(size) # type: ignore[arg-type]
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -86,10 +88,12 @@ def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) ->
((5, 0, 5, 10), ValueError), ((5, 0, 5, 10), ValueError),
), ),
) )
def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None: def test_args_box_error(
size: str | tuple[int, int, int, int], expected_error: type[Exception]
) -> None:
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
with pytest.raises(expected_error): with pytest.raises(expected_error):
im.reduce(2, size).size im.reduce(2, size).size # type: ignore[arg-type]
@pytest.mark.parametrize("mode", ("P", "1", "I;16")) @pytest.mark.parametrize("mode", ("P", "1", "I;16"))

View File

@ -16,7 +16,7 @@ from .helper import (
def test_sanity() -> None: def test_sanity() -> None:
im = hopper() im = hopper()
assert im.thumbnail((100, 100)) is None im.thumbnail((100, 100))
assert im.size == (100, 100) assert im.size == (100, 100)

View File

@ -1562,7 +1562,11 @@ def test_compute_regular_polygon_vertices(
], ],
) )
def test_compute_regular_polygon_vertices_input_error_handling( def test_compute_regular_polygon_vertices_input_error_handling(
n_sides, bounding_circle, rotation, expected_error, error_message n_sides: int,
bounding_circle: int | tuple[int | tuple[int] | str, ...],
rotation: int | str,
expected_error: type[Exception],
error_message: str,
) -> None: ) -> None:
with pytest.raises(expected_error) as e: with pytest.raises(expected_error) as e:
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)

View File

@ -224,7 +224,7 @@ def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
line_spacing = font.getbbox("A")[3] + 4 line_spacing = font.getbbox("A")[3] + 4
lines = TEST_TEXT.split("\n") lines = TEST_TEXT.split("\n")
y = 0 y: float = 0
for line in lines: for line in lines:
draw.text((0, y), line, font=font) draw.text((0, y), line, font=font)
y += line_spacing y += line_spacing

View File

@ -454,7 +454,7 @@ def test_autocontrast_cutoff() -> None:
# Test the cutoff argument of autocontrast # Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img: with Image.open("Tests/images/bw_gradient.png") as img:
def autocontrast(cutoff: int | tuple[int, int]): def autocontrast(cutoff: int | tuple[int, int]) -> list[int]:
return ImageOps.autocontrast(img, cutoff).histogram() return ImageOps.autocontrast(img, cutoff).histogram()
assert autocontrast(10) == autocontrast((10, 10)) assert autocontrast(10) == autocontrast((10, 10))

View File

@ -70,7 +70,7 @@ if is_win32():
] ]
CreateDIBSection.restype = ctypes.wintypes.HBITMAP CreateDIBSection.restype = ctypes.wintypes.HBITMAP
def serialize_dib(bi, pixels) -> bytearray: def serialize_dib(bi: BITMAPINFOHEADER, pixels: ctypes.c_void_p) -> bytearray:
bf = BITMAPFILEHEADER() bf = BITMAPFILEHEADER()
bf.bfType = 0x4D42 bf.bfType = 0x4D42
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize

View File

@ -11,7 +11,7 @@ import pytest
"args, report", "args, report",
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)), ((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),
) )
def test_main(args, report) -> None: def test_main(args: list[str], report: bool) -> None:
args = [sys.executable, "-m"] + args args = [sys.executable, "-m"] + args
out = subprocess.check_output(args).decode("utf-8") out = subprocess.check_output(args).decode("utf-8")
lines = out.splitlines() lines = out.splitlines()

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from typing import TYPE_CHECKING, Any
import pytest import pytest
@ -8,13 +9,19 @@ from PIL import Image
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
numpy = pytest.importorskip("numpy", reason="NumPy not installed") if TYPE_CHECKING:
import numpy
import numpy.typing
else:
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
TEST_IMAGE_SIZE = (10, 10) TEST_IMAGE_SIZE = (10, 10)
def test_numpy_to_image() -> None: def test_numpy_to_image() -> None:
def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: def to_image(
dtype: numpy.typing.DTypeLike, bands: int = 1, boolean: int = 0
) -> Image.Image:
if bands == 1: if bands == 1:
if boolean: if boolean:
data = [0, 255] * 50 data = [0, 255] * 50
@ -99,14 +106,16 @@ def test_1d_array() -> None:
assert_image(Image.fromarray(a), "L", (1, 5)) assert_image(Image.fromarray(a), "L", (1, 5))
def _test_img_equals_nparray(img: Image.Image, np) -> None: def _test_img_equals_nparray(
assert len(np.shape) >= 2 img: Image.Image, np_img: numpy.typing.NDArray[Any]
np_size = np.shape[1], np.shape[0] ) -> None:
assert len(np_img.shape) >= 2
np_size = np_img.shape[1], np_img.shape[0]
assert img.size == np_size assert img.size == np_size
px = img.load() px = img.load()
for x in range(0, img.size[0], int(img.size[0] / 10)): for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)): for y in range(0, img.size[1], int(img.size[1] / 10)):
assert_deep_equal(px[x, y], np[y, x]) assert_deep_equal(px[x, y], np_img[y, x])
def test_16bit() -> None: def test_16bit() -> None:
@ -157,7 +166,7 @@ def test_save_tiff_uint16() -> None:
("HSV", numpy.uint8), ("HSV", numpy.uint8),
), ),
) )
def test_to_array(mode: str, dtype) -> None: def test_to_array(mode: str, dtype: numpy.typing.DTypeLike) -> None:
img = hopper(mode) img = hopper(mode)
# Resize to non-square # Resize to non-square
@ -207,7 +216,7 @@ def test_putdata() -> None:
numpy.float64, numpy.float64,
), ),
) )
def test_roundtrip_eye(dtype) -> None: def test_roundtrip_eye(dtype: numpy.typing.DTypeLike) -> None:
arr = numpy.eye(10, dtype=dtype) arr = numpy.eye(10, dtype=dtype)
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))

View File

@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
import shutil import shutil
from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Callable from typing import IO, Callable
import pytest import pytest
@ -22,11 +23,11 @@ class TestShellInjection:
self, self,
tmp_path: Path, tmp_path: Path,
src_img: Image.Image, src_img: Image.Image,
save_func: Callable[[Image.Image, int, str], None], save_func: Callable[[Image.Image, IO[bytes], str | bytes], None],
) -> None: ) -> None:
for filename in test_filenames: for filename in test_filenames:
dest_file = str(tmp_path / filename) dest_file = str(tmp_path / filename)
save_func(src_img, 0, dest_file) save_func(src_img, BytesIO(), dest_file)
# If file can't be opened, shell injection probably occurred # If file can't be opened, shell injection probably occurred
with Image.open(dest_file) as im: with Image.open(dest_file) as im:
im.load() im.load()

View File

@ -103,7 +103,7 @@ def bdf_char(
class BdfFontFile(FontFile.FontFile): class BdfFontFile(FontFile.FontFile):
"""Font file plugin for the X11 BDF format.""" """Font file plugin for the X11 BDF format."""
def __init__(self, fp: BinaryIO): def __init__(self, fp: BinaryIO) -> None:
super().__init__() super().__init__()
s = fp.readline() s = fp.readline()

View File

@ -34,12 +34,16 @@ from __future__ import annotations
import math import math
import numbers import numbers
import struct import struct
from types import ModuleType
from typing import TYPE_CHECKING, AnyStr, Sequence, cast from typing import TYPE_CHECKING, AnyStr, Sequence, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._deprecate import deprecate from ._deprecate import deprecate
from ._typing import Coords from ._typing import Coords
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont
""" """
A simple 2D drawing interface for PIL images. A simple 2D drawing interface for PIL images.
<p> <p>
@ -93,9 +97,6 @@ class ImageDraw:
self.fontmode = "L" # aliasing is okay for other modes self.fontmode = "L" # aliasing is okay for other modes
self.fill = False self.fill = False
if TYPE_CHECKING:
from . import ImageFont
def getfont( def getfont(
self, self,
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
@ -879,7 +880,7 @@ class ImageDraw:
return bbox return bbox
def Draw(im, mode: str | None = None) -> ImageDraw: def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
""" """
A simple 2D drawing interface for PIL images. A simple 2D drawing interface for PIL images.
@ -891,7 +892,7 @@ def Draw(im, mode: str | None = None) -> ImageDraw:
defaults to the mode of the image. defaults to the mode of the image.
""" """
try: try:
return im.getdraw(mode) return getattr(im, "getdraw")(mode)
except AttributeError: except AttributeError:
return ImageDraw(im, mode) return ImageDraw(im, mode)
@ -903,7 +904,9 @@ except AttributeError:
Outline = None Outline = None
def getdraw(im=None, hints=None): def getdraw(
im: Image.Image | None = None, hints: list[str] | None = None
) -> tuple[ImageDraw2.Draw | None, ModuleType]:
""" """
:param im: The image to draw in. :param im: The image to draw in.
:param hints: An optional list of hints. Deprecated. :param hints: An optional list of hints. Deprecated.
@ -913,9 +916,8 @@ def getdraw(im=None, hints=None):
deprecate("'hints' parameter", 12) deprecate("'hints' parameter", 12)
from . import ImageDraw2 from . import ImageDraw2
if im: draw = ImageDraw2.Draw(im) if im is not None else None
im = ImageDraw2.Draw(im) return draw, ImageDraw2
return im, ImageDraw2
def floodfill( def floodfill(

View File

@ -54,7 +54,7 @@ class ImagePalette:
self._palette = palette self._palette = palette
@property @property
def colors(self): def colors(self) -> dict[tuple[int, int, int] | tuple[int, int, int, int], int]:
if self._colors is None: if self._colors is None:
mode_len = len(self.mode) mode_len = len(self.mode)
self._colors = {} self._colors = {}
@ -66,7 +66,9 @@ class ImagePalette:
return self._colors return self._colors
@colors.setter @colors.setter
def colors(self, colors): def colors(
self, colors: dict[tuple[int, int, int] | tuple[int, int, int, int], int]
) -> None:
self._colors = colors self._colors = colors
def copy(self) -> ImagePalette: def copy(self) -> ImagePalette:
@ -107,11 +109,13 @@ class ImagePalette:
# Declare tostring as an alias for tobytes # Declare tostring as an alias for tobytes
tostring = tobytes tostring = tobytes
def _new_color_index(self, image=None, e=None): def _new_color_index(
self, image: Image.Image | None = None, e: Exception | None = None
) -> int:
if not isinstance(self.palette, bytearray): if not isinstance(self.palette, bytearray):
self._palette = bytearray(self.palette) self._palette = bytearray(self.palette)
index = len(self.palette) // 3 index = len(self.palette) // 3
special_colors = () special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
if image: if image:
special_colors = ( special_colors = (
image.info.get("background"), image.info.get("background"),

View File

@ -63,7 +63,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
msg = "not an MIC file; no image entries" msg = "not an MIC file; no image entries"
raise SyntaxError(msg) raise SyntaxError(msg)
self.frame = None self.frame = -1
self._n_frames = len(self.images) self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1 self.is_animated = self._n_frames > 1
@ -85,7 +85,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.frame = frame self.frame = frame
def tell(self): def tell(self) -> int:
return self.frame return self.frame
def close(self) -> None: def close(self) -> None:

View File

@ -39,7 +39,7 @@ import struct
import warnings import warnings
import zlib import zlib
from enum import IntEnum from enum import IntEnum
from typing import IO, Any from typing import IO, TYPE_CHECKING, Any, NoReturn
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -48,6 +48,9 @@ from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from ._binary import o32be as o32 from ._binary import o32be as o32
if TYPE_CHECKING:
from . import _imaging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
is_cid = re.compile(rb"\w\w\w\w").match is_cid = re.compile(rb"\w\w\w\w").match
@ -249,6 +252,9 @@ class iTXt(str):
""" """
lang: str | bytes | None
tkey: str | bytes | None
@staticmethod @staticmethod
def __new__(cls, text, lang=None, tkey=None): def __new__(cls, text, lang=None, tkey=None):
""" """
@ -270,10 +276,10 @@ class PngInfo:
""" """
def __init__(self): def __init__(self) -> None:
self.chunks = [] self.chunks: list[tuple[bytes, bytes, bool]] = []
def add(self, cid, data, after_idat=False): def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None:
"""Appends an arbitrary chunk. Use with caution. """Appends an arbitrary chunk. Use with caution.
:param cid: a byte string, 4 bytes long. :param cid: a byte string, 4 bytes long.
@ -283,12 +289,16 @@ class PngInfo:
""" """
chunk = [cid, data] self.chunks.append((cid, data, after_idat))
if after_idat:
chunk.append(True)
self.chunks.append(tuple(chunk))
def add_itxt(self, key, value, lang="", tkey="", zip=False): def add_itxt(
self,
key: str | bytes,
value: str | bytes,
lang: str | bytes = "",
tkey: str | bytes = "",
zip: bool = False,
) -> None:
"""Appends an iTXt chunk. """Appends an iTXt chunk.
:param key: latin-1 encodable text key name :param key: latin-1 encodable text key name
@ -316,7 +326,9 @@ class PngInfo:
else: else:
self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value)
def add_text(self, key, value, zip=False): def add_text(
self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False
) -> None:
"""Appends a text chunk. """Appends a text chunk.
:param key: latin-1 encodable text key name :param key: latin-1 encodable text key name
@ -326,7 +338,13 @@ class PngInfo:
""" """
if isinstance(value, iTXt): if isinstance(value, iTXt):
return self.add_itxt(key, value, value.lang, value.tkey, zip=zip) return self.add_itxt(
key,
value,
value.lang if value.lang is not None else b"",
value.tkey if value.tkey is not None else b"",
zip=zip,
)
# The tEXt chunk stores latin-1 text # The tEXt chunk stores latin-1 text
if not isinstance(value, bytes): if not isinstance(value, bytes):
@ -434,7 +452,7 @@ class PngStream(ChunkStream):
raise SyntaxError(msg) raise SyntaxError(msg)
return s return s
def chunk_IDAT(self, pos, length): def chunk_IDAT(self, pos: int, length: int) -> NoReturn:
# image data # image data
if "bbox" in self.im_info: if "bbox" in self.im_info:
tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)]
@ -447,7 +465,7 @@ class PngStream(ChunkStream):
msg = "image data found" msg = "image data found"
raise EOFError(msg) raise EOFError(msg)
def chunk_IEND(self, pos, length): def chunk_IEND(self, pos: int, length: int) -> NoReturn:
msg = "end of PNG image" msg = "end of PNG image"
raise EOFError(msg) raise EOFError(msg)
@ -821,7 +839,10 @@ class PngImageFile(ImageFile.ImageFile):
msg = "no more images in APNG file" msg = "no more images in APNG file"
raise EOFError(msg) from e raise EOFError(msg) from e
def _seek(self, frame, rewind=False): def _seek(self, frame: int, rewind: bool = False) -> None:
assert self.png is not None
self.dispose: _imaging.ImagingCore | None
if frame == 0: if frame == 0:
if rewind: if rewind:
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)
@ -906,14 +927,14 @@ class PngImageFile(ImageFile.ImageFile):
if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
self.dispose_op = Disposal.OP_BACKGROUND self.dispose_op = Disposal.OP_BACKGROUND
self.dispose = None
if self.dispose_op == Disposal.OP_PREVIOUS: if self.dispose_op == Disposal.OP_PREVIOUS:
self.dispose = self._prev_im.copy() if self._prev_im:
self.dispose = self._crop(self.dispose, self.dispose_extent) self.dispose = self._prev_im.copy()
self.dispose = self._crop(self.dispose, self.dispose_extent)
elif self.dispose_op == Disposal.OP_BACKGROUND: elif self.dispose_op == Disposal.OP_BACKGROUND:
self.dispose = Image.core.fill(self.mode, self.size) self.dispose = Image.core.fill(self.mode, self.size)
self.dispose = self._crop(self.dispose, self.dispose_extent) self.dispose = self._crop(self.dispose, self.dispose_extent)
else:
self.dispose = None
def tell(self) -> int: def tell(self) -> int:
return self.__frame return self.__frame
@ -1026,7 +1047,7 @@ class PngImageFile(ImageFile.ImageFile):
return None return None
return self.getexif()._get_merged_dict() return self.getexif()._get_merged_dict()
def getexif(self): def getexif(self) -> Image.Exif:
if "exif" not in self.info: if "exif" not in self.info:
self.load() self.load()
@ -1346,7 +1367,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, cid, data) chunk(fp, cid, data)
elif cid[1:2].islower(): elif cid[1:2].islower():
# Private chunk # Private chunk
after_idat = info_chunk[2:3] after_idat = len(info_chunk) == 3 and info_chunk[2]
if not after_idat: if not after_idat:
chunk(fp, cid, data) chunk(fp, cid, data)
@ -1425,7 +1446,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
cid, data = info_chunk[:2] cid, data = info_chunk[:2]
if cid[1:2].islower(): if cid[1:2].islower():
# Private chunk # Private chunk
after_idat = info_chunk[2:3] after_idat = len(info_chunk) == 3 and info_chunk[2]
if after_idat: if after_idat:
chunk(fp, cid, data) chunk(fp, cid, data)

View File

@ -50,7 +50,7 @@ import warnings
from collections.abc import MutableMapping from collections.abc import MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational from numbers import Number, Rational
from typing import IO, TYPE_CHECKING, Any, Callable from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -384,7 +384,7 @@ class IFDRational(Rational):
def __repr__(self) -> str: def __repr__(self) -> str:
return str(float(self._val)) return str(float(self._val))
def __hash__(self): def __hash__(self) -> int:
return self._val.__hash__() return self._val.__hash__()
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
@ -551,7 +551,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
_load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {}
_write_dispatch: dict[int, Callable[..., Any]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {}
def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): def __init__(
self,
ifh: bytes = b"II\052\0\0\0\0\0",
prefix: bytes | None = None,
group: int | None = None,
) -> None:
"""Initialize an ImageFileDirectory. """Initialize an ImageFileDirectory.
To construct an ImageFileDirectory from a real file, pass the 8-byte To construct an ImageFileDirectory from a real file, pass the 8-byte
@ -575,7 +580,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise SyntaxError(msg) raise SyntaxError(msg)
self._bigtiff = ifh[2] == 43 self._bigtiff = ifh[2] == 43
self.group = group self.group = group
self.tagtype = {} self.tagtype: dict[int, int] = {}
""" Dictionary of tag types """ """ Dictionary of tag types """
self.reset() self.reset()
(self.next,) = ( (self.next,) = (
@ -587,18 +592,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
offset = property(lambda self: self._offset) offset = property(lambda self: self._offset)
@property @property
def legacy_api(self): def legacy_api(self) -> bool:
return self._legacy_api return self._legacy_api
@legacy_api.setter @legacy_api.setter
def legacy_api(self, value): def legacy_api(self, value: bool) -> NoReturn:
msg = "Not allowing setting of legacy api" msg = "Not allowing setting of legacy api"
raise Exception(msg) raise Exception(msg)
def reset(self): def reset(self) -> None:
self._tags_v1 = {} # will remain empty if legacy_api is false self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false
self._tags_v2 = {} # main tag storage self._tags_v2: dict[int, Any] = {} # main tag storage
self._tagdata = {} self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None self._next = None
self._offset = None self._offset = None
@ -2039,7 +2044,7 @@ class AppendingTiffWriter:
num_tags = self.readShort() num_tags = self.readShort()
self.f.seek(num_tags * 12, os.SEEK_CUR) self.f.seek(num_tags * 12, os.SEEK_CUR)
def write(self, data): def write(self, data: bytes) -> int | None:
return self.f.write(data) return self.f.write(data)
def readShort(self) -> int: def readShort(self) -> int:
@ -2122,7 +2127,9 @@ class AppendingTiffWriter:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(4, os.SEEK_CUR)
def fixOffsets(self, count, isShort=False, isLong=False): def fixOffsets(
self, count: int, isShort: bool = False, isLong: bool = False
) -> None:
if not isShort and not isLong: if not isShort and not isLong:
msg = "offset is neither short nor long" msg = "offset is neither short nor long"
raise RuntimeError(msg) raise RuntimeError(msg)

View File

@ -18,5 +18,5 @@ class ImagingDecoder:
class ImagingEncoder: class ImagingEncoder:
def __getattr__(self, name: str) -> Any: ... def __getattr__(self, name: str) -> Any: ...
def font(image, glyphdata: bytes) -> ImagingFont: ... def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ...
def __getattr__(name: str) -> Any: ... def __getattr__(name: str) -> Any: ...

View File

@ -4,6 +4,7 @@ import collections
import os import os
import sys import sys
import warnings import warnings
from typing import IO
import PIL import PIL
@ -223,7 +224,7 @@ def get_supported() -> list[str]:
return ret return ret
def pilinfo(out=None, supported_formats=True): def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
""" """
Prints information about this installation of Pillow. Prints information about this installation of Pillow.
This function can be called with ``python3 -m PIL``. This function can be called with ``python3 -m PIL``.
@ -244,9 +245,9 @@ def pilinfo(out=None, supported_formats=True):
print("-" * 68, file=out) print("-" * 68, file=out)
print(f"Pillow {PIL.__version__}", file=out) print(f"Pillow {PIL.__version__}", file=out)
py_version = sys.version.splitlines() py_version_lines = sys.version.splitlines()
print(f"Python {py_version[0].strip()}", file=out) print(f"Python {py_version_lines[0].strip()}", file=out)
for py_version in py_version[1:]: for py_version in py_version_lines[1:]:
print(f" {py_version.strip()}", file=out) print(f" {py_version.strip()}", file=out)
print("-" * 68, file=out) print("-" * 68, file=out)
print(f"Python executable is {sys.executable or 'unknown'}", file=out) print(f"Python executable is {sys.executable or 'unknown'}", file=out)
@ -282,9 +283,12 @@ def pilinfo(out=None, supported_formats=True):
("xcb", "XCB (X protocol)"), ("xcb", "XCB (X protocol)"),
]: ]:
if check(name): if check(name):
if name == "jpg" and check_feature("libjpeg_turbo"): v: str | None = None
v = "libjpeg-turbo " + version_feature("libjpeg_turbo") if name == "jpg":
else: libjpeg_turbo_version = version_feature("libjpeg_turbo")
if libjpeg_turbo_version is not None:
v = "libjpeg-turbo " + libjpeg_turbo_version
if v is None:
v = version(name) v = version(name)
if v is not None: if v is not None:
version_static = name in ("pil", "jpg") version_static = name in ("pil", "jpg")