mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-12 18:26:17 +03:00
Merge branch 'main' into type_hint_image
This commit is contained in:
commit
66ad49774d
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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: ...
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user