Merge branch 'main' into apng

This commit is contained in:
Andrew Murray 2024-06-25 22:42:47 +10:00 committed by GitHub
commit 272d235ab4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
163 changed files with 2091 additions and 1303 deletions

View File

@ -32,10 +32,10 @@ install:
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-test-images.zip -oc:\
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.3.0
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
- choco install ghostscript --version=10.3.1
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\

View File

@ -1 +1 @@
cibuildwheel==2.18.1
cibuildwheel==2.19.1

View File

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

View File

@ -86,7 +86,7 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.3.0 --no-progress
choco install ghostscript --version=10.3.1 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images

View File

@ -16,9 +16,9 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.4.0
HARFBUZZ_VERSION=8.5.0
LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.2
JPEGTURBO_VERSION=3.0.3
OPENJPEG_VERSION=2.5.2
XZ_VERSION=5.4.5
TIFF_VERSION=4.6.0
@ -33,9 +33,9 @@ if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
else
ZLIB_VERSION=1.2.8
fi
LIBWEBP_VERSION=1.3.2
LIBWEBP_VERSION=1.4.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.16.1
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
@ -70,7 +70,7 @@ function build {
fi
build_new_zlib
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib

View File

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

View File

@ -5,6 +5,36 @@ Changelog (Pillow)
10.4.0 (unreleased)
-------------------
- 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]
- Deprecate ImageDraw.getdraw hints parameter #8124
[radarhere, hugovk]
- Added ImageDraw circle() #8085
[void4, hugovk, radarhere]

View File

@ -44,6 +44,7 @@ def test_direct() -> None:
caccess = im.im.pixel_access(False)
access = PyAccess.new(im, False)
assert access is not None
assert caccess[(0, 0)] == access[(0, 0)]
print(f"Size: {im.width}x{im.height}")

View File

@ -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))

View 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])

View File

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

View File

@ -38,7 +38,9 @@ def test_version() -> None:
assert function(name) == version
if name != "PIL":
if name == "zlib" and version is not None:
version = version.replace(".zlib-ng", "")
version = re.sub(".zlib-ng$", "", version)
elif name == "libtiff" and version is not None:
version = re.sub("t$", "", version)
assert version is None or re.search(r"\d+(\.\d+)*$", version)
for module in features.modules:
@ -124,7 +126,7 @@ def test_unsupported_module() -> None:
@pytest.mark.parametrize("supported_formats", (True, False))
def test_pilinfo(supported_formats) -> None:
def test_pilinfo(supported_formats: bool) -> None:
buf = io.StringIO()
features.pilinfo(buf, supported_formats=supported_formats)
out = buf.getvalue()

View File

@ -140,7 +140,7 @@ def test_load_dib() -> None:
(124, "g/pal8v5.bmp"),
),
)
def test_dib_header_size(header_size, path):
def test_dib_header_size(header_size: int, path: str) -> None:
image_path = "Tests/images/bmp/" + path
with open(image_path, "rb") as fp:
data = fp.read()[14:]

View File

@ -1,10 +1,11 @@
from __future__ import annotations
from pathlib import Path
from typing import IO
import pytest
from PIL import BufrStubImagePlugin, Image
from PIL import BufrStubImagePlugin, Image, ImageFile
from .helper import hopper
@ -50,30 +51,33 @@ def test_save(tmp_path: Path) -> None:
def test_handler(tmp_path: Path) -> None:
class TestHandler:
class TestHandler(ImageFile.StubHandler):
opened = False
loaded = False
saved = False
def open(self, im) -> None:
def open(self, im: ImageFile.StubImageFile) -> None:
self.opened = True
def load(self, im):
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
self.loaded = True
im.fp.close()
return Image.new("RGB", (1, 1))
def save(self, im, fp, filename) -> None:
def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
handler = TestHandler()
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)

View File

@ -53,6 +53,7 @@ def test_closed_file() -> None:
def test_seek_after_close() -> None:
im = Image.open("Tests/images/iss634.gif")
assert isinstance(im, GifImagePlugin.GifImageFile)
im.load()
im.close()
@ -377,7 +378,8 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
img = img.convert("RGB")
tempfile = str(tmp_path / "temp.gif")
GifImagePlugin._save_netpbm(img, 0, tempfile)
b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("RGB"), 0)
@ -388,7 +390,8 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
img = img.convert("L")
tempfile = str(tmp_path / "temp.gif")
GifImagePlugin._save_netpbm(img, 0, tempfile)
b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("L"), 0)
@ -648,7 +651,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
assert rgb_img.getpixel((50, 50)) == circle
# Check that frame transparency wasn't added unnecessarily
assert img._frame_transparency is None
assert getattr(img, "_frame_transparency") is None
def test_dispose2_diff(tmp_path: Path) -> None:

View File

@ -5,7 +5,7 @@ from typing import IO
import pytest
from PIL import GribStubImagePlugin, Image
from PIL import GribStubImagePlugin, Image, ImageFile
from .helper import hopper
@ -51,7 +51,7 @@ def test_save(tmp_path: Path) -> None:
def test_handler(tmp_path: Path) -> None:
class TestHandler:
class TestHandler(ImageFile.StubHandler):
opened = False
loaded = False
saved = False
@ -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)

View File

@ -1,11 +1,12 @@
from __future__ import annotations
from io import BytesIO
from pathlib import Path
from typing import IO
import pytest
from PIL import Hdf5StubImagePlugin, Image
from PIL import Hdf5StubImagePlugin, Image, ImageFile
TEST_FILE = "Tests/images/hdf5.h5"
@ -41,7 +42,7 @@ def test_load() -> None:
def test_save() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
dummy_fp = None
dummy_fp = BytesIO()
dummy_filename = "dummy.filename"
# Act / Assert: stub cannot save without an implemented handler
@ -52,7 +53,7 @@ def test_save() -> None:
def test_handler(tmp_path: Path) -> None:
class TestHandler:
class TestHandler(ImageFile.StubHandler):
opened = False
loaded = False
saved = False
@ -65,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
@ -72,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)

View File

@ -171,7 +171,7 @@ class TestFileJpeg:
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
)
def test_dpi(self, test_image_path: str) -> None:
def test(xdpi: int, ydpi: int | None = None):
def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None:
with Image.open(test_image_path) as im:
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
return im.info.get("dpi")
@ -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)

View File

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

View File

@ -54,7 +54,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_version(self) -> None:
version = features.version_codec("libtiff")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
assert re.search(r"\d+\.\d+\.\d+t?$", version)
def test_g4_tiff(self, tmp_path: Path) -> None:
"""Test the ordinary file path load path"""

View File

@ -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)

View 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)

View 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

View File

@ -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)

View File

@ -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()
@ -759,6 +772,7 @@ class TestFileTiff:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]

View File

@ -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:

View File

@ -5,6 +5,7 @@ import re
import sys
import warnings
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)
@ -198,7 +201,9 @@ class TestFileWebp:
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
)
@skip_unless_feature("webp_anim")
def test_invalid_background(self, background, tmp_path: Path) -> None:
def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path
) -> None:
temp_file = str(tmp_path / "temp.webp")
im = hopper()
with pytest.raises(OSError):

View File

