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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ jobs:
choco install nasm --no-progress choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH 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 echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images # Install extra test images

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ from typing import Any, Callable, Sequence
import pytest import pytest
from packaging.version import parse as parse_version 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__) logger = logging.getLogger(__name__)
@ -240,7 +240,7 @@ class PillowLeakTestCase:
# helpers # helpers
def fromstring(data: bytes) -> Image.Image: def fromstring(data: bytes) -> ImageFile.ImageFile:
return Image.open(BytesIO(data)) return Image.open(BytesIO(data))

View File

@ -354,10 +354,10 @@ class TestColorLut3DCoreAPI:
class TestColorLut3DFilter: class TestColorLut3DFilter:
def test_wrong_args(self) -> None: def test_wrong_args(self) -> None:
with pytest.raises(ValueError, match="should be either an integer"): 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"): 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"): with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
ImageFilter.Color3DLUT((11, 11, 1), [1]) ImageFilter.Color3DLUT((11, 11, 1), [1])

View File

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

View File

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

View File

@ -140,7 +140,7 @@ def test_load_dib() -> None:
(124, "g/pal8v5.bmp"), (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 image_path = "Tests/images/bmp/" + path
with open(image_path, "rb") as fp: with open(image_path, "rb") as fp:
data = fp.read()[14:] data = fp.read()[14:]

View File

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

View File

@ -53,6 +53,7 @@ def test_closed_file() -> None:
def test_seek_after_close() -> None: def test_seek_after_close() -> None:
im = Image.open("Tests/images/iss634.gif") im = Image.open("Tests/images/iss634.gif")
assert isinstance(im, GifImagePlugin.GifImageFile)
im.load() im.load()
im.close() im.close()
@ -377,7 +378,8 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
img = img.convert("RGB") img = img.convert("RGB")
tempfile = str(tmp_path / "temp.gif") 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: with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("RGB"), 0) 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") img = img.convert("L")
tempfile = str(tmp_path / "temp.gif") 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: with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("L"), 0) 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 assert rgb_img.getpixel((50, 50)) == circle
# Check that frame transparency wasn't added unnecessarily # 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: def test_dispose2_diff(tmp_path: Path) -> None:

View File

@ -5,7 +5,7 @@ from typing import IO
import pytest import pytest
from PIL import GribStubImagePlugin, Image from PIL import GribStubImagePlugin, Image, ImageFile
from .helper import hopper from .helper import hopper
@ -51,7 +51,7 @@ def test_save(tmp_path: Path) -> None:
def test_handler(tmp_path: Path) -> None: def test_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler(ImageFile.StubHandler):
opened = False opened = False
loaded = False loaded = False
saved = False saved = False
@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) 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: def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True self.saved = True
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
GribStubImagePlugin.register_handler(handler) GribStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert handler.opened assert handler.opened
assert not handler.loaded assert not handler.is_loaded()
im.load() im.load()
assert handler.loaded assert handler.is_loaded()
temp_file = str(tmp_path / "temp.grib") temp_file = str(tmp_path / "temp.grib")
im.save(temp_file) im.save(temp_file)

View File

@ -1,11 +1,12 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import IO from typing import IO
import pytest import pytest
from PIL import Hdf5StubImagePlugin, Image from PIL import Hdf5StubImagePlugin, Image, ImageFile
TEST_FILE = "Tests/images/hdf5.h5" TEST_FILE = "Tests/images/hdf5.h5"
@ -41,7 +42,7 @@ def test_load() -> None:
def test_save() -> None: def test_save() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
dummy_fp = None dummy_fp = BytesIO()
dummy_filename = "dummy.filename" dummy_filename = "dummy.filename"
# Act / Assert: stub cannot save without an implemented handler # Act / Assert: stub cannot save without an implemented handler
@ -52,7 +53,7 @@ def test_save() -> None:
def test_handler(tmp_path: Path) -> None: def test_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler(ImageFile.StubHandler):
opened = False opened = False
loaded = False loaded = False
saved = False saved = False
@ -65,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) 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: def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True self.saved = True
@ -72,10 +76,10 @@ def test_handler(tmp_path: Path) -> None:
Hdf5StubImagePlugin.register_handler(handler) Hdf5StubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert handler.opened assert handler.opened
assert not handler.loaded assert not handler.is_loaded()
im.load() im.load()
assert handler.loaded assert handler.is_loaded()
temp_file = str(tmp_path / "temp.h5") temp_file = str(tmp_path / "temp.h5")
im.save(temp_file) im.save(temp_file)

View File

