Merge branch 'main' into exififd
|
@ -32,10 +32,10 @@ install:
|
|||
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||
- 7z x pillow-test-images.zip -oc:\
|
||||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
|
||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
|
||||
- 7z x nasm-win64.zip -oc:\
|
||||
- choco install ghostscript --version=10.3.0
|
||||
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
||||
- choco install ghostscript --version=10.3.1
|
||||
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.18.1
|
||||
cibuildwheel==2.19.1
|
||||
|
|
6
.github/workflows/macos-install.sh
vendored
|
@ -7,11 +7,15 @@ brew install \
|
|||
ghostscript \
|
||||
libimagequant \
|
||||
libjpeg \
|
||||
libraqm \
|
||||
libtiff \
|
||||
little-cms2 \
|
||||
openjpeg \
|
||||
webp
|
||||
if [[ "$ImageOS" == "macos13" ]]; then
|
||||
brew install --ignore-dependencies libraqm
|
||||
else
|
||||
brew install libraqm
|
||||
fi
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
|
||||
# TODO Update condition when cffi supports 3.13
|
||||
|
|
2
.github/workflows/test-windows.yml
vendored
|
@ -86,7 +86,7 @@ jobs:
|
|||
choco install nasm --no-progress
|
||||
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||
|
||||
choco install ghostscript --version=10.3.0 --no-progress
|
||||
choco install ghostscript --version=10.3.1 --no-progress
|
||||
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
|
||||
|
||||
# Install extra test images
|
||||
|
|
10
.github/workflows/wheels-dependencies.sh
vendored
|
@ -16,9 +16,9 @@ ARCHIVE_SDIR=pillow-depends-main
|
|||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.2
|
||||
HARFBUZZ_VERSION=8.4.0
|
||||
HARFBUZZ_VERSION=8.5.0
|
||||
LIBPNG_VERSION=1.6.43
|
||||
JPEGTURBO_VERSION=3.0.2
|
||||
JPEGTURBO_VERSION=3.0.3
|
||||
OPENJPEG_VERSION=2.5.2
|
||||
XZ_VERSION=5.4.5
|
||||
TIFF_VERSION=4.6.0
|
||||
|
@ -33,9 +33,9 @@ if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
|
|||
else
|
||||
ZLIB_VERSION=1.2.8
|
||||
fi
|
||||
LIBWEBP_VERSION=1.3.2
|
||||
LIBWEBP_VERSION=1.4.0
|
||||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.16.1
|
||||
LIBXCB_VERSION=1.17.0
|
||||
BROTLI_VERSION=1.1.0
|
||||
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
||||
|
@ -70,7 +70,7 @@ function build {
|
|||
fi
|
||||
build_new_zlib
|
||||
|
||||
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
|
||||
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
||||
|
|
|
@ -3,7 +3,7 @@ version: 2
|
|||
formats: [pdf]
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-lts-latest
|
||||
tools:
|
||||
python: "3"
|
||||
jobs:
|
||||
|
|
30
CHANGES.rst
|
@ -5,6 +5,36 @@ Changelog (Pillow)
|
|||
10.4.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
- Read IM and TIFF images as RGB, rather than RGBX #7997
|
||||
[radarhere]
|
||||
|
||||
- Only preserve TIFF IPTC_NAA_CHUNK tag if type is BYTE or UNDEFINED #7948
|
||||
[radarhere]
|
||||
|
||||
- Clarify ImageDraw2 error message when size is missing #8165
|
||||
[radarhere]
|
||||
|
||||
- Support unpacking more rawmodes to RGBA palettes #7966
|
||||
[radarhere]
|
||||
|
||||
- Removed support for Qt 5 #8159
|
||||
[radarhere]
|
||||
|
||||
- Improve ``ImageFont.freetype`` support for XDG directories on Linux #8135
|
||||
[mamg22, radarhere]
|
||||
|
||||
- Improved consistency of XMP handling #8069
|
||||
[radarhere]
|
||||
|
||||
- Use pkg-config to help find libwebp and raqm #8142
|
||||
[radarhere]
|
||||
|
||||
- Accept 't' suffix for libtiff version #8126, #8129
|
||||
[radarhere]
|
||||
|
||||
- Deprecate ImageDraw.getdraw hints parameter #8124
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Added ImageDraw circle() #8085
|
||||
[void4, hugovk, radarhere]
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ def test_direct() -> None:
|
|||
caccess = im.im.pixel_access(False)
|
||||
access = PyAccess.new(im, False)
|
||||
|
||||
assert access is not None
|
||||
assert caccess[(0, 0)] == access[(0, 0)]
|
||||
|
||||
print(f"Size: {im.width}x{im.height}")
|
||||
|
|
|
@ -18,7 +18,7 @@ from typing import Any, Callable, Sequence
|
|||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image, ImageMath, features
|
||||
from PIL import Image, ImageFile, ImageMath, features
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -240,7 +240,7 @@ class PillowLeakTestCase:
|
|||
# helpers
|
||||
|
||||
|
||||
def fromstring(data: bytes) -> Image.Image:
|
||||
def fromstring(data: bytes) -> ImageFile.ImageFile:
|
||||
return Image.open(BytesIO(data))
|
||||
|
||||
|
||||
|
|
|
@ -354,10 +354,10 @@ class TestColorLut3DCoreAPI:
|
|||
class TestColorLut3DFilter:
|
||||
def test_wrong_args(self) -> None:
|
||||
with pytest.raises(ValueError, match="should be either an integer"):
|
||||
ImageFilter.Color3DLUT("small", [1])
|
||||
ImageFilter.Color3DLUT("small", [1]) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError, match="should be either an integer"):
|
||||
ImageFilter.Color3DLUT((11, 11), [1])
|
||||
ImageFilter.Color3DLUT((11, 11), [1]) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
|
||||
ImageFilter.Color3DLUT((11, 11, 1), [1])
|
||||
|
|
|
@ -12,7 +12,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
|
|||
|
||||
|
||||
class TestDecompressionBomb:
|
||||
def teardown_method(self, method) -> None:
|
||||
def teardown_method(self) -> None:
|
||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||
|
||||
def test_no_warning_small_file(self) -> None:
|
||||
|
|
|
@ -38,7 +38,9 @@ def test_version() -> None:
|
|||
assert function(name) == version
|
||||
if name != "PIL":
|
||||
if name == "zlib" and version is not None:
|
||||
version = version.replace(".zlib-ng", "")
|
||||
version = re.sub(".zlib-ng$", "", version)
|
||||
elif name == "libtiff" and version is not None:
|
||||
version = re.sub("t$", "", version)
|
||||
assert version is None or re.search(r"\d+(\.\d+)*$", version)
|
||||
|
||||
for module in features.modules:
|
||||
|
@ -124,7 +126,7 @@ def test_unsupported_module() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("supported_formats", (True, False))
|
||||
def test_pilinfo(supported_formats) -> None:
|
||||
def test_pilinfo(supported_formats: bool) -> None:
|
||||
buf = io.StringIO()
|
||||
features.pilinfo(buf, supported_formats=supported_formats)
|
||||
out = buf.getvalue()
|
||||
|
|
|
@ -140,7 +140,7 @@ def test_load_dib() -> None:
|
|||
(124, "g/pal8v5.bmp"),
|
||||
),
|
||||
)
|
||||
def test_dib_header_size(header_size, path):
|
||||
def test_dib_header_size(header_size: int, path: str) -> None:
|
||||
image_path = "Tests/images/bmp/" + path
|
||||
with open(image_path, "rb") as fp:
|
||||
data = fp.read()[14:]
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import BufrStubImagePlugin, Image
|
||||
from PIL import BufrStubImagePlugin, Image, ImageFile
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
|
@ -50,30 +51,33 @@ def test_save(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_handler(tmp_path: Path) -> None:
|
||||
class TestHandler:
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
opened = False
|
||||
loaded = False
|
||||
saved = False
|
||||
|
||||
def open(self, im) -> None:
|
||||
def open(self, im: ImageFile.StubImageFile) -> None:
|
||||
self.opened = True
|
||||
|
||||
def load(self, im):
|
||||
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
|
||||
self.loaded = True
|
||||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def save(self, im, fp, filename) -> None:
|
||||
def is_loaded(self) -> bool:
|
||||
return self.loaded
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
handler = TestHandler()
|
||||
BufrStubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
assert not handler.is_loaded()
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.bufr")
|
||||
im.save(temp_file)
|
||||
|
|
|
@ -53,6 +53,7 @@ def test_closed_file() -> None:
|
|||
|
||||
def test_seek_after_close() -> None:
|
||||
im = Image.open("Tests/images/iss634.gif")
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
im.load()
|
||||
im.close()
|
||||
|
||||
|
@ -377,7 +378,8 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
|
|||
img = img.convert("RGB")
|
||||
|
||||
tempfile = str(tmp_path / "temp.gif")
|
||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
||||
b = BytesIO()
|
||||
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||
with Image.open(tempfile) as reloaded:
|
||||
assert_image_similar(img, reloaded.convert("RGB"), 0)
|
||||
|
||||
|
@ -388,7 +390,8 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
|
|||
img = img.convert("L")
|
||||
|
||||
tempfile = str(tmp_path / "temp.gif")
|
||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
||||
b = BytesIO()
|
||||
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||
with Image.open(tempfile) as reloaded:
|
||||
assert_image_similar(img, reloaded.convert("L"), 0)
|
||||
|
||||
|
@ -648,7 +651,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
|
|||
assert rgb_img.getpixel((50, 50)) == circle
|
||||
|
||||
# Check that frame transparency wasn't added unnecessarily
|
||||
assert img._frame_transparency is None
|
||||
assert getattr(img, "_frame_transparency") is None
|
||||
|
||||
|
||||
def test_dispose2_diff(tmp_path: Path) -> None:
|
||||
|
|
|
@ -5,7 +5,7 @@ from typing import IO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import GribStubImagePlugin, Image
|
||||
from PIL import GribStubImagePlugin, Image, ImageFile
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
|
@ -51,7 +51,7 @@ def test_save(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_handler(tmp_path: Path) -> None:
|
||||
class TestHandler:
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
opened = False
|
||||
loaded = False
|
||||
saved = False
|
||||
|
@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
return self.loaded
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
|
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
|
|||
GribStubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
assert not handler.is_loaded()
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.grib")
|
||||
im.save(temp_file)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Hdf5StubImagePlugin, Image
|
||||
from PIL import Hdf5StubImagePlugin, Image, ImageFile
|
||||
|
||||
TEST_FILE = "Tests/images/hdf5.h5"
|
||||
|
||||
|
@ -41,7 +42,7 @@ def test_load() -> None:
|
|||
def test_save() -> None:
|
||||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
dummy_fp = None
|
||||
dummy_fp = BytesIO()
|
||||
dummy_filename = "dummy.filename"
|
||||
|
||||
# Act / Assert: stub cannot save without an implemented handler
|
||||
|
@ -52,7 +53,7 @@ def test_save() -> None:
|
|||
|
||||
|
||||
def test_handler(tmp_path: Path) -> None:
|
||||
class TestHandler:
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
opened = False
|
||||
loaded = False
|
||||
saved = False
|
||||
|
@ -65,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
return self.loaded
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
|
@ -72,10 +76,10 @@ def test_handler(tmp_path: Path) -> None:
|
|||
Hdf5StubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
assert not handler.is_loaded()
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.h5")
|
||||
im.save(temp_file)
|
||||
|
|
|
@ -171,7 +171,7 @@ class TestFileJpeg:
|
|||
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
|
||||
)
|
||||
def test_dpi(self, test_image_path: str) -> None:
|
||||
def test(xdpi: int, ydpi: int | None = None):
|
||||
def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None:
|
||||
with Image.open(test_image_path) as im:
|
||||
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
|
||||
return im.info.get("dpi")
|
||||
|
@ -443,7 +443,9 @@ class TestFileJpeg:
|
|||
assert_image(im1, im2.mode, im2.size)
|
||||
|
||||
def test_subsampling(self) -> None:
|
||||
def getsampling(im: JpegImagePlugin.JpegImageFile):
|
||||
def getsampling(
|
||||
im: JpegImagePlugin.JpegImageFile,
|
||||
) -> tuple[int, int, int, int, int, int]:
|
||||
layer = im.layer
|
||||
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
|
||||
|
||||
|
@ -699,7 +701,7 @@ class TestFileJpeg:
|
|||
def test_save_cjpeg(self, tmp_path: Path) -> None:
|
||||
with Image.open(TEST_FILE) as img:
|
||||
tempfile = str(tmp_path / "temp.jpg")
|
||||
JpegImagePlugin._save_cjpeg(img, 0, tempfile)
|
||||
JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
|
||||
# Default save quality is 75%, so a tiny bit of difference is alright
|
||||
assert_image_similar_tofile(img, tempfile, 17)
|
||||
|
||||
|
@ -917,24 +919,25 @@ class TestFileJpeg:
|
|||
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
|
||||
assert im.info["icc_profile"] == b"profile"
|
||||
|
||||
def test_jpeg_magic_number(self) -> None:
|
||||
def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
size = 4097
|
||||
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
|
||||
buffer.max_pos = 0
|
||||
max_pos = 0
|
||||
orig_read = buffer.read
|
||||
|
||||
def read(n=-1):
|
||||
def read(n: int | None = -1) -> bytes:
|
||||
nonlocal max_pos
|
||||
res = orig_read(n)
|
||||
buffer.max_pos = max(buffer.max_pos, buffer.tell())
|
||||
max_pos = max(max_pos, buffer.tell())
|
||||
return res
|
||||
|
||||
buffer.read = read
|
||||
monkeypatch.setattr(buffer, "read", read)
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open(buffer):
|
||||
pass
|
||||
|
||||
# Assert the entire file has not been read
|
||||
assert 0 < buffer.max_pos < size
|
||||
assert 0 < max_pos < size
|
||||
|
||||
def test_getxmp(self) -> None:
|
||||
with Image.open("Tests/images/xmp_test.jpg") as im:
|
||||
|
@ -945,6 +948,7 @@ class TestFileJpeg:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
xmp = im.getxmp()
|
||||
|
||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||
|
@ -1029,8 +1033,10 @@ class TestFileJpeg:
|
|||
|
||||
def test_repr_jpeg(self) -> None:
|
||||
im = hopper()
|
||||
b = im._repr_jpeg_()
|
||||
assert b is not None
|
||||
|
||||
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
|
||||
with Image.open(BytesIO(b)) as repr_jpeg:
|
||||
assert repr_jpeg.format == "JPEG"
|
||||
assert_image_similar(im, repr_jpeg, 17)
|
||||
|
||||
|
|
|
@ -460,7 +460,7 @@ def test_plt_marker() -> None:
|
|||
out.seek(length - 2, os.SEEK_CUR)
|
||||
|
||||
|
||||
def test_9bit():
|
||||
def test_9bit() -> None:
|
||||
with Image.open("Tests/images/9bit.j2k") as im:
|
||||
assert im.mode == "I;16"
|
||||
assert im.size == (128, 128)
|
||||
|
|
|
@ -54,7 +54,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
def test_version(self) -> None:
|
||||
version = features.version_codec("libtiff")
|
||||
assert version is not None
|
||||
assert re.search(r"\d+\.\d+\.\d+$", version)
|
||||
assert re.search(r"\d+\.\d+\.\d+t?$", version)
|
||||
|
||||
def test_g4_tiff(self, tmp_path: Path) -> None:
|
||||
"""Test the ordinary file path load path"""
|
||||
|
|
|
@ -535,8 +535,10 @@ class TestFilePng:
|
|||
|
||||
def test_repr_png(self) -> None:
|
||||
im = hopper()
|
||||
b = im._repr_png_()
|
||||
assert b is not None
|
||||
|
||||
with Image.open(BytesIO(im._repr_png_())) as repr_png:
|
||||
with Image.open(BytesIO(b)) as repr_png:
|
||||
assert repr_png.format == "PNG"
|
||||
assert_image_equal(im, repr_png)
|
||||
|
||||
|
@ -683,6 +685,7 @@ class TestFilePng:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
xmp = im.getxmp()
|
||||
|
||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||
|
@ -767,16 +770,12 @@ class TestFilePng:
|
|||
def test_save_stdout(self, buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
mystdout = MyStdOut()
|
||||
else:
|
||||
mystdout = BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
sys.stdout = mystdout # type: ignore[assignment]
|
||||
|
||||
with Image.open(TEST_PNG_FILE) as im:
|
||||
im.save(sys.stdout, "PNG")
|
||||
|
@ -784,7 +783,7 @@ class TestFilePng:
|
|||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if buffer:
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
|
||||
|
|
|
@ -368,16 +368,12 @@ def test_mimetypes(tmp_path: Path) -> None:
|
|||
def test_save_stdout(buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
mystdout = MyStdOut()
|
||||
else:
|
||||
mystdout = BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
sys.stdout = mystdout # type: ignore[assignment]
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.save(sys.stdout, "PPM")
|
||||
|
@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
|
|||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if buffer:
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, TEST_FILE)
|
||||
|
|
|
@ -162,7 +162,7 @@ def test_combined_larger_than_size() -> None:
|
|||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
||||
],
|
||||
)
|
||||
def test_crashes(test_file: str, raises) -> None:
|
||||
def test_crashes(test_file: str, raises: type[Exception]) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with pytest.raises(raises):
|
||||
with Image.open(f):
|
||||
|
|
|
@ -105,6 +105,7 @@ def test_load_image_series() -> None:
|
|||
img_list = SpiderImagePlugin.loadImageSeries(file_list)
|
||||
|
||||
# Assert
|
||||
assert img_list is not None
|
||||
assert len(img_list) == 1
|
||||
assert isinstance(img_list[0], Image.Image)
|
||||
assert img_list[0].size == (128, 128)
|
||||
|
|
|
@ -113,14 +113,14 @@ class TestFileTiff:
|
|||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||
|
||||
def test_seek_too_large(self):
|
||||
def test_seek_too_large(self) -> None:
|
||||
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
||||
Image.open("Tests/images/seek_too_large.tif")
|
||||
|
||||
def test_set_legacy_api(self) -> None:
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
with pytest.raises(Exception) as e:
|
||||
ifd.legacy_api = None
|
||||
ifd.legacy_api = False
|
||||
assert str(e.value) == "Not allowing setting of legacy api"
|
||||
|
||||
def test_xyres_tiff(self) -> None:
|
||||
|
@ -621,6 +621,19 @@ class TestFileTiff:
|
|||
|
||||
assert_image_equal_tofile(im, tmpfile)
|
||||
|
||||
def test_iptc(self, tmp_path: Path) -> None:
|
||||
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im = hopper()
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
ifd[33723] = 1
|
||||
ifd.tagtype[33723] = 4
|
||||
im.tag_v2 = ifd
|
||||
im.save(outfile)
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
assert 33723 not in im.tag_v2
|
||||
|
||||
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im = hopper()
|
||||
|
@ -759,6 +772,7 @@ class TestFileTiff:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
xmp = im.getxmp()
|
||||
|
||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||
|
|
|
@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
|
|||
|
||||
from .helper import assert_deep_equal, hopper
|
||||
|
||||
TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}
|
||||
TAG_IDS: dict[str, int] = {
|
||||
info.name: info.value
|
||||
for info in TiffTags.TAGS_V2.values()
|
||||
if info.value is not None
|
||||
}
|
||||
|
||||
|
||||
def test_rt_metadata(tmp_path: Path) -> None:
|
||||
|
@ -411,8 +415,8 @@ def test_empty_values() -> None:
|
|||
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
||||
info.load(data)
|
||||
# Should not raise ValueError.
|
||||
info = dict(info)
|
||||
assert 33432 in info
|
||||
info_dict = dict(info)
|
||||
assert 33432 in info_dict
|
||||
|
||||
|
||||
def test_photoshop_info(tmp_path: Path) -> None:
|
||||
|
|
|
@ -5,6 +5,7 @@ import re
|
|||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -70,7 +71,9 @@ class TestFileWebp:
|
|||
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
||||
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
|
||||
|
||||
def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None:
|
||||
def _roundtrip(
|
||||
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
|
||||
) -> None:
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
|
||||
hopper(mode).save(temp_file, **args)
|
||||
|
@ -198,7 +201,9 @@ class TestFileWebp:
|
|||
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
|
||||
)
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_invalid_background(self, background, tmp_path: Path) -> None:
|
||||
def test_invalid_background(
|
||||
self, background: int | tuple[int, ...], tmp_path: Path
|
||||
) -> None:
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
im = hopper()
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -69,7 +69,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
|||
are visually similar to the originals.
|
||||
"""
|
||||
|
||||
def check(temp_file) -> None:
|
||||
def check(temp_file: str) -> None:
|
||||
with Image.open(temp_file) as im:
|
||||
assert im.n_frames == 2
|
||||
|
||||
|
|
|
@ -129,6 +129,7 @@ def test_getxmp() -> None:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
assert (
|
||||
im.getxmp()["xmpmeta"]["xmptk"]
|
||||
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, WmfImagePlugin
|
||||
from PIL import Image, ImageFile, WmfImagePlugin
|
||||
|
||||
from .helper import assert_image_similar_tofile, hopper
|
||||
|
||||
|
@ -34,10 +35,13 @@ def test_load() -> None:
|
|||
|
||||
|
||||
def test_register_handler(tmp_path: Path) -> None:
|
||||
class TestHandler:
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
methodCalled = False
|
||||
|
||||
def save(self, im, fp, filename) -> None:
|
||||
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.methodCalled = True
|
||||
|
||||
handler = TestHandler()
|
||||
|
@ -70,7 +74,7 @@ def test_load_set_dpi() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
|
||||
def test_save(ext, tmp_path: Path) -> None:
|
||||
def test_save(ext: str, tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
|
||||
tmpfile = str(tmp_path / ("temp" + ext))
|
||||
|
|
|
@ -34,7 +34,7 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
|
|||
|
||||
def test_leak(self) -> None:
|
||||
if features.check_module("freetype2"):
|
||||
ImageFont.core = _util.DeferredError(ImportError)
|
||||
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
||||
try:
|
||||
default_font = ImageFont.load_default()
|
||||
finally:
|
||||
|
|
|
@ -8,7 +8,7 @@ import sys
|
|||
import tempfile
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
from typing import IO, Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -152,7 +152,7 @@ class TestImage:
|
|||
|
||||
def test_stringio(self) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
with Image.open(io.StringIO()):
|
||||
with Image.open(io.StringIO()): # type: ignore[arg-type]
|
||||
pass
|
||||
|
||||
def test_pathlib(self, tmp_path: Path) -> None:
|
||||
|
@ -175,11 +175,19 @@ class TestImage:
|
|||
def test_fp_name(self, tmp_path: Path) -> None:
|
||||
temp_file = str(tmp_path / "temp.jpg")
|
||||
|
||||
class FP:
|
||||
class FP(io.BytesIO):
|
||||
name: str
|
||||
|
||||
def write(self, b: bytes) -> None:
|
||||
pass
|
||||
if sys.version_info >= (3, 12):
|
||||
from collections.abc import Buffer
|
||||
|
||||
def write(self, data: Buffer) -> int:
|
||||
return len(data)
|
||||
|
||||
else:
|
||||
|
||||
def write(self, data: Any) -> int:
|
||||
return len(data)
|
||||
|
||||
fp = FP()
|
||||
fp.name = temp_file
|
||||
|
@ -393,13 +401,13 @@ class TestImage:
|
|||
|
||||
# errors
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, "invalid source")
|
||||
source.alpha_composite(over, "invalid destination") # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, (0, 0), "invalid destination")
|
||||
source.alpha_composite(over, (0, 0), "invalid source") # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, 0)
|
||||
source.alpha_composite(over, 0) # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, (0, 0), 0)
|
||||
source.alpha_composite(over, (0, 0), 0) # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
source.alpha_composite(over, (0, 0), (0, -1))
|
||||
|
||||
|
@ -909,6 +917,10 @@ class TestImage:
|
|||
assert tag not in exif.get_ifd(0x8769)
|
||||
assert exif.get_ifd(0xA005)
|
||||
|
||||
def test_empty_xmp(self) -> None:
|
||||
with Image.open("Tests/images/hopper.gif") as im:
|
||||
assert im.getxmp() == {}
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||
im = Image.new("RGB", size)
|
||||
|
|
|
@ -259,6 +259,7 @@ class TestCffi(AccessTest):
|
|||
caccess = im.im.pixel_access(False)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
access = PyAccess.new(im, False)
|
||||
assert access is not None
|
||||
|
||||
w, h = im.size
|
||||
for x in range(0, w, 10):
|
||||
|
@ -289,6 +290,7 @@ class TestCffi(AccessTest):
|
|||
caccess = im.im.pixel_access(False)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
access = PyAccess.new(im, False)
|
||||
assert access is not None
|
||||
|
||||
w, h = im.size
|
||||
for x in range(0, w, 10):
|
||||
|
@ -299,6 +301,8 @@ class TestCffi(AccessTest):
|
|||
# Attempt to set the value on a read-only image
|
||||
with pytest.warns(DeprecationWarning):
|
||||
access = PyAccess.new(im, True)
|
||||
assert access is not None
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
access[(0, 0)] = color
|
||||
|
||||
|
@ -341,6 +345,8 @@ class TestCffi(AccessTest):
|
|||
im = Image.new(mode, (1, 1))
|
||||
with pytest.warns(DeprecationWarning):
|
||||
access = PyAccess.new(im, False)
|
||||
assert access is not None
|
||||
|
||||
access.putpixel((0, 0), color)
|
||||
|
||||
if len(color) == 3:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
@ -13,13 +13,16 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
|||
|
||||
im = hopper().resize((128, 100))
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
def test_toarray() -> None:
|
||||
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
|
||||
ai = numpy.array(im.convert(mode))
|
||||
return ai.shape, ai.dtype.str, ai.nbytes
|
||||
|
||||
def test_with_dtype(dtype) -> None:
|
||||
def test_with_dtype(dtype: npt.DTypeLike) -> None:
|
||||
ai = numpy.array(im, dtype=dtype)
|
||||
assert ai.dtype == dtype
|
||||
|
||||
|
|
|
@ -16,7 +16,9 @@ def draft_roundtrip(
|
|||
im = Image.new(in_mode, in_size)
|
||||
data = tostring(im, "JPEG")
|
||||
im = fromstring(data)
|
||||
mode, box = im.draft(req_mode, req_size)
|
||||
result = im.draft(req_mode, req_size)
|
||||
assert result is not None
|
||||
box = result[1]
|
||||
scale, _ = im.decoderconfig
|
||||
assert box[:2] == (0, 0)
|
||||
assert (im.width - scale) < box[2] <= im.width
|
||||
|
|
|
@ -137,7 +137,7 @@ def test_builtinfilter_p() -> None:
|
|||
builtin_filter = ImageFilter.BuiltinFilter()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
builtin_filter.filter(hopper("P"))
|
||||
builtin_filter.filter(hopper("P").im)
|
||||
|
||||
|
||||
def test_kernel_not_enough_coefficients() -> None:
|
||||
|
|
|
@ -68,7 +68,11 @@ def test_sanity() -> None:
|
|||
),
|
||||
)
|
||||
def test_properties(
|
||||
mode, expected_base, expected_type, expected_bands, expected_band_names
|
||||
mode: str,
|
||||
expected_base: str,
|
||||
expected_type: str,
|
||||
expected_bands: int,
|
||||
expected_band_names: tuple[str, ...],
|
||||
) -> None:
|
||||
assert Image.getmodebase(mode) == expected_base
|
||||
assert Image.getmodetype(mode) == expected_type
|
||||
|
|
|
@ -338,3 +338,8 @@ class TestImagingPaste:
|
|||
|
||||
im.copy().paste(im2)
|
||||
im.copy().paste(im2, (0, 0))
|
||||
|
||||
def test_incorrect_abbreviated_form(self) -> None:
|
||||
im = Image.new("L", (1, 1))
|
||||
with pytest.raises(ValueError):
|
||||
im.paste(im, im, im)
|
||||
|
|
|
@ -61,4 +61,4 @@ def test_f_lut() -> None:
|
|||
def test_f_mode() -> None:
|
||||
im = hopper("F")
|
||||
with pytest.raises(ValueError):
|
||||
im.point(None)
|
||||
im.point([])
|
||||
|
|
|
@ -79,6 +79,7 @@ def test_putpalette_with_alpha_values() -> None:
|
|||
(
|
||||
("RGBA", (1, 2, 3, 4)),
|
||||
("RGBAX", (1, 2, 3, 4, 0)),
|
||||
("ARGB", (4, 1, 2, 3)),
|
||||
),
|
||||
)
|
||||
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
|
||||
|
|
|
@ -98,7 +98,7 @@ def test_quantize_dither_diff() -> None:
|
|||
@pytest.mark.parametrize(
|
||||
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
|
||||
)
|
||||
def test_quantize_kmeans(method) -> None:
|
||||
def test_quantize_kmeans(method: Image.Quantize) -> None:
|
||||
im = hopper()
|
||||
no_kmeans = im.quantize(kmeans=0, method=method)
|
||||
kmeans = im.quantize(kmeans=1, method=method)
|
||||
|
|
|
@ -56,10 +56,12 @@ def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) ->
|
|||
@pytest.mark.parametrize(
|
||||
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
|
||||
)
|
||||
def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None:
|
||||
def test_args_factor_error(
|
||||
size: float | tuple[int, int], expected_error: type[Exception]
|
||||
) -> None:
|
||||
im = Image.new("L", (10, 10))
|
||||
with pytest.raises(expected_error):
|
||||
im.reduce(size)
|
||||
im.reduce(size) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -86,10 +88,12 @@ def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) ->
|
|||
((5, 0, 5, 10), ValueError),
|
||||
),
|
||||
)
|
||||
def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None:
|
||||
def test_args_box_error(
|
||||
size: str | tuple[int, int, int, int], expected_error: type[Exception]
|
||||
) -> None:
|
||||
im = Image.new("L", (10, 10))
|
||||
with pytest.raises(expected_error):
|
||||
im.reduce(2, size).size
|
||||
im.reduce(2, size).size # type: ignore[arg-type]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "1", "I;16"))
|
||||
|
|
|
@ -445,7 +445,7 @@ class TestCoreResampleBox:
|
|||
im.resize((32, 32), resample, (20, 20, 100, 20))
|
||||
|
||||
with pytest.raises(TypeError, match="must be sequence of length 4"):
|
||||
im.resize((32, 32), resample, (im.width, im.height))
|
||||
im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError, match="can't be negative"):
|
||||
im.resize((32, 32), resample, (-20, 20, 100, 100))
|
||||
|
|
|
@ -124,8 +124,8 @@ def test_fastpath_translate() -> None:
|
|||
def test_center() -> None:
|
||||
im = hopper()
|
||||
rotate(im, im.mode, 45, center=(0, 0))
|
||||
rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0))
|
||||
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0))
|
||||
rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0))
|
||||
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0))
|
||||
|
||||
|
||||
def test_rotate_no_fill() -> None:
|
||||
|
|
|
@ -16,7 +16,7 @@ from .helper import (
|
|||
|
||||
def test_sanity() -> None:
|
||||
im = hopper()
|
||||
assert im.thumbnail((100, 100)) is None
|
||||
im.thumbnail((100, 100))
|
||||
|
||||
assert im.size == (100, 100)
|
||||
|
||||
|
@ -111,7 +111,9 @@ def test_load_first_unless_jpeg() -> None:
|
|||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
draft = im.draft
|
||||
|
||||
def im_draft(mode: str, size: tuple[int, int]):
|
||||
def im_draft(
|
||||
mode: str, size: tuple[int, int]
|
||||
) -> tuple[str, tuple[int, int, float, float]] | None:
|
||||
result = draft(mode, size)
|
||||
assert result is not None
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ def test_sanity() -> None:
|
|||
|
||||
|
||||
def test_flags() -> None:
|
||||
assert ImageCms.Flags.NONE == 0
|
||||
assert ImageCms.Flags.NONE.value == 0
|
||||
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
|
||||
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
|
||||
|
||||
|
@ -569,9 +569,9 @@ def assert_aux_channel_preserved(
|
|||
for delta in nine_grid_deltas:
|
||||
channel_data.paste(
|
||||
channel_pattern,
|
||||
tuple(
|
||||
paste_offset[c] + delta[c] * channel_pattern.size[c]
|
||||
for c in range(2)
|
||||
(
|
||||
paste_offset[0] + delta[0] * channel_pattern.size[0],
|
||||
paste_offset[1] + delta[1] * channel_pattern.size[1],
|
||||
),
|
||||
)
|
||||
chans.append(channel_data)
|
||||
|
@ -642,7 +642,8 @@ def test_auxiliary_channels_isolated() -> None:
|
|||
# convert with and without AUX data, test colors are equal
|
||||
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
|
||||
source_profile = ImageCms.createProfile(src_colorSpace)
|
||||
destination_profile = ImageCms.createProfile(dst_format[1])
|
||||
dst_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], dst_format[1])
|
||||
destination_profile = ImageCms.createProfile(dst_colorSpace)
|
||||
source_image = src_format[3]
|
||||
test_transform = ImageCms.buildTransform(
|
||||
source_profile,
|
||||
|
|
|
@ -448,6 +448,7 @@ def test_shape1() -> None:
|
|||
x3, y3 = 95, 5
|
||||
|
||||
# Act
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.move(x0, y0)
|
||||
s.curve(x1, y1, x2, y2, x3, y3)
|
||||
|
@ -469,6 +470,7 @@ def test_shape2() -> None:
|
|||
x3, y3 = 5, 95
|
||||
|
||||
# Act
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.move(x0, y0)
|
||||
s.curve(x1, y1, x2, y2, x3, y3)
|
||||
|
@ -487,6 +489,7 @@ def test_transform() -> None:
|
|||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# Act
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.line(0, 0)
|
||||
s.transform((0, 0, 0, 0, 0, 0))
|
||||
|
@ -913,7 +916,12 @@ def test_rounded_rectangle_translucent(
|
|||
def test_floodfill(bbox: Coords) -> None:
|
||||
red = ImageColor.getrgb("red")
|
||||
|
||||
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
|
||||
mode_values: list[tuple[str, int | tuple[int, ...]]] = [
|
||||
("L", 1),
|
||||
("RGBA", (255, 0, 0, 0)),
|
||||
("RGB", red),
|
||||
]
|
||||
for mode, value in mode_values:
|
||||
# Arrange
|
||||
im = Image.new(mode, (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -1429,6 +1437,7 @@ def test_same_color_outline(bbox: Coords) -> None:
|
|||
x2, y2 = 95, 50
|
||||
x3, y3 = 95, 5
|
||||
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.move(x0, y0)
|
||||
s.curve(x1, y1, x2, y2, x3, y3)
|
||||
|
@ -1467,7 +1476,7 @@ def test_same_color_outline(bbox: Coords) -> None:
|
|||
(4, "square", {}),
|
||||
(8, "regular_octagon", {}),
|
||||
(4, "square_rotate_45", {"rotation": 45}),
|
||||
(3, "triangle_width", {"width": 5, "outline": "yellow"}),
|
||||
(3, "triangle_width", {"outline": "yellow", "width": 5}),
|
||||
],
|
||||
)
|
||||
def test_draw_regular_polygon(
|
||||
|
@ -1477,7 +1486,10 @@ def test_draw_regular_polygon(
|
|||
filename = f"Tests/images/imagedraw_{polygon_name}.png"
|
||||
draw = ImageDraw.Draw(im)
|
||||
bounding_circle = ((W // 2, H // 2), 25)
|
||||
draw.regular_polygon(bounding_circle, n_sides, fill="red", **args)
|
||||
rotation = int(args.get("rotation", 0))
|
||||
outline = args.get("outline")
|
||||
width = int(args.get("width", 1))
|
||||
draw.regular_polygon(bounding_circle, n_sides, rotation, "red", outline, width)
|
||||
assert_image_equal_tofile(im, filename)
|
||||
|
||||
|
||||
|
@ -1562,10 +1574,14 @@ def test_compute_regular_polygon_vertices(
|
|||
],
|
||||
)
|
||||
def test_compute_regular_polygon_vertices_input_error_handling(
|
||||
n_sides, bounding_circle, rotation, expected_error, error_message
|
||||
n_sides: int,
|
||||
bounding_circle: int | tuple[int | tuple[int] | str, ...],
|
||||
rotation: int | str,
|
||||
expected_error: type[Exception],
|
||||
error_message: str,
|
||||
) -> None:
|
||||
with pytest.raises(expected_error) as e:
|
||||
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
|
||||
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
|
||||
assert str(e.value) == error_message
|
||||
|
||||
|
||||
|
@ -1624,3 +1640,8 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
|
|||
draw.rectangle(xy)
|
||||
with pytest.raises(ValueError):
|
||||
draw.rounded_rectangle(xy)
|
||||
|
||||
|
||||
def test_getdraw() -> None:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
ImageDraw.getdraw(None, [])
|
||||
|
|
|
@ -51,9 +51,18 @@ def test_sanity() -> None:
|
|||
pen = ImageDraw2.Pen("blue", width=7)
|
||||
draw.line(list(range(10)), pen)
|
||||
|
||||
draw, handler = ImageDraw.getdraw(im)
|
||||
draw2, handler = ImageDraw.getdraw(im)
|
||||
assert draw2 is not None
|
||||
pen = ImageDraw2.Pen("blue", width=7)
|
||||
draw.line(list(range(10)), pen)
|
||||
draw2.line(list(range(10)), pen)
|
||||
|
||||
|
||||
def test_mode() -> None:
|
||||
draw = ImageDraw2.Draw("L", (1, 1))
|
||||
assert draw.image.mode == "L"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageDraw2.Draw("L")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
|
|
|
@ -209,7 +209,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
|
|||
|
||||
super().__init__(mode, *args)
|
||||
|
||||
def decode(self, buffer):
|
||||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
# eof
|
||||
return -1, 0
|
||||
|
||||
|
@ -222,7 +222,7 @@ class MockPyEncoder(ImageFile.PyEncoder):
|
|||
|
||||
super().__init__(mode, *args)
|
||||
|
||||
def encode(self, buffer):
|
||||
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
||||
return 1, 1, b""
|
||||
|
||||
def cleanup(self) -> None:
|
||||
|
@ -351,7 +351,9 @@ class TestPyEncoder(CodecsTest):
|
|||
ImageFile._save(
|
||||
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
||||
)
|
||||
assert MockPyEncoder.last.cleanup_called
|
||||
last: MockPyEncoder | None = MockPyEncoder.last
|
||||
assert last
|
||||
assert last.cleanup_called
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
|
@ -381,7 +383,7 @@ class TestPyEncoder(CodecsTest):
|
|||
def test_encode(self) -> None:
|
||||
encoder = ImageFile.PyEncoder(None)
|
||||
with pytest.raises(NotImplementedError):
|
||||
encoder.encode(None)
|
||||
encoder.encode(0)
|
||||
|
||||
bytes_consumed, errcode = encoder.encode_to_pyfd()
|
||||
assert bytes_consumed == 0
|
||||
|
|
|
@ -209,7 +209,7 @@ def test_getlength(
|
|||
assert length == length_raqm
|
||||
|
||||
|
||||
def test_float_size() -> None:
|
||||
def test_float_size(layout_engine: ImageFont.Layout) -> None:
|
||||
lengths = []
|
||||
for size in (48, 48.5, 49):
|
||||
f = ImageFont.truetype(
|
||||
|
@ -224,7 +224,7 @@ def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
|
|||
draw = ImageDraw.Draw(im)
|
||||
line_spacing = font.getbbox("A")[3] + 4
|
||||
lines = TEST_TEXT.split("\n")
|
||||
y = 0
|
||||
y: float = 0
|
||||
for line in lines:
|
||||
draw.text((0, y), line, font=font)
|
||||
y += line_spacing
|
||||
|
@ -494,8 +494,8 @@ def test_default_font() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
|
||||
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
|
||||
@pytest.mark.parametrize("mode", ("", "1", "RGBA"))
|
||||
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
|
||||
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
||||
|
||||
|
||||
|
@ -548,7 +548,7 @@ def test_find_font(
|
|||
|
||||
def loadable_font(
|
||||
filepath: str, size: int, index: int, encoding: str, *args: Any
|
||||
):
|
||||
) -> ImageFont.FreeTypeFont:
|
||||
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
|
||||
if filepath == path_to_fake:
|
||||
return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
|
||||
|
@ -564,6 +564,7 @@ def test_find_font(
|
|||
# catching syntax like errors
|
||||
monkeypatch.setattr(sys, "platform", platform)
|
||||
if platform == "linux":
|
||||
monkeypatch.setenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
|
||||
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
|
||||
|
||||
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
|
||||
|
|
|
@ -14,7 +14,7 @@ original_core = ImageFont.core
|
|||
|
||||
def setup_module() -> None:
|
||||
if features.check_module("freetype2"):
|
||||
ImageFont.core = _util.DeferredError(ImportError)
|
||||
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
||||
|
||||
|
||||
def teardown_module() -> None:
|
||||
|
@ -76,3 +76,8 @@ def test_oom() -> None:
|
|||
font = ImageFont.ImageFont()
|
||||
font._load_pilfont_data(fp, Image.new("L", (1, 1)))
|
||||
font.getmask("A" * 1_000_000)
|
||||
|
||||
|
||||
def test_freetypefont_without_freetype() -> None:
|
||||
with pytest.raises(ImportError):
|
||||
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
|
||||
|
|
|
@ -89,6 +89,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
|||
p.communicate()
|
||||
|
||||
im = ImageGrab.grabclipboard()
|
||||
assert isinstance(im, list)
|
||||
assert len(im) == 1
|
||||
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
|
||||
|
||||
|
@ -105,6 +106,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
|||
p.communicate()
|
||||
|
||||
im = ImageGrab.grabclipboard()
|
||||
assert isinstance(im, Image.Image)
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
@ -120,6 +122,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
|||
with open(image_path, "rb") as fp:
|
||||
subprocess.call(["wl-copy"], stdin=fp)
|
||||
im = ImageGrab.grabclipboard()
|
||||
assert isinstance(im, Image.Image)
|
||||
assert_image_equal_tofile(im, image_path)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
|
|
@ -454,7 +454,7 @@ def test_autocontrast_cutoff() -> None:
|
|||
# Test the cutoff argument of autocontrast
|
||||
with Image.open("Tests/images/bw_gradient.png") as img:
|
||||
|
||||
def autocontrast(cutoff: int | tuple[int, int]):
|
||||
def autocontrast(cutoff: int | tuple[int, int]) -> list[int]:
|
||||
return ImageOps.autocontrast(img, cutoff).histogram()
|
||||
|
||||
assert autocontrast(10) == autocontrast((10, 10))
|
||||
|
|
|
@ -58,7 +58,6 @@ def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None:
|
|||
blur = ImageFilter.GaussianBlur
|
||||
with pytest.raises(ValueError):
|
||||
im.convert("1").filter(blur)
|
||||
blur(im.convert("L"))
|
||||
with pytest.raises(ValueError):
|
||||
im.convert("I").filter(blur)
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -102,7 +101,7 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
|
|||
assert i.im.getpixel((x, y))[c] >= 250
|
||||
# Fuzzy match.
|
||||
|
||||
def gp(x, y):
|
||||
def gp(x: int, y: int) -> tuple[int, ...]:
|
||||
return i.im.getpixel((x, y))
|
||||
|
||||
assert 236 <= gp(7, 4)[0] <= 239
|
||||
|
|
|
@ -45,7 +45,7 @@ def test_getcolor() -> None:
|
|||
|
||||
# Test unknown color specifier
|
||||
with pytest.raises(ValueError):
|
||||
palette.getcolor("unknown")
|
||||
palette.getcolor("unknown") # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_getcolor_rgba_color_rgb_palette() -> None:
|
||||
|
@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None:
|
|||
|
||||
palette.save(f)
|
||||
|
||||
p = ImagePalette.load(f)
|
||||
lut = ImagePalette.load(f)
|
||||
|
||||
# load returns raw palette information
|
||||
assert len(p[0]) == 768
|
||||
assert p[1] == "RGB"
|
||||
assert len(lut[0]) == 768
|
||||
assert lut[1] == "RGB"
|
||||
|
||||
p = ImagePalette.raw(p[1], p[0])
|
||||
p = ImagePalette.raw(lut[1], lut[0])
|
||||
assert isinstance(p, ImagePalette.ImagePalette)
|
||||
assert p.palette == palette.tobytes()
|
||||
|
||||
|
|
|
@ -41,18 +41,13 @@ def test_rgb() -> None:
|
|||
checkrgb(0, 0, 255)
|
||||
|
||||
|
||||
def test_image() -> None:
|
||||
modes = ["1", "RGB", "RGBA", "L", "P"]
|
||||
qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage
|
||||
if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
|
||||
modes.append("I;16")
|
||||
|
||||
for mode in modes:
|
||||
im = hopper(mode)
|
||||
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
|
||||
if mode not in ("RGB", "RGBA"):
|
||||
im = im.convert("RGB")
|
||||
assert_image_similar(roundtripped_im, im, 1)
|
||||
@pytest.mark.parametrize("mode", ("1", "RGB", "RGBA", "L", "P", "I;16"))
|
||||
def test_image(mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
|
||||
if mode not in ("RGB", "RGBA"):
|
||||
im = im.convert("RGB")
|
||||
assert_image_similar(roundtripped_im, im, 1)
|
||||
|
||||
|
||||
def test_closed_file() -> None:
|
||||
|
|
|
@ -45,21 +45,22 @@ if is_win32():
|
|||
memcpy = ctypes.cdll.msvcrt.memcpy
|
||||
memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
|
||||
|
||||
CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC
|
||||
windll = getattr(ctypes, "windll")
|
||||
CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
|
||||
CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC]
|
||||
CreateCompatibleDC.restype = ctypes.wintypes.HDC
|
||||
|
||||
DeleteDC = ctypes.windll.gdi32.DeleteDC
|
||||
DeleteDC = windll.gdi32.DeleteDC
|
||||
DeleteDC.argtypes = [ctypes.wintypes.HDC]
|
||||
|
||||
SelectObject = ctypes.windll.gdi32.SelectObject
|
||||
SelectObject = windll.gdi32.SelectObject
|
||||
SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ]
|
||||
SelectObject.restype = ctypes.wintypes.HGDIOBJ
|
||||
|
||||
DeleteObject = ctypes.windll.gdi32.DeleteObject
|
||||
DeleteObject = windll.gdi32.DeleteObject
|
||||
DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ]
|
||||
|
||||
CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection
|
||||
CreateDIBSection = windll.gdi32.CreateDIBSection
|
||||
CreateDIBSection.argtypes = [
|
||||
ctypes.wintypes.HDC,
|
||||
ctypes.c_void_p,
|
||||
|
@ -70,7 +71,7 @@ if is_win32():
|
|||
]
|
||||
CreateDIBSection.restype = ctypes.wintypes.HBITMAP
|
||||
|
||||
def serialize_dib(bi, pixels) -> bytearray:
|
||||
def serialize_dib(bi: BITMAPINFOHEADER, pixels: ctypes.c_void_p) -> bytearray:
|
||||
bf = BITMAPFILEHEADER()
|
||||
bf.bfType = 0x4D42
|
||||
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize
|
||||
|
|
|
@ -11,7 +11,7 @@ import pytest
|
|||
"args, report",
|
||||
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),
|
||||
)
|
||||
def test_main(args, report) -> None:
|
||||
def test_main(args: list[str], report: bool) -> None:
|
||||
args = [sys.executable, "-m"] + args
|
||||
out = subprocess.check_output(args).decode("utf-8")
|
||||
lines = out.splitlines()
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, _typing
|
||||
|
||||
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
|
||||
|
||||
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
||||
if TYPE_CHECKING:
|
||||
import numpy
|
||||
import numpy.typing as npt
|
||||
else:
|
||||
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
||||
|
||||
TEST_IMAGE_SIZE = (10, 10)
|
||||
|
||||
|
||||
def test_numpy_to_image() -> None:
|
||||
def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image:
|
||||
def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
|
||||
if bands == 1:
|
||||
if boolean:
|
||||
data = [0, 255] * 50
|
||||
|
@ -99,14 +104,14 @@ def test_1d_array() -> None:
|
|||
assert_image(Image.fromarray(a), "L", (1, 5))
|
||||
|
||||
|
||||
def _test_img_equals_nparray(img: Image.Image, np) -> None:
|
||||
assert len(np.shape) >= 2
|
||||
np_size = np.shape[1], np.shape[0]
|
||||
def _test_img_equals_nparray(img: Image.Image, np_img: _typing.NumpyArray) -> None:
|
||||
assert len(np_img.shape) >= 2
|
||||
np_size = np_img.shape[1], np_img.shape[0]
|
||||
assert img.size == np_size
|
||||
px = img.load()
|
||||
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
||||
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
||||
assert_deep_equal(px[x, y], np[y, x])
|
||||
assert_deep_equal(px[x, y], np_img[y, x])
|
||||
|
||||
|
||||
def test_16bit() -> None:
|
||||
|
@ -157,7 +162,7 @@ def test_save_tiff_uint16() -> None:
|
|||
("HSV", numpy.uint8),
|
||||
),
|
||||
)
|
||||
def test_to_array(mode: str, dtype) -> None:
|
||||
def test_to_array(mode: str, dtype: npt.DTypeLike) -> None:
|
||||
img = hopper(mode)
|
||||
|
||||
# Resize to non-square
|
||||
|
@ -207,7 +212,7 @@ def test_putdata() -> None:
|
|||
numpy.float64,
|
||||
),
|
||||
)
|
||||
def test_roundtrip_eye(dtype) -> None:
|
||||
def test_roundtrip_eye(dtype: npt.DTypeLike) -> None:
|
||||
arr = numpy.eye(10, dtype=dtype)
|
||||
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))
|
||||
|
||||
|
|
|
@ -54,16 +54,12 @@ def test_stdout(buffer: bool) -> None:
|
|||
# Temporarily redirect stdout
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
mystdout = MyStdOut()
|
||||
else:
|
||||
mystdout = BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
sys.stdout = mystdout # type: ignore[assignment]
|
||||
|
||||
ps = PSDraw.PSDraw()
|
||||
_create_document(ps)
|
||||
|
@ -71,6 +67,6 @@ def test_stdout(buffer: bool) -> None:
|
|||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if buffer:
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
assert mystdout.getvalue() != b""
|
||||
|
|
|
@ -46,7 +46,7 @@ def roundtrip(expected: Image.Image) -> None:
|
|||
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
|
||||
def test_sanity(tmp_path: Path) -> None:
|
||||
# Segfault test
|
||||
app = QApplication([])
|
||||
app: QApplication | None = QApplication([])
|
||||
ex = Example()
|
||||
assert app # Silence warning
|
||||
assert ex # Silence warning
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import IO, Callable
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -22,11 +23,11 @@ class TestShellInjection:
|
|||
self,
|
||||
tmp_path: Path,
|
||||
src_img: Image.Image,
|
||||
save_func: Callable[[Image.Image, int, str], None],
|
||||
save_func: Callable[[Image.Image, IO[bytes], str | bytes], None],
|
||||
) -> None:
|
||||
for filename in test_filenames:
|
||||
dest_file = str(tmp_path / filename)
|
||||
save_func(src_img, 0, dest_file)
|
||||
save_func(src_img, BytesIO(), dest_file)
|
||||
# If file can't be opened, shell injection probably occurred
|
||||
with Image.open(dest_file) as im:
|
||||
im.load()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# install libimagequant
|
||||
|
||||
archive_name=libimagequant
|
||||
archive_version=4.3.0
|
||||
archive_version=4.3.1
|
||||
|
||||
archive=$archive_name-$archive_version
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install webp
|
||||
|
||||
archive=libwebp-1.3.2
|
||||
archive=libwebp-1.4.0
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -115,6 +115,13 @@ Support for LibTIFF earlier than 4
|
|||
Support for LibTIFF earlier than version 4 has been deprecated.
|
||||
Upgrade to a newer version of LibTIFF instead.
|
||||
|
||||
ImageDraw.getdraw hints parameter
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 10.4.0
|
||||
|
||||
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
@ -279,7 +286,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
|
|||
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
||||
offset.
|
||||
|
||||
.. image:: ./example/size_vs_bbox.png
|
||||
.. image:: ./example/size_vs_bbox.webp
|
||||
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
||||
:align: center
|
||||
|
||||
|
|
Before Width: | Height: | Size: 9.5 KiB |
|
@ -26,5 +26,5 @@ if __name__ == "__main__":
|
|||
d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
|
||||
if y != 0:
|
||||
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
|
||||
im.save("docs/example/anchors.png")
|
||||
im.save("docs/example/anchors.webp")
|
||||
im.show()
|
||||
|
|
BIN
docs/example/anchors.webp
Normal file
After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/image_thumbnail.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_contain.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 38 KiB |
BIN
docs/example/imageops_cover.webp
Normal file
After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 28 KiB |
BIN
docs/example/imageops_fit.webp
Normal file
After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_pad.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 13 KiB |
BIN
docs/example/size_vs_bbox.webp
Normal file
After Width: | Height: | Size: 4.8 KiB |
|
@ -144,10 +144,12 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
|||
.. py:currentmodule:: PIL.Image
|
||||
|
||||
.. data:: Resampling.NEAREST
|
||||
:noindex:
|
||||
|
||||
Pick one nearest pixel from the input image. Ignore all other input pixels.
|
||||
|
||||
.. data:: Resampling.BOX
|
||||
:noindex:
|
||||
|
||||
Each pixel of source image contributes to one pixel of the
|
||||
destination image with identical weights.
|
||||
|
@ -158,6 +160,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
|||
.. versionadded:: 3.4.0
|
||||
|
||||
.. data:: Resampling.BILINEAR
|
||||
:noindex:
|
||||
|
||||
For resize calculate the output pixel value using linear interpolation
|
||||
on all pixels that may contribute to the output value.
|
||||
|
@ -165,6 +168,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
|||
in the input image is used.
|
||||
|
||||
.. data:: Resampling.HAMMING
|
||||
:noindex:
|
||||
|
||||
Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have
|
||||
dislocations on local level like with :data:`Resampling.BOX`.
|
||||
|
@ -174,6 +178,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
|||
.. versionadded:: 3.4.0
|
||||
|
||||
.. data:: Resampling.BICUBIC
|
||||
:noindex:
|
||||
|
||||
For resize calculate the output pixel value using cubic interpolation
|
||||
on all pixels that may contribute to the output value.
|
||||
|
@ -181,6 +186,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
|||
in the input image is used.
|
||||
|
||||
.. data:: Resampling.LANCZOS
|
||||
:noindex:
|
||||
|
||||
Calculate the output pixel value using a high-quality Lanczos filter (a
|
||||
truncated sinc) on all pixels that may contribute to the output value.
|
||||
|
|
|
@ -132,7 +132,7 @@ of the two lines.
|
|||
|
||||
.. comment: Image generated with ../example/anchors.py
|
||||
|
||||
.. image:: ../example/anchors.png
|
||||
.. image:: ../example/anchors.webp
|
||||
:alt: Text anchor examples
|
||||
:align: center
|
||||
|
||||
|
|
|
@ -278,26 +278,26 @@ choose to resize relative to a given size.
|
|||
|
||||
from PIL import Image, ImageOps
|
||||
size = (100, 150)
|
||||
with Image.open("Tests/images/hopper.png") as im:
|
||||
ImageOps.contain(im, size).save("imageops_contain.png")
|
||||
ImageOps.cover(im, size).save("imageops_cover.png")
|
||||
ImageOps.fit(im, size).save("imageops_fit.png")
|
||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
|
||||
with Image.open("Tests/images/hopper.webp") as im:
|
||||
ImageOps.contain(im, size).save("imageops_contain.webp")
|
||||
ImageOps.cover(im, size).save("imageops_cover.webp")
|
||||
ImageOps.fit(im, size).save("imageops_fit.webp")
|
||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
|
||||
|
||||
# thumbnail() can also be used,
|
||||
# but will modify the image object in place
|
||||
im.thumbnail(size)
|
||||
im.save("imageops_thumbnail.png")
|
||||
im.save("image_thumbnail.webp")
|
||||
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||
+================+===========================================+============================================+==========================================+========================================+========================================+
|
||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||
+================+============================================+=============================================+===========================================+=========================================+=========================================+
|
||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|Resulting image | .. image:: ../example/image_thumbnail.webp | .. image:: ../example/imageops_contain.webp | .. image:: ../example/imageops_cover.webp | .. image:: ../example/imageops_fit.webp | .. image:: ../example/imageops_pad.webp |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|
||||
.. _color-transforms:
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **libimagequant** provides improved color quantization
|
||||
|
||||
* Pillow has been tested with libimagequant **2.6-4.3**
|
||||
* Pillow has been tested with libimagequant **2.6-4.3.1**
|
||||
* Libimagequant is licensed GPLv3, which is more restrictive than
|
||||
the Pillow license, therefore we will not be distributing binaries
|
||||
with libimagequant support enabled.
|
||||
|
|
|
@ -78,8 +78,6 @@ Constructing images
|
|||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autofunction:: new
|
||||
.. autoclass:: SupportsArrayInterface
|
||||
:show-inheritance:
|
||||
.. autofunction:: fromarray
|
||||
.. autofunction:: frombytes
|
||||
.. autofunction:: frombuffer
|
||||
|
@ -197,6 +195,7 @@ This helps to get the bounding box coordinates of the input image::
|
|||
.. automethod:: PIL.Image.Image.getpalette
|
||||
.. automethod:: PIL.Image.Image.getpixel
|
||||
.. automethod:: PIL.Image.Image.getprojection
|
||||
.. automethod:: PIL.Image.Image.getxmp
|
||||
.. automethod:: PIL.Image.Image.histogram
|
||||
.. automethod:: PIL.Image.Image.paste
|
||||
.. automethod:: PIL.Image.Image.point
|
||||
|
@ -365,6 +364,14 @@ Classes
|
|||
.. autoclass:: PIL.Image.ImagePointHandler
|
||||
.. autoclass:: PIL.Image.ImageTransformHandler
|
||||
|
||||
Protocols
|
||||
---------
|
||||
|
||||
.. autoclass:: SupportsArrayInterface
|
||||
:show-inheritance:
|
||||
.. autoclass:: SupportsGetData
|
||||
:show-inheritance:
|
||||
|
||||
Constants
|
||||
---------
|
||||
|
||||
|
@ -418,7 +425,6 @@ See :ref:`concept-filters` for details.
|
|||
.. autoclass:: Resampling
|
||||
:members:
|
||||
:undoc-members:
|
||||
:noindex:
|
||||
|
||||
Dither modes
|
||||
^^^^^^^^^^^^
|
||||
|
|
|
@ -691,23 +691,7 @@ Methods
|
|||
:param hints: An optional list of hints.
|
||||
:returns: A (drawing context, drawing resource factory) tuple.
|
||||
|
||||
.. py:method:: floodfill(image, xy, value, border=None, thresh=0)
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
|
||||
Fills a bounded region with a given color.
|
||||
|
||||
:param image: Target image.
|
||||
:param xy: Seed position (a 2-item coordinate tuple).
|
||||
:param value: Fill color.
|
||||
:param border: Optional border value. If given, the region consists of
|
||||
pixels with a color different from the border color. If not given,
|
||||
the region consists of pixels having the same color as the seed
|
||||
pixel.
|
||||
:param thresh: Optional threshold value which specifies a maximum
|
||||
tolerable difference of a pixel value from the 'background' in
|
||||
order for it to be replaced. Useful for filling regions of non-
|
||||
homogeneous, but similar, colors.
|
||||
.. autofunction:: PIL.ImageDraw.floodfill
|
||||
|
||||
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
|
||||
.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
||||
|
|
|
@ -36,26 +36,26 @@ Resize relative to a given size
|
|||
|
||||
from PIL import Image, ImageOps
|
||||
size = (100, 150)
|
||||
with Image.open("Tests/images/hopper.png") as im:
|
||||
ImageOps.contain(im, size).save("imageops_contain.png")
|
||||
ImageOps.cover(im, size).save("imageops_cover.png")
|
||||
ImageOps.fit(im, size).save("imageops_fit.png")
|
||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
|
||||
with Image.open("Tests/images/hopper.webp") as im:
|
||||
ImageOps.contain(im, size).save("imageops_contain.webp")
|
||||
ImageOps.cover(im, size).save("imageops_cover.webp")
|
||||
ImageOps.fit(im, size).save("imageops_fit.webp")
|
||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
|
||||
|
||||
# thumbnail() can also be used,
|
||||
# but will modify the image object in place
|
||||
im.thumbnail(size)
|
||||
im.save("imageops_thumbnail.png")
|
||||
im.save("image_thumbnail.webp")
|
||||
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||
+================+===========================================+============================================+==========================================+========================================+========================================+
|
||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||
+================+============================================+=============================================+===========================================+=========================================+=========================================+
|
||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|Resulting image | .. image:: ../example/image_thumbnail.webp | .. image:: ../example/imageops_contain.webp | .. image:: ../example/imageops_cover.webp | .. image:: ../example/imageops_fit.webp | .. image:: ../example/imageops_pad.webp |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||
|
||||
.. autofunction:: contain
|
||||
.. autofunction:: cover
|
||||
|
|
|
@ -44,42 +44,23 @@ Access using negative indexes is also possible. ::
|
|||
-----------------------------
|
||||
|
||||
.. class:: PixelAccess
|
||||
:canonical: PIL.Image.core.PixelAccess
|
||||
|
||||
.. method:: __setitem__(self, xy, color):
|
||||
.. method:: __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]
|
||||
|
||||
Modifies the pixel at x,y. The color is given as a single
|
||||
numerical value for single band images, and a tuple for
|
||||
multi-band images
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
|
||||
|
||||
.. method:: __getitem__(self, xy):
|
||||
|
||||
Returns the pixel at x,y. The pixel is returned as a single
|
||||
value for single band images or a tuple for multiple band
|
||||
images
|
||||
Returns the pixel at x,y. The pixel is returned as a single
|
||||
value for single band images or a tuple for multi-band images.
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:returns: a pixel value for single band images, a tuple of
|
||||
pixel values for multiband images.
|
||||
pixel values for multiband images.
|
||||
|
||||
.. method:: putpixel(self, xy, color):
|
||||
.. method:: __setitem__(self, xy: tuple[int, int], color: float | tuple[int, ...]) -> None
|
||||
|
||||
Modifies the pixel at x,y. The color is given as a single
|
||||
numerical value for single band images, and a tuple for
|
||||
multi-band images. In addition to this, RGB and RGBA tuples
|
||||
are accepted for P and PA images.
|
||||
multi-band images.
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
|
||||
|
||||
.. method:: getpixel(self, xy):
|
||||
|
||||
Returns the pixel at x,y. The pixel is returned as a single
|
||||
value for single band images or a tuple for multiple band
|
||||
images
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y).
|
||||
:returns: a pixel value for single band images, a tuple of
|
||||
pixel values for multiband images.
|
||||
:param color: The pixel value according to its mode,
|
||||
e.g. tuple (r, g, b) for RGB mode.
|
||||
|
|
|
@ -44,3 +44,4 @@ Access using negative indexes is also possible. ::
|
|||
|
||||
.. autoclass:: PIL.PyAccess.PyAccess()
|
||||
:members:
|
||||
:special-members: __getitem__, __setitem__
|
||||
|
|
|
@ -33,6 +33,10 @@ Internal Modules
|
|||
Provides a convenient way to import type hints that are not available
|
||||
on some Python versions.
|
||||
|
||||
.. py:class:: NumpyArray
|
||||
|
||||
Typing alias.
|
||||
|
||||
.. py:class:: StrOrBytesPath
|
||||
|
||||
Typing alias.
|
||||
|
|
|
@ -34,6 +34,11 @@ Support for LibTIFF earlier than 4
|
|||
Support for LibTIFF earlier than version 4 has been deprecated.
|
||||
Upgrade to a newer version of LibTIFF instead.
|
||||
|
||||
ImageDraw.getdraw hints parameter
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
|
|
|
@ -18,9 +18,9 @@ is not secure.
|
|||
|
||||
- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve
|
||||
orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead.
|
||||
- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It
|
||||
will now use ``defusedxml`` instead. If the dependency is not present, an empty
|
||||
dictionary will be returned and a warning raised.
|
||||
- ``getxmp()`` was added to :py:class:`~PIL.JpegImagePlugin.JpegImageFile` in Pillow
|
||||
8.2.0. It will now use ``defusedxml`` instead. If the dependency is not present, an
|
||||
empty dictionary will be returned and a warning raised.
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
|
|
@ -98,7 +98,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
|
|||
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
||||
offset.
|
||||
|
||||
.. image:: ../example/size_vs_bbox.png
|
||||
.. image:: ../example/size_vs_bbox.webp
|
||||
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
||||
:align: center
|
||||
|
||||
|
|
4
setup.py
|
@ -37,7 +37,9 @@ IMAGEQUANT_ROOT = None
|
|||
JPEG2K_ROOT = None
|
||||
JPEG_ROOT = None
|
||||
LCMS_ROOT = None
|
||||
RAQM_ROOT = None
|
||||
TIFF_ROOT = None
|
||||
WEBP_ROOT = None
|
||||
ZLIB_ROOT = None
|
||||
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
|
||||
|
||||
|
@ -459,6 +461,8 @@ class pil_build_ext(build_ext):
|
|||
"FREETYPE_ROOT": "freetype2",
|
||||
"HARFBUZZ_ROOT": "harfbuzz",
|
||||
"FRIBIDI_ROOT": "fribidi",
|
||||
"RAQM_ROOT": "raqm",
|
||||
"WEBP_ROOT": "libwebp",
|
||||
"LCMS_ROOT": "lcms2",
|
||||
"IMAGEQUANT_ROOT": "libimagequant",
|
||||
}.items():
|
||||
|
|
|
@ -103,7 +103,7 @@ def bdf_char(
|
|||
class BdfFontFile(FontFile.FontFile):
|
||||
"""Font file plugin for the X11 BDF format."""
|
||||
|
||||
def __init__(self, fp: BinaryIO):
|
||||
def __init__(self, fp: BinaryIO) -> None:
|
||||
super().__init__()
|
||||
|
||||
s = fp.readline()
|
||||
|
|
|
@ -31,6 +31,7 @@ BLP files come in many different flavours:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import os
|
||||
import struct
|
||||
from enum import IntEnum
|
||||
|
@ -60,7 +61,9 @@ def unpack_565(i: int) -> tuple[int, int, int]:
|
|||
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
|
||||
|
||||
|
||||
def decode_dxt1(data, alpha=False):
|
||||
def decode_dxt1(
|
||||
data: bytes, alpha: bool = False
|
||||
) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
||||
"""
|
||||
input: one "row" of data (i.e. will produce 4*width pixels)
|
||||
"""
|
||||
|
@ -68,9 +71,9 @@ def decode_dxt1(data, alpha=False):
|
|||
blocks = len(data) // 8 # number of blocks in row
|
||||
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
||||
|
||||
for block in range(blocks):
|
||||
for block_index in range(blocks):
|
||||
# Decode next 8-byte block.
|
||||
idx = block * 8
|
||||
idx = block_index * 8
|
||||
color0, color1, bits = struct.unpack_from("<HHI", data, idx)
|
||||
|
||||
r0, g0, b0 = unpack_565(color0)
|
||||
|
@ -115,7 +118,7 @@ def decode_dxt1(data, alpha=False):
|
|||
return ret
|
||||
|
||||
|
||||
def decode_dxt3(data):
|
||||
def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
||||
"""
|
||||
input: one "row" of data (i.e. will produce 4*width pixels)
|
||||
"""
|
||||
|
@ -123,8 +126,8 @@ def decode_dxt3(data):
|
|||
blocks = len(data) // 16 # number of blocks in row
|
||||
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
||||
|
||||
for block in range(blocks):
|
||||
idx = block * 16
|
||||
for block_index in range(blocks):
|
||||
idx = block_index * 16
|
||||
block = data[idx : idx + 16]
|
||||
# Decode next 16-byte block.
|
||||
bits = struct.unpack_from("<8B", block)
|
||||
|
@ -168,7 +171,7 @@ def decode_dxt3(data):
|
|||
return ret
|
||||
|
||||
|
||||
def decode_dxt5(data):
|
||||
def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
||||
"""
|
||||
input: one "row" of data (i.e. will produce 4 * width pixels)
|
||||
"""
|
||||
|
@ -176,8 +179,8 @@ def decode_dxt5(data):
|
|||
blocks = len(data) // 16 # number of blocks in row
|
||||
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
||||
|
||||
for block in range(blocks):
|
||||
idx = block * 16
|
||||
for block_index in range(blocks):
|
||||
idx = block_index * 16
|
||||
block = data[idx : idx + 16]
|
||||
# Decode next 16-byte block.
|
||||
a0, a1 = struct.unpack_from("<BB", block)
|
||||
|
@ -276,7 +279,7 @@ class BlpImageFile(ImageFile.ImageFile):
|
|||
class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
try:
|
||||
self._read_blp_header()
|
||||
self._load()
|
||||
|
@ -285,6 +288,10 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
raise OSError(msg) from e
|
||||
return -1, 0
|
||||
|
||||
@abc.abstractmethod
|
||||
def _load(self) -> None:
|
||||
pass
|
||||
|
||||
def _read_blp_header(self) -> None:
|
||||
assert self.fd is not None
|
||||
self.fd.seek(4)
|
||||
|
@ -318,7 +325,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
ret.append((b, g, r, a))
|
||||
return ret
|
||||
|
||||
def _read_bgra(self, palette):
|
||||
def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
|
||||
data = bytearray()
|
||||
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
|
||||
while True:
|
||||
|
@ -327,7 +334,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
except struct.error:
|
||||
break
|
||||
b, g, r, a = palette[offset]
|
||||
d = (r, g, b)
|
||||
d: tuple[int, ...] = (r, g, b)
|
||||
if self._blp_alpha_depth:
|
||||
d += (a,)
|
||||
data.extend(d)
|
||||
|
@ -423,6 +430,7 @@ class BLPEncoder(ImageFile.PyEncoder):
|
|||
|
||||
def _write_palette(self) -> bytes:
|
||||
data = b""
|
||||
assert self.im is not None
|
||||
palette = self.im.getpalette("RGBA", "RGBA")
|
||||
for i in range(len(palette) // 4):
|
||||
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
||||
|
@ -431,12 +439,13 @@ class BLPEncoder(ImageFile.PyEncoder):
|
|||
data += b"\x00" * 4
|
||||
return data
|
||||
|
||||
def encode(self, bufsize):
|
||||
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
||||
palette_data = self._write_palette()
|
||||
|
||||
offset = 20 + 16 * 4 * 2 + len(palette_data)
|
||||
data = struct.pack("<16I", offset, *((0,) * 15))
|
||||
|
||||
assert self.im is not None
|
||||
w, h = self.im.size
|
||||
data += struct.pack("<16I", w * h, *((0,) * 15))
|
||||
|
||||
|
@ -449,7 +458,7 @@ class BLPEncoder(ImageFile.PyEncoder):
|
|||
return len(data), 0, data
|
||||
|
||||
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||
if im.mode != "P":
|
||||
msg = "Unsupported BLP image mode"
|
||||
raise ValueError(msg)
|
||||
|
|
|
@ -301,7 +301,8 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
class BmpRleDecoder(ImageFile.PyDecoder):
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
assert self.fd is not None
|
||||
rle4 = self.args[1]
|
||||
data = bytearray()
|
||||
x = 0
|
||||
|
@ -395,12 +396,12 @@ SAVE = {
|
|||
}
|
||||
|
||||
|
||||
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||
_save(im, fp, filename, False)
|
||||
|
||||
|
||||
def _save(
|
||||
im: Image.Image, fp: IO[bytes], filename: str, bitmap_header: bool = True
|
||||
im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
|
||||
) -> None:
|
||||
try:
|
||||
rawmode, bits, colors = SAVE[im.mode]
|
||||
|
|
|
@ -17,7 +17,7 @@ from . import Image, ImageFile
|
|||
_handler = None
|
||||
|
||||
|
||||
def register_handler(handler: ImageFile.StubHandler) -> None:
|
||||
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||
"""
|
||||
Install application-specific BUFR image handler.
|
||||
|
||||
|
@ -60,7 +60,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
|
|||
return _handler
|
||||
|
||||
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||
if _handler is None or not hasattr(_handler, "save"):
|
||||
msg = "BUFR save handler not installed"
|
||||
raise OSError(msg)
|
||||
|
|
|
@ -380,6 +380,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
elif pfflags & DDPF.PALETTEINDEXED8:
|
||||
self._mode = "P"
|
||||
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
|
||||
self.palette.mode = "RGBA"
|
||||
elif pfflags & DDPF.FOURCC:
|
||||
offset = header_size + 4
|
||||
if fourcc == D3DFMT.DXT1:
|
||||
|
@ -480,7 +481,8 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
class DdsRgbDecoder(ImageFile.PyDecoder):
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
assert self.fd is not None
|
||||
bitcount, masks = self.args
|
||||
|
||||
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
|
||||
|
@ -511,7 +513,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
|
|||
return -1, 0
|
||||
|
||||
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||
if im.mode not in ("RGB", "RGBA", "L", "LA"):
|
||||
msg = f"cannot write mode {im.mode} as DDS"
|
||||
raise OSError(msg)
|
||||
|
|
|
@ -27,6 +27,7 @@ import re
|
|||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import IO
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i32le as i32
|
||||
|
@ -229,6 +230,11 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
trailer_reached = False
|
||||
|
||||
def check_required_header_comments() -> None:
|
||||
"""
|
||||
The EPS specification requires that some headers exist.
|
||||
This should be checked when the header comments formally end,
|
||||
when image data starts, or when the file ends, whichever comes first.
|
||||
"""
|
||||
if "PS-Adobe" not in self.info:
|
||||
msg = 'EPS header missing "%!PS-Adobe" comment'
|
||||
raise SyntaxError(msg)
|
||||
|
@ -236,7 +242,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
msg = 'EPS header missing "%%BoundingBox" comment'
|
||||
raise SyntaxError(msg)
|
||||
|
||||
def _read_comment(s):
|
||||
def _read_comment(s: str) -> bool:
|
||||
nonlocal reading_trailer_comments
|
||||
try:
|
||||
m = split.match(s)
|
||||
|
@ -244,33 +250,33 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
msg = "not an EPS file"
|
||||
raise SyntaxError(msg) from e
|
||||
|
||||
if m:
|
||||
k, v = m.group(1, 2)
|
||||
self.info[k] = v
|
||||
if k == "BoundingBox":
|
||||
if v == "(atend)":
|
||||
reading_trailer_comments = True
|
||||
elif not self._size or (
|
||||
trailer_reached and reading_trailer_comments
|
||||
):
|
||||
try:
|
||||
# Note: The DSC spec says that BoundingBox
|
||||
# fields should be integers, but some drivers
|
||||
# put floating point values there anyway.
|
||||
box = [int(float(i)) for i in v.split()]
|
||||
self._size = box[2] - box[0], box[3] - box[1]
|
||||
self.tile = [
|
||||
("eps", (0, 0) + self.size, offset, (length, box))
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
if not m:
|
||||
return False
|
||||
|
||||
k, v = m.group(1, 2)
|
||||
self.info[k] = v
|
||||
if k == "BoundingBox":
|
||||
if v == "(atend)":
|
||||
reading_trailer_comments = True
|
||||
elif not self._size or (trailer_reached and reading_trailer_comments):
|
||||
try:
|
||||
# Note: The DSC spec says that BoundingBox
|
||||
# fields should be integers, but some drivers
|
||||
# put floating point values there anyway.
|
||||
box = [int(float(i)) for i in v.split()]
|
||||
self._size = box[2] - box[0], box[3] - box[1]
|
||||
self.tile = [("eps", (0, 0) + self.size, offset, (length, box))]
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
while True:
|
||||
byte = self.fp.read(1)
|
||||
if byte == b"":
|
||||
# if we didn't read a byte we must be at the end of the file
|
||||
if bytes_read == 0:
|
||||
if reading_header_comments:
|
||||
check_required_header_comments()
|
||||
break
|
||||
elif byte in b"\r\n":
|
||||
# if we read a line ending character, ignore it and parse what
|
||||
|
@ -366,8 +372,6 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
trailer_reached = True
|
||||
bytes_read = 0
|
||||
|
||||
check_required_header_comments()
|
||||
|
||||
if not self._size:
|
||||
msg = "cannot determine EPS bounding box"
|
||||
raise OSError(msg)
|
||||
|
@ -413,7 +417,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def _save(im, fp, filename, eps=1):
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
|
||||
"""EPS Writer for the Python Imaging Library."""
|
||||
|
||||
# make sure image data is available
|
||||
|
|