Merge branch 'main' into init

This commit is contained in:
Andrew Murray 2024-06-19 09:07:46 +10:00 committed by GitHub
commit f3979dc851
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 295 additions and 176 deletions

View File

@ -35,7 +35,7 @@ install:
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip - curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
- 7z x nasm-win64.zip -oc:\ - 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.3.1 - choco install ghostscript --version=10.3.1
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.00.0\bin;%PATH% - path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - ps: |
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\

View File

@ -1 +1 @@
cibuildwheel==2.19.0 cibuildwheel==2.19.1

View File

@ -7,11 +7,15 @@ brew install \
ghostscript \ ghostscript \
libimagequant \ libimagequant \
libjpeg \ libjpeg \
libraqm \
libtiff \ libtiff \
little-cms2 \ little-cms2 \
openjpeg \ openjpeg \
webp webp
if [[ "$ImageOS" == "macos13" ]]; then
brew install --ignore-dependencies libraqm
else
brew install libraqm
fi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
# TODO Update condition when cffi supports 3.13 # TODO Update condition when cffi supports 3.13

View File

@ -3,7 +3,7 @@ version: 2
formats: [pdf] formats: [pdf]
build: build:
os: ubuntu-22.04 os: ubuntu-lts-latest
tools: tools:
python: "3" python: "3"
jobs: jobs:

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

@ -156,7 +156,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
if TYPE_CHECKING:
import numpy
import numpy.typing
else:
numpy = pytest.importorskip("numpy", reason="NumPy not installed") 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

@ -37,7 +37,9 @@ IMAGEQUANT_ROOT = None
JPEG2K_ROOT = None JPEG2K_ROOT = None
JPEG_ROOT = None JPEG_ROOT = None
LCMS_ROOT = None LCMS_ROOT = None
RAQM_ROOT = None
TIFF_ROOT = None TIFF_ROOT = None
WEBP_ROOT = None
ZLIB_ROOT = None ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
@ -459,6 +461,8 @@ class pil_build_ext(build_ext):
"FREETYPE_ROOT": "freetype2", "FREETYPE_ROOT": "freetype2",
"HARFBUZZ_ROOT": "harfbuzz", "HARFBUZZ_ROOT": "harfbuzz",
"FRIBIDI_ROOT": "fribidi", "FRIBIDI_ROOT": "fribidi",
"RAQM_ROOT": "raqm",
"WEBP_ROOT": "libwebp",
"LCMS_ROOT": "lcms2", "LCMS_ROOT": "lcms2",
"IMAGEQUANT_ROOT": "libimagequant", "IMAGEQUANT_ROOT": "libimagequant",
}.items(): }.items():

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

@ -115,7 +115,11 @@ class FitsImageFile(ImageFile.ImageFile):
elif number_of_bits in (-32, -64): elif number_of_bits in (-32, -64):
self._mode = "F" self._mode = "F"
args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,) args: tuple[str | int, ...]
if decoder_name == "raw":
args = (self.mode, 0, -1)
else:
args = (number_of_bits,)
return decoder_name, offset, args return decoder_name, offset, args

View File

@ -458,6 +458,8 @@ class GifImageFile(ImageFile.ImageFile):
frame_im = self.im.convert("RGBA") frame_im = self.im.convert("RGBA")
else: else:
frame_im = self.im.convert("RGB") frame_im = self.im.convert("RGB")
assert self.dispose_extent is not None
frame_im = self._crop(frame_im, self.dispose_extent) frame_im = self._crop(frame_im, self.dispose_extent)
self.im = self._prev_im self.im = self._prev_im

View File