@ -171,7 +171,7 @@ class TestFileJpeg:
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
) )
def test_dpi(self, test_image_path: str) -> None: 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: with Image.open(test_image_path) as im:
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
return im.info.get("dpi") return im.info.get("dpi")
@ -443,7 +443,9 @@ class TestFileJpeg:
assert_image(im1, im2.mode, im2.size) assert_image(im1, im2.mode, im2.size)
def test_subsampling(self) -> None: def test_subsampling(self) -> None:
def getsampling(im: JpegImagePlugin.JpegImageFile): def getsampling(
im: JpegImagePlugin.JpegImageFile,
) -> tuple[int, int, int, int, int, int]:
layer = im.layer layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
@ -699,7 +701,7 @@ class TestFileJpeg:
def test_save_cjpeg(self, tmp_path: Path) -> None: def test_save_cjpeg(self, tmp_path: Path) -> None:
with Image.open(TEST_FILE) as img: with Image.open(TEST_FILE) as img:
tempfile = str(tmp_path / "temp.jpg") tempfile = str(tmp_path / "temp.jpg")
JpegImagePlugin._save_cjpeg(img, 0, tempfile) JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright # Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17) assert_image_similar_tofile(img, tempfile, 17)
@ -917,24 +919,25 @@ class TestFileJpeg:
with Image.open("Tests/images/icc-after-SOF.jpg") as im: with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile" assert im.info["icc_profile"] == b"profile"
def test_jpeg_magic_number(self) -> None: def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
size = 4097 size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer.max_pos = 0 max_pos = 0
orig_read = buffer.read orig_read = buffer.read
def read(n=-1): def read(n: int | None = -1) -> bytes:
nonlocal max_pos
res = orig_read(n) res = orig_read(n)
buffer.max_pos = max(buffer.max_pos, buffer.tell()) max_pos = max(max_pos, buffer.tell())
return res return res
buffer.read = read monkeypatch.setattr(buffer, "read", read)
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
with Image.open(buffer): with Image.open(buffer):
pass pass
# Assert the entire file has not been read # Assert the entire file has not been read
assert 0 < buffer.max_pos < size assert 0 < max_pos < size
def test_getxmp(self) -> None: def test_getxmp(self) -> None:
with Image.open("Tests/images/xmp_test.jpg") as im: with Image.open("Tests/images/xmp_test.jpg") as im:
@ -945,6 +948,7 @@ class TestFileJpeg:
): ):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert "xmp" in im.info
xmp = im.getxmp() xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"] description = xmp["xmpmeta"]["RDF"]["Description"]
@ -1029,8 +1033,10 @@ class TestFileJpeg:
def test_repr_jpeg(self) -> None: def test_repr_jpeg(self) -> None:
im = hopper() 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 repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17) 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) out.seek(length - 2, os.SEEK_CUR)
def test_9bit(): def test_9bit() -> None:
with Image.open("Tests/images/9bit.j2k") as im: with Image.open("Tests/images/9bit.j2k") as im:
assert im.mode == "I;16" assert im.mode == "I;16"
assert im.size == (128, 128) assert im.size == (128, 128)

View File

@ -54,7 +54,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_version(self) -> None: def test_version(self) -> None:
version = features.version_codec("libtiff") version = features.version_codec("libtiff")
assert version is not None 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: def test_g4_tiff(self, tmp_path: Path) -> None:
"""Test the ordinary file path load path""" """Test the ordinary file path load path"""

View File

@ -535,8 +535,10 @@ class TestFilePng:
def test_repr_png(self) -> None: def test_repr_png(self) -> None:
im = hopper() 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 repr_png.format == "PNG"
assert_image_equal(im, repr_png) assert_image_equal(im, repr_png)
@ -655,11 +657,12 @@ class TestFilePng:
png.call(cid, 0, 0) png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False 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") im = hopper("P")
out = str(tmp_path / "temp.png") 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: with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 48 assert len(reloaded.png.im_palette[1]) == 48
@ -683,6 +686,7 @@ class TestFilePng:
): ):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert "xmp" in im.info
xmp = im.getxmp() xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"] description = xmp["xmpmeta"]["RDF"]["Description"]
@ -767,16 +771,12 @@ class TestFilePng:
def test_save_stdout(self, buffer: bool) -> None: def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer:
class MyStdOut: class MyStdOut:
buffer = BytesIO() buffer = BytesIO()
mystdout = MyStdOut() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
else:
mystdout = BytesIO()
sys.stdout = mystdout sys.stdout = mystdout # type: ignore[assignment]
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG") im.save(sys.stdout, "PNG")
@ -784,7 +784,7 @@ class TestFilePng:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if buffer: if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded: with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE) 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: def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer:
class MyStdOut: class MyStdOut:
buffer = BytesIO() buffer = BytesIO()
mystdout = MyStdOut() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
else:
mystdout = BytesIO()
sys.stdout = mystdout sys.stdout = mystdout # type: ignore[assignment]
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM") im.save(sys.stdout, "PPM")
@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if buffer: if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded: with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_FILE) assert_image_equal_tofile(reloaded, TEST_FILE)

View File

@ -4,7 +4,7 @@ import warnings
import pytest 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 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( @pytest.mark.parametrize(
"test_file,raises", "test_file,raises",
[ [
(
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
UnidentifiedImageError,
),
(
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
UnidentifiedImageError,
),
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.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 open(test_file, "rb") as f:
with pytest.raises(raises): with pytest.raises(raises):
with Image.open(f): with Image.open(f):
pass 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) img_list = SpiderImagePlugin.loadImageSeries(file_list)
# Assert # Assert
assert img_list is not None
assert len(img_list) == 1 assert len(img_list) == 1
assert isinstance(img_list[0], Image.Image) assert isinstance(img_list[0], Image.Image)
assert img_list[0].size == (128, 128) assert img_list[0].size == (128, 128)

View File

@ -113,14 +113,14 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_seek_too_large(self): def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"): with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif") Image.open("Tests/images/seek_too_large.tif")
def test_set_legacy_api(self) -> None: def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:
ifd.legacy_api = None ifd.legacy_api = False
assert str(e.value) == "Not allowing setting of legacy api" assert str(e.value) == "Not allowing setting of legacy api"
def test_xyres_tiff(self) -> None: def test_xyres_tiff(self) -> None:
@ -621,6 +621,19 @@ class TestFileTiff:
assert_image_equal_tofile(im, tmpfile) 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: def test_rowsperstrip(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im = hopper() im = hopper()
@ -759,6 +772,7 @@ class TestFileTiff:
): ):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert "xmp" in im.info
xmp = im.getxmp() xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"] description = xmp["xmpmeta"]["RDF"]["Description"]

View File

@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import assert_deep_equal, hopper 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: def test_rt_metadata(tmp_path: Path) -> None:
@ -411,8 +415,8 @@ def test_empty_values() -> None:
info = TiffImagePlugin.ImageFileDirectory_v2(head) info = TiffImagePlugin.ImageFileDirectory_v2(head)
info.load(data) info.load(data)
# Should not raise ValueError. # Should not raise ValueError.
info = dict(info) info_dict = dict(info)
assert 33432 in info assert 33432 in info_dict
def test_photoshop_info(tmp_path: Path) -> None: def test_photoshop_info(tmp_path: Path) -> None:

View File

@ -5,6 +5,7 @@ import re
import sys import sys
import warnings import warnings
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@ -70,7 +71,9 @@ class TestFileWebp:
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) 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") temp_file = str(tmp_path / "temp.webp")
hopper(mode).save(temp_file, **args) hopper(mode).save(temp_file, **args)
@ -198,7 +201,9 @@ class TestFileWebp:
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
) )
@skip_unless_feature("webp_anim") @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") temp_file = str(tmp_path / "temp.webp")
im = hopper() im = hopper()
with pytest.raises(OSError): 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. are visually similar to the originals.
""" """
def check(temp_file) -> None: def check(temp_file: str) -> None:
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert im.n_frames == 2 assert im.n_frames == 2

