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
- 7z x nasm-win64.zip -oc:\
- 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\
- ps: |
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 \
libimagequant \
libjpeg \
libraqm \
libtiff \
little-cms2 \
openjpeg \
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"
# TODO Update condition when cffi supports 3.13

View File

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

View File

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

View File

@ -443,7 +443,9 @@ class TestFileJpeg:
assert_image(im1, im2.mode, im2.size)
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
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:
with Image.open(TEST_FILE) as img:
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
assert_image_similar_tofile(img, tempfile, 17)
@ -917,24 +919,25 @@ class TestFileJpeg:
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
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
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer.max_pos = 0
max_pos = 0
orig_read = buffer.read
def read(n=-1):
def read(n: int | None = -1) -> bytes:
nonlocal max_pos
res = orig_read(n)
buffer.max_pos = max(buffer.max_pos, buffer.tell())
max_pos = max(max_pos, buffer.tell())
return res
buffer.read = read
monkeypatch.setattr(buffer, "read", read)
with pytest.raises(UnidentifiedImageError):
with Image.open(buffer):
pass
# Assert the entire file has not been read
assert 0 < buffer.max_pos < size
assert 0 < max_pos < size
def test_getxmp(self) -> None:
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)
def test_9bit():
def test_9bit() -> None:
with Image.open("Tests/images/9bit.j2k") as im:
assert im.mode == "I;16"
assert im.size == (128, 128)

View File

@ -113,7 +113,7 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif")
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"):
Image.open("Tests/images/seek_too_large.tif")

View File

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

View File

@ -68,7 +68,11 @@ def test_sanity() -> None:
),
)
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:
assert Image.getmodebase(mode) == expected_base
assert Image.getmodetype(mode) == expected_type

View File

@ -113,13 +113,13 @@ def test_array_F() -> None:
def test_not_flattened() -> None:
im = Image.new("L", (1, 1))
with pytest.raises(TypeError):
im.putdata([[0]])
im.putdata([[0]]) # type: ignore[list-item]
with pytest.raises(TypeError):
im.putdata([[0]], 2)
im.putdata([[0]], 2) # type: ignore[list-item]
with pytest.raises(TypeError):
im = Image.new("I", (1, 1))
im.putdata([[0]])
im.putdata([[0]]) # type: ignore[list-item]
with pytest.raises(TypeError):
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(
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
)
def test_quantize_kmeans(method) -> None:
def test_quantize_kmeans(method: Image.Quantize) -> None:
im = hopper()
no_kmeans = im.quantize(kmeans=0, 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(
"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))
with pytest.raises(expected_error):
im.reduce(size)
im.reduce(size) # type: ignore[arg-type]
@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),
),
)
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))
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"))

View File