@ -410,7 +410,9 @@ def init() -> bool:
# Codec factories (used by tobytes/frombytes and ImageFile.load) # Codec factories (used by tobytes/frombytes and ImageFile.load)
def _getdecoder(mode, decoder_name, args, extra=()): def _getdecoder(
mode: str, decoder_name: str, args: Any, extra: tuple[Any, ...] = ()
) -> core.ImagingDecoder | ImageFile.PyDecoder:
# tweak arguments # tweak arguments
if args is None: if args is None:
args = () args = ()
@ -433,7 +435,9 @@ def _getdecoder(mode, decoder_name, args, extra=()):
return decoder(mode, *args + extra) return decoder(mode, *args + extra)
def _getencoder(mode, encoder_name, args, extra=()): def _getencoder(
mode: str, encoder_name: str, args: Any, extra: tuple[Any, ...] = ()
) -> core.ImagingEncoder | ImageFile.PyEncoder:
# tweak arguments # tweak arguments
if args is None: if args is None:
args = () args = ()
@ -545,7 +549,7 @@ class Image:
return self._size return self._size
@property @property
def mode(self): def mode(self) -> str:
return self._mode return self._mode
def _prepare(self): def _prepare(self):
@ -571,7 +575,7 @@ class Image:
self.palette = ImagePalette.ImagePalette() self.palette = ImagePalette.ImagePalette()
return self return self
def _new(self, im) -> Image: def _new(self, im: core.ImagingCore) -> Image:
new = Image._init(im) new = Image._init(im)
if im.mode in ("P", "PA") and self.palette: if im.mode in ("P", "PA") and self.palette:
new.palette = self.palette.copy() new.palette = self.palette.copy()
@ -697,7 +701,7 @@ class Image:
) )
) )
def _repr_image(self, image_format, **kwargs): def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None:
"""Helper function for iPython display hook. """Helper function for iPython display hook.
:param image_format: Image format. :param image_format: Image format.
@ -710,14 +714,14 @@ class Image:
return None return None
return b.getvalue() return b.getvalue()
def _repr_png_(self): def _repr_png_(self) -> bytes | None:
"""iPython display hook support for PNG format. """iPython display hook support for PNG format.
:returns: PNG version of the image as bytes :returns: PNG version of the image as bytes
""" """
return self._repr_image("PNG", compress_level=1) return self._repr_image("PNG", compress_level=1)
def _repr_jpeg_(self): def _repr_jpeg_(self) -> bytes | None:
"""iPython display hook support for JPEG format. """iPython display hook support for JPEG format.
:returns: JPEG version of the image as bytes :returns: JPEG version of the image as bytes
@ -764,7 +768,7 @@ class Image:
self.putpalette(palette) self.putpalette(palette)
self.frombytes(data) self.frombytes(data)
def tobytes(self, encoder_name: str = "raw", *args) -> bytes: def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes:
""" """
Return image as a bytes object. Return image as a bytes object.
@ -786,12 +790,13 @@ class Image:
:returns: A :py:class:`bytes` object. :returns: A :py:class:`bytes` object.
""" """
encoder_args: Any = args
if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple):
# may pass tuple instead of argument list # may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple): encoder_args = encoder_args[0]
args = args[0]
if encoder_name == "raw" and args == (): if encoder_name == "raw" and encoder_args == ():
args = self.mode encoder_args = self.mode
self.load() self.load()
@ -799,7 +804,7 @@ class Image:
return b"" return b""
# unpack data # unpack data
e = _getencoder(self.mode, encoder_name, args) e = _getencoder(self.mode, encoder_name, encoder_args)
e.setimage(self.im) e.setimage(self.im)
bufsize = max(65536, self.size[0] * 4) # see RawEncode.c bufsize = max(65536, self.size[0] * 4) # see RawEncode.c
@ -842,7 +847,9 @@ class Image:
] ]
) )
def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: def frombytes(
self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any
) -> None:
""" """
Loads this image with pixel data from a bytes object. Loads this image with pixel data from a bytes object.
@ -853,16 +860,17 @@ class Image:
if self.width == 0 or self.height == 0: if self.width == 0 or self.height == 0:
return return
decoder_args: Any = args
if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
# may pass tuple instead of argument list # may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple): decoder_args = decoder_args[0]
args = args[0]
# default format # default format
if decoder_name == "raw" and args == (): if decoder_name == "raw" and decoder_args == ():
args = self.mode decoder_args = self.mode
# unpack data # unpack data
d = _getdecoder(self.mode, decoder_name, args) d = _getdecoder(self.mode, decoder_name, decoder_args)
d.setimage(self.im) d.setimage(self.im)
s = d.decode(data) s = d.decode(data)
@ -1006,9 +1014,11 @@ class Image:
if has_transparency and self.im.bands == 3: if has_transparency and self.im.bands == 3:
transparency = new_im.info["transparency"] transparency = new_im.info["transparency"]
def convert_transparency(m, v): def convert_transparency(
v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 m: tuple[float, ...], v: tuple[int, int, int]
return max(0, min(255, int(v))) ) -> int:
value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
return max(0, min(255, int(value)))
if mode == "L": if mode == "L":
transparency = convert_transparency(matrix, transparency) transparency = convert_transparency(matrix, transparency)
@ -1260,7 +1270,7 @@ class Image:
__copy__ = copy __copy__ = copy
def crop(self, box: tuple[int, int, int, int] | None = None) -> Image: def crop(self, box: tuple[float, float, float, float] | None = None) -> Image:
""" """
Returns a rectangular region from this image. The box is a Returns a rectangular region from this image. The box is a
4-tuple defining the left, upper, right, and lower pixel 4-tuple defining the left, upper, right, and lower pixel
@ -1286,7 +1296,9 @@ class Image:
self.load() self.load()
return self._new(self._crop(self.im, box)) return self._new(self._crop(self.im, box))
def _crop(self, im, box): def _crop(
self, im: core.ImagingCore, box: tuple[float, float, float, float]
) -> core.ImagingCore:
""" """
Returns a rectangular region from the core image object im. Returns a rectangular region from the core image object im.
@ -1458,7 +1470,7 @@ class Image:
return self.im.getextrema() return self.im.getextrema()
def _getxmp(self, xmp_tags): def _getxmp(self, xmp_tags):
def get_name(tag): def get_name(tag: str) -> str:
return re.sub("^{[^}]+}", "", tag) return re.sub("^{[^}]+}", "", tag)
def get_value(element): def get_value(element):
@ -1559,7 +1571,11 @@ class Image:
fp = io.BytesIO(data) fp = io.BytesIO(data)
with open(fp) as im: with open(fp) as im:
if thumbnail_offset is None: from . import TiffImagePlugin
if thumbnail_offset is None and isinstance(
im, TiffImagePlugin.TiffImageFile
):
im._frame_pos = [ifd_offset] im._frame_pos = [ifd_offset]
im._seek(0) im._seek(0)
im.load() im.load()
@ -1813,7 +1829,9 @@ class Image:
else: else:
self.im.paste(im, box) self.im.paste(im, box)
def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): def alpha_composite(
self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0)
) -> None:
"""'In-place' analog of Image.alpha_composite. Composites an image """'In-place' analog of Image.alpha_composite. Composites an image
onto this image. onto this image.
@ -1828,32 +1846,35 @@ class Image:
""" """
if not isinstance(source, (list, tuple)): if not isinstance(source, (list, tuple)):
msg = "Source must be a tuple" msg = "Source must be a list or tuple"
raise ValueError(msg) raise ValueError(msg)
if not isinstance(dest, (list, tuple)): if not isinstance(dest, (list, tuple)):
msg = "Destination must be a tuple" msg = "Destination must be a list or tuple"
raise ValueError(msg) raise ValueError(msg)
if len(source) not in (2, 4):
msg = "Source must be a 2 or 4-tuple" if len(source) == 4:
overlay_crop_box = tuple(source)
elif len(source) == 2:
overlay_crop_box = tuple(source) + im.size
else:
msg = "Source must be a sequence of length 2 or 4"
raise ValueError(msg) raise ValueError(msg)
if not len(dest) == 2: if not len(dest) == 2:
msg = "Destination must be a 2-tuple" msg = "Destination must be a sequence of length 2"
raise ValueError(msg) raise ValueError(msg)
if min(source) < 0: if min(source) < 0:
msg = "Source must be non-negative" msg = "Source must be non-negative"
raise ValueError(msg) raise ValueError(msg)
if len(source) == 2: # over image, crop if it's not the whole image.
source = source + im.size if overlay_crop_box == (0, 0) + im.size:
# over image, crop if it's not the whole thing.
if source == (0, 0) + im.size:
overlay = im overlay = im
else: else:
overlay = im.crop(source) overlay = im.crop(overlay_crop_box)
# target for the paste # target for the paste
box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) box = tuple(dest) + (dest[0] + overlay.width, dest[1] + overlay.height)
# destination image. don't copy if we're using the whole image. # destination image. don't copy if we're using the whole image.
if box == (0, 0) + self.size: if box == (0, 0) + self.size:
@ -1864,7 +1885,11 @@ class Image:
result = alpha_composite(background, overlay) result = alpha_composite(background, overlay)
self.paste(result, box) self.paste(result, box)
def point(self, lut, mode: str | None = None) -> Image: def point(
self,
lut: Sequence[float] | Callable[[int], float] | ImagePointHandler,
mode: str | None = None,
) -> Image:
""" """
Maps this image through a lookup table or function. Maps this image through a lookup table or function.
@ -1901,7 +1926,9 @@ class Image:
scale, offset = _getscaleoffset(lut) scale, offset = _getscaleoffset(lut)
return self._new(self.im.point_transform(scale, offset)) return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table # for other modes, convert the function to a table
lut = [lut(i) for i in range(256)] * self.im.bands flatLut = [lut(i) for i in range(256)] * self.im.bands
else:
flatLut = lut
if self.mode == "F": if self.mode == "F":
# FIXME: _imaging returns a confusing error message for this case # FIXME: _imaging returns a confusing error message for this case
@ -1909,8 +1936,8 @@ class Image:
raise ValueError(msg) raise ValueError(msg)
if mode != "F": if mode != "F":
lut = [round(i) for i in lut] flatLut = [round(i) for i in flatLut]
return self._new(self.im.point(lut, mode)) return self._new(self.im.point(flatLut, mode))
def putalpha(self, alpha): def putalpha(self, alpha):
""" """
@ -2983,29 +3010,29 @@ def _wedge() -> Image:
return Image._init(core.wedge("L")) return Image._init(core.wedge("L"))
def _check_size(size): def _check_size(size: Any) -> None:
""" """
Common check to enforce type and sanity check on size tuples Common check to enforce type and sanity check on size tuples
:param size: Should be a 2 tuple of (width, height) :param size: Should be a 2 tuple of (width, height)
:returns: True, or raises a ValueError :returns: None, or raises a ValueError
""" """
if not isinstance(size, (list, tuple)): if not isinstance(size, (list, tuple)):
msg = "Size must be a tuple" msg = "Size must be a list or tuple"
raise ValueError(msg) raise ValueError(msg)
if len(size) != 2: if len(size) != 2:
msg = "Size must be a tuple of length 2" msg = "Size must be a sequence of length 2"
raise ValueError(msg) raise ValueError(msg)
if size[0] < 0 or size[1] < 0: if size[0] < 0 or size[1] < 0:
msg = "Width and height must be >= 0" msg = "Width and height must be >= 0"
raise ValueError(msg) raise ValueError(msg)
return True
def new( def new(
mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0 mode: str,
size: tuple[int, int] | list[int],
color: float | tuple[float, ...] | str | None = 0,
) -> Image: ) -> Image:
""" """
Creates a new image with the given mode and size. Creates a new image with the given mode and size.
@ -3057,7 +3084,13 @@ def new(
return im return im
def frombytes(mode, size, data, decoder_name: str = "raw", *args) -> Image: def frombytes(
mode: str,
size: tuple[int, int],
data: bytes | bytearray,
decoder_name: str = "raw",
*args: Any,
) -> Image:
""" """
Creates a copy of an image memory from pixel data in a buffer. Creates a copy of an image memory from pixel data in a buffer.
@ -3085,18 +3118,21 @@ def frombytes(mode, size, data, decoder_name: str = "raw", *args) -> Image:
im = new(mode, size) im = new(mode, size)
if im.width != 0 and im.height != 0: if im.width != 0 and im.height != 0:
decoder_args: Any = args
if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
# may pass tuple instead of argument list # may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple): decoder_args = decoder_args[0]
args = args[0]
if decoder_name == "raw" and args == (): if decoder_name == "raw" and decoder_args == ():
args = mode decoder_args = mode
im.frombytes(data, decoder_name, args) im.frombytes(data, decoder_name, decoder_args)
return im return im
def frombuffer(mode: str, size, data, decoder_name: str = "raw", *args) -> Image: def frombuffer(
mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any
) -> Image:
""" """
Creates an image memory referencing pixel data in a byte buffer. Creates an image memory referencing pixel data in a byte buffer.
@ -3553,7 +3589,7 @@ def merge(mode: str, bands: Sequence[Image]) -> Image:
def register_open( def register_open(
id, id: str,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
accept: Callable[[bytes], bool | str] | None = None, accept: Callable[[bytes], bool | str] | None = None,
) -> None: ) -> None:
@ -3687,7 +3723,7 @@ def _show(image: Image, **options: Any) -> None:
def effect_mandelbrot( def effect_mandelbrot(
size: tuple[int, int], extent: tuple[int, int, int, int], quality: int size: tuple[int, int], extent: tuple[float, float, float, float], quality: int
) -> Image: ) -> Image:
""" """
Generate a Mandelbrot set covering the given extent. Generate a Mandelbrot set covering the given extent.
@ -3734,19 +3770,18 @@ def radial_gradient(mode: str) -> Image:
# Resources # Resources
def _apply_env_variables(env=None) -> None: def _apply_env_variables(env: dict[str, str] | None = None) -> None:
if env is None: env_dict = env if env is not None else os.environ
env = os.environ
for var_name, setter in [ for var_name, setter in [
("PILLOW_ALIGNMENT", core.set_alignment), ("PILLOW_ALIGNMENT", core.set_alignment),
("PILLOW_BLOCK_SIZE", core.set_block_size), ("PILLOW_BLOCK_SIZE", core.set_block_size),
("PILLOW_BLOCKS_MAX", core.set_blocks_max), ("PILLOW_BLOCKS_MAX", core.set_blocks_max),
]: ]:
if var_name not in env: if var_name not in env_dict:
continue continue
var = env[var_name].lower() var = env_dict[var_name].lower()
units = 1 units = 1
for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]:
@ -3755,13 +3790,13 @@ def _apply_env_variables(env=None) -> None:
var = var[: -len(postfix)] var = var[: -len(postfix)]
try: try:
var = int(var) * units var_int = int(var) * units
except ValueError: except ValueError:
warnings.warn(f"{var_name} is not int") warnings.warn(f"{var_name} is not int")
continue continue
try: try:
setter(var) setter(var_int)
except ValueError as e: except ValueError as e:
warnings.warn(f"{var_name}: {e}") warnings.warn(f"{var_name}: {e}")

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