View File

@ -129,6 +129,7 @@ def test_getxmp() -> None:
): ):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert "xmp" in im.info
assert ( assert (
im.getxmp()["xmpmeta"]["xmptk"] im.getxmp()["xmpmeta"]["xmptk"]
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 " == "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 __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import IO
import pytest import pytest
from PIL import Image, WmfImagePlugin from PIL import Image, ImageFile, WmfImagePlugin
from .helper import assert_image_similar_tofile, hopper from .helper import assert_image_similar_tofile, hopper
@ -34,10 +35,13 @@ def test_load() -> None:
def test_register_handler(tmp_path: Path) -> None: def test_register_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler(ImageFile.StubHandler):
methodCalled = False 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 self.methodCalled = True
handler = TestHandler() handler = TestHandler()
@ -70,7 +74,7 @@ def test_load_set_dpi() -> None:
@pytest.mark.parametrize("ext", (".wmf", ".emf")) @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() im = hopper()
tmpfile = str(tmp_path / ("temp" + ext)) tmpfile = str(tmp_path / ("temp" + ext))

View File

@ -34,7 +34,7 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
def test_leak(self) -> None: def test_leak(self) -> None:
if features.check_module("freetype2"): if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError) ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try: try:
default_font = ImageFont.load_default() default_font = ImageFont.load_default()
finally: finally:

View File

@ -8,7 +8,7 @@ import sys
import tempfile import tempfile
import warnings import warnings
from pathlib import Path from pathlib import Path
from typing import IO from typing import IO, Any
import pytest import pytest
@ -152,7 +152,7 @@ class TestImage:
def test_stringio(self) -> None: def test_stringio(self) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
with Image.open(io.StringIO()): with Image.open(io.StringIO()): # type: ignore[arg-type]
pass pass
def test_pathlib(self, tmp_path: Path) -> None: def test_pathlib(self, tmp_path: Path) -> None:
@ -175,11 +175,19 @@ class TestImage:
def test_fp_name(self, tmp_path: Path) -> None: def test_fp_name(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.jpg") temp_file = str(tmp_path / "temp.jpg")
class FP: class FP(io.BytesIO):
name: str name: str
def write(self, b: bytes) -> None: if sys.version_info >= (3, 12):
pass 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 = FP()
fp.name = temp_file fp.name = temp_file
@ -393,13 +401,13 @@ class TestImage:
# errors # errors
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, "invalid source") source.alpha_composite(over, "invalid destination") # type: ignore[arg-type]
with pytest.raises(ValueError): 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): with pytest.raises(ValueError):
source.alpha_composite(over, 0) source.alpha_composite(over, 0) # type: ignore[arg-type]
with pytest.raises(ValueError): 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): with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), (0, -1)) source.alpha_composite(over, (0, 0), (0, -1))
@ -909,6 +917,10 @@ class TestImage:
assert tag not in exif.get_ifd(0x8769) assert tag not in exif.get_ifd(0x8769)
assert exif.get_ifd(0xA005) 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))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None: def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size) im = Image.new("RGB", size)

View File

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

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
import pytest import pytest
from packaging.version import parse as parse_version 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)) im = hopper().resize((128, 100))
if TYPE_CHECKING:
import numpy.typing as npt
def test_toarray() -> None: def test_toarray() -> None:
def test(mode: str) -> tuple[tuple[int, ...], str, int]: def test(mode: str) -> tuple[tuple[int, ...], str, int]:
ai = numpy.array(im.convert(mode)) ai = numpy.array(im.convert(mode))
return ai.shape, ai.dtype.str, ai.nbytes 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) ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype assert ai.dtype == dtype

View File