@ -16,7 +16,7 @@ from .helper import (
def test_sanity() -> None:
im = hopper()
assert im.thumbnail((100, 100)) is None
im.thumbnail((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(
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:
with pytest.raises(expected_error) as e:
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)
line_spacing = font.getbbox("A")[3] + 4
lines = TEST_TEXT.split("\n")
y = 0
y: float = 0
for line in lines:
draw.text((0, y), line, font=font)
y += line_spacing

View File

@ -454,7 +454,7 @@ def test_autocontrast_cutoff() -> None:
# Test the cutoff argument of autocontrast
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()
assert autocontrast(10) == autocontrast((10, 10))

View File

@ -70,7 +70,7 @@ if is_win32():
]
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.bfType = 0x4D42
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize

View File

@ -11,7 +11,7 @@ import pytest
"args, report",
((["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
out = subprocess.check_output(args).decode("utf-8")
lines = out.splitlines()

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Any
import pytest
@ -8,13 +9,19 @@ from PIL import Image
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)
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 boolean:
data = [0, 255] * 50
@ -99,14 +106,16 @@ def test_1d_array() -> None:
assert_image(Image.fromarray(a), "L", (1, 5))
def _test_img_equals_nparray(img: Image.Image, np) -> None:
assert len(np.shape) >= 2
np_size = np.shape[1], np.shape[0]
def _test_img_equals_nparray(
img: Image.Image, np_img: numpy.typing.NDArray[Any]
) -> None:
assert len(np_img.shape) >= 2
np_size = np_img.shape[1], np_img.shape[0]
assert img.size == np_size
px = img.load()
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
assert_deep_equal(px[x, y], np[y, x])
assert_deep_equal(px[x, y], np_img[y, x])
def test_16bit() -> None:
@ -157,7 +166,7 @@ def test_save_tiff_uint16() -> None:
("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)
# Resize to non-square
@ -207,7 +216,7 @@ def test_putdata() -> None:
numpy.float64,
),
)
def test_roundtrip_eye(dtype) -> None:
def test_roundtrip_eye(dtype: numpy.typing.DTypeLike) -> None:
arr = numpy.eye(10, dtype=dtype)
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))

View File

@ -1,8 +1,9 @@
from __future__ import annotations
import shutil
from io import BytesIO
from pathlib import Path
from typing import Callable
from typing import IO, Callable
import pytest
@ -22,11 +23,11 @@ class TestShellInjection:
self,
tmp_path: Path,
src_img: Image.Image,
save_func: Callable[[Image.Image, int, str], None],
save_func: Callable[[Image.Image, IO[bytes], str | bytes], None],
) -> None:
for filename in test_filenames:
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
with Image.open(dest_file) as im:
im.load()

View File

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

View File

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

View File

@ -115,7 +115,11 @@ class FitsImageFile(ImageFile.ImageFile):
elif number_of_bits in (-32, -64):
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

View File

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

View File