@ -69,7 +69,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
are visually similar to the originals.
"""
def check(temp_file) -> None:
def check(temp_file: str) -> None:
with Image.open(temp_file) as im:
assert im.n_frames == 2

View File

@ -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 "

View File

@ -1,10 +1,11 @@
from __future__ import annotations
from pathlib import Path
from typing import IO
import pytest
from PIL import Image, WmfImagePlugin
from PIL import Image, ImageFile, WmfImagePlugin
from .helper import assert_image_similar_tofile, hopper
@ -34,10 +35,13 @@ def test_load() -> None:
def test_register_handler(tmp_path: Path) -> None:
class TestHandler:
class TestHandler(ImageFile.StubHandler):
methodCalled = False
def save(self, im, fp, filename) -> None:
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
return Image.new("RGB", (1, 1))
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.methodCalled = True
handler = TestHandler()
@ -70,7 +74,7 @@ def test_load_set_dpi() -> None:
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
def test_save(ext, tmp_path: Path) -> None:
def test_save(ext: str, tmp_path: Path) -> None:
im = hopper()
tmpfile = str(tmp_path / ("temp" + ext))

View File

@ -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:

View File

@ -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)

View File

@ -259,6 +259,7 @@ class TestCffi(AccessTest):
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
w, h = im.size
for x in range(0, w, 10):
@ -289,6 +290,7 @@ class TestCffi(AccessTest):
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
w, h = im.size
for x in range(0, w, 10):
@ -299,6 +301,8 @@ class TestCffi(AccessTest):
# Attempt to set the value on a read-only image
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, True)
assert access is not None
with pytest.raises(ValueError):
access[(0, 0)] = color
@ -341,6 +345,8 @@ class TestCffi(AccessTest):
im = Image.new(mode, (1, 1))
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
assert access is not None
access.putpixel((0, 0), color)
if len(color) == 3:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

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

View File

@ -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)

View File

@ -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([])

View File

@ -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:

View File

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

View File

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

View File

@ -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))

View File

@ -124,8 +124,8 @@ def test_fastpath_translate() -> None:
def test_center() -> None:
im = hopper()
rotate(im, im.mode, 45, center=(0, 0))
rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0))
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0))
rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0))
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0))
def test_rotate_no_fill() -> None:

View File

@ -16,7 +16,7 @@ from .helper import (
def test_sanity() -> None:
im = hopper()
assert im.thumbnail((100, 100)) is None
im.thumbnail((100, 100))
assert im.size == (100, 100)
@ -111,7 +111,9 @@ def test_load_first_unless_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft
def im_draft(mode: str, size: tuple[int, int]):
def im_draft(
mode: str, size: tuple[int, int]
) -> tuple[str, tuple[int, int, float, float]] | None:
result = draft(mode, size)
assert result is not None

View File

@ -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,

View File

@ -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))
@ -913,7 +916,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 +1437,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 +1476,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 +1486,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 +1574,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
@ -1624,3 +1640,8 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
draw.rectangle(xy)
with pytest.raises(ValueError):
draw.rounded_rectangle(xy)
def test_getdraw() -> None:
with pytest.warns(DeprecationWarning):
ImageDraw.getdraw(None, [])

View File

@ -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)

View File

@ -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

View File

@ -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",
[

View 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")

View File

@ -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(

View File

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

View File

@ -58,7 +58,6 @@ def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None:
blur = ImageFilter.GaussianBlur
with pytest.raises(ValueError):
im.convert("1").filter(blur)
blur(im.convert("L"))
with pytest.raises(ValueError):
im.convert("I").filter(blur)
with pytest.raises(ValueError):
@ -102,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

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

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

View File

@ -1,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)))

View File

@ -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""

View File

@ -46,7 +46,7 @@ def roundtrip(expected: Image.Image) -> None:
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_sanity(tmp_path: Path) -> None:
# Segfault test
app = QApplication([])
app: QApplication | None = QApplication([])
ex = Example()
assert app # Silence warning
assert ex # Silence warning

View File

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

View File

@ -2,7 +2,7 @@
# install libimagequant
archive_name=libimagequant
archive_version=4.3.0
archive_version=4.3.1
archive=$archive_name-$archive_version

View File

@ -1,7 +1,7 @@
#!/bin/bash
# install webp
archive=libwebp-1.3.2
archive=libwebp-1.4.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -9,9 +9,9 @@ PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
PAPEROPT_a4 = --define latex_paper_size=a4
PAPEROPT_letter = --define latex_paper_size=letter
ALLSPHINXOPTS = --doctree-dir $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
@ -51,42 +51,42 @@ install-sphinx:
.PHONY: html
html:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
$(SPHINXBUILD) --builder html --fail-on-warning --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
$(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
$(SPHINXBUILD) --builder singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
$(SPHINXBUILD) --builder pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
$(SPHINXBUILD) --builder json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
$(SPHINXBUILD) --builder htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
@ -94,7 +94,7 @@ htmlhelp:
.PHONY: qthelp
qthelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
$(SPHINXBUILD) --builder qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@ -105,7 +105,7 @@ qthelp:
.PHONY: devhelp
devhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
$(SPHINXBUILD) --builder devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@ -116,14 +116,14 @@ devhelp:
.PHONY: epub
epub:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
$(SPHINXBUILD) --builder epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: latex
latex:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
$(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
@ -132,7 +132,7 @@ latex:
.PHONY: latexpdf
latexpdf:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
$(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
@ -140,21 +140,21 @@ latexpdf:
.PHONY: text
text:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
$(SPHINXBUILD) --builder text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
$(SPHINXBUILD) --builder man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
$(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
@ -163,7 +163,7 @@ texinfo:
.PHONY: info
info:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
$(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
@ -171,21 +171,21 @@ info:
.PHONY: gettext
gettext:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
$(SPHINXBUILD) --builder gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
$(SPHINXBUILD) --builder changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
$(SPHINXBUILD) --builder linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
@ -193,7 +193,7 @@ linkcheck:
.PHONY: doctest
doctest:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
$(SPHINXBUILD) --builder doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -115,6 +115,13 @@ Support for LibTIFF earlier than 4
Support for LibTIFF earlier than version 4 has been deprecated.
Upgrade to a newer version of LibTIFF instead.
ImageDraw.getdraw hints parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
Removed features
----------------
@ -279,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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -144,10 +144,12 @@ pixel, the Python Imaging Library provides different resampling *filters*.
.. py:currentmodule:: PIL.Image
.. data:: Resampling.NEAREST
:noindex:
Pick one nearest pixel from the input image. Ignore all other input pixels.
.. data:: Resampling.BOX
:noindex:
Each pixel of source image contributes to one pixel of the
destination image with identical weights.
@ -158,6 +160,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
.. versionadded:: 3.4.0
.. data:: Resampling.BILINEAR
:noindex:
For resize calculate the output pixel value using linear interpolation
on all pixels that may contribute to the output value.
@ -165,6 +168,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
in the input image is used.
.. data:: Resampling.HAMMING
:noindex:
Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have
dislocations on local level like with :data:`Resampling.BOX`.
@ -174,6 +178,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
.. versionadded:: 3.4.0
.. data:: Resampling.BICUBIC
:noindex:
For resize calculate the output pixel value using cubic interpolation
on all pixels that may contribute to the output value.
@ -181,6 +186,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
in the input image is used.
.. data:: Resampling.LANCZOS
:noindex:
Calculate the output pixel value using a high-quality Lanczos filter (a
truncated sinc) on all pixels that may contribute to the output value.

View File

@ -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

View File

@ -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:

View File

@ -68,7 +68,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.3**
* Pillow has been tested with libimagequant **2.6-4.3.1**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.

View File

@ -78,8 +78,6 @@ Constructing images
^^^^^^^^^^^^^^^^^^^
.. autofunction:: new
.. autoclass:: SupportsArrayInterface
:show-inheritance:
.. autofunction:: fromarray
.. autofunction:: frombytes
.. autofunction:: frombuffer
@ -197,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
@ -365,6 +364,14 @@ Classes
.. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImageTransformHandler
Protocols
---------
.. autoclass:: SupportsArrayInterface
:show-inheritance:
.. autoclass:: SupportsGetData
:show-inheritance:
Constants
---------
@ -418,7 +425,6 @@ See :ref:`concept-filters` for details.
.. autoclass:: Resampling
:members:
:undoc-members:
:noindex:
Dither modes
^^^^^^^^^^^^

View File

@ -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

View File

@ -57,6 +57,10 @@ Classes
:undoc-members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.StubHandler()
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.StubImageFile()
:members:
:show-inheritance:

View File

@ -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

View File

@ -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.

View File

@ -44,3 +44,4 @@ Access using negative indexes is also possible. ::
.. autoclass:: PIL.PyAccess.PyAccess()
:members:
:special-members: __getitem__, __setitem__

View File

@ -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.

View File

@ -34,6 +34,11 @@ Support for LibTIFF earlier than 4
Support for LibTIFF earlier than version 4 has been deprecated.
Upgrade to a newer version of LibTIFF instead.
ImageDraw.getdraw hints parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
API Changes
===========

View File

@ -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
============

View File

@ -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

View File

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

View File

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

View File

@ -31,10 +31,12 @@ BLP files come in many different flavours:
from __future__ import annotations
import abc
import os
import struct
from enum import IntEnum
from io import BytesIO
from typing import IO
from . import Image, ImageFile
@ -55,11 +57,13 @@ class AlphaEncoding(IntEnum):
DXT5 = 7
def unpack_565(i):
def unpack_565(i: int) -> tuple[int, int, int]:
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
def decode_dxt1(data, alpha=False):
def decode_dxt1(
data: bytes, alpha: bool = False
) -> tuple[bytearray, bytearray, bytearray, bytearray]:
"""
input: one "row" of data (i.e. will produce 4*width pixels)
"""
@ -67,9 +71,9 @@ def decode_dxt1(data, alpha=False):
blocks = len(data) // 8 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks):
for block_index in range(blocks):
# Decode next 8-byte block.
idx = block * 8
idx = block_index * 8
color0, color1, bits = struct.unpack_from("<HHI", data, idx)
r0, g0, b0 = unpack_565(color0)
@ -114,7 +118,7 @@ def decode_dxt1(data, alpha=False):
return ret
def decode_dxt3(data):
def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
"""
input: one "row" of data (i.e. will produce 4*width pixels)
"""
@ -122,8 +126,8 @@ def decode_dxt3(data):
blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks):
idx = block * 16
for block_index in range(blocks):
idx = block_index * 16
block = data[idx : idx + 16]
# Decode next 16-byte block.
bits = struct.unpack_from("<8B", block)
@ -167,7 +171,7 @@ def decode_dxt3(data):
return ret
def decode_dxt5(data):
def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
"""
input: one "row" of data (i.e. will produce 4 * width pixels)
"""
@ -175,8 +179,8 @@ def decode_dxt5(data):
blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks):
idx = block * 16
for block_index in range(blocks):
idx = block_index * 16
block = data[idx : idx + 16]
# Decode next 16-byte block.
a0, a1 = struct.unpack_from("<BB", block)
@ -275,7 +279,7 @@ class BlpImageFile(ImageFile.ImageFile):
class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
def decode(self, buffer: bytes) -> tuple[int, int]:
try:
self._read_blp_header()
self._load()
@ -284,7 +288,12 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
raise OSError(msg) from e
return -1, 0
def _read_blp_header(self):
@abc.abstractmethod
def _load(self) -> None:
pass
def _read_blp_header(self) -> None:
assert self.fd is not None
self.fd.seek(4)
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
@ -303,10 +312,10 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length):
def _safe_read(self, length: int) -> bytes:
return ImageFile._safe_read(self.fd, length)
def _read_palette(self):
def _read_palette(self) -> list[tuple[int, int, int, int]]:
ret = []
for i in range(256):
try:
@ -316,7 +325,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a))
return ret
def _read_bgra(self, palette):
def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
@ -325,7 +334,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
except struct.error:
break
b, g, r, a = palette[offset]
d = (r, g, b)
d: tuple[int, ...] = (r, g, b)
if self._blp_alpha_depth:
d += (a,)
data.extend(d)
@ -349,29 +358,30 @@ class BLP1Decoder(_BLPBaseDecoder):
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
raise BLPFormatError(msg)
def _decode_jpeg_stream(self):
def _decode_jpeg_stream(self) -> None:
from .JpegImagePlugin import JpegImageFile
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size)
assert self.fd is not None
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._blp_lengths[0])
data = jpeg_header + data
data = BytesIO(data)
image = JpegImageFile(data)
image = JpegImageFile(BytesIO(data))
Image._decompression_bomb_check(image.size)
if image.mode == "CMYK":
decoder_name, extents, offset, args = image.tile[0]
image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
r, g, b = image.convert("RGB").split()
image = Image.merge("RGB", (b, g, r))
self.set_as_raw(image.tobytes())
reversed_image = Image.merge("RGB", (b, g, r))
self.set_as_raw(reversed_image.tobytes())
class BLP2Decoder(_BLPBaseDecoder):
def _load(self):
def _load(self) -> None:
palette = self._read_palette()
assert self.fd is not None
self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1:
@ -420,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]
@ -428,12 +439,13 @@ class BLPEncoder(ImageFile.PyEncoder):
data += b"\x00" * 4
return data
def encode(self, bufsize):
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
palette_data = self._write_palette()
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))
@ -446,7 +458,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data
def _save(im, fp, filename):
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode != "P":
msg = "Unsupported BLP image mode"
raise ValueError(msg)

View File

@ -25,6 +25,7 @@
from __future__ import annotations
import os
from typing import IO
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@ -52,7 +53,7 @@ def _accept(prefix: bytes) -> bool:
return prefix[:2] == b"BM"
def _dib_accept(prefix):
def _dib_accept(prefix: bytes) -> bool:
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
@ -300,7 +301,8 @@ class BmpImageFile(ImageFile.ImageFile):
class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
rle4 = self.args[1]
data = bytearray()
x = 0
@ -394,11 +396,13 @@ SAVE = {
}
def _dib_save(im, fp, filename):
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, False)
def _save(im, fp, filename, bitmap_header=True):
def _save(
im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
) -> None:
try:
rawmode, bits, colors = SAVE[im.mode]
except KeyError as e:

View File

@ -10,12 +10,14 @@
#
from __future__ import annotations
from typing import IO
from . import Image, ImageFile
_handler = None
def register_handler(handler):
def register_handler(handler: ImageFile.StubHandler | None) -> None:
"""
Install application-specific BUFR image handler.
@ -54,11 +56,11 @@ class BufrStubImageFile(ImageFile.StubImageFile):
if loader:
loader.open(self)
def _load(self):
def _load(self) -> ImageFile.StubHandler | None:
return _handler
def _save(im, fp, filename):
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"):
msg = "BUFR save handler not installed"
raise OSError(msg)

Some files were not shown because too many files have changed in this diff Show More