@ -16,7 +16,9 @@ def draft_roundtrip(
im = Image.new(in_mode, in_size) im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG") data = tostring(im, "JPEG")
im = fromstring(data) 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 scale, _ = im.decoderconfig
assert box[:2] == (0, 0) assert box[:2] == (0, 0)
assert (im.width - scale) < box[2] <= im.width assert (im.width - scale) < box[2] <= im.width

View File

@ -137,7 +137,7 @@ def test_builtinfilter_p() -> None:
builtin_filter = ImageFilter.BuiltinFilter() builtin_filter = ImageFilter.BuiltinFilter()
with pytest.raises(ValueError): with pytest.raises(ValueError):
builtin_filter.filter(hopper("P")) builtin_filter.filter(hopper("P").im)
def test_kernel_not_enough_coefficients() -> None: def test_kernel_not_enough_coefficients() -> None:

View File

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

View File

@ -338,3 +338,8 @@ class TestImagingPaste:
im.copy().paste(im2) im.copy().paste(im2)
im.copy().paste(im2, (0, 0)) 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: def test_f_mode() -> None:
im = hopper("F") im = hopper("F")
with pytest.raises(ValueError): 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)), ("RGBA", (1, 2, 3, 4)),
("RGBAX", (1, 2, 3, 4, 0)), ("RGBAX", (1, 2, 3, 4, 0)),
("ARGB", (4, 1, 2, 3)),
), ),
) )
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: 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( @pytest.mark.parametrize(
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE) "method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
) )
def test_quantize_kmeans(method) -> None: def test_quantize_kmeans(method: Image.Quantize) -> None:
im = hopper() im = hopper()
no_kmeans = im.quantize(kmeans=0, method=method) no_kmeans = im.quantize(kmeans=0, method=method)
kmeans = im.quantize(kmeans=1, method=method) kmeans = im.quantize(kmeans=1, method=method)

View File

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

View File

@ -445,7 +445,7 @@ class TestCoreResampleBox:
im.resize((32, 32), resample, (20, 20, 100, 20)) im.resize((32, 32), resample, (20, 20, 100, 20))
with pytest.raises(TypeError, match="must be sequence of length 4"): 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"): with pytest.raises(ValueError, match="can't be negative"):
im.resize((32, 32), resample, (-20, 20, 100, 100)) im.resize((32, 32), resample, (-20, 20, 100, 100))

View File

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

View File

@ -16,7 +16,7 @@ from .helper import (
def test_sanity() -> None: def test_sanity() -> None:
im = hopper() im = hopper()
assert im.thumbnail((100, 100)) is None im.thumbnail((100, 100))
assert im.size == (100, 100) assert im.size == (100, 100)
@ -111,7 +111,9 @@ def test_load_first_unless_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft 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) result = draft(mode, size)
assert result is not None assert result is not None

View File

@ -103,7 +103,7 @@ def test_sanity() -> None:
def test_flags() -> 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(0) == ImageCms.Flags.NONE
assert ImageCms.Flags.GRIDPOINTS(256) == 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: for delta in nine_grid_deltas:
channel_data.paste( channel_data.paste(
channel_pattern, channel_pattern,
tuple( (
paste_offset[c] + delta[c] * channel_pattern.size[c] paste_offset[0] + delta[0] * channel_pattern.size[0],
for c in range(2) paste_offset[1] + delta[1] * channel_pattern.size[1],
), ),
) )
chans.append(channel_data) chans.append(channel_data)
@ -642,7 +642,8 @@ def test_auxiliary_channels_isolated() -> None:
# convert with and without AUX data, test colors are equal # convert with and without AUX data, test colors are equal
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1]) src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
source_profile = ImageCms.createProfile(src_colorSpace) 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] source_image = src_format[3]
test_transform = ImageCms.buildTransform( test_transform = ImageCms.buildTransform(
source_profile, source_profile,

View File

@ -448,6 +448,7 @@ def test_shape1() -> None:
x3, y3 = 95, 5 x3, y3 = 95, 5
# Act # Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline() s = ImageDraw.Outline()
s.move(x0, y0) s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3) s.curve(x1, y1, x2, y2, x3, y3)
@ -469,6 +470,7 @@ def test_shape2() -> None:
x3, y3 = 5, 95 x3, y3 = 5, 95
# Act # Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline() s = ImageDraw.Outline()
s.move(x0, y0) s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3) s.curve(x1, y1, x2, y2, x3, y3)
@ -487,6 +489,7 @@ def test_transform() -> None:
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline() s = ImageDraw.Outline()
s.line(0, 0) s.line(0, 0)
s.transform((0, 0, 0, 0, 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: def test_floodfill(bbox: Coords) -> None:
red = ImageColor.getrgb("red") 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 # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -1429,6 +1437,7 @@ def test_same_color_outline(bbox: Coords) -> None:
x2, y2 = 95, 50 x2, y2 = 95, 50
x3, y3 = 95, 5 x3, y3 = 95, 5
assert ImageDraw.Outline is not None
s = ImageDraw.Outline() s = ImageDraw.Outline()
s.move(x0, y0) s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3) s.curve(x1, y1, x2, y2, x3, y3)
@ -1467,7 +1476,7 @@ def test_same_color_outline(bbox: Coords) -> None:
(4, "square", {}), (4, "square", {}),
(8, "regular_octagon", {}), (8, "regular_octagon", {}),
(4, "square_rotate_45", {"rotation": 45}), (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( def test_draw_regular_polygon(
@ -1477,7 +1486,10 @@ def test_draw_regular_polygon(
filename = f"Tests/images/imagedraw_{polygon_name}.png" filename = f"Tests/images/imagedraw_{polygon_name}.png"
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
bounding_circle = ((W // 2, H // 2), 25) 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) 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( def test_compute_regular_polygon_vertices_input_error_handling(
n_sides, bounding_circle, rotation, expected_error, error_message n_sides: int,
bounding_circle: int | tuple[int | tuple[int] | str, ...],
rotation: int | str,
expected_error: type[Exception],
error_message: str,
) -> None: ) -> None:
with pytest.raises(expected_error) as e: with pytest.raises(expected_error) as e:
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
assert str(e.value) == error_message 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) draw.rectangle(xy)
with pytest.raises(ValueError): with pytest.raises(ValueError):
draw.rounded_rectangle(xy) 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) pen = ImageDraw2.Pen("blue", width=7)
draw.line(list(range(10)), pen) 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) 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) @pytest.mark.parametrize("bbox", BBOX)

View File

@ -209,7 +209,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args) super().__init__(mode, *args)
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
# eof # eof
return -1, 0 return -1, 0
@ -222,7 +222,7 @@ class MockPyEncoder(ImageFile.PyEncoder):
super().__init__(mode, *args) super().__init__(mode, *args)
def encode(self, buffer): def encode(self, bufsize: int) -> tuple[int, int, bytes]:
return 1, 1, b"" return 1, 1, b""
def cleanup(self) -> None: def cleanup(self) -> None:
@ -351,7 +351,9 @@ class TestPyEncoder(CodecsTest):
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] 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): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
@ -381,7 +383,7 @@ class TestPyEncoder(CodecsTest):
def test_encode(self) -> None: def test_encode(self) -> None:
encoder = ImageFile.PyEncoder(None) encoder = ImageFile.PyEncoder(None)
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode(None) encoder.encode(0)
bytes_consumed, errcode = encoder.encode_to_pyfd() bytes_consumed, errcode = encoder.encode_to_pyfd()
assert bytes_consumed == 0 assert bytes_consumed == 0