@ -410,7 +410,9 @@ def init() -> bool:
# 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
if args is None:
args = ()
@ -433,7 +435,9 @@ def _getdecoder(mode, decoder_name, 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
if args is None:
args = ()
@ -545,7 +549,7 @@ class Image:
return self._size
@property
def mode(self):
def mode(self) -> str:
return self._mode
def _prepare(self):
@ -571,7 +575,7 @@ class Image:
self.palette = ImagePalette.ImagePalette()
return self
def _new(self, im) -> Image:
def _new(self, im: core.ImagingCore) -> Image:
new = Image._init(im)
if im.mode in ("P", "PA") and self.palette:
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.
:param image_format: Image format.
@ -710,14 +714,14 @@ class Image:
return None
return b.getvalue()
def _repr_png_(self):
def _repr_png_(self) -> bytes | None:
"""iPython display hook support for PNG format.
:returns: PNG version of the image as bytes
"""
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.
:returns: JPEG version of the image as bytes
@ -764,7 +768,7 @@ class Image:
self.putpalette(palette)
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.
@ -786,12 +790,13 @@ class Image:
:returns: A :py:class:`bytes` object.
"""
# may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple):
args = args[0]
encoder_args: Any = args
if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple):
# may pass tuple instead of argument list
encoder_args = encoder_args[0]
if encoder_name == "raw" and args == ():
args = self.mode
if encoder_name == "raw" and encoder_args == ():
encoder_args = self.mode
self.load()
@ -799,7 +804,7 @@ class Image:
return b""
# unpack data
e = _getencoder(self.mode, encoder_name, args)
e = _getencoder(self.mode, encoder_name, encoder_args)
e.setimage(self.im)
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.
@ -853,16 +860,17 @@ class Image:
if self.width == 0 or self.height == 0:
return
# may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple):
args = args[0]
decoder_args: Any = args
if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
# may pass tuple instead of argument list
decoder_args = decoder_args[0]
# default format
if decoder_name == "raw" and args == ():
args = self.mode
if decoder_name == "raw" and decoder_args == ():
decoder_args = self.mode
# unpack data
d = _getdecoder(self.mode, decoder_name, args)
d = _getdecoder(self.mode, decoder_name, decoder_args)
d.setimage(self.im)
s = d.decode(data)
@ -1006,9 +1014,11 @@ class Image:
if has_transparency and self.im.bands == 3:
transparency = new_im.info["transparency"]
def convert_transparency(m, v):
v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
return max(0, min(255, int(v)))
def convert_transparency(
m: tuple[float, ...], v: tuple[int, int, int]
) -> 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":
transparency = convert_transparency(matrix, transparency)
@ -1260,7 +1270,7 @@ class Image:
__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
4-tuple defining the left, upper, right, and lower pixel
@ -1286,7 +1296,9 @@ class Image:
self.load()
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.
@ -1458,7 +1470,7 @@ class Image:
return self.im.getextrema()
def _getxmp(self, xmp_tags):
def get_name(tag):
def get_name(tag: str) -> str:
return re.sub("^{[^}]+}", "", tag)
def get_value(element):
@ -1559,7 +1571,11 @@ class Image:
fp = io.BytesIO(data)
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._seek(0)
im.load()
@ -1813,7 +1829,9 @@ class Image:
else:
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
onto this image.
@ -1828,32 +1846,35 @@ class Image:
"""
if not isinstance(source, (list, tuple)):
msg = "Source must be a tuple"
msg = "Source must be a list or tuple"
raise ValueError(msg)
if not isinstance(dest, (list, tuple)):
msg = "Destination must be a tuple"
msg = "Destination must be a list or tuple"
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)
if not len(dest) == 2:
msg = "Destination must be a 2-tuple"
msg = "Destination must be a sequence of length 2"
raise ValueError(msg)
if min(source) < 0:
msg = "Source must be non-negative"
raise ValueError(msg)
if len(source) == 2:
source = source + im.size
# over image, crop if it's not the whole thing.
if source == (0, 0) + im.size:
# over image, crop if it's not the whole image.
if overlay_crop_box == (0, 0) + im.size:
overlay = im
else:
overlay = im.crop(source)
overlay = im.crop(overlay_crop_box)
# 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.
if box == (0, 0) + self.size:
@ -1864,7 +1885,11 @@ class Image:
result = alpha_composite(background, overlay)
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.
@ -1901,7 +1926,9 @@ class Image:
scale, offset = _getscaleoffset(lut)
return self._new(self.im.point_transform(scale, offset))
# 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":
# FIXME: _imaging returns a confusing error message for this case
@ -1909,8 +1936,8 @@ class Image:
raise ValueError(msg)
if mode != "F":
lut = [round(i) for i in lut]
return self._new(self.im.point(lut, mode))
flatLut = [round(i) for i in flatLut]
return self._new(self.im.point(flatLut, mode))
def putalpha(self, alpha):
"""
@ -2983,29 +3010,29 @@ def _wedge() -> Image:
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
: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)):
msg = "Size must be a tuple"
msg = "Size must be a list or tuple"
raise ValueError(msg)
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)
if size[0] < 0 or size[1] < 0:
msg = "Width and height must be >= 0"
raise ValueError(msg)
return True
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:
"""
Creates a new image with the given mode and size.
@ -3057,7 +3084,13 @@ def new(
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.
@ -3085,18 +3118,21 @@ def frombytes(mode, size, data, decoder_name: str = "raw", *args) -> Image:
im = new(mode, size)
if im.width != 0 and im.height != 0:
# may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple):
args = args[0]
decoder_args: Any = args
if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
# may pass tuple instead of argument list
decoder_args = decoder_args[0]
if decoder_name == "raw" and args == ():
args = mode
if decoder_name == "raw" and decoder_args == ():
decoder_args = mode
im.frombytes(data, decoder_name, args)
im.frombytes(data, decoder_name, decoder_args)
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.
@ -3553,7 +3589,7 @@ def merge(mode: str, bands: Sequence[Image]) -> Image:
def register_open(
id,
id: str,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
accept: Callable[[bytes], bool | str] | None = None,
) -> None:
@ -3687,7 +3723,7 @@ def _show(image: Image, **options: Any) -> None:
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:
"""
Generate a Mandelbrot set covering the given extent.
@ -3734,19 +3770,18 @@ def radial_gradient(mode: str) -> Image:
# Resources
def _apply_env_variables(env=None) -> None:
if env is None:
env = os.environ
def _apply_env_variables(env: dict[str, str] | None = None) -> None:
env_dict = env if env is not None else os.environ
for var_name, setter in [
("PILLOW_ALIGNMENT", core.set_alignment),
("PILLOW_BLOCK_SIZE", core.set_block_size),
("PILLOW_BLOCKS_MAX", core.set_blocks_max),
]:
if var_name not in env:
if var_name not in env_dict:
continue
var = env[var_name].lower()
var = env_dict[var_name].lower()
units = 1
for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]:
@ -3755,13 +3790,13 @@ def _apply_env_variables(env=None) -> None:
var = var[: -len(postfix)]
try:
var = int(var) * units
var_int = int(var) * units
except ValueError:
warnings.warn(f"{var_name} is not int")
continue
try:
setter(var)
setter(var_int)
except ValueError as e:
warnings.warn(f"{var_name}: {e}")

View File

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

View File

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

View File

@ -122,7 +122,7 @@ def _parse_codestream(fp):
elif csiz == 4:
mode = "RGBA"
else:
mode = None
mode = ""
return size, mode
@ -237,7 +237,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
msg = "not a JPEG 2000 file"
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"
raise SyntaxError(msg)

View File

@ -63,7 +63,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
msg = "not an MIC file; no image entries"
raise SyntaxError(msg)
self.frame = None
self.frame = -1
self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1
@ -85,7 +85,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.frame = frame
def tell(self):
def tell(self) -> int:
return self.frame
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
# Palm does grayscale from white (0) to black (1)
bpp = im.encoderinfo["bpp"]
im = im.point(
lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift)
)
maxval = (1 << bpp) - 1
shift = 8 - bpp
im = im.point(lambda x: maxval - (x >> shift))
elif im.info.get("bpp") in (1, 2, 4):
# here we assume that even though the inherent mode is 8-bit grayscale,
# only the lower bpp bits are significant.
# We invert them to match the Palm.
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:
msg = f"cannot write mode {im.mode} as Palm"
raise OSError(msg)

View File

@ -39,7 +39,7 @@ import struct
import warnings
import zlib
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 ._binary import i16be as i16
@ -48,6 +48,9 @@ from ._binary import o8
from ._binary import o16be as o16
from ._binary import o32be as o32
if TYPE_CHECKING:
from . import _imaging
logger = logging.getLogger(__name__)
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
def __new__(cls, text, lang=None, tkey=None):
"""
@ -270,10 +276,10 @@ class PngInfo:
"""
def __init__(self):
self.chunks = []
def __init__(self) -> None:
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.
:param cid: a byte string, 4 bytes long.
@ -283,12 +289,16 @@ class PngInfo:
"""
chunk = [cid, data]
if after_idat:
chunk.append(True)
self.chunks.append(tuple(chunk))
self.chunks.append((cid, data, after_idat))
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.
:param key: latin-1 encodable text key name
@ -316,7 +326,9 @@ class PngInfo:
else:
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.
:param key: latin-1 encodable text key name
@ -326,7 +338,13 @@ class PngInfo:
"""
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
if not isinstance(value, bytes):
@ -434,7 +452,7 @@ class PngStream(ChunkStream):
raise SyntaxError(msg)
return s
def chunk_IDAT(self, pos, length):
def chunk_IDAT(self, pos: int, length: int) -> NoReturn:
# image data
if "bbox" in self.im_info:
tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)]
@ -447,7 +465,7 @@ class PngStream(ChunkStream):
msg = "image data found"
raise EOFError(msg)
def chunk_IEND(self, pos, length):
def chunk_IEND(self, pos: int, length: int) -> NoReturn:
msg = "end of PNG image"
raise EOFError(msg)
@ -821,7 +839,10 @@ class PngImageFile(ImageFile.ImageFile):
msg = "no more images in APNG file"
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 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:
self.dispose_op = Disposal.OP_BACKGROUND
self.dispose = None
if self.dispose_op == Disposal.OP_PREVIOUS:
self.dispose = self._prev_im.copy()
self.dispose = self._crop(self.dispose, self.dispose_extent)
if self._prev_im:
self.dispose = self._prev_im.copy()
self.dispose = self._crop(self.dispose, self.dispose_extent)
elif self.dispose_op == Disposal.OP_BACKGROUND:
self.dispose = Image.core.fill(self.mode, self.size)
self.dispose = self._crop(self.dispose, self.dispose_extent)
else:
self.dispose = None
def tell(self) -> int:
return self.__frame
@ -1026,7 +1047,7 @@ class PngImageFile(ImageFile.ImageFile):
return None
return self.getexif()._get_merged_dict()
def getexif(self):
def getexif(self) -> Image.Exif:
if "exif" not in self.info:
self.load()
@ -1346,7 +1367,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, cid, data)
elif cid[1:2].islower():
# Private chunk
after_idat = info_chunk[2:3]
after_idat = len(info_chunk) == 3 and info_chunk[2]
if not after_idat:
chunk(fp, cid, data)
@ -1425,7 +1446,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
cid, data = info_chunk[:2]
if cid[1:2].islower():
# Private chunk
after_idat = info_chunk[2:3]
after_idat = len(info_chunk) == 3 and info_chunk[2]
if after_idat:
chunk(fp, cid, data)

View File

@ -50,7 +50,7 @@ import warnings
from collections.abc import MutableMapping
from fractions import Fraction
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 ._binary import i16be as i16
@ -384,7 +384,7 @@ class IFDRational(Rational):
def __repr__(self) -> str:
return str(float(self._val))
def __hash__(self):
def __hash__(self) -> int:
return self._val.__hash__()
def __eq__(self, other: object) -> bool:
@ -551,7 +551,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
_load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], 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.
To construct an ImageFileDirectory from a real file, pass the 8-byte
@ -575,7 +580,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise SyntaxError(msg)
self._bigtiff = ifh[2] == 43
self.group = group
self.tagtype = {}
self.tagtype: dict[int, int] = {}
""" Dictionary of tag types """
self.reset()
(self.next,) = (
@ -587,18 +592,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
offset = property(lambda self: self._offset)
@property
def legacy_api(self):
def legacy_api(self) -> bool:
return self._legacy_api
@legacy_api.setter
def legacy_api(self, value):
def legacy_api(self, value: bool) -> NoReturn:
msg = "Not allowing setting of legacy api"
raise Exception(msg)
def reset(self):
self._tags_v1 = {} # will remain empty if legacy_api is false
self._tags_v2 = {} # main tag storage
self._tagdata = {}
def reset(self) -> None:
self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false
self._tags_v2: dict[int, Any] = {} # main tag storage
self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None
self._offset = None
@ -2039,7 +2044,7 @@ class AppendingTiffWriter:
num_tags = self.readShort()
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)
def readShort(self) -> int:
@ -2122,7 +2127,9 @@ class AppendingTiffWriter:
# skip the locally stored value that is not an offset
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:
msg = "offset is neither short nor long"
raise RuntimeError(msg)

View File

@ -12,5 +12,11 @@ class ImagingDraw:
class PixelAccess:
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: ...

View File

@ -4,6 +4,7 @@ import collections
import os
import sys
import warnings
from typing import IO
import PIL
@ -223,7 +224,7 @@ def get_supported() -> list[str]:
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.
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(f"Pillow {PIL.__version__}", file=out)
py_version = sys.version.splitlines()
print(f"Python {py_version[0].strip()}", file=out)
for py_version in py_version[1:]:
py_version_lines = sys.version.splitlines()
print(f"Python {py_version_lines[0].strip()}", file=out)
for py_version in py_version_lines[1:]:
print(f" {py_version.strip()}", file=out)
print("-" * 68, 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)"),
]:
if check(name):
if name == "jpg" and check_feature("libjpeg_turbo"):
v = "libjpeg-turbo " + version_feature("libjpeg_turbo")
else:
v: str | None = None
if name == "jpg":
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)
if v is not None:
version_static = name in ("pil", "jpg")