Merge branch 'main' into progress
|
@ -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\
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.19.0
|
||||
cibuildwheel==2.19.1
|
||||
|
|
6
.github/workflows/macos-install.sh
vendored
|
@ -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
|
||||
|
|
|
@ -3,7 +3,7 @@ version: 2
|
|||
formats: [pdf]
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-lts-latest
|
||||
tools:
|
||||
python: "3"
|
||||
jobs:
|
||||
|
|
45
CHANGES.rst
|
@ -5,6 +5,51 @@ Changelog (Pillow)
|
|||
10.4.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
- Do not use first frame duration for other frames when saving APNG images #8104
|
||||
[radarhere]
|
||||
|
||||
- Consider I;16 pixel size when using a 1 mode mask #8112
|
||||
[radarhere]
|
||||
|
||||
- When saving multiple PNG frames, convert to mode rather than raw mode #8087
|
||||
[radarhere]
|
||||
|
||||
- Added byte support to FreeTypeFont #8141
|
||||
[radarhere]
|
||||
|
||||
- Allow float center for rotate operations #8114
|
||||
[radarhere]
|
||||
|
||||
- Do not read layers immediately when opening PSD images #8039
|
||||
[radarhere]
|
||||
|
||||
- Restore original thread state #8065
|
||||
[radarhere]
|
||||
|
||||
- Read IM and TIFF images as RGB, rather than RGBX #7997
|
||||
[radarhere]
|
||||
|
||||
- Only preserve TIFF IPTC_NAA_CHUNK tag if type is BYTE or UNDEFINED #7948
|
||||
[radarhere]
|
||||
|
||||
- Clarify ImageDraw2 error message when size is missing #8165
|
||||
[radarhere]
|
||||
|
||||
- Support unpacking more rawmodes to RGBA palettes #7966
|
||||
[radarhere]
|
||||
|
||||
- Removed support for Qt 5 #8159
|
||||
[radarhere]
|
||||
|
||||
- Improve ``ImageFont.freetype`` support for XDG directories on Linux #8135
|
||||
[mamg22, radarhere]
|
||||
|
||||
- Improved consistency of XMP handling #8069
|
||||
[radarhere]
|
||||
|
||||
- Use pkg-config to help find libwebp and raqm #8142
|
||||
[radarhere]
|
||||
|
||||
- Accept 't' suffix for libtiff version #8126, #8129
|
||||
[radarhere]
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ from typing import Any, Callable, Sequence
|
|||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image, ImageMath, features
|
||||
from PIL import Image, ImageFile, ImageMath, features
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -240,7 +240,7 @@ class PillowLeakTestCase:
|
|||
# helpers
|
||||
|
||||
|
||||
def fromstring(data: bytes) -> Image.Image:
|
||||
def fromstring(data: bytes) -> ImageFile.ImageFile:
|
||||
return Image.open(BytesIO(data))
|
||||
|
||||
|
||||
|
|
BIN
Tests/images/imagedraw_polygon_width_I.tiff
Normal file
BIN
Tests/images/ultrahdr.jpg
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
Tests/images/unknown_mode.j2k
Normal file
|
@ -354,10 +354,10 @@ class TestColorLut3DCoreAPI:
|
|||
class TestColorLut3DFilter:
|
||||
def test_wrong_args(self) -> None:
|
||||
with pytest.raises(ValueError, match="should be either an integer"):
|
||||
ImageFilter.Color3DLUT("small", [1])
|
||||
ImageFilter.Color3DLUT("small", [1]) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError, match="should be either an integer"):
|
||||
ImageFilter.Color3DLUT((11, 11), [1])
|
||||
ImageFilter.Color3DLUT((11, 11), [1]) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
|
||||
ImageFilter.Color3DLUT((11, 11, 1), [1])
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -759,10 +759,21 @@ def test_different_modes_in_later_frames(
|
|||
assert reloaded.mode == mode
|
||||
|
||||
|
||||
def test_apng_repeated_seeks_give_correct_info() -> None:
|
||||
def test_different_durations(tmp_path: Path) -> None:
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
with Image.open("Tests/images/apng/different_durations.png") as im:
|
||||
for i in range(3):
|
||||
for _ in range(3):
|
||||
im.seek(0)
|
||||
assert im.info["duration"] == 4000
|
||||
|
||||
im.seek(1)
|
||||
assert im.info["duration"] == 1000
|
||||
|
||||
im.save(test_file, save_all=True)
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert reloaded.info["duration"] == 4000
|
||||
|
||||
reloaded.seek(1)
|
||||
assert reloaded.info["duration"] == 1000
|
||||
|
|
|
@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
return self.loaded
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
|
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
|
|||
BufrStubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
assert not handler.is_loaded()
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.bufr")
|
||||
im.save(temp_file)
|
||||
|
|
|
@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
return self.loaded
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
|
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
|
|||
GribStubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
assert not handler.is_loaded()
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.grib")
|
||||
im.save(temp_file)
|
||||
|
|
|
@ -66,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
return self.loaded
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
|
@ -73,10 +76,10 @@ def test_handler(tmp_path: Path) -> None:
|
|||
Hdf5StubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
assert not handler.is_loaded()
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.h5")
|
||||
im.save(temp_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:
|
||||
|
@ -945,6 +948,7 @@ class TestFileJpeg:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
xmp = im.getxmp()
|
||||
|
||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||
|
@ -1029,8 +1033,10 @@ class TestFileJpeg:
|
|||
|
||||
def test_repr_jpeg(self) -> None:
|
||||
im = hopper()
|
||||
b = im._repr_jpeg_()
|
||||
assert b is not None
|
||||
|
||||
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
|
||||
with Image.open(BytesIO(b)) as repr_jpeg:
|
||||
assert repr_jpeg.format == "JPEG"
|
||||
assert_image_similar(im, repr_jpeg, 17)
|
||||
|
||||
|
|
|
@ -335,9 +335,15 @@ def test_issue_6194() -> None:
|
|||
assert im.getpixel((5, 5)) == 31
|
||||
|
||||
|
||||
def test_unknown_j2k_mode() -> None:
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open("Tests/images/unknown_mode.j2k"):
|
||||
pass
|
||||
|
||||
|
||||
def test_unbound_local() -> None:
|
||||
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
|
||||
with pytest.raises(OSError):
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open("Tests/images/unbound_variable.jp2"):
|
||||
pass
|
||||
|
||||
|
@ -460,7 +466,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)
|
||||
|
|
|
@ -226,6 +226,11 @@ def test_eoferror() -> None:
|
|||
im.seek(n_frames - 1)
|
||||
|
||||
|
||||
def test_ultra_hdr() -> None:
|
||||
with Image.open("Tests/images/ultrahdr.jpg") as im:
|
||||
assert im.format == "JPEG"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_image_grab(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
|
|
|
@ -535,8 +535,10 @@ class TestFilePng:
|
|||
|
||||
def test_repr_png(self) -> None:
|
||||
im = hopper()
|
||||
b = im._repr_png_()
|
||||
assert b is not None
|
||||
|
||||
with Image.open(BytesIO(im._repr_png_())) as repr_png:
|
||||
with Image.open(BytesIO(b)) as repr_png:
|
||||
assert repr_png.format == "PNG"
|
||||
assert_image_equal(im, repr_png)
|
||||
|
||||
|
@ -655,11 +657,12 @@ class TestFilePng:
|
|||
png.call(cid, 0, 0)
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
def test_specify_bits(self, tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize("save_all", (True, False))
|
||||
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
|
||||
im = hopper("P")
|
||||
|
||||
out = str(tmp_path / "temp.png")
|
||||
im.save(out, bits=4)
|
||||
im.save(out, bits=4, save_all=save_all)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert len(reloaded.png.im_palette[1]) == 48
|
||||
|
@ -683,6 +686,7 @@ class TestFilePng:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
xmp = im.getxmp()
|
||||
|
||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||
|
@ -767,16 +771,12 @@ class TestFilePng:
|
|||
def test_save_stdout(self, buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
mystdout = MyStdOut()
|
||||
else:
|
||||
mystdout = BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
sys.stdout = mystdout # type: ignore[assignment]
|
||||
|
||||
with Image.open(TEST_PNG_FILE) as im:
|
||||
im.save(sys.stdout, "PNG")
|
||||
|
@ -784,7 +784,7 @@ class TestFilePng:
|
|||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if buffer:
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
|
||||
|
|
|
@ -368,16 +368,12 @@ def test_mimetypes(tmp_path: Path) -> None:
|
|||
def test_save_stdout(buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
mystdout = MyStdOut()
|
||||
else:
|
||||
mystdout = BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
sys.stdout = mystdout # type: ignore[assignment]
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.save(sys.stdout, "PPM")
|
||||
|
@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
|
|||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if buffer:
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, TEST_FILE)
|
||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, PsdImagePlugin, UnidentifiedImageError
|
||||
from PIL import Image, PsdImagePlugin
|
||||
|
||||
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
|
||||
|
||||
|
@ -150,20 +150,26 @@ def test_combined_larger_than_size() -> None:
|
|||
@pytest.mark.parametrize(
|
||||
"test_file,raises",
|
||||
[
|
||||
(
|
||||
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
||||
UnidentifiedImageError,
|
||||
),
|
||||
(
|
||||
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
||||
UnidentifiedImageError,
|
||||
),
|
||||
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
||||
],
|
||||
)
|
||||
def test_crashes(test_file: str, raises) -> None:
|
||||
def test_crashes(test_file: str, raises: type[Exception]) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with pytest.raises(raises):
|
||||
with Image.open(f):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
[
|
||||
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
||||
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
||||
],
|
||||
)
|
||||
def test_layer_crashes(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
with pytest.raises(SyntaxError):
|
||||
im.layers
|
||||
|
|
|
@ -105,6 +105,7 @@ def test_load_image_series() -> None:
|
|||
img_list = SpiderImagePlugin.loadImageSeries(file_list)
|
||||
|
||||
# Assert
|
||||
assert img_list is not None
|
||||
assert len(img_list) == 1
|
||||
assert isinstance(img_list[0], Image.Image)
|
||||
assert img_list[0].size == (128, 128)
|
||||
|
|
|
@ -113,14 +113,14 @@ 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")
|
||||
|
||||
def test_set_legacy_api(self) -> None:
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
with pytest.raises(Exception) as e:
|
||||
ifd.legacy_api = None
|
||||
ifd.legacy_api = False
|
||||
assert str(e.value) == "Not allowing setting of legacy api"
|
||||
|
||||
def test_xyres_tiff(self) -> None:
|
||||
|
@ -621,6 +621,19 @@ class TestFileTiff:
|
|||
|
||||
assert_image_equal_tofile(im, tmpfile)
|
||||
|
||||
def test_iptc(self, tmp_path: Path) -> None:
|
||||
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im = hopper()
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
ifd[33723] = 1
|
||||
ifd.tagtype[33723] = 4
|
||||
im.tag_v2 = ifd
|
||||
im.save(outfile)
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
assert 33723 not in im.tag_v2
|
||||
|
||||
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im = hopper()
|
||||
|
@ -810,6 +823,7 @@ class TestFileTiff:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
xmp = im.getxmp()
|
||||
|
||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||
|
|
|
@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
|
|||
|
||||
from .helper import assert_deep_equal, hopper
|
||||
|
||||
TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}
|
||||
TAG_IDS: dict[str, int] = {
|
||||
info.name: info.value
|
||||
for info in TiffTags.TAGS_V2.values()
|
||||
if info.value is not None
|
||||
}
|
||||
|
||||
|
||||
def test_rt_metadata(tmp_path: Path) -> None:
|
||||
|
@ -411,8 +415,8 @@ def test_empty_values() -> None:
|
|||
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
||||
info.load(data)
|
||||
# Should not raise ValueError.
|
||||
info = dict(info)
|
||||
assert 33432 in info
|
||||
info_dict = dict(info)
|
||||
assert 33432 in info_dict
|
||||
|
||||
|
||||
def test_photoshop_info(tmp_path: Path) -> None:
|
||||
|
|
|
@ -5,6 +5,7 @@ import sys
|
|||
import warnings
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -70,7 +71,9 @@ class TestFileWebp:
|
|||
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
||||
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
|
||||
|
||||
def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None:
|
||||
def _roundtrip(
|
||||
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
|
||||
) -> None:
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
|
||||
hopper(mode).save(temp_file, **args)
|
||||
|
|
|
@ -129,6 +129,7 @@ def test_getxmp() -> None:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
assert (
|
||||
im.getxmp()["xmpmeta"]["xmptk"]
|
||||
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
|
||||
|
|
|
@ -34,7 +34,7 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
|
|||
|
||||
def test_leak(self) -> None:
|
||||
if features.check_module("freetype2"):
|
||||
ImageFont.core = _util.DeferredError(ImportError)
|
||||
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
||||
try:
|
||||
default_font = ImageFont.load_default()
|
||||
finally:
|
||||
|
|
|
@ -8,7 +8,7 @@ import sys
|
|||
import tempfile
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
from typing import IO, Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -152,7 +152,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:
|
||||
|
@ -175,11 +175,19 @@ class TestImage:
|
|||
def test_fp_name(self, tmp_path: Path) -> None:
|
||||
temp_file = str(tmp_path / "temp.jpg")
|
||||
|
||||
class FP:
|
||||
class FP(io.BytesIO):
|
||||
name: str
|
||||
|
||||
def write(self, b: bytes) -> None:
|
||||
pass
|
||||
if sys.version_info >= (3, 12):
|
||||
from collections.abc import Buffer
|
||||
|
||||
def write(self, data: Buffer) -> int:
|
||||
return len(data)
|
||||
|
||||
else:
|
||||
|
||||
def write(self, data: Any) -> int:
|
||||
return len(data)
|
||||
|
||||
fp = FP()
|
||||
fp.name = temp_file
|
||||
|
@ -393,13 +401,13 @@ class TestImage:
|
|||
|
||||
# errors
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, "invalid source")
|
||||
source.alpha_composite(over, "invalid destination") # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, (0, 0), "invalid destination")
|
||||
source.alpha_composite(over, (0, 0), "invalid source") # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, 0)
|
||||
source.alpha_composite(over, 0) # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, (0, 0), 0)
|
||||
source.alpha_composite(over, (0, 0), 0) # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, (0, 0), (0, -1))
|
||||
|
||||
|
@ -909,6 +917,10 @@ class TestImage:
|
|||
assert tag not in exif.get_ifd(0x8769)
|
||||
assert exif.get_ifd(0xA005)
|
||||
|
||||
def test_empty_xmp(self) -> None:
|
||||
with Image.open("Tests/images/hopper.gif") as im:
|
||||
assert im.getxmp() == {}
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||
im = Image.new("RGB", size)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
@ -13,13 +13,16 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
|||
|
||||
im = hopper().resize((128, 100))
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
def test_toarray() -> None:
|
||||
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
|
||||
ai = numpy.array(im.convert(mode))
|
||||
return ai.shape, ai.dtype.str, ai.nbytes
|
||||
|
||||
def test_with_dtype(dtype) -> None:
|
||||
def test_with_dtype(dtype: npt.DTypeLike) -> None:
|
||||
ai = numpy.array(im, dtype=dtype)
|
||||
assert ai.dtype == dtype
|
||||
|
||||
|
|
|
@ -16,7 +16,9 @@ def draft_roundtrip(
|
|||
im = Image.new(in_mode, in_size)
|
||||
data = tostring(im, "JPEG")
|
||||
im = fromstring(data)
|
||||
mode, box = im.draft(req_mode, req_size)
|
||||
result = im.draft(req_mode, req_size)
|
||||
assert result is not None
|
||||
box = result[1]
|
||||
scale, _ = im.decoderconfig
|
||||
assert box[:2] == (0, 0)
|
||||
assert (im.width - scale) < box[2] <= im.width
|
||||
|
|
|
@ -137,7 +137,7 @@ def test_builtinfilter_p() -> None:
|
|||
builtin_filter = ImageFilter.BuiltinFilter()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
builtin_filter.filter(hopper("P"))
|
||||
builtin_filter.filter(hopper("P").im)
|
||||
|
||||
|
||||
def test_kernel_not_enough_coefficients() -> None:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -338,3 +338,8 @@ class TestImagingPaste:
|
|||
|
||||
im.copy().paste(im2)
|
||||
im.copy().paste(im2, (0, 0))
|
||||
|
||||
def test_incorrect_abbreviated_form(self) -> None:
|
||||
im = Image.new("L", (1, 1))
|
||||
with pytest.raises(ValueError):
|
||||
im.paste(im, im, im)
|
||||
|
|
|
@ -61,4 +61,4 @@ def test_f_lut() -> None:
|
|||
def test_f_mode() -> None:
|
||||
im = hopper("F")
|
||||
with pytest.raises(ValueError):
|
||||
im.point(None)
|
||||
im.point([])
|
||||
|
|
|
@ -79,6 +79,7 @@ def test_putpalette_with_alpha_values() -> None:
|
|||
(
|
||||
("RGBA", (1, 2, 3, 4)),
|
||||
("RGBAX", (1, 2, 3, 4, 0)),
|
||||
("ARGB", (4, 1, 2, 3)),
|
||||
),
|
||||
)
|
||||
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -445,7 +445,7 @@ class TestCoreResampleBox:
|
|||
im.resize((32, 32), resample, (20, 20, 100, 20))
|
||||
|
||||
with pytest.raises(TypeError, match="must be sequence of length 4"):
|
||||
im.resize((32, 32), resample, (im.width, im.height))
|
||||
im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError, match="can't be negative"):
|
||||
im.resize((32, 32), resample, (-20, 20, 100, 100))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ def test_sanity() -> None:
|
|||
|
||||
|
||||
def test_flags() -> None:
|
||||
assert ImageCms.Flags.NONE == 0
|
||||
assert ImageCms.Flags.NONE.value == 0
|
||||
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
|
||||
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
|
||||
|
||||
|
@ -569,9 +569,9 @@ def assert_aux_channel_preserved(
|
|||
for delta in nine_grid_deltas:
|
||||
channel_data.paste(
|
||||
channel_pattern,
|
||||
tuple(
|
||||
paste_offset[c] + delta[c] * channel_pattern.size[c]
|
||||
for c in range(2)
|
||||
(
|
||||
paste_offset[0] + delta[0] * channel_pattern.size[0],
|
||||
paste_offset[1] + delta[1] * channel_pattern.size[1],
|
||||
),
|
||||
)
|
||||
chans.append(channel_data)
|
||||
|
@ -642,7 +642,8 @@ def test_auxiliary_channels_isolated() -> None:
|
|||
# convert with and without AUX data, test colors are equal
|
||||
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
|
||||
source_profile = ImageCms.createProfile(src_colorSpace)
|
||||
destination_profile = ImageCms.createProfile(dst_format[1])
|
||||
dst_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], dst_format[1])
|
||||
destination_profile = ImageCms.createProfile(dst_colorSpace)
|
||||
source_image = src_format[3]
|
||||
test_transform = ImageCms.buildTransform(
|
||||
source_profile,
|
||||
|
|
|
@ -448,6 +448,7 @@ def test_shape1() -> None:
|
|||
x3, y3 = 95, 5
|
||||
|
||||
# Act
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.move(x0, y0)
|
||||
s.curve(x1, y1, x2, y2, x3, y3)
|
||||
|
@ -469,6 +470,7 @@ def test_shape2() -> None:
|
|||
x3, y3 = 5, 95
|
||||
|
||||
# Act
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.move(x0, y0)
|
||||
s.curve(x1, y1, x2, y2, x3, y3)
|
||||
|
@ -487,6 +489,7 @@ def test_transform() -> None:
|
|||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# Act
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.line(0, 0)
|
||||
s.transform((0, 0, 0, 0, 0, 0))
|
||||
|
@ -631,6 +634,19 @@ def test_polygon(points: Coords) -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("points", POINTS)
|
||||
def test_polygon_width_I16(points: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("I;16", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# Act
|
||||
draw.polygon(points, outline=0xFFFF, width=2)
|
||||
|
||||
# Assert
|
||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon_width_I.tiff")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("RGB", "L"))
|
||||
@pytest.mark.parametrize("kite_points", KITE_POINTS)
|
||||
def test_polygon_kite(
|
||||
|
@ -913,7 +929,12 @@ def test_rounded_rectangle_translucent(
|
|||
def test_floodfill(bbox: Coords) -> None:
|
||||
red = ImageColor.getrgb("red")
|
||||
|
||||
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
|
||||
mode_values: list[tuple[str, int | tuple[int, ...]]] = [
|
||||
("L", 1),
|
||||
("RGBA", (255, 0, 0, 0)),
|
||||
("RGB", red),
|
||||
]
|
||||
for mode, value in mode_values:
|
||||
# Arrange
|
||||
im = Image.new(mode, (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -1429,6 +1450,7 @@ def test_same_color_outline(bbox: Coords) -> None:
|
|||
x2, y2 = 95, 50
|
||||
x3, y3 = 95, 5
|
||||
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.move(x0, y0)
|
||||
s.curve(x1, y1, x2, y2, x3, y3)
|
||||
|
@ -1467,7 +1489,7 @@ def test_same_color_outline(bbox: Coords) -> None:
|
|||
(4, "square", {}),
|
||||
(8, "regular_octagon", {}),
|
||||
(4, "square_rotate_45", {"rotation": 45}),
|
||||
(3, "triangle_width", {"width": 5, "outline": "yellow"}),
|
||||
(3, "triangle_width", {"outline": "yellow", "width": 5}),
|
||||
],
|
||||
)
|
||||
def test_draw_regular_polygon(
|
||||
|
@ -1477,7 +1499,10 @@ def test_draw_regular_polygon(
|
|||
filename = f"Tests/images/imagedraw_{polygon_name}.png"
|
||||
draw = ImageDraw.Draw(im)
|
||||
bounding_circle = ((W // 2, H // 2), 25)
|
||||
draw.regular_polygon(bounding_circle, n_sides, fill="red", **args)
|
||||
rotation = int(args.get("rotation", 0))
|
||||
outline = args.get("outline")
|
||||
width = int(args.get("width", 1))
|
||||
draw.regular_polygon(bounding_circle, n_sides, rotation, "red", outline, width)
|
||||
assert_image_equal_tofile(im, filename)
|
||||
|
||||
|
||||
|
@ -1562,10 +1587,14 @@ 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)
|
||||
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
|
||||
assert str(e.value) == error_message
|
||||
|
||||
|
||||
|
@ -1626,6 +1655,6 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
|
|||
draw.rounded_rectangle(xy)
|
||||
|
||||
|
||||
def test_getdraw():
|
||||
def test_getdraw() -> None:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
ImageDraw.getdraw(None, [])
|
||||
|
|
|
@ -51,9 +51,18 @@ def test_sanity() -> None:
|
|||
pen = ImageDraw2.Pen("blue", width=7)
|
||||
draw.line(list(range(10)), pen)
|
||||
|
||||
draw, handler = ImageDraw.getdraw(im)
|
||||
draw2, handler = ImageDraw.getdraw(im)
|
||||
assert draw2 is not None
|
||||
pen = ImageDraw2.Pen("blue", width=7)
|
||||
draw.line(list(range(10)), pen)
|
||||
draw2.line(list(range(10)), pen)
|
||||
|
||||
|
||||
def test_mode() -> None:
|
||||
draw = ImageDraw2.Draw("L", (1, 1))
|
||||
assert draw.image.mode == "L"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageDraw2.Draw("L")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
|
|
|
@ -209,7 +209,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
|
|||
|
||||
super().__init__(mode, *args)
|
||||
|
||||
def decode(self, buffer):
|
||||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
# eof
|
||||
return -1, 0
|
||||
|
||||
|
@ -222,7 +222,7 @@ class MockPyEncoder(ImageFile.PyEncoder):
|
|||
|
||||
super().__init__(mode, *args)
|
||||
|
||||
def encode(self, buffer):
|
||||
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
||||
return 1, 1, b""
|
||||
|
||||
def cleanup(self) -> None:
|
||||
|
@ -351,7 +351,9 @@ class TestPyEncoder(CodecsTest):
|
|||
ImageFile._save(
|
||||
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
||||
)
|
||||
assert MockPyEncoder.last.cleanup_called
|
||||
last: MockPyEncoder | None = MockPyEncoder.last
|
||||
assert last
|
||||
assert last.cleanup_called
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
|
@ -381,7 +383,7 @@ class TestPyEncoder(CodecsTest):
|
|||
def test_encode(self) -> None:
|
||||
encoder = ImageFile.PyEncoder(None)
|
||||
with pytest.raises(NotImplementedError):
|
||||
encoder.encode(None)
|
||||
encoder.encode(0)
|
||||
|
||||
bytes_consumed, errcode = encoder.encode_to_pyfd()
|
||||
assert bytes_consumed == 0
|
||||
|
|
|
@ -209,7 +209,7 @@ def test_getlength(
|
|||
assert length == length_raqm
|
||||
|
||||
|
||||
def test_float_size() -> None:
|
||||
def test_float_size(layout_engine: ImageFont.Layout) -> None:
|
||||
lengths = []
|
||||
for size in (48, 48.5, 49):
|
||||
f = ImageFont.truetype(
|
||||
|
@ -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
|
||||
|
@ -494,8 +494,8 @@ def test_default_font() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
|
||||
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
|
||||
@pytest.mark.parametrize("mode", ("", "1", "RGBA"))
|
||||
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
|
||||
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
||||
|
||||
|
||||
|
@ -548,7 +548,7 @@ def test_find_font(
|
|||
|
||||
def loadable_font(
|
||||
filepath: str, size: int, index: int, encoding: str, *args: Any
|
||||
):
|
||||
) -> ImageFont.FreeTypeFont:
|
||||
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
|
||||
if filepath == path_to_fake:
|
||||
return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
|
||||
|
@ -564,6 +564,7 @@ def test_find_font(
|
|||
# catching syntax like errors
|
||||
monkeypatch.setattr(sys, "platform", platform)
|
||||
if platform == "linux":
|
||||
monkeypatch.setenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
|
||||
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
|
||||
|
||||
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
|
||||
|
@ -1096,6 +1097,23 @@ def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
|
|||
imagefont.getmask("A" * 1_000_001)
|
||||
|
||||
|
||||
def test_bytes(font: ImageFont.FreeTypeFont) -> None:
|
||||
assert font.getlength(b"test") == font.getlength("test")
|
||||
|
||||
assert font.getbbox(b"test") == font.getbbox("test")
|
||||
|
||||
assert_image_equal(
|
||||
Image.Image()._new(font.getmask(b"test")),
|
||||
Image.Image()._new(font.getmask("test")),
|
||||
)
|
||||
|
||||
assert_image_equal(
|
||||
Image.Image()._new(font.getmask2(b"test")[0]),
|
||||
Image.Image()._new(font.getmask2("test")[0]),
|
||||
)
|
||||
assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
[
|
||||
|
|
|
@ -14,7 +14,7 @@ original_core = ImageFont.core
|
|||
|
||||
def setup_module() -> None:
|
||||
if features.check_module("freetype2"):
|
||||
ImageFont.core = _util.DeferredError(ImportError)
|
||||
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
||||
|
||||
|
||||
def teardown_module() -> None:
|
||||
|
@ -76,3 +76,8 @@ def test_oom() -> None:
|
|||
font = ImageFont.ImageFont()
|
||||
font._load_pilfont_data(fp, Image.new("L", (1, 1)))
|
||||
font.getmask("A" * 1_000_000)
|
||||
|
||||
|
||||
def test_freetypefont_without_freetype() -> None:
|
||||
with pytest.raises(ImportError):
|
||||
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
|
||||
|
|
|
@ -89,6 +89,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
|||
p.communicate()
|
||||
|
||||
im = ImageGrab.grabclipboard()
|
||||
assert isinstance(im, list)
|
||||
assert len(im) == 1
|
||||
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
|
||||
|
||||
|
@ -105,6 +106,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
|||
p.communicate()
|
||||
|
||||
im = ImageGrab.grabclipboard()
|
||||
assert isinstance(im, Image.Image)
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
@ -120,6 +122,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
|||
with open(image_path, "rb") as fp:
|
||||
subprocess.call(["wl-copy"], stdin=fp)
|
||||
im = ImageGrab.grabclipboard()
|
||||
assert isinstance(im, Image.Image)
|
||||
assert_image_equal_tofile(im, image_path)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -101,7 +101,7 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
|
|||
assert i.im.getpixel((x, y))[c] >= 250
|
||||
# Fuzzy match.
|
||||
|
||||
def gp(x, y):
|
||||
def gp(x: int, y: int) -> tuple[int, ...]:
|
||||
return i.im.getpixel((x, y))
|
||||
|
||||
assert 236 <= gp(7, 4)[0] <= 239
|
||||
|
|
|
@ -45,7 +45,7 @@ def test_getcolor() -> None:
|
|||
|
||||
# Test unknown color specifier
|
||||
with pytest.raises(ValueError):
|
||||
palette.getcolor("unknown")
|
||||
palette.getcolor("unknown") # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_getcolor_rgba_color_rgb_palette() -> None:
|
||||
|
@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None:
|
|||
|
||||
palette.save(f)
|
||||
|
||||
p = ImagePalette.load(f)
|
||||
lut = ImagePalette.load(f)
|
||||
|
||||
# load returns raw palette information
|
||||
assert len(p[0]) == 768
|
||||
assert p[1] == "RGB"
|
||||
assert len(lut[0]) == 768
|
||||
assert lut[1] == "RGB"
|
||||
|
||||
p = ImagePalette.raw(p[1], p[0])
|
||||
p = ImagePalette.raw(lut[1], lut[0])
|
||||
assert isinstance(p, ImagePalette.ImagePalette)
|
||||
assert p.palette == palette.tobytes()
|
||||
|
||||
|
|
|
@ -41,18 +41,13 @@ def test_rgb() -> None:
|
|||
checkrgb(0, 0, 255)
|
||||
|
||||
|
||||
def test_image() -> None:
|
||||
modes = ["1", "RGB", "RGBA", "L", "P"]
|
||||
qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage
|
||||
if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
|
||||
modes.append("I;16")
|
||||
|
||||
for mode in modes:
|
||||
im = hopper(mode)
|
||||
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
|
||||
if mode not in ("RGB", "RGBA"):
|
||||
im = im.convert("RGB")
|
||||
assert_image_similar(roundtripped_im, im, 1)
|
||||
@pytest.mark.parametrize("mode", ("1", "RGB", "RGBA", "L", "P", "I;16"))
|
||||
def test_image(mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
|
||||
if mode not in ("RGB", "RGBA"):
|
||||
im = im.convert("RGB")
|
||||
assert_image_similar(roundtripped_im, im, 1)
|
||||
|
||||
|
||||
def test_closed_file() -> None:
|
||||
|
|
|
@ -45,21 +45,22 @@ if is_win32():
|
|||
memcpy = ctypes.cdll.msvcrt.memcpy
|
||||
memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
|
||||
|
||||
CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC
|
||||
windll = getattr(ctypes, "windll")
|
||||
CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
|
||||
CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC]
|
||||
CreateCompatibleDC.restype = ctypes.wintypes.HDC
|
||||
|
||||
DeleteDC = ctypes.windll.gdi32.DeleteDC
|
||||
DeleteDC = windll.gdi32.DeleteDC
|
||||
DeleteDC.argtypes = [ctypes.wintypes.HDC]
|
||||
|
||||
SelectObject = ctypes.windll.gdi32.SelectObject
|
||||
SelectObject = windll.gdi32.SelectObject
|
||||
SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ]
|
||||
SelectObject.restype = ctypes.wintypes.HGDIOBJ
|
||||
|
||||
DeleteObject = ctypes.windll.gdi32.DeleteObject
|
||||
DeleteObject = windll.gdi32.DeleteObject
|
||||
DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ]
|
||||
|
||||
CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection
|
||||
CreateDIBSection = windll.gdi32.CreateDIBSection
|
||||
CreateDIBSection.argtypes = [
|
||||
ctypes.wintypes.HDC,
|
||||
ctypes.c_void_p,
|
||||
|
@ -70,7 +71,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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, _typing
|
||||
|
||||
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 as npt
|
||||
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: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
|
||||
if bands == 1:
|
||||
if boolean:
|
||||
data = [0, 255] * 50
|
||||
|
@ -99,14 +104,14 @@ 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: _typing.NumpyArray) -> 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 +162,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: npt.DTypeLike) -> None:
|
||||
img = hopper(mode)
|
||||
|
||||
# Resize to non-square
|
||||
|
@ -207,7 +212,7 @@ def test_putdata() -> None:
|
|||
numpy.float64,
|
||||
),
|
||||
)
|
||||
def test_roundtrip_eye(dtype) -> None:
|
||||
def test_roundtrip_eye(dtype: npt.DTypeLike) -> None:
|
||||
arr = numpy.eye(10, dtype=dtype)
|
||||
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))
|
||||
|
||||
|
|
|
@ -54,16 +54,12 @@ def test_stdout(buffer: bool) -> None:
|
|||
# Temporarily redirect stdout
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
mystdout = MyStdOut()
|
||||
else:
|
||||
mystdout = BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
sys.stdout = mystdout # type: ignore[assignment]
|
||||
|
||||
ps = PSDraw.PSDraw()
|
||||
_create_document(ps)
|
||||
|
@ -71,6 +67,6 @@ def test_stdout(buffer: bool) -> None:
|
|||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if buffer:
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
assert mystdout.getvalue() != b""
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -286,7 +286,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
|
|||
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
||||
offset.
|
||||
|
||||
.. image:: ./example/size_vs_bbox.png
|
||||
.. image:: ./example/size_vs_bbox.webp
|
||||
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
||||
:align: center
|
||||
|
||||
|
|
Before Width: | Height: | Size: 9.5 KiB |
|
@ -26,5 +26,5 @@ if __name__ == "__main__":
|
|||
d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
|
||||
if y != 0:
|
||||
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
|
||||
im.save("docs/example/anchors.png")
|
||||
im.save("docs/example/anchors.webp")
|
||||
im.show()
|
||||
|
|
BIN
docs/example/anchors.webp
Normal file
After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/image_thumbnail.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_contain.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 38 KiB |
BIN
docs/example/imageops_cover.webp
Normal file
After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 28 KiB |
BIN
docs/example/imageops_fit.webp
Normal file
After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_pad.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 13 KiB |
BIN
docs/example/size_vs_bbox.webp
Normal file
After Width: | Height: | Size: 4.8 KiB |
|
@ -132,7 +132,7 @@ of the two lines.
|
|||
|
||||
.. comment: Image generated with ../example/anchors.py
|
||||
|
||||
.. image:: ../example/anchors.png
|
||||
.. image:: ../example/anchors.webp
|
||||
:alt: Text anchor examples
|
||||
:align: center
|
||||
|
||||
|
|
|
@ -278,26 +278,26 @@ choose to resize relative to a given size.
|
|||
|
||||
from PIL import Image, ImageOps
|
||||
size = (100, 150)
|
||||
with Image.open("Tests/images/hopper.png") as im:
|
||||
ImageOps.contain(im, size).save("imageops_contain.png")
|
||||
ImageOps.cover(im, size).save("imageops_cover.png")
|
||||
ImageOps.fit(im, size).save("imageops_fit.png")
|
||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
|
||||
with Image.open("Tests/images/hopper.webp") as im:
|
||||
ImageOps.contain(im, size).save("imageops_contain.webp")
|
||||
ImageOps.cover(im, size).save("imageops_cover.webp")
|
||||
ImageOps.fit(im, size).save("imageops_fit.webp")
|
||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
|
||||
|
||||
# thumbnail() can also be used,
|
||||
# but will modify the image object in place
|
||||
im.thumbnail(size)
|
||||
im.save("imageops_thumbnail.png")
|
||||
im.save("image_thumbnail.webp")
|
||||
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||
+================+===========================================+============================================+==========================================+========================================+========================================+
|
||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||
+================+============================================+=============================================+===========================================+=========================================+=========================================+
|
||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|Resulting image | .. image:: ../example/image_thumbnail.webp | .. image:: ../example/imageops_contain.webp | .. image:: ../example/imageops_cover.webp | .. image:: ../example/imageops_fit.webp | .. image:: ../example/imageops_pad.webp |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|
||||
.. _color-transforms:
|
||||
|
||||
|
|
|
@ -195,6 +195,7 @@ This helps to get the bounding box coordinates of the input image::
|
|||
.. automethod:: PIL.Image.Image.getpalette
|
||||
.. automethod:: PIL.Image.Image.getpixel
|
||||
.. automethod:: PIL.Image.Image.getprojection
|
||||
.. automethod:: PIL.Image.Image.getxmp
|
||||
.. automethod:: PIL.Image.Image.histogram
|
||||
.. automethod:: PIL.Image.Image.paste
|
||||
.. automethod:: PIL.Image.Image.point
|
||||
|
|
|
@ -691,23 +691,7 @@ Methods
|
|||
:param hints: An optional list of hints.
|
||||
:returns: A (drawing context, drawing resource factory) tuple.
|
||||
|
||||
.. py:method:: floodfill(image, xy, value, border=None, thresh=0)
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
|
||||
Fills a bounded region with a given color.
|
||||
|
||||
:param image: Target image.
|
||||
:param xy: Seed position (a 2-item coordinate tuple).
|
||||
:param value: Fill color.
|
||||
:param border: Optional border value. If given, the region consists of
|
||||
pixels with a color different from the border color. If not given,
|
||||
the region consists of pixels having the same color as the seed
|
||||
pixel.
|
||||
:param thresh: Optional threshold value which specifies a maximum
|
||||
tolerable difference of a pixel value from the 'background' in
|
||||
order for it to be replaced. Useful for filling regions of non-
|
||||
homogeneous, but similar, colors.
|
||||
.. autofunction:: PIL.ImageDraw.floodfill
|
||||
|
||||
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
|
||||
.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
||||
|
|
|
@ -36,26 +36,26 @@ Resize relative to a given size
|
|||
|
||||
from PIL import Image, ImageOps
|
||||
size = (100, 150)
|
||||
with Image.open("Tests/images/hopper.png") as im:
|
||||
ImageOps.contain(im, size).save("imageops_contain.png")
|
||||
ImageOps.cover(im, size).save("imageops_cover.png")
|
||||
ImageOps.fit(im, size).save("imageops_fit.png")
|
||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
|
||||
with Image.open("Tests/images/hopper.webp") as im:
|
||||
ImageOps.contain(im, size).save("imageops_contain.webp")
|
||||
ImageOps.cover(im, size).save("imageops_cover.webp")
|
||||
ImageOps.fit(im, size).save("imageops_fit.webp")
|
||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
|
||||
|
||||
# thumbnail() can also be used,
|
||||
# but will modify the image object in place
|
||||
im.thumbnail(size)
|
||||
im.save("imageops_thumbnail.png")
|
||||
im.save("image_thumbnail.webp")
|
||||
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||
+================+===========================================+============================================+==========================================+========================================+========================================+
|
||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||
+================+============================================+=============================================+===========================================+=========================================+=========================================+
|
||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|Resulting image | .. image:: ../example/image_thumbnail.webp | .. image:: ../example/imageops_contain.webp | .. image:: ../example/imageops_cover.webp | .. image:: ../example/imageops_fit.webp | .. image:: ../example/imageops_pad.webp |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|
||||
.. autofunction:: contain
|
||||
.. autofunction:: cover
|
||||
|
|
|
@ -44,42 +44,23 @@ Access using negative indexes is also possible. ::
|
|||
-----------------------------
|
||||
|
||||
.. class:: PixelAccess
|
||||
:canonical: PIL.Image.core.PixelAccess
|
||||
|
||||
.. method:: __setitem__(self, xy, color):
|
||||
.. method:: __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]
|
||||
|
||||
Modifies the pixel at x,y. The color is given as a single
|
||||
numerical value for single band images, and a tuple for
|
||||
multi-band images
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
|
||||
|
||||
.. method:: __getitem__(self, xy):
|
||||
|
||||
Returns the pixel at x,y. The pixel is returned as a single
|
||||
value for single band images or a tuple for multiple band
|
||||
images
|
||||
Returns the pixel at x,y. The pixel is returned as a single
|
||||
value for single band images or a tuple for multi-band images.
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:returns: a pixel value for single band images, a tuple of
|
||||
pixel values for multiband images.
|
||||
pixel values for multiband images.
|
||||
|
||||
.. method:: putpixel(self, xy, color):
|
||||
.. method:: __setitem__(self, xy: tuple[int, int], color: float | tuple[int, ...]) -> None
|
||||
|
||||
Modifies the pixel at x,y. The color is given as a single
|
||||
numerical value for single band images, and a tuple for
|
||||
multi-band images. In addition to this, RGB and RGBA tuples
|
||||
are accepted for P and PA images.
|
||||
multi-band images.
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
|
||||
|
||||
.. method:: getpixel(self, xy):
|
||||
|
||||
Returns the pixel at x,y. The pixel is returned as a single
|
||||
value for single band images or a tuple for multiple band
|
||||
images
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:returns: a pixel value for single band images, a tuple of
|
||||
pixel values for multiband images.
|
||||
:param color: The pixel value according to its mode,
|
||||
e.g. tuple (r, g, b) for RGB mode.
|
||||
|
|
|
@ -44,3 +44,4 @@ Access using negative indexes is also possible. ::
|
|||
|
||||
.. autoclass:: PIL.PyAccess.PyAccess()
|
||||
:members:
|
||||
:special-members: __getitem__, __setitem__
|
||||
|
|
|
@ -33,6 +33,10 @@ Internal Modules
|
|||
Provides a convenient way to import type hints that are not available
|
||||
on some Python versions.
|
||||
|
||||
.. py:class:: NumpyArray
|
||||
|
||||
Typing alias.
|
||||
|
||||
.. py:class:: StrOrBytesPath
|
||||
|
||||
Typing alias.
|
||||
|
|
|
@ -18,9 +18,9 @@ is not secure.
|
|||
|
||||
- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve
|
||||
orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead.
|
||||
- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It
|
||||
will now use ``defusedxml`` instead. If the dependency is not present, an empty
|
||||
dictionary will be returned and a warning raised.
|
||||
- ``getxmp()`` was added to :py:class:`~PIL.JpegImagePlugin.JpegImageFile` in Pillow
|
||||
8.2.0. It will now use ``defusedxml`` instead. If the dependency is not present, an
|
||||
empty dictionary will be returned and a warning raised.
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
|
|
@ -98,7 +98,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
|
|||
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
||||
offset.
|
||||
|
||||
.. image:: ../example/size_vs_bbox.png
|
||||
.. image:: ../example/size_vs_bbox.webp
|
||||
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
||||
:align: center
|
||||
|
||||
|
|
4
setup.py
|
@ -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():
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -430,6 +430,7 @@ class BLPEncoder(ImageFile.PyEncoder):
|
|||
|
||||
def _write_palette(self) -> bytes:
|
||||
data = b""
|
||||
assert self.im is not None
|
||||
palette = self.im.getpalette("RGBA", "RGBA")
|
||||
for i in range(len(palette) // 4):
|
||||
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
||||
|
@ -444,6 +445,7 @@ class BLPEncoder(ImageFile.PyEncoder):
|
|||
offset = 20 + 16 * 4 * 2 + len(palette_data)
|
||||
data = struct.pack("<16I", offset, *((0,) * 15))
|
||||
|
||||
assert self.im is not None
|
||||
w, h = self.im.size
|
||||
data += struct.pack("<16I", w * h, *((0,) * 15))
|
||||
|
||||
|
|
|
@ -380,6 +380,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
elif pfflags & DDPF.PALETTEINDEXED8:
|
||||
self._mode = "P"
|
||||
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
|
||||
self.palette.mode = "RGBA"
|
||||
elif pfflags & DDPF.FOURCC:
|
||||
offset = header_size + 4
|
||||
if fourcc == D3DFMT.DXT1:
|
||||
|
|
|
@ -230,6 +230,11 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
trailer_reached = False
|
||||
|
||||
def check_required_header_comments() -> None:
|
||||
"""
|
||||
The EPS specification requires that some headers exist.
|
||||
This should be checked when the header comments formally end,
|
||||
when image data starts, or when the file ends, whichever comes first.
|
||||
"""
|
||||
if "PS-Adobe" not in self.info:
|
||||
msg = 'EPS header missing "%!PS-Adobe" comment'
|
||||
raise SyntaxError(msg)
|
||||
|
@ -270,6 +275,8 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
if byte == b"":
|
||||
# if we didn't read a byte we must be at the end of the file
|
||||
if bytes_read == 0:
|
||||
if reading_header_comments:
|
||||
check_required_header_comments()
|
||||
break
|
||||
elif byte in b"\r\n":
|
||||
# if we read a line ending character, ignore it and parse what
|
||||
|
@ -365,8 +372,6 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
trailer_reached = True
|
||||
bytes_read = 0
|
||||
|
||||
check_required_header_comments()
|
||||
|
||||
if not self._size:
|
||||
msg = "cannot determine EPS bounding box"
|
||||
raise OSError(msg)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -432,7 +432,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self._prev_im = self.im
|
||||
if self._frame_palette:
|
||||
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
|
||||
self.im.putpalette(*self._frame_palette.getdata())
|
||||
self.im.putpalette("RGB", *self._frame_palette.getdata())
|
||||
else:
|
||||
self.im = None
|
||||
self._mode = temp_mode
|
||||
|
@ -457,6 +457,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
|
||||
|
|
|
@ -79,7 +79,7 @@ OPEN = {
|
|||
"LA image": ("LA", "LA;L"),
|
||||
"PA image": ("LA", "PA;L"),
|
||||
"RGBA image": ("RGBA", "RGBA;L"),
|
||||
"RGBX image": ("RGBX", "RGBX;L"),
|
||||
"RGBX image": ("RGB", "RGBX;L"),
|
||||
"CMYK image": ("CMYK", "CMYK;L"),
|
||||
"YCC image": ("YCbCr", "YCbCr;L"),
|
||||
}
|
||||
|
|
291
src/PIL/Image.py
|
@ -41,7 +41,16 @@ import warnings
|
|||
from collections.abc import Callable, MutableMapping
|
||||
from enum import IntEnum
|
||||
from types import ModuleType
|
||||
from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, Tuple, cast
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Tuple,
|
||||
cast,
|
||||
)
|
||||
|
||||
# VERSION was removed in Pillow 6.0.0.
|
||||
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
||||
|
@ -218,7 +227,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
|
|||
# Registries
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ImageFile
|
||||
from . import ImageFile, PyAccess
|
||||
ID: list[str] = []
|
||||
OPEN: dict[
|
||||
str,
|
||||
|
@ -410,7 +419,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 +444,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 = ()
|
||||
|
@ -550,10 +563,10 @@ class Image:
|
|||
return self._size
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
def mode(self) -> str:
|
||||
return self._mode
|
||||
|
||||
def _new(self, im) -> Image:
|
||||
def _new(self, im: core.ImagingCore) -> Image:
|
||||
new = Image()
|
||||
new.im = im
|
||||
new._mode = im.mode
|
||||
|
@ -687,7 +700,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.
|
||||
|
@ -700,14 +713,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
|
||||
|
@ -754,7 +767,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.
|
||||
|
||||
|
@ -776,12 +789,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()
|
||||
|
||||
|
@ -789,7 +803,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
|
||||
|
@ -832,7 +846,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.
|
||||
|
||||
|
@ -843,16 +859,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)
|
||||
|
||||
|
@ -863,7 +880,7 @@ class Image:
|
|||
msg = "cannot decode image data"
|
||||
raise ValueError(msg)
|
||||
|
||||
def load(self):
|
||||
def load(self) -> core.PixelAccess | PyAccess.PyAccess | None:
|
||||
"""
|
||||
Allocates storage for the image and loads the pixel data. In
|
||||
normal cases, you don't need to call this method, since the
|
||||
|
@ -876,12 +893,12 @@ class Image:
|
|||
operations. See :ref:`file-handling` for more information.
|
||||
|
||||
:returns: An image access object.
|
||||
:rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
|
||||
:rtype: :py:class:`.PixelAccess` or :py:class:`.PyAccess`
|
||||
"""
|
||||
if self.im is not None and self.palette and self.palette.dirty:
|
||||
# realize palette
|
||||
mode, arr = self.palette.getdata()
|
||||
self.im.putpalette(mode, arr)
|
||||
self.im.putpalette(self.palette.mode, mode, arr)
|
||||
self.palette.dirty = 0
|
||||
self.palette.rawmode = None
|
||||
if "transparency" in self.info and mode in ("LA", "PA"):
|
||||
|
@ -891,9 +908,9 @@ class Image:
|
|||
self.im.putpalettealphas(self.info["transparency"])
|
||||
self.palette.mode = "RGBA"
|
||||
else:
|
||||
palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB"
|
||||
self.palette.mode = palette_mode
|
||||
self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
|
||||
self.palette.palette = self.im.getpalette(
|
||||
self.palette.mode, self.palette.mode
|
||||
)
|
||||
|
||||
if self.im is not None:
|
||||
if cffi and USE_CFFI_ACCESS:
|
||||
|
@ -905,6 +922,7 @@ class Image:
|
|||
if self.pyaccess:
|
||||
return self.pyaccess
|
||||
return self.im.pixel_access(self.readonly)
|
||||
return None
|
||||
|
||||
def verify(self) -> None:
|
||||
"""
|
||||
|
@ -996,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)
|
||||
|
@ -1092,7 +1112,10 @@ class Image:
|
|||
del new_im.info["transparency"]
|
||||
if trns is not None:
|
||||
try:
|
||||
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
|
||||
new_im.info["transparency"] = new_im.palette.getcolor(
|
||||
cast(Tuple[int, int, int], trns), # trns was converted to RGB
|
||||
new_im,
|
||||
)
|
||||
except Exception:
|
||||
# if we can't make a transparent color, don't leave the old
|
||||
# transparency hanging around to mess us up.
|
||||
|
@ -1142,7 +1165,10 @@ class Image:
|
|||
if trns is not None:
|
||||
if new_im.mode == "P":
|
||||
try:
|
||||
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
|
||||
new_im.info["transparency"] = new_im.palette.getcolor(
|
||||
cast(Tuple[int, int, int], trns), # trns was converted to RGB
|
||||
new_im,
|
||||
)
|
||||
except ValueError as e:
|
||||
del new_im.info["transparency"]
|
||||
if str(e) != "cannot allocate more than 256 colors":
|
||||
|
@ -1158,7 +1184,7 @@ class Image:
|
|||
def quantize(
|
||||
self,
|
||||
colors: int = 256,
|
||||
method: Quantize | None = None,
|
||||
method: int | None = None,
|
||||
kmeans: int = 0,
|
||||
palette=None,
|
||||
dither: Dither = Dither.FLOYDSTEINBERG,
|
||||
|
@ -1250,7 +1276,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
|
||||
|
@ -1276,7 +1302,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.
|
||||
|
||||
|
@ -1297,7 +1325,7 @@ class Image:
|
|||
return im.crop((x0, y0, x1, y1))
|
||||
|
||||
def draft(
|
||||
self, mode: str | None, size: tuple[int, int]
|
||||
self, mode: str | None, size: tuple[int, int] | None
|
||||
) -> tuple[str, tuple[int, int, float, float]] | None:
|
||||
"""
|
||||
Configures the image file loader so it returns a version of the
|
||||
|
@ -1447,8 +1475,15 @@ class Image:
|
|||
return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
|
||||
return self.im.getextrema()
|
||||
|
||||
def _getxmp(self, xmp_tags):
|
||||
def get_name(tag):
|
||||
def getxmp(self):
|
||||
"""
|
||||
Returns a dictionary containing the XMP tags.
|
||||
Requires defusedxml to be installed.
|
||||
|
||||
:returns: XMP tags in a dictionary.
|
||||
"""
|
||||
|
||||
def get_name(tag: str) -> str:
|
||||
return re.sub("^{[^}]+}", "", tag)
|
||||
|
||||
def get_value(element):
|
||||
|
@ -1474,9 +1509,10 @@ class Image:
|
|||
if ElementTree is None:
|
||||
warnings.warn("XMP data cannot be read without defusedxml dependency")
|
||||
return {}
|
||||
else:
|
||||
root = ElementTree.fromstring(xmp_tags)
|
||||
return {get_name(root.tag): get_value(root)}
|
||||
if "xmp" not in self.info:
|
||||
return {}
|
||||
root = ElementTree.fromstring(self.info["xmp"])
|
||||
return {get_name(root.tag): get_value(root)}
|
||||
|
||||
def getexif(self) -> Exif:
|
||||
"""
|
||||
|
@ -1549,7 +1585,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()
|
||||
|
@ -1633,7 +1673,9 @@ class Image:
|
|||
|
||||
del self.info["transparency"]
|
||||
|
||||
def getpixel(self, xy):
|
||||
def getpixel(
|
||||
self, xy: tuple[int, int] | list[int]
|
||||
) -> float | tuple[int, ...] | None:
|
||||
"""
|
||||
Returns the pixel value at a given position.
|
||||
|
||||
|
@ -1720,7 +1762,7 @@ class Image:
|
|||
def paste(
|
||||
self,
|
||||
im: Image | str | float | tuple[float, ...],
|
||||
box: tuple[int, int, int, int] | tuple[int, int] | None = None,
|
||||
box: Image | tuple[int, int, int, int] | tuple[int, int] | None = None,
|
||||
mask: Image | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
|
@ -1762,10 +1804,14 @@ class Image:
|
|||
:param mask: An optional mask image.
|
||||
"""
|
||||
|
||||
if isImageType(box) and mask is None:
|
||||
if isImageType(box):
|
||||
if mask is not None:
|
||||
msg = "If using second argument as mask, third argument must be None"
|
||||
raise ValueError(msg)
|
||||
# abbreviated paste(im, mask) syntax
|
||||
mask = box
|
||||
box = None
|
||||
assert not isinstance(box, Image)
|
||||
|
||||
if box is None:
|
||||
box = (0, 0)
|
||||
|
@ -1803,7 +1849,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.
|
||||
|
||||
|
@ -1818,32 +1866,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:
|
||||
|
@ -1854,7 +1905,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.
|
||||
|
||||
|
@ -1891,7 +1946,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
|
||||
|
@ -1899,18 +1956,17 @@ 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):
|
||||
def putalpha(self, alpha: Image | int) -> None:
|
||||
"""
|
||||
Adds or replaces the alpha layer in this image. If the image
|
||||
does not have an alpha layer, it's converted to "LA" or "RGBA".
|
||||
The new layer must be either "L" or "1".
|
||||
|
||||
:param alpha: The new alpha layer. This can either be an "L" or "1"
|
||||
image having the same size as this image, or an integer or
|
||||
other color value.
|
||||
image having the same size as this image, or an integer.
|
||||
"""
|
||||
|
||||
self._ensure_mutable()
|
||||
|
@ -1949,6 +2005,7 @@ class Image:
|
|||
alpha = alpha.convert("L")
|
||||
else:
|
||||
# constant alpha
|
||||
alpha = cast(int, alpha) # see python/typing#1013
|
||||
try:
|
||||
self.im.fillband(band, alpha)
|
||||
except (AttributeError, ValueError):
|
||||
|
@ -1960,7 +2017,10 @@ class Image:
|
|||
self.im.putband(alpha.im, band)
|
||||
|
||||
def putdata(
|
||||
self, data: Sequence[float], scale: float = 1.0, offset: float = 0.0
|
||||
self,
|
||||
data: Sequence[float] | Sequence[Sequence[int]],
|
||||
scale: float = 1.0,
|
||||
offset: float = 0.0,
|
||||
) -> None:
|
||||
"""
|
||||
Copies pixel data from a flattened sequence object into the image. The
|
||||
|
@ -2011,10 +2071,12 @@ class Image:
|
|||
palette = ImagePalette.raw(rawmode, data)
|
||||
self._mode = "PA" if "A" in self.mode else "P"
|
||||
self.palette = palette
|
||||
self.palette.mode = "RGB"
|
||||
self.palette.mode = "RGBA" if "A" in rawmode else "RGB"
|
||||
self.load() # install new palette
|
||||
|
||||
def putpixel(self, xy, value):
|
||||
def putpixel(
|
||||
self, xy: tuple[int, int], value: float | tuple[int, ...] | list[int]
|
||||
) -> None:
|
||||
"""
|
||||
Modifies the pixel at the given position. The color is given as
|
||||
a single numerical value for single-band images, and a tuple for
|
||||
|
@ -2052,9 +2114,8 @@ class Image:
|
|||
if self.mode == "PA":
|
||||
alpha = value[3] if len(value) == 4 else 255
|
||||
value = value[:3]
|
||||
value = self.palette.getcolor(value, self)
|
||||
if self.mode == "PA":
|
||||
value = (value, alpha)
|
||||
palette_index = self.palette.getcolor(value, self)
|
||||
value = (palette_index, alpha) if self.mode == "PA" else palette_index
|
||||
return self.im.putpixel(xy, value)
|
||||
|
||||
def remap_palette(self, dest_map, source_palette=None):
|
||||
|
@ -2126,7 +2187,7 @@ class Image:
|
|||
# m_im.putpalette(mapping_palette, 'L') # converts to 'P'
|
||||
# or just force it.
|
||||
# UNDONE -- this is part of the general issue with palettes
|
||||
m_im.im.putpalette(palette_mode + ";L", m_im.palette.tobytes())
|
||||
m_im.im.putpalette(palette_mode, palette_mode + ";L", m_im.palette.tobytes())
|
||||
|
||||
m_im = m_im.convert("L")
|
||||
|
||||
|
@ -2307,7 +2368,7 @@ class Image:
|
|||
angle: float,
|
||||
resample: Resampling = Resampling.NEAREST,
|
||||
expand: int | bool = False,
|
||||
center: tuple[int, int] | None = None,
|
||||
center: tuple[float, float] | None = None,
|
||||
translate: tuple[int, int] | None = None,
|
||||
fillcolor: float | tuple[float, ...] | str | None = None,
|
||||
) -> Image:
|
||||
|
@ -2375,10 +2436,7 @@ class Image:
|
|||
else:
|
||||
post_trans = translate
|
||||
if center is None:
|
||||
# FIXME These should be rounded to ints?
|
||||
rotn_center = (w / 2.0, h / 2.0)
|
||||
else:
|
||||
rotn_center = center
|
||||
center = (w / 2, h / 2)
|
||||
|
||||
angle = -math.radians(angle)
|
||||
matrix = [
|
||||
|
@ -2395,10 +2453,10 @@ class Image:
|
|||
return a * x + b * y + c, d * x + e * y + f
|
||||
|
||||
matrix[2], matrix[5] = transform(
|
||||
-rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix
|
||||
-center[0] - post_trans[0], -center[1] - post_trans[1], matrix
|
||||
)
|
||||
matrix[2] += rotn_center[0]
|
||||
matrix[5] += rotn_center[1]
|
||||
matrix[2] += center[0]
|
||||
matrix[5] += center[1]
|
||||
|
||||
if expand:
|
||||
# calculate output size
|
||||
|
@ -2638,7 +2696,7 @@ class Image:
|
|||
self,
|
||||
size: tuple[float, float],
|
||||
resample: Resampling = Resampling.BICUBIC,
|
||||
reducing_gap: float = 2.0,
|
||||
reducing_gap: float | None = 2.0,
|
||||
) -> None:
|
||||
"""
|
||||
Make this image into a thumbnail. This method modifies the
|
||||
|
@ -2699,11 +2757,12 @@ class Image:
|
|||
return x, y
|
||||
|
||||
box = None
|
||||
final_size: tuple[int, int]
|
||||
if reducing_gap is not None:
|
||||
preserved_size = preserve_aspect_ratio()
|
||||
if preserved_size is None:
|
||||
return
|
||||
size = preserved_size
|
||||
final_size = preserved_size
|
||||
|
||||
res = self.draft(
|
||||
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
|
||||
|
@ -2717,13 +2776,13 @@ class Image:
|
|||
preserved_size = preserve_aspect_ratio()
|
||||
if preserved_size is None:
|
||||
return
|
||||
size = preserved_size
|
||||
final_size = preserved_size
|
||||
|
||||
if self.size != size:
|
||||
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
|
||||
if self.size != final_size:
|
||||
im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)
|
||||
|
||||
self.im = im.im
|
||||
self._size = size
|
||||
self._size = final_size
|
||||
self._mode = self.im.mode
|
||||
|
||||
self.readonly = 0
|
||||
|
@ -2990,29 +3049,29 @@ def _wedge() -> Image:
|
|||
return Image()._new(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.
|
||||
|
@ -3061,7 +3120,13 @@ def new(
|
|||
return im._new(core.fill(mode, size, color))
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
@ -3089,18 +3154,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.
|
||||
|
||||
|
@ -3557,7 +3625,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:
|
||||
|
@ -3691,7 +3759,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.
|
||||
|
@ -3738,19 +3806,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)]:
|
||||
|
@ -3759,13 +3826,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}")
|
||||
|
||||
|
|
|
@ -34,12 +34,25 @@ from __future__ import annotations
|
|||
import math
|
||||
import numbers
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, AnyStr, Sequence, cast
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, AnyStr, Callable, List, Sequence, Tuple, Union, cast
|
||||
|
||||
from . import Image, ImageColor
|
||||
from ._deprecate import deprecate
|
||||
from ._typing import Coords
|
||||
|
||||
# experimental access to the outline API
|
||||
Outline: Callable[[], Image.core._Outline] | None
|
||||
try:
|
||||
Outline = Image.core.outline
|
||||
except AttributeError:
|
||||
Outline = None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ImageDraw2, ImageFont
|
||||
|
||||
_Ink = Union[float, Tuple[int, ...], str]
|
||||
|
||||
"""
|
||||
A simple 2D drawing interface for PIL images.
|
||||
<p>
|
||||
|
@ -49,7 +62,9 @@ directly.
|
|||
|
||||
|
||||
class ImageDraw:
|
||||
font = None
|
||||
font: (
|
||||
ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
|
||||
) = None
|
||||
|
||||
def __init__(self, im: Image.Image, mode: str | None = None) -> None:
|
||||
"""
|
||||
|
@ -93,9 +108,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:
|
||||
|
@ -133,34 +145,47 @@ class ImageDraw:
|
|||
else:
|
||||
return self.getfont()
|
||||
|
||||
def _getink(self, ink, fill=None) -> tuple[int | None, int | None]:
|
||||
def _getink(
|
||||
self, ink: _Ink | None, fill: _Ink | None = None
|
||||
) -> tuple[int | None, int | None]:
|
||||
result_ink = None
|
||||
result_fill = None
|
||||
if ink is None and fill is None:
|
||||
if self.fill:
|
||||
fill = self.ink
|
||||
result_fill = self.ink
|
||||
else:
|
||||
ink = self.ink
|
||||
result_ink = self.ink
|
||||
else:
|
||||
if ink is not None:
|
||||
if isinstance(ink, str):
|
||||
ink = ImageColor.getcolor(ink, self.mode)
|
||||
if self.palette and not isinstance(ink, numbers.Number):
|
||||
ink = self.palette.getcolor(ink, self._image)
|
||||
ink = self.draw.draw_ink(ink)
|
||||
result_ink = self.draw.draw_ink(ink)
|
||||
if fill is not None:
|
||||
if isinstance(fill, str):
|
||||
fill = ImageColor.getcolor(fill, self.mode)
|
||||
if self.palette and not isinstance(fill, numbers.Number):
|
||||
fill = self.palette.getcolor(fill, self._image)
|
||||
fill = self.draw.draw_ink(fill)
|
||||
return ink, fill
|
||||
result_fill = self.draw.draw_ink(fill)
|
||||
return result_ink, result_fill
|
||||
|
||||
def arc(self, xy: Coords, start, end, fill=None, width=1) -> None:
|
||||
def arc(
|
||||
self,
|
||||
xy: Coords,
|
||||
start: float,
|
||||
end: float,
|
||||
fill: _Ink | None = None,
|
||||
width: int = 1,
|
||||
) -> None:
|
||||
"""Draw an arc."""
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
self.draw.draw_arc(xy, start, end, ink, width)
|
||||
|
||||
def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None:
|
||||
def bitmap(
|
||||
self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None
|
||||
) -> None:
|
||||
"""Draw a bitmap."""
|
||||
bitmap.load()
|
||||
ink, fill = self._getink(fill)
|
||||
|
@ -169,30 +194,55 @@ class ImageDraw:
|
|||
if ink is not None:
|
||||
self.draw.draw_bitmap(xy, bitmap.im, ink)
|
||||
|
||||
def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None:
|
||||
def chord(
|
||||
self,
|
||||
xy: Coords,
|
||||
start: float,
|
||||
end: float,
|
||||
fill: _Ink | None = None,
|
||||
outline: _Ink | None = None,
|
||||
width: int = 1,
|
||||
) -> None:
|
||||
"""Draw a chord."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_chord(xy, start, end, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
ink, fill_ink = self._getink(outline, fill)
|
||||
if fill_ink is not None:
|
||||
self.draw.draw_chord(xy, start, end, fill_ink, 1)
|
||||
if ink is not None and ink != fill_ink and width != 0:
|
||||
self.draw.draw_chord(xy, start, end, ink, 0, width)
|
||||
|
||||
def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None:
|
||||
def ellipse(
|
||||
self,
|
||||
xy: Coords,
|
||||
fill: _Ink | None = None,
|
||||
outline: _Ink | None = None,
|
||||
width: int = 1,
|
||||
) -> None:
|
||||
"""Draw an ellipse."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_ellipse(xy, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
ink, fill_ink = self._getink(outline, fill)
|
||||
if fill_ink is not None:
|
||||
self.draw.draw_ellipse(xy, fill_ink, 1)
|
||||
if ink is not None and ink != fill_ink and width != 0:
|
||||
self.draw.draw_ellipse(xy, ink, 0, width)
|
||||
|
||||
def circle(
|
||||
self, xy: Sequence[float], radius: float, fill=None, outline=None, width=1
|
||||
self,
|
||||
xy: Sequence[float],
|
||||
radius: float,
|
||||
fill: _Ink | None = None,
|
||||
outline: _Ink | None = None,
|
||||
width: int = 1,
|
||||
) -> None:
|
||||
"""Draw a circle given center coordinates and a radius."""
|
||||
ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius)
|
||||
self.ellipse(ellipse_xy, fill, outline, width)
|
||||
|
||||
def line(self, xy: Coords, fill=None, width=0, joint=None) -> None:
|
||||
def line(
|
||||
self,
|
||||
xy: Coords,
|
||||
fill: _Ink | None = None,
|
||||
width: int = 0,
|
||||
joint: str | None = None,
|
||||
) -> None:
|
||||
"""Draw a line, or a connected sequence of line segments."""
|
||||
ink = self._getink(fill)[0]
|
||||
if ink is not None:
|
||||
|
@ -222,7 +272,7 @@ class ImageDraw:
|
|||
|
||||
def coord_at_angle(
|
||||
coord: Sequence[float], angle: float
|
||||
) -> tuple[float, float]:
|
||||
) -> tuple[float, ...]:
|
||||
x, y = coord
|
||||
angle -= 90
|
||||
distance = width / 2 - 1
|
||||
|
@ -263,37 +313,54 @@ class ImageDraw:
|
|||
]
|
||||
self.line(gap_coords, fill, width=3)
|
||||
|
||||
def shape(self, shape, fill=None, outline=None) -> None:
|
||||
def shape(
|
||||
self,
|
||||
shape: Image.core._Outline,
|
||||
fill: _Ink | None = None,
|
||||
outline: _Ink | None = None,
|
||||
) -> None:
|
||||
"""(Experimental) Draw a shape."""
|
||||
shape.close()
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_outline(shape, fill, 1)
|
||||
if ink is not None and ink != fill:
|
||||
ink, fill_ink = self._getink(outline, fill)
|
||||
if fill_ink is not None:
|
||||
self.draw.draw_outline(shape, fill_ink, 1)
|
||||
if ink is not None and ink != fill_ink:
|
||||
self.draw.draw_outline(shape, ink, 0)
|
||||
|
||||
def pieslice(
|
||||
self, xy: Coords, start, end, fill=None, outline=None, width=1
|
||||
self,
|
||||
xy: Coords,
|
||||
start: float,
|
||||
end: float,
|
||||
fill: _Ink | None = None,
|
||||
outline: _Ink | None = None,
|
||||
width: int = 1,
|
||||
) -> None:
|
||||
"""Draw a pieslice."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_pieslice(xy, start, end, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
ink, fill_ink = self._getink(outline, fill)
|
||||
if fill_ink is not None:
|
||||
self.draw.draw_pieslice(xy, start, end, fill_ink, 1)
|
||||
if ink is not None and ink != fill_ink and width != 0:
|
||||
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
|
||||
|
||||
def point(self, xy: Coords, fill=None) -> None:
|
||||
def point(self, xy: Coords, fill: _Ink | None = None) -> None:
|
||||
"""Draw one or more individual pixels."""
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
self.draw.draw_points(xy, ink)
|
||||
|
||||
def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None:
|
||||
def polygon(
|
||||
self,
|
||||
xy: Coords,
|
||||
fill: _Ink | None = None,
|
||||
outline: _Ink | None = None,
|
||||
width: int = 1,
|
||||
) -> None:
|
||||
"""Draw a polygon."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_polygon(xy, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
ink, fill_ink = self._getink(outline, fill)
|
||||
if fill_ink is not None:
|
||||
self.draw.draw_polygon(xy, fill_ink, 1)
|
||||
if ink is not None and ink != fill_ink and width != 0:
|
||||
if width == 1:
|
||||
self.draw.draw_polygon(xy, ink, 0, width)
|
||||
elif self.im is not None:
|
||||
|
@ -319,22 +386,41 @@ class ImageDraw:
|
|||
self.im.paste(im.im, (0, 0) + im.size, mask.im)
|
||||
|
||||
def regular_polygon(
|
||||
self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1
|
||||
self,
|
||||
bounding_circle: Sequence[Sequence[float] | float],
|
||||
n_sides: int,
|
||||
rotation: float = 0,
|
||||
fill: _Ink | None = None,
|
||||
outline: _Ink | None = None,
|
||||
width: int = 1,
|
||||
) -> None:
|
||||
"""Draw a regular polygon."""
|
||||
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
|
||||
self.polygon(xy, fill, outline, width)
|
||||
|
||||
def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None:
|
||||
def rectangle(
|
||||
self,
|
||||
xy: Coords,
|
||||
fill: _Ink | None = None,
|
||||
outline: _Ink | None = None,
|
||||
width: int = 1,
|
||||
) -> None:
|
||||
"""Draw a rectangle."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_rectangle(xy, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
ink, fill_ink = self._getink(outline, fill)
|
||||
if fill_ink is not None:
|
||||
self.draw.draw_rectangle(xy, fill_ink, 1)
|
||||
if ink is not None and ink != fill_ink and width != 0:
|
||||
self.draw.draw_rectangle(xy, ink, 0, width)
|
||||
|
||||
def rounded_rectangle(
|
||||
self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None
|
||||
self,
|
||||
xy: Coords,
|
||||
radius: float = 0,
|
||||
fill: _Ink | None = None,
|
||||
outline: _Ink | None = None,
|
||||
width: int = 1,
|
||||
*,
|
||||
corners: tuple[bool, bool, bool, bool] | None = None,
|
||||
) -> None:
|
||||
"""Draw a rounded rectangle."""
|
||||
if isinstance(xy[0], (list, tuple)):
|
||||
|
@ -376,10 +462,10 @@ class ImageDraw:
|
|||
# that is a rectangle
|
||||
return self.rectangle(xy, fill, outline, width)
|
||||
|
||||
r = d // 2
|
||||
ink, fill = self._getink(outline, fill)
|
||||
r = int(d // 2)
|
||||
ink, fill_ink = self._getink(outline, fill)
|
||||
|
||||
def draw_corners(pieslice) -> None:
|
||||
def draw_corners(pieslice: bool) -> None:
|
||||
parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
|
||||
if full_x:
|
||||
# Draw top and bottom halves
|
||||
|
@ -409,32 +495,32 @@ class ImageDraw:
|
|||
)
|
||||
for part in parts:
|
||||
if pieslice:
|
||||
self.draw.draw_pieslice(*(part + (fill, 1)))
|
||||
self.draw.draw_pieslice(*(part + (fill_ink, 1)))
|
||||
else:
|
||||
self.draw.draw_arc(*(part + (ink, width)))
|
||||
|
||||
if fill is not None:
|
||||
if fill_ink is not None:
|
||||
draw_corners(True)
|
||||
|
||||
if full_x:
|
||||
self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1)
|
||||
self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
|
||||
else:
|
||||
self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1)
|
||||
self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
|
||||
if not full_x and not full_y:
|
||||
left = [x0, y0, x0 + r, y1]
|
||||
if corners[0]:
|
||||
left[1] += r + 1
|
||||
if corners[3]:
|
||||
left[3] -= r + 1
|
||||
self.draw.draw_rectangle(left, fill, 1)
|
||||
self.draw.draw_rectangle(left, fill_ink, 1)
|
||||
|
||||
right = [x1 - r, y0, x1, y1]
|
||||
if corners[1]:
|
||||
right[1] += r + 1
|
||||
if corners[2]:
|
||||
right[3] -= r + 1
|
||||
self.draw.draw_rectangle(right, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
self.draw.draw_rectangle(right, fill_ink, 1)
|
||||
if ink is not None and ink != fill_ink and width != 0:
|
||||
draw_corners(False)
|
||||
|
||||
if not full_x:
|
||||
|
@ -529,10 +615,11 @@ class ImageDraw:
|
|||
embedded_color,
|
||||
)
|
||||
|
||||
def getink(fill):
|
||||
ink, fill = self._getink(fill)
|
||||
def getink(fill: _Ink | None) -> int:
|
||||
ink, fill_ink = self._getink(fill)
|
||||
if ink is None:
|
||||
return fill
|
||||
assert fill_ink is not None
|
||||
return fill_ink
|
||||
return ink
|
||||
|
||||
def draw_text(ink, stroke_width=0, stroke_offset=None) -> None:
|
||||
|
@ -879,7 +966,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,19 +978,14 @@ 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)
|
||||
|
||||
|
||||
# experimental access to the outline API
|
||||
try:
|
||||
Outline = Image.core.outline
|
||||
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 +995,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(
|
||||
|
@ -926,7 +1007,9 @@ def floodfill(
|
|||
thresh: float = 0,
|
||||
) -> None:
|
||||
"""
|
||||
(experimental) Fills a bounded region with a given color.
|
||||
.. warning:: This method is experimental.
|
||||
|
||||
Fills a bounded region with a given color.
|
||||
|
||||
:param image: Target image.
|
||||
:param xy: Seed position (a 2-item coordinate tuple). See
|
||||
|
@ -944,6 +1027,7 @@ def floodfill(
|
|||
# based on an implementation by Eric S. Raymond
|
||||
# amended by yo1995 @20180806
|
||||
pixel = image.load()
|
||||
assert pixel is not None
|
||||
x, y = xy
|
||||
try:
|
||||
background = pixel[x, y]
|
||||
|
@ -981,12 +1065,12 @@ def floodfill(
|
|||
|
||||
|
||||
def _compute_regular_polygon_vertices(
|
||||
bounding_circle, n_sides, rotation
|
||||
bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
|
||||
) -> list[tuple[float, float]]:
|
||||
"""
|
||||
Generate a list of vertices for a 2D regular polygon.
|
||||
|
||||
:param bounding_circle: The bounding circle is a tuple defined
|
||||
:param bounding_circle: The bounding circle is a sequence defined
|
||||
by a point and radius. The polygon is inscribed in this circle.
|
||||
(e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
|
||||
:param n_sides: Number of sides
|
||||
|
@ -1024,7 +1108,7 @@ def _compute_regular_polygon_vertices(
|
|||
# 1. Error Handling
|
||||
# 1.1 Check `n_sides` has an appropriate value
|
||||
if not isinstance(n_sides, int):
|
||||
msg = "n_sides should be an int"
|
||||
msg = "n_sides should be an int" # type: ignore[unreachable]
|
||||
raise TypeError(msg)
|
||||
if n_sides < 3:
|
||||
msg = "n_sides should be an int > 2"
|
||||
|
@ -1036,9 +1120,24 @@ def _compute_regular_polygon_vertices(
|
|||
raise TypeError(msg)
|
||||
|
||||
if len(bounding_circle) == 3:
|
||||
*centroid, polygon_radius = bounding_circle
|
||||
elif len(bounding_circle) == 2:
|
||||
centroid, polygon_radius = bounding_circle
|
||||
if not all(isinstance(i, (int, float)) for i in bounding_circle):
|
||||
msg = "bounding_circle should only contain numeric data"
|
||||
raise ValueError(msg)
|
||||
|
||||
*centroid, polygon_radius = cast(List[float], list(bounding_circle))
|
||||
elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
|
||||
if not all(
|
||||
isinstance(i, (int, float)) for i in bounding_circle[0]
|
||||
) or not isinstance(bounding_circle[1], (int, float)):
|
||||
msg = "bounding_circle should only contain numeric data"
|
||||
raise ValueError(msg)
|
||||
|
||||
if len(bounding_circle[0]) != 2:
|
||||
msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
|
||||
raise ValueError(msg)
|
||||
|
||||
centroid = cast(List[float], list(bounding_circle[0]))
|
||||
polygon_radius = cast(float, bounding_circle[1])
|
||||
else:
|
||||
msg = (
|
||||
"bounding_circle should contain 2D coordinates "
|
||||
|
@ -1046,25 +1145,17 @@ def _compute_regular_polygon_vertices(
|
|||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)):
|
||||
msg = "bounding_circle should only contain numeric data"
|
||||
raise ValueError(msg)
|
||||
|
||||
if not len(centroid) == 2:
|
||||
msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
|
||||
raise ValueError(msg)
|
||||
|
||||
if polygon_radius <= 0:
|
||||
msg = "bounding_circle radius should be > 0"
|
||||
raise ValueError(msg)
|
||||
|
||||
# 1.3 Check `rotation` has an appropriate value
|
||||
if not isinstance(rotation, (int, float)):
|
||||
msg = "rotation should be an int or float"
|
||||
msg = "rotation should be an int or float" # type: ignore[unreachable]
|
||||
raise ValueError(msg)
|
||||
|
||||
# 2. Define Helper Functions
|
||||
def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]:
|
||||
def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
|
||||
return (
|
||||
round(
|
||||
point[0] * math.cos(math.radians(360 - degrees))
|
||||
|
@ -1080,7 +1171,7 @@ def _compute_regular_polygon_vertices(
|
|||
),
|
||||
)
|
||||
|
||||
def _compute_polygon_vertex(angle: float) -> tuple[int, int]:
|
||||
def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
|
||||
start_point = [polygon_radius, 0]
|
||||
return _apply_rotation(start_point, angle)
|
||||
|
||||
|
|
|
@ -24,7 +24,10 @@
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import BinaryIO
|
||||
|
||||
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
|
||||
from ._typing import StrOrBytesPath
|
||||
|
||||
|
||||
class Pen:
|
||||
|
@ -45,7 +48,9 @@ class Brush:
|
|||
class Font:
|
||||
"""Stores a TrueType font and color"""
|
||||
|
||||
def __init__(self, color, file, size=12):
|
||||
def __init__(
|
||||
self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12
|
||||
) -> None:
|
||||
# FIXME: add support for bitmap fonts
|
||||
self.color = ImageColor.getrgb(color)
|
||||
self.font = ImageFont.truetype(file, size)
|
||||
|
@ -56,8 +61,16 @@ class Draw:
|
|||
(Experimental) WCK-style drawing interface
|
||||
"""
|
||||
|
||||
def __init__(self, image, size=None, color=None):
|
||||
if not hasattr(image, "im"):
|
||||
def __init__(
|
||||
self,
|
||||
image: Image.Image | str,
|
||||
size: tuple[int, int] | list[int] | None = None,
|
||||
color: float | tuple[float, ...] | str | None = None,
|
||||
) -> None:
|
||||
if isinstance(image, str):
|
||||
if size is None:
|
||||
msg = "If image argument is mode string, size must be a list or tuple"
|
||||
raise ValueError(msg)
|
||||
image = Image.new(image, size, color)
|
||||
self.draw = ImageDraw.Draw(image)
|
||||
self.image = image
|
||||
|
|
|
@ -65,7 +65,7 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`,
|
|||
# Helpers
|
||||
|
||||
|
||||
def _get_oserror(error, *, encoder):
|
||||
def _get_oserror(error: int, *, encoder: bool) -> OSError:
|
||||
try:
|
||||
msg = Image.core.getcodecstatus(error)
|
||||
except AttributeError:
|
||||
|
@ -76,7 +76,7 @@ def _get_oserror(error, *, encoder):
|
|||
return OSError(msg)
|
||||
|
||||
|
||||
def raise_oserror(error):
|
||||
def raise_oserror(error: int) -> OSError:
|
||||
deprecate(
|
||||
"raise_oserror",
|
||||
12,
|
||||
|
@ -154,11 +154,12 @@ class ImageFile(Image.Image):
|
|||
self.fp.close()
|
||||
raise
|
||||
|
||||
def get_format_mimetype(self):
|
||||
def get_format_mimetype(self) -> str | None:
|
||||
if self.custom_mimetype:
|
||||
return self.custom_mimetype
|
||||
if self.format is not None:
|
||||
return Image.MIME.get(self.format.upper())
|
||||
return None
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.tile = []
|
||||
|
@ -365,7 +366,7 @@ class StubImageFile(ImageFile):
|
|||
certain format, but relies on external code to load the file.
|
||||
"""
|
||||
|
||||
def _open(self):
|
||||
def _open(self) -> None:
|
||||
msg = "StubImageFile subclass must implement _open"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
|
@ -381,7 +382,7 @@ class StubImageFile(ImageFile):
|
|||
self.__dict__ = image.__dict__
|
||||
return image.load()
|
||||
|
||||
def _load(self):
|
||||
def _load(self) -> StubHandler | None:
|
||||
"""(Hook) Find actual image loader."""
|
||||
msg = "StubImageFile subclass must implement _load"
|
||||
raise NotImplementedError(msg)
|
||||
|
@ -621,7 +622,7 @@ class PyCodecState:
|
|||
self.xoff = 0
|
||||
self.yoff = 0
|
||||
|
||||
def extents(self):
|
||||
def extents(self) -> tuple[int, int, int, int]:
|
||||
return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize
|
||||
|
||||
|
||||
|
@ -661,7 +662,7 @@ class PyCodec:
|
|||
"""
|
||||
self.fd = fd
|
||||
|
||||
def setimage(self, im, extents=None):
|
||||
def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None:
|
||||
"""
|
||||
Called from ImageFile to set the core output image for the codec
|
||||
|
||||
|
@ -710,10 +711,10 @@ class PyDecoder(PyCodec):
|
|||
_pulls_fd = False
|
||||
|
||||
@property
|
||||
def pulls_fd(self):
|
||||
def pulls_fd(self) -> bool:
|
||||
return self._pulls_fd
|
||||
|
||||
def decode(self, buffer):
|
||||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
"""
|
||||
Override to perform the decoding process.
|
||||
|
||||
|
@ -738,6 +739,7 @@ class PyDecoder(PyCodec):
|
|||
if not rawmode:
|
||||
rawmode = self.mode
|
||||
d = Image._getdecoder(self.mode, "raw", rawmode)
|
||||
assert self.im is not None
|
||||
d.setimage(self.im, self.state.extents())
|
||||
s = d.decode(data)
|
||||
|
||||
|
@ -760,7 +762,7 @@ class PyEncoder(PyCodec):
|
|||
_pushes_fd = False
|
||||
|
||||
@property
|
||||
def pushes_fd(self):
|
||||
def pushes_fd(self) -> bool:
|
||||
return self._pushes_fd
|
||||
|
||||
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
||||
|
@ -775,7 +777,7 @@ class PyEncoder(PyCodec):
|
|||
msg = "unavailable in base encoder"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
def encode_to_pyfd(self):
|
||||
def encode_to_pyfd(self) -> tuple[int, int]:
|
||||
"""
|
||||
If ``pushes_fd`` is ``True``, then this method will be used,
|
||||
and ``encode()`` will only be called once.
|
||||
|
@ -787,6 +789,7 @@ class PyEncoder(PyCodec):
|
|||
return 0, -8 # bad configuration
|
||||
bytes_consumed, errcode, data = self.encode(0)
|
||||
if data:
|
||||
assert self.fd is not None
|
||||
self.fd.write(data)
|
||||
return bytes_consumed, errcode
|
||||
|
||||
|
|
|
@ -19,12 +19,16 @@ from __future__ import annotations
|
|||
import abc
|
||||
import functools
|
||||
from types import ModuleType
|
||||
from typing import Any, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Callable, Sequence, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import _imaging
|
||||
from ._typing import NumpyArray
|
||||
|
||||
|
||||
class Filter:
|
||||
@abc.abstractmethod
|
||||
def filter(self, image):
|
||||
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
||||
pass
|
||||
|
||||
|
||||
|
@ -33,7 +37,9 @@ class MultibandFilter(Filter):
|
|||
|
||||
|
||||
class BuiltinFilter(MultibandFilter):
|
||||
def filter(self, image):
|
||||
filterargs: tuple[Any, ...]
|
||||
|
||||
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
||||
if image.mode == "P":
|
||||
msg = "cannot filter palette images"
|
||||
raise ValueError(msg)
|
||||
|
@ -91,7 +97,7 @@ class RankFilter(Filter):
|
|||
self.size = size
|
||||
self.rank = rank
|
||||
|
||||
def filter(self, image):
|
||||
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
||||
if image.mode == "P":
|
||||
msg = "cannot filter palette images"
|
||||
raise ValueError(msg)
|
||||
|
@ -158,7 +164,7 @@ class ModeFilter(Filter):
|
|||
def __init__(self, size: int = 3) -> None:
|
||||
self.size = size
|
||||
|
||||
def filter(self, image):
|
||||
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
||||
return image.modefilter(self.size)
|
||||
|
||||
|
||||
|
@ -176,9 +182,9 @@ class GaussianBlur(MultibandFilter):
|
|||
def __init__(self, radius: float | Sequence[float] = 2) -> None:
|
||||
self.radius = radius
|
||||
|
||||
def filter(self, image):
|
||||
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
||||
xy = self.radius
|
||||
if not isinstance(xy, (tuple, list)):
|
||||
if isinstance(xy, (int, float)):
|
||||
xy = (xy, xy)
|
||||
if xy == (0, 0):
|
||||
return image.copy()
|
||||
|
@ -208,9 +214,9 @@ class BoxBlur(MultibandFilter):
|
|||
raise ValueError(msg)
|
||||
self.radius = radius
|
||||
|
||||
def filter(self, image):
|
||||
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
||||
xy = self.radius
|
||||
if not isinstance(xy, (tuple, list)):
|
||||
if isinstance(xy, (int, float)):
|
||||
xy = (xy, xy)
|
||||
if xy == (0, 0):
|
||||
return image.copy()
|
||||
|
@ -241,7 +247,7 @@ class UnsharpMask(MultibandFilter):
|
|||
self.percent = percent
|
||||
self.threshold = threshold
|
||||
|
||||
def filter(self, image):
|
||||
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
||||
return image.unsharp_mask(self.radius, self.percent, self.threshold)
|
||||
|
||||
|
||||
|
@ -387,8 +393,13 @@ class Color3DLUT(MultibandFilter):
|
|||
name = "Color 3D LUT"
|
||||
|
||||
def __init__(
|
||||
self, size, table, channels: int = 3, target_mode: str | None = None, **kwargs
|
||||
):
|
||||
self,
|
||||
size: int | tuple[int, int, int],
|
||||
table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray,
|
||||
channels: int = 3,
|
||||
target_mode: str | None = None,
|
||||
**kwargs: bool,
|
||||
) -> None:
|
||||
if channels not in (3, 4):
|
||||
msg = "Only 3 or 4 output channels are supported"
|
||||
raise ValueError(msg)
|
||||
|
@ -410,15 +421,16 @@ class Color3DLUT(MultibandFilter):
|
|||
pass
|
||||
|
||||
if numpy and isinstance(table, numpy.ndarray):
|
||||
numpy_table: NumpyArray = table
|
||||
if copy_table:
|
||||
table = table.copy()
|
||||
numpy_table = numpy_table.copy()
|
||||
|
||||
if table.shape in [
|
||||
if numpy_table.shape in [
|
||||
(items * channels,),
|
||||
(items, channels),
|
||||
(size[2], size[1], size[0], channels),
|
||||
]:
|
||||
table = table.reshape(items * channels)
|
||||
table = numpy_table.reshape(items * channels)
|
||||
else:
|
||||
wrong_size = True
|
||||
|
||||
|
@ -428,7 +440,8 @@ class Color3DLUT(MultibandFilter):
|
|||
|
||||
# Convert to a flat list
|
||||
if table and isinstance(table[0], (list, tuple)):
|
||||
table, raw_table = [], table
|
||||
raw_table = cast(Sequence[Sequence[int]], table)
|
||||
flat_table: list[int] = []
|
||||
for pixel in raw_table:
|
||||
if len(pixel) != channels:
|
||||
msg = (
|
||||
|
@ -436,7 +449,8 @@ class Color3DLUT(MultibandFilter):
|
|||
f"have a length of {channels}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
table.extend(pixel)
|
||||
flat_table.extend(pixel)
|
||||
table = flat_table
|
||||
|
||||
if wrong_size or len(table) != items * channels:
|
||||
msg = (
|
||||
|
@ -449,7 +463,7 @@ class Color3DLUT(MultibandFilter):
|
|||
self.table = table
|
||||
|
||||
@staticmethod
|
||||
def _check_size(size: Any) -> list[int]:
|
||||
def _check_size(size: Any) -> tuple[int, int, int]:
|
||||
try:
|
||||
_, _, _ = size
|
||||
except ValueError as e:
|
||||
|
@ -457,7 +471,7 @@ class Color3DLUT(MultibandFilter):
|
|||
raise ValueError(msg) from e
|
||||
except TypeError:
|
||||
size = (size, size, size)
|
||||
size = [int(x) for x in size]
|
||||
size = tuple(int(x) for x in size)
|
||||
for size_1d in size:
|
||||
if not 2 <= size_1d <= 65:
|
||||
msg = "Size should be in [2, 65] range."
|
||||
|
@ -465,7 +479,13 @@ class Color3DLUT(MultibandFilter):
|
|||
return size
|
||||
|
||||
@classmethod
|
||||
def generate(cls, size, callback, channels=3, target_mode=None):
|
||||
def generate(
|
||||
cls,
|
||||
size: int | tuple[int, int, int],
|
||||
callback: Callable[[float, float, float], tuple[float, ...]],
|
||||
channels: int = 3,
|
||||
target_mode: str | None = None,
|
||||
) -> Color3DLUT:
|
||||
"""Generates new LUT using provided callback.
|
||||
|
||||
:param size: Size of the table. Passed to the constructor.
|
||||
|
@ -482,7 +502,7 @@ class Color3DLUT(MultibandFilter):
|
|||
msg = "Only 3 or 4 output channels are supported"
|
||||
raise ValueError(msg)
|
||||
|
||||
table = [0] * (size_1d * size_2d * size_3d * channels)
|
||||
table: list[float] = [0] * (size_1d * size_2d * size_3d * channels)
|
||||
idx_out = 0
|
||||
for b in range(size_3d):
|
||||
for g in range(size_2d):
|
||||
|
@ -500,7 +520,13 @@ class Color3DLUT(MultibandFilter):
|
|||
_copy_table=False,
|
||||
)
|
||||
|
||||
def transform(self, callback, with_normals=False, channels=None, target_mode=None):
|
||||
def transform(
|
||||
self,
|
||||
callback: Callable[..., tuple[float, ...]],
|
||||
with_normals: bool = False,
|
||||
channels: int | None = None,
|
||||
target_mode: str | None = None,
|
||||
) -> Color3DLUT:
|
||||
"""Transforms the table values using provided callback and returns
|
||||
a new LUT with altered values.
|
||||
|
||||
|
@ -564,7 +590,7 @@ class Color3DLUT(MultibandFilter):
|
|||
r.append(f"target_mode={self.mode}")
|
||||
return "<{}>".format(" ".join(r))
|
||||
|
||||
def filter(self, image):
|
||||
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
||||
from . import Image
|
||||
|
||||
return image.color_lut_3d(
|
||||
|
|
|
@ -33,11 +33,12 @@ import sys
|
|||
import warnings
|
||||
from enum import IntEnum
|
||||
from io import BytesIO
|
||||
from types import ModuleType
|
||||
from typing import IO, TYPE_CHECKING, Any, BinaryIO
|
||||
|
||||
from . import Image
|
||||
from ._typing import StrOrBytesPath
|
||||
from ._util import is_path
|
||||
from ._util import DeferredError, is_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ImageFile
|
||||
|
@ -53,11 +54,10 @@ class Layout(IntEnum):
|
|||
MAX_STRING_LENGTH = 1_000_000
|
||||
|
||||
|
||||
core: ModuleType | DeferredError
|
||||
try:
|
||||
from . import _imagingft as core
|
||||
except ImportError as ex:
|
||||
from ._util import DeferredError
|
||||
|
||||
core = DeferredError.new(ex)
|
||||
|
||||
|
||||
|
@ -199,6 +199,7 @@ class FreeTypeFont:
|
|||
"""FreeType font wrapper (requires _imagingft service)"""
|
||||
|
||||
font: Font
|
||||
font_bytes: bytes
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -210,6 +211,9 @@ class FreeTypeFont:
|
|||
) -> None:
|
||||
# FIXME: use service provider instead
|
||||
|
||||
if isinstance(core, DeferredError):
|
||||
raise core.ex
|
||||
|
||||
if size <= 0:
|
||||
msg = "font size must be greater than 0"
|
||||
raise ValueError(msg)
|
||||
|
@ -279,7 +283,7 @@ class FreeTypeFont:
|
|||
return self.font.ascent, self.font.descent
|
||||
|
||||
def getlength(
|
||||
self, text: str, mode="", direction=None, features=None, language=None
|
||||
self, text: str | bytes, mode="", direction=None, features=None, language=None
|
||||
) -> float:
|
||||
"""
|
||||
Returns length (in pixels with 1/64 precision) of given text when rendered
|
||||
|
@ -354,7 +358,7 @@ class FreeTypeFont:
|
|||
|
||||
def getbbox(
|
||||
self,
|
||||
text: str,
|
||||
text: str | bytes,
|
||||
mode: str = "",
|
||||
direction: str | None = None,
|
||||
features: list[str] | None = None,
|
||||
|
@ -511,7 +515,7 @@ class FreeTypeFont:
|
|||
|
||||
def getmask2(
|
||||
self,
|
||||
text: str,
|
||||
text: str | bytes,
|
||||
mode="",
|
||||
direction=None,
|
||||
features=None,
|
||||
|
@ -730,7 +734,7 @@ class TransposedFont:
|
|||
return 0, 0, height, width
|
||||
return 0, 0, width, height
|
||||
|
||||
def getlength(self, text: str, *args, **kwargs) -> float:
|
||||
def getlength(self, text: str | bytes, *args, **kwargs) -> float:
|
||||
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
|
||||
msg = "text length is undefined for text rotated by 90 or 270 degrees"
|
||||
raise ValueError(msg)
|
||||
|
@ -775,10 +779,15 @@ def truetype(
|
|||
|
||||
:param font: A filename or file-like object containing a TrueType font.
|
||||
If the file is not found in this filename, the loader may also
|
||||
search in other directories, such as the :file:`fonts/`
|
||||
directory on Windows or :file:`/Library/Fonts/`,
|
||||
:file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on
|
||||
macOS.
|
||||
search in other directories, such as:
|
||||
|
||||
* The :file:`fonts/` directory on Windows,
|
||||
* :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/`
|
||||
and :file:`~/Library/Fonts/` on macOS.
|
||||
* :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`,
|
||||
and :file:`/usr/share/fonts` on Linux; or those specified by
|
||||
the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables
|
||||
for user-installed and system-wide fonts, respectively.
|
||||
|
||||
:param size: The requested size, in pixels.
|
||||
:param index: Which font face to load (default is first available face).
|
||||
|
@ -837,12 +846,21 @@ def truetype(
|
|||
if windir:
|
||||
dirs.append(os.path.join(windir, "fonts"))
|
||||
elif sys.platform in ("linux", "linux2"):
|
||||
lindirs = os.environ.get("XDG_DATA_DIRS")
|
||||
if not lindirs:
|
||||
# According to the freedesktop spec, XDG_DATA_DIRS should
|
||||
# default to /usr/share
|
||||
lindirs = "/usr/share"
|
||||
dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")]
|
||||
data_home = os.environ.get("XDG_DATA_HOME")
|
||||
if not data_home:
|
||||
# The freedesktop spec defines the following default directory for
|
||||
# when XDG_DATA_HOME is unset or empty. This user-level directory
|
||||
# takes precedence over system-level directories.
|
||||
data_home = os.path.expanduser("~/.local/share")
|
||||
xdg_dirs = [data_home]
|
||||
|
||||
data_dirs = os.environ.get("XDG_DATA_DIRS")
|
||||
if not data_dirs:
|
||||
# Similarly, defaults are defined for the system-level directories
|
||||
data_dirs = "/usr/local/share:/usr/share"
|
||||
xdg_dirs += data_dirs.split(":")
|
||||
|
||||
dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs]
|
||||
elif sys.platform == "darwin":
|
||||
dirs += [
|
||||
"/Library/Fonts",
|
||||
|
@ -903,7 +921,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
|
|||
:return: A font object.
|
||||
"""
|
||||
f: FreeTypeFont | ImageFont
|
||||
if core.__class__.__name__ == "module" or size is not None:
|
||||
if isinstance(core, ModuleType) or size is not None:
|
||||
f = truetype(
|
||||
BytesIO(
|
||||
base64.b64decode(
|
||||
|
|
|
@ -26,7 +26,13 @@ import tempfile
|
|||
from . import Image
|
||||
|
||||
|
||||
def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None):
|
||||
def grab(
|
||||
bbox: tuple[int, int, int, int] | None = None,
|
||||
include_layered_windows: bool = False,
|
||||
all_screens: bool = False,
|
||||
xdisplay: str | None = None,
|
||||
) -> Image.Image:
|
||||
im: Image.Image
|
||||
if xdisplay is None:
|
||||
if sys.platform == "darwin":
|
||||
fh, filepath = tempfile.mkstemp(".png")
|
||||
|
@ -63,14 +69,16 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
|
|||
left, top, right, bottom = bbox
|
||||
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
|
||||
return im
|
||||
# Cast to Optional[str] needed for Windows and macOS.
|
||||
display_name: str | None = xdisplay
|
||||
try:
|
||||
if not Image.core.HAVE_XCB:
|
||||
msg = "Pillow was built without XCB support"
|
||||
raise OSError(msg)
|
||||
size, data = Image.core.grabscreen_x11(xdisplay)
|
||||
size, data = Image.core.grabscreen_x11(display_name)
|
||||
except OSError:
|
||||
if (
|
||||
xdisplay is None
|
||||
display_name is None
|
||||
and sys.platform not in ("darwin", "win32")
|
||||
and shutil.which("gnome-screenshot")
|
||||
):
|
||||
|
@ -94,7 +102,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
|
|||
return im
|
||||
|
||||
|
||||
def grabclipboard():
|
||||
def grabclipboard() -> Image.Image | list[str] | None:
|
||||
if sys.platform == "darwin":
|
||||
fh, filepath = tempfile.mkstemp(".png")
|
||||
os.close(fh)
|
||||
|
|
|
@ -717,6 +717,9 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
|
|||
exif_image.info["XML:com.adobe.xmp"] = re.sub(
|
||||
pattern, "", exif_image.info["XML:com.adobe.xmp"]
|
||||
)
|
||||
exif_image.info["xmp"] = re.sub(
|
||||
pattern.encode(), b"", exif_image.info["xmp"]
|
||||
)
|
||||
if not in_place:
|
||||
return transposed_image
|
||||
elif not in_place:
|
||||
|
|
|
@ -38,23 +38,27 @@ class ImagePalette:
|
|||
Defaults to an empty palette.
|
||||
"""
|
||||
|
||||
def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
mode: str = "RGB",
|
||||
palette: Sequence[int] | bytes | bytearray | None = None,
|
||||
) -> None:
|
||||
self.mode = mode
|
||||
self.rawmode = None # if set, palette contains raw data
|
||||
self.rawmode: str | None = None # if set, palette contains raw data
|
||||
self.palette = palette or bytearray()
|
||||
self.dirty: int | None = None
|
||||
|
||||
@property
|
||||
def palette(self):
|
||||
def palette(self) -> Sequence[int] | bytes | bytearray:
|
||||
return self._palette
|
||||
|
||||
@palette.setter
|
||||
def palette(self, palette):
|
||||
self._colors = None
|
||||
def palette(self, palette: Sequence[int] | bytes | bytearray) -> None:
|
||||
self._colors: dict[tuple[int, ...], int] | None = None
|
||||
self._palette = palette
|
||||
|
||||
@property
|
||||
def colors(self):
|
||||
def colors(self) -> dict[tuple[int, ...], int]:
|
||||
if self._colors is None:
|
||||
mode_len = len(self.mode)
|
||||
self._colors = {}
|
||||
|
@ -66,7 +70,7 @@ class ImagePalette:
|
|||
return self._colors
|
||||
|
||||
@colors.setter
|
||||
def colors(self, colors):
|
||||
def colors(self, colors: dict[tuple[int, ...], int]) -> None:
|
||||
self._colors = colors
|
||||
|
||||
def copy(self) -> ImagePalette:
|
||||
|
@ -80,7 +84,7 @@ class ImagePalette:
|
|||
|
||||
return new
|
||||
|
||||
def getdata(self) -> tuple[str, bytes]:
|
||||
def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]:
|
||||
"""
|
||||
Get palette contents in format suitable for the low-level
|
||||
``im.putpalette`` primitive.
|
||||
|
@ -107,11 +111,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"),
|
||||
|
@ -133,7 +139,7 @@ class ImagePalette:
|
|||
|
||||
def getcolor(
|
||||
self,
|
||||
color: tuple[int, int, int] | tuple[int, int, int, int],
|
||||
color: tuple[int, ...],
|
||||
image: Image.Image | None = None,
|
||||
) -> int:
|
||||
"""Given an rgb tuple, allocate palette entry.
|
||||
|
@ -158,12 +164,13 @@ class ImagePalette:
|
|||
except KeyError as e:
|
||||
# allocate new color slot
|
||||
index = self._new_color_index(image, e)
|
||||
assert isinstance(self._palette, bytearray)
|
||||
self.colors[color] = index
|
||||
if index * 3 < len(self.palette):
|
||||
self._palette = (
|
||||
self.palette[: index * 3]
|
||||
self._palette[: index * 3]
|
||||
+ bytes(color)
|
||||
+ self.palette[index * 3 + 3 :]
|
||||
+ self._palette[index * 3 + 3 :]
|
||||
)
|
||||
else:
|
||||
self._palette += bytes(color)
|
||||
|
@ -200,7 +207,7 @@ class ImagePalette:
|
|||
# Internal
|
||||
|
||||
|
||||
def raw(rawmode, data) -> ImagePalette:
|
||||
def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
|
||||
palette = ImagePalette()
|
||||
palette.rawmode = rawmode
|
||||
palette.palette = data
|
||||
|
@ -212,9 +219,9 @@ def raw(rawmode, data) -> ImagePalette:
|
|||
# Factories
|
||||
|
||||
|
||||
def make_linear_lut(black, white):
|
||||
def make_linear_lut(black: int, white: float) -> list[int]:
|
||||
if black == 0:
|
||||
return [white * i // 255 for i in range(256)]
|
||||
return [int(white * i // 255) for i in range(256)]
|
||||
|
||||
msg = "unavailable when black is non-zero"
|
||||
raise NotImplementedError(msg) # FIXME
|
||||
|
@ -247,15 +254,22 @@ def wedge(mode: str = "RGB") -> ImagePalette:
|
|||
return ImagePalette(mode, [i // len(mode) for i in palette])
|
||||
|
||||
|
||||
def load(filename):
|
||||
def load(filename: str) -> tuple[bytes, str]:
|
||||
# FIXME: supports GIMP gradients only
|
||||
|
||||
with open(filename, "rb") as fp:
|
||||
for paletteHandler in [
|
||||
paletteHandlers: list[
|
||||
type[
|
||||
GimpPaletteFile.GimpPaletteFile
|
||||
| GimpGradientFile.GimpGradientFile
|
||||
| PaletteFile.PaletteFile
|
||||
]
|
||||
] = [
|
||||
GimpPaletteFile.GimpPaletteFile,
|
||||
GimpGradientFile.GimpGradientFile,
|
||||
PaletteFile.PaletteFile,
|
||||
]:
|
||||
]
|
||||
for paletteHandler in paletteHandlers:
|
||||
try:
|
||||
fp.seek(0)
|
||||
lut = paletteHandler(fp).getpalette()
|
||||
|
|
|
@ -152,7 +152,7 @@ def _toqclass_helper(im):
|
|||
elif im.mode == "RGBA":
|
||||
data = im.tobytes("raw", "BGRA")
|
||||
format = qt_format.Format_ARGB32
|
||||
elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
|
||||
elif im.mode == "I;16":
|
||||
im = im.point(lambda i: i * 256)
|
||||
|
||||
format = qt_format.Format_Grayscale16
|
||||
|
@ -196,7 +196,7 @@ if qt_is_installed:
|
|||
self.setColorTable(im_data["colortable"])
|
||||
|
||||
|
||||
def toqimage(im):
|
||||
def toqimage(im) -> ImageQt:
|
||||
return ImageQt(im)
|
||||
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class Transform(Image.ImageTransformHandler):
|
|||
|
||||
method: Image.Transform
|
||||
|
||||
def __init__(self, data: Sequence[int]) -> None:
|
||||
def __init__(self, data: Sequence[Any]) -> None:
|
||||
self.data = data
|
||||
|
||||
def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
|
||||
|
|