View File

@ -209,7 +209,7 @@ def test_getlength(
assert length == length_raqm assert length == length_raqm
def test_float_size() -> None: def test_float_size(layout_engine: ImageFont.Layout) -> None:
lengths = [] lengths = []
for size in (48, 48.5, 49): for size in (48, 48.5, 49):
f = ImageFont.truetype( f = ImageFont.truetype(
@ -224,7 +224,7 @@ def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
line_spacing = font.getbbox("A")[3] + 4 line_spacing = font.getbbox("A")[3] + 4
lines = TEST_TEXT.split("\n") lines = TEST_TEXT.split("\n")
y = 0 y: float = 0
for line in lines: for line in lines:
draw.text((0, y), line, font=font) draw.text((0, y), line, font=font)
y += line_spacing y += line_spacing
@ -494,8 +494,8 @@ def test_default_font() -> None:
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png") assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
@pytest.mark.parametrize("mode", (None, "1", "RGBA")) @pytest.mark.parametrize("mode", ("", "1", "RGBA"))
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
assert (0, 4, 12, 16) == font.getbbox("A", mode) assert (0, 4, 12, 16) == font.getbbox("A", mode)
@ -548,7 +548,7 @@ def test_find_font(
def loadable_font( def loadable_font(
filepath: str, size: int, index: int, encoding: str, *args: Any filepath: str, size: int, index: int, encoding: str, *args: Any
): ) -> ImageFont.FreeTypeFont:
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont") _freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
if filepath == path_to_fake: if filepath == path_to_fake:
return _freeTypeFont(FONT_PATH, size, index, encoding, *args) return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
@ -564,6 +564,7 @@ def test_find_font(
# catching syntax like errors # catching syntax like errors
monkeypatch.setattr(sys, "platform", platform) monkeypatch.setattr(sys, "platform", platform)
if platform == "linux": if platform == "linux":
monkeypatch.setenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: 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) 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( @pytest.mark.parametrize(
"test_file", "test_file",
[ [

View File

@ -14,7 +14,7 @@ original_core = ImageFont.core
def setup_module() -> None: def setup_module() -> None:
if features.check_module("freetype2"): if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError) ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
def teardown_module() -> None: def teardown_module() -> None:
@ -76,3 +76,8 @@ def test_oom() -> None:
font = ImageFont.ImageFont() font = ImageFont.ImageFont()
font._load_pilfont_data(fp, Image.new("L", (1, 1))) font._load_pilfont_data(fp, Image.new("L", (1, 1)))
font.getmask("A" * 1_000_000) 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() p.communicate()
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert isinstance(im, list)
assert len(im) == 1 assert len(im) == 1
assert os.path.samefile(im[0], "Tests/images/hopper.gif") assert os.path.samefile(im[0], "Tests/images/hopper.gif")
@ -105,6 +106,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
p.communicate() p.communicate()
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, "Tests/images/hopper.png") assert_image_equal_tofile(im, "Tests/images/hopper.png")
@pytest.mark.skipif( @pytest.mark.skipif(
@ -120,6 +122,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
with open(image_path, "rb") as fp: with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp) subprocess.call(["wl-copy"], stdin=fp)
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, image_path) assert_image_equal_tofile(im, image_path)
@pytest.mark.skipif( @pytest.mark.skipif(

View File

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

View File

@ -58,7 +58,6 @@ def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None:
blur = ImageFilter.GaussianBlur blur = ImageFilter.GaussianBlur
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.convert("1").filter(blur) im.convert("1").filter(blur)
blur(im.convert("L"))
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.convert("I").filter(blur) im.convert("I").filter(blur)
with pytest.raises(ValueError): 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 assert i.im.getpixel((x, y))[c] >= 250
# Fuzzy match. # Fuzzy match.
def gp(x, y): def gp(x: int, y: int) -> tuple[int, ...]:
return i.im.getpixel((x, y)) return i.im.getpixel((x, y))
assert 236 <= gp(7, 4)[0] <= 239 assert 236 <= gp(7, 4)[0] <= 239

View File

@ -45,7 +45,7 @@ def test_getcolor() -> None:
# Test unknown color specifier # Test unknown color specifier
with pytest.raises(ValueError): with pytest.raises(ValueError):
palette.getcolor("unknown") palette.getcolor("unknown") # type: ignore[arg-type]
def test_getcolor_rgba_color_rgb_palette() -> None: def test_getcolor_rgba_color_rgb_palette() -> None:
@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None:
palette.save(f) palette.save(f)
p = ImagePalette.load(f) lut = ImagePalette.load(f)
# load returns raw palette information # load returns raw palette information
assert len(p[0]) == 768 assert len(lut[0]) == 768
assert p[1] == "RGB" assert lut[1] == "RGB"
p = ImagePalette.raw(p[1], p[0]) p = ImagePalette.raw(lut[1], lut[0])
assert isinstance(p, ImagePalette.ImagePalette) assert isinstance(p, ImagePalette.ImagePalette)
assert p.palette == palette.tobytes() assert p.palette == palette.tobytes()

View File

@ -41,13 +41,8 @@ def test_rgb() -> None:
checkrgb(0, 0, 255) checkrgb(0, 0, 255)
def test_image() -> None: @pytest.mark.parametrize("mode", ("1", "RGB", "RGBA", "L", "P", "I;16"))
modes = ["1", "RGB", "RGBA", "L", "P"] def test_image(mode: str) -> None:
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) im = hopper(mode)
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im)) roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
if mode not in ("RGB", "RGBA"): if mode not in ("RGB", "RGBA"):

View File

@ -45,21 +45,22 @@ if is_win32():
memcpy = ctypes.cdll.msvcrt.memcpy memcpy = ctypes.cdll.msvcrt.memcpy
memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] 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.argtypes = [ctypes.wintypes.HDC]
CreateCompatibleDC.restype = ctypes.wintypes.HDC CreateCompatibleDC.restype = ctypes.wintypes.HDC
DeleteDC = ctypes.windll.gdi32.DeleteDC DeleteDC = windll.gdi32.DeleteDC
DeleteDC.argtypes = [ctypes.wintypes.HDC] DeleteDC.argtypes = [ctypes.wintypes.HDC]
SelectObject = ctypes.windll.gdi32.SelectObject SelectObject = windll.gdi32.SelectObject
SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ] SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ]
SelectObject.restype = ctypes.wintypes.HGDIOBJ SelectObject.restype = ctypes.wintypes.HGDIOBJ
DeleteObject = ctypes.windll.gdi32.DeleteObject DeleteObject = windll.gdi32.DeleteObject
DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ] DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ]
CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection CreateDIBSection = windll.gdi32.CreateDIBSection
CreateDIBSection.argtypes = [ CreateDIBSection.argtypes = [
ctypes.wintypes.HDC, ctypes.wintypes.HDC,
ctypes.c_void_p, ctypes.c_void_p,
@ -70,7 +71,7 @@ if is_win32():
] ]
CreateDIBSection.restype = ctypes.wintypes.HBITMAP CreateDIBSection.restype = ctypes.wintypes.HBITMAP
def serialize_dib(bi, pixels) -> bytearray: def serialize_dib(bi: BITMAPINFOHEADER, pixels: ctypes.c_void_p) -> bytearray:
bf = BITMAPFILEHEADER() bf = BITMAPFILEHEADER()
bf.bfType = 0x4D42 bf.bfType = 0x4D42
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize

View File

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

View File

@ -1,20 +1,25 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from typing import TYPE_CHECKING
import pytest import pytest
from PIL import Image from PIL import Image, _typing
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
if TYPE_CHECKING:
import numpy
import numpy.typing as npt
else:
numpy = pytest.importorskip("numpy", reason="NumPy not installed") numpy = pytest.importorskip("numpy", reason="NumPy not installed")
TEST_IMAGE_SIZE = (10, 10) TEST_IMAGE_SIZE = (10, 10)
def test_numpy_to_image() -> None: def test_numpy_to_image() -> None:
def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
if bands == 1: if bands == 1:
if boolean: if boolean:
data = [0, 255] * 50 data = [0, 255] * 50
@ -99,14 +104,14 @@ def test_1d_array() -> None:
assert_image(Image.fromarray(a), "L", (1, 5)) assert_image(Image.fromarray(a), "L", (1, 5))
def _test_img_equals_nparray(img: Image.Image, np) -> None: def _test_img_equals_nparray(img: Image.Image, np_img: _typing.NumpyArray) -> None:
assert len(np.shape) >= 2 assert len(np_img.shape) >= 2
np_size = np.shape[1], np.shape[0] np_size = np_img.shape[1], np_img.shape[0]
assert img.size == np_size assert img.size == np_size
px = img.load() px = img.load()
for x in range(0, img.size[0], int(img.size[0] / 10)): for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)): for y in range(0, img.size[1], int(img.size[1] / 10)):
assert_deep_equal(px[x, y], np[y, x]) assert_deep_equal(px[x, y], np_img[y, x])
def test_16bit() -> None: def test_16bit() -> None:
@ -157,7 +162,7 @@ def test_save_tiff_uint16() -> None:
("HSV", numpy.uint8), ("HSV", numpy.uint8),
), ),
) )
def test_to_array(mode: str, dtype) -> None: def test_to_array(mode: str, dtype: npt.DTypeLike) -> None:
img = hopper(mode) img = hopper(mode)
# Resize to non-square # Resize to non-square
@ -207,7 +212,7 @@ def test_putdata() -> None:
numpy.float64, numpy.float64,
), ),
) )
def test_roundtrip_eye(dtype) -> None: def test_roundtrip_eye(dtype: npt.DTypeLike) -> None:
arr = numpy.eye(10, dtype=dtype) arr = numpy.eye(10, dtype=dtype)
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))

View File

@ -54,16 +54,12 @@ def test_stdout(buffer: bool) -> None:
# Temporarily redirect stdout # Temporarily redirect stdout
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer:
class MyStdOut: class MyStdOut:
buffer = BytesIO() buffer = BytesIO()
mystdout = MyStdOut() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
else:
mystdout = BytesIO()
sys.stdout = mystdout sys.stdout = mystdout # type: ignore[assignment]
ps = PSDraw.PSDraw() ps = PSDraw.PSDraw()
_create_document(ps) _create_document(ps)
@ -71,6 +67,6 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if buffer: if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
assert mystdout.getvalue() != b"" 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") @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_sanity(tmp_path: Path) -> None: def test_sanity(tmp_path: Path) -> None:
# Segfault test # Segfault test
app = QApplication([]) app: QApplication | None = QApplication([])
ex = Example() ex = Example()
assert app # Silence warning assert app # Silence warning
assert ex # Silence warning assert ex # Silence warning