@ -122,7 +122,7 @@ def _parse_codestream(fp):
elif csiz == 4: elif csiz == 4:
mode = "RGBA" mode = "RGBA"
else: else:
mode = None mode = ""
return size, mode return size, mode
@ -237,7 +237,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
msg = "not a JPEG 2000 file" msg = "not a JPEG 2000 file"
raise SyntaxError(msg) raise SyntaxError(msg)
if self.size is None or self.mode is None: if self.size is None or not self.mode:
msg = "unable to determine size/mode" msg = "unable to determine size/mode"
raise SyntaxError(msg) raise SyntaxError(msg)

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

@ -129,15 +129,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# and invert it because # and invert it because
# Palm does grayscale from white (0) to black (1) # Palm does grayscale from white (0) to black (1)
bpp = im.encoderinfo["bpp"] bpp = im.encoderinfo["bpp"]
im = im.point( maxval = (1 << bpp) - 1
lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift) shift = 8 - bpp
) im = im.point(lambda x: maxval - (x >> shift))
elif im.info.get("bpp") in (1, 2, 4): elif im.info.get("bpp") in (1, 2, 4):
# here we assume that even though the inherent mode is 8-bit grayscale, # here we assume that even though the inherent mode is 8-bit grayscale,
# only the lower bpp bits are significant. # only the lower bpp bits are significant.
# We invert them to match the Palm. # We invert them to match the Palm.
bpp = im.info["bpp"] bpp = im.info["bpp"]
im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) maxval = (1 << bpp) - 1
im = im.point(lambda x: maxval - (x & maxval))
else: else:
msg = f"cannot write mode {im.mode} as Palm" msg = f"cannot write mode {im.mode} as Palm"
raise OSError(msg) raise OSError(msg)

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:
if self._prev_im:
self.dispose = self._prev_im.copy() self.dispose = self._prev_im.copy()
self.dispose = self._crop(self.dispose, self.dispose_extent) 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

@ -12,5 +12,11 @@ class ImagingDraw:
class PixelAccess: class PixelAccess:
def __getattr__(self, name: str) -> Any: ... def __getattr__(self, name: str) -> Any: ...
def font(image, glyphdata: bytes) -> ImagingFont: ... class ImagingDecoder:
def __getattr__(self, name: str) -> Any: ...
class ImagingEncoder:
def __getattr__(self, name: str) -> Any: ...
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")