View File

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

View File

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

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install webp # 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 ./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 BUILDDIR = _build
# Internal variables. # Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_a4 = --define latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter PAPEROPT_letter = --define latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . ALLSPHINXOPTS = --doctree-dir $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others # the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
@ -51,42 +51,42 @@ install-sphinx:
.PHONY: html .PHONY: html
html: html:
$(MAKE) install-sphinx $(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
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml .PHONY: dirhtml
dirhtml: dirhtml:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml $(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo @echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml .PHONY: singlehtml
singlehtml: singlehtml:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml $(SPHINXBUILD) --builder singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo @echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle .PHONY: pickle
pickle: pickle:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle $(SPHINXBUILD) --builder pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo @echo
@echo "Build finished; now you can process the pickle files." @echo "Build finished; now you can process the pickle files."
.PHONY: json .PHONY: json
json: json:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json $(SPHINXBUILD) --builder json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo @echo
@echo "Build finished; now you can process the JSON files." @echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp .PHONY: htmlhelp
htmlhelp: htmlhelp:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp $(SPHINXBUILD) --builder htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo @echo
@echo "Build finished; now you can run HTML Help Workshop with the" \ @echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp." ".hhp project file in $(BUILDDIR)/htmlhelp."
@ -94,7 +94,7 @@ htmlhelp:
.PHONY: qthelp .PHONY: qthelp
qthelp: qthelp:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp $(SPHINXBUILD) --builder qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo @echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:" ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@ -105,7 +105,7 @@ qthelp:
.PHONY: devhelp .PHONY: devhelp
devhelp: devhelp:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp $(SPHINXBUILD) --builder devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo @echo
@echo "Build finished." @echo "Build finished."
@echo "To view the help file:" @echo "To view the help file:"
@ -116,14 +116,14 @@ devhelp:
.PHONY: epub .PHONY: epub
epub: epub:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub $(SPHINXBUILD) --builder epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo @echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub." @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: latex .PHONY: latex
latex: latex:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo @echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \ @echo "Run \`make' in that directory to run these through (pdf)latex" \
@ -132,7 +132,7 @@ latex:
.PHONY: latexpdf .PHONY: latexpdf
latexpdf: latexpdf:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..." @echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf $(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
@ -140,21 +140,21 @@ latexpdf:
.PHONY: text .PHONY: text
text: text:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text $(SPHINXBUILD) --builder text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo @echo
@echo "Build finished. The text files are in $(BUILDDIR)/text." @echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man .PHONY: man
man: man:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man $(SPHINXBUILD) --builder man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo @echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man." @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo .PHONY: texinfo
texinfo: texinfo:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo @echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \ @echo "Run \`make' in that directory to run these through makeinfo" \
@ -163,7 +163,7 @@ texinfo:
.PHONY: info .PHONY: info
info: info:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..." @echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
@ -171,21 +171,21 @@ info:
.PHONY: gettext .PHONY: gettext
gettext: gettext:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale $(SPHINXBUILD) --builder gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo @echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes .PHONY: changes
changes: changes:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes $(SPHINXBUILD) --builder changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo @echo
@echo "The overview file is in $(BUILDDIR)/changes." @echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck .PHONY: linkcheck
linkcheck: linkcheck:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto $(SPHINXBUILD) --builder linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@echo @echo
@echo "Link check complete; look for any errors in the above output " \ @echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt." "or in $(BUILDDIR)/linkcheck/output.txt."
@ -193,7 +193,7 @@ linkcheck:
.PHONY: doctest .PHONY: doctest
doctest: doctest:
$(MAKE) install-sphinx $(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 " \ @echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt." "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. Support for LibTIFF earlier than version 4 has been deprecated.
Upgrade to a newer version of LibTIFF instead. 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 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 of the text, while the new ``bbox`` methods distinguish this as a ``top``
offset. 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. :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 :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) d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
if y != 0: if y != 0:
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3) 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() 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 .. py:currentmodule:: PIL.Image
.. data:: Resampling.NEAREST .. data:: Resampling.NEAREST
:noindex:
Pick one nearest pixel from the input image. Ignore all other input pixels. Pick one nearest pixel from the input image. Ignore all other input pixels.
.. data:: Resampling.BOX .. data:: Resampling.BOX
:noindex:
Each pixel of source image contributes to one pixel of the Each pixel of source image contributes to one pixel of the
destination image with identical weights. destination image with identical weights.
@ -158,6 +160,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
.. versionadded:: 3.4.0 .. versionadded:: 3.4.0
.. data:: Resampling.BILINEAR .. data:: Resampling.BILINEAR
:noindex:
For resize calculate the output pixel value using linear interpolation For resize calculate the output pixel value using linear interpolation
on all pixels that may contribute to the output value. 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. in the input image is used.
.. data:: Resampling.HAMMING .. data:: Resampling.HAMMING
:noindex:
Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have
dislocations on local level like with :data:`Resampling.BOX`. 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 .. versionadded:: 3.4.0
.. data:: Resampling.BICUBIC .. data:: Resampling.BICUBIC
:noindex:
For resize calculate the output pixel value using cubic interpolation For resize calculate the output pixel value using cubic interpolation
on all pixels that may contribute to the output value. 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. in the input image is used.
.. data:: Resampling.LANCZOS .. data:: Resampling.LANCZOS
:noindex:
Calculate the output pixel value using a high-quality Lanczos filter (a Calculate the output pixel value using a high-quality Lanczos filter (a
truncated sinc) on all pixels that may contribute to the output value. 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 .. comment: Image generated with ../example/anchors.py
.. image:: ../example/anchors.png .. image:: ../example/anchors.webp
:alt: Text anchor examples :alt: Text anchor examples
:align: center :align: center

View File

@ -278,26 +278,26 @@ choose to resize relative to a given size.
from PIL import Image, ImageOps from PIL import Image, ImageOps
size = (100, 150) size = (100, 150)
with Image.open("Tests/images/hopper.png") as im: with Image.open("Tests/images/hopper.webp") as im:
ImageOps.contain(im, size).save("imageops_contain.png") ImageOps.contain(im, size).save("imageops_contain.webp")
ImageOps.cover(im, size).save("imageops_cover.png") ImageOps.cover(im, size).save("imageops_cover.webp")
ImageOps.fit(im, size).save("imageops_fit.png") ImageOps.fit(im, size).save("imageops_fit.webp")
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png") ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
# thumbnail() can also be used, # thumbnail() can also be used,
# but will modify the image object in place # but will modify the image object in place
im.thumbnail(size) 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` | | | :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)`` | |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 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`` | |Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ +----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
.. _color-transforms: .. _color-transforms:

View File

@ -68,7 +68,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **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 * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.

View File

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

View File

@ -691,23 +691,7 @@ Methods
:param hints: An optional list of hints. :param hints: An optional list of hints.
:returns: A (drawing context, drawing resource factory) tuple. :returns: A (drawing context, drawing resource factory) tuple.
.. py:method:: floodfill(image, xy, value, border=None, thresh=0) .. autofunction:: PIL.ImageDraw.floodfill
.. 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.
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/ .. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist .. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist

View File

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

View File

@ -36,26 +36,26 @@ Resize relative to a given size
from PIL import Image, ImageOps from PIL import Image, ImageOps
size = (100, 150) size = (100, 150)
with Image.open("Tests/images/hopper.png") as im: with Image.open("Tests/images/hopper.webp") as im:
ImageOps.contain(im, size).save("imageops_contain.png") ImageOps.contain(im, size).save("imageops_contain.webp")
ImageOps.cover(im, size).save("imageops_cover.png") ImageOps.cover(im, size).save("imageops_cover.webp")
ImageOps.fit(im, size).save("imageops_fit.png") ImageOps.fit(im, size).save("imageops_fit.webp")
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png") ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
# thumbnail() can also be used, # thumbnail() can also be used,
# but will modify the image object in place # but will modify the image object in place
im.thumbnail(size) 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` | | | :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)`` | |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 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`` | |Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+ +----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
.. autofunction:: contain .. autofunction:: contain
.. autofunction:: cover .. autofunction:: cover

View File

@ -44,42 +44,23 @@ Access using negative indexes is also possible. ::
----------------------------- -----------------------------
.. class:: PixelAccess .. 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 Returns the pixel at x,y. The pixel is returned as a single
value for single band images or a tuple for multiple band value for single band images or a tuple for multi-band images.
images
:param xy: The pixel coordinate, given as (x, y). :param xy: The pixel coordinate, given as (x, y).
:returns: a pixel value for single band images, a tuple of :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 Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for numerical value for single band images, and a tuple for
multi-band images. In addition to this, RGB and RGBA tuples multi-band images.
are accepted for P and PA images.
:param xy: The pixel coordinate, given as (x, y). :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) :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.

View File

@ -44,3 +44,4 @@ Access using negative indexes is also possible. ::
.. autoclass:: PIL.PyAccess.PyAccess() .. autoclass:: PIL.PyAccess.PyAccess()
:members: :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 Provides a convenient way to import type hints that are not available
on some Python versions. on some Python versions.
.. py:class:: NumpyArray
Typing alias.
.. py:class:: StrOrBytesPath .. py:class:: StrOrBytesPath
Typing alias. Typing alias.

View File

@ -34,6 +34,11 @@ Support for LibTIFF earlier than 4
Support for LibTIFF earlier than version 4 has been deprecated. Support for LibTIFF earlier than version 4 has been deprecated.
Upgrade to a newer version of LibTIFF instead. 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 API Changes
=========== ===========

View File

@ -18,9 +18,9 @@ is not secure.
- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve - :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. 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 - ``getxmp()`` was added to :py:class:`~PIL.JpegImagePlugin.JpegImageFile` in Pillow
will now use ``defusedxml`` instead. If the dependency is not present, an empty 8.2.0. It will now use ``defusedxml`` instead. If the dependency is not present, an
dictionary will be returned and a warning raised. empty dictionary will be returned and a warning raised.
Deprecations 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 of the text, while the new ``bbox`` methods distinguish this as a ``top``
offset. 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. :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 :align: center

View File

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

View File

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

View File

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

View File

@ -25,6 +25,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
from typing import IO
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16 from ._binary import i16le as i16
@ -52,7 +53,7 @@ def _accept(prefix: bytes) -> bool:
return prefix[:2] == b"BM" 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] return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
@ -300,7 +301,8 @@ class BmpImageFile(ImageFile.ImageFile):
class BmpRleDecoder(ImageFile.PyDecoder): class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True _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] rle4 = self.args[1]
data = bytearray() data = bytearray()
x = 0 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) _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: try:
rawmode, bits, colors = SAVE[im.mode] rawmode, bits, colors = SAVE[im.mode]
except KeyError as e: except KeyError as e:

View File

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

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