Merge branch 'main' into xmp

This commit is contained in:
Andrew Murray 2024-06-19 09:06:10 +10:00 committed by GitHub
commit 7ab3aee7bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
100 changed files with 1100 additions and 678 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,12 @@ Changelog (Pillow)
10.4.0 (unreleased) 10.4.0 (unreleased)
------------------- -------------------
- 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

@ -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,20 +51,20 @@ 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 save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True self.saved = True
handler = TestHandler() handler = TestHandler()

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

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

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:

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

@ -113,7 +113,7 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_seek_too_large(self): def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"): with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif") Image.open("Tests/images/seek_too_large.tif")

View File

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

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

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

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

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

@ -113,13 +113,13 @@ def test_array_F() -> None:
def test_not_flattened() -> None: def test_not_flattened() -> None:
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.putdata([[0]]) im.putdata([[0]]) # type: ignore[list-item]
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.putdata([[0]], 2) im.putdata([[0]], 2) # type: ignore[list-item]
with pytest.raises(TypeError): with pytest.raises(TypeError):
im = Image.new("I", (1, 1)) im = Image.new("I", (1, 1))
im.putdata([[0]]) im.putdata([[0]]) # type: ignore[list-item]
with pytest.raises(TypeError): with pytest.raises(TypeError):
im = Image.new("F", (1, 1)) im = Image.new("F", (1, 1))
im.putdata([[0]]) im.putdata([[0]]) # type: ignore[list-item]

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

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

@ -1562,7 +1562,11 @@ def test_compute_regular_polygon_vertices(
], ],
) )
def test_compute_regular_polygon_vertices_input_error_handling( def test_compute_regular_polygon_vertices_input_error_handling(
n_sides, bounding_circle, rotation, expected_error, error_message n_sides: int,
bounding_circle: int | tuple[int | tuple[int] | str, ...],
rotation: int | str,
expected_error: type[Exception],
error_message: str,
) -> None: ) -> None:
with pytest.raises(expected_error) as e: with pytest.raises(expected_error) as e:
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
@ -1624,3 +1628,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():
with pytest.warns(DeprecationWarning):
ImageDraw.getdraw(None, [])

View File

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

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

View File

@ -70,7 +70,7 @@ if is_win32():
] ]
CreateDIBSection.restype = ctypes.wintypes.HBITMAP CreateDIBSection.restype = ctypes.wintypes.HBITMAP
def serialize_dib(bi, pixels) -> bytearray: def serialize_dib(bi: BITMAPINFOHEADER, pixels: ctypes.c_void_p) -> bytearray:
bf = BITMAPFILEHEADER() bf = BITMAPFILEHEADER()
bf.bfType = 0x4D42 bf.bfType = 0x4D42
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize

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

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

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

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

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

@ -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,6 +31,7 @@ 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
@ -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 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)
""" """
@ -68,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)
@ -115,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)
""" """
@ -123,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)
@ -168,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)
""" """
@ -176,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)
@ -276,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()
@ -285,6 +288,10 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
raise OSError(msg) from e raise OSError(msg) from e
return -1, 0 return -1, 0
@abc.abstractmethod
def _load(self) -> None:
pass
def _read_blp_header(self) -> None: def _read_blp_header(self) -> None:
assert self.fd is not None assert self.fd is not None
self.fd.seek(4) self.fd.seek(4)
@ -318,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:
@ -327,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)
@ -431,7 +438,7 @@ 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)
@ -449,7 +456,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data 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": 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: ImageFile.StubHandler) -> None: def register_handler(handler: ImageFile.StubHandler | None) -> None:
""" """
Install application-specific BUFR image handler. Install application-specific BUFR image handler.
@ -58,7 +60,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
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)

View File

@ -16,6 +16,7 @@ import io
import struct import struct
import sys import sys
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
from typing import IO
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i32le as i32 from ._binary import i32le as i32
@ -479,7 +480,8 @@ class DdsImageFile(ImageFile.ImageFile):
class DdsRgbDecoder(ImageFile.PyDecoder): class DdsRgbDecoder(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
bitcount, masks = self.args bitcount, masks = self.args
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
@ -510,7 +512,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
return -1, 0 return -1, 0
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode not in ("RGB", "RGBA", "L", "LA"): if im.mode not in ("RGB", "RGBA", "L", "LA"):
msg = f"cannot write mode {im.mode} as DDS" msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg) raise OSError(msg)

View File

@ -27,6 +27,7 @@ import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i32le as i32 from ._binary import i32le as i32
@ -236,7 +237,7 @@ class EpsImageFile(ImageFile.ImageFile):
msg = 'EPS header missing "%%BoundingBox" comment' msg = 'EPS header missing "%%BoundingBox" comment'
raise SyntaxError(msg) raise SyntaxError(msg)
def _read_comment(s): def _read_comment(s: str) -> bool:
nonlocal reading_trailer_comments nonlocal reading_trailer_comments
try: try:
m = split.match(s) m = split.match(s)
@ -244,27 +245,25 @@ class EpsImageFile(ImageFile.ImageFile):
msg = "not an EPS file" msg = "not an EPS file"
raise SyntaxError(msg) from e raise SyntaxError(msg) from e
if m: if not m:
k, v = m.group(1, 2) return False
self.info[k] = v
if k == "BoundingBox": k, v = m.group(1, 2)
if v == "(atend)": self.info[k] = v
reading_trailer_comments = True if k == "BoundingBox":
elif not self._size or ( if v == "(atend)":
trailer_reached and reading_trailer_comments reading_trailer_comments = True
): elif not self._size or (trailer_reached and reading_trailer_comments):
try: try:
# Note: The DSC spec says that BoundingBox # Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers # fields should be integers, but some drivers
# put floating point values there anyway. # put floating point values there anyway.
box = [int(float(i)) for i in v.split()] box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1] self._size = box[2] - box[0], box[3] - box[1]
self.tile = [ self.tile = [("eps", (0, 0) + self.size, offset, (length, box))]
("eps", (0, 0) + self.size, offset, (length, box)) except Exception:
] pass
except Exception: return True
pass
return True
while True: while True:
byte = self.fp.read(1) byte = self.fp.read(1)
@ -413,7 +412,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.""" """EPS Writer for the Python Imaging Library."""
# make sure image data is available # make sure image data is available

View File

@ -115,14 +115,18 @@ class FitsImageFile(ImageFile.ImageFile):
elif number_of_bits in (-32, -64): elif number_of_bits in (-32, -64):
self._mode = "F" self._mode = "F"
args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,) args: tuple[str | int, ...]
if decoder_name == "raw":
args = (self.mode, 0, -1)
else:
args = (number_of_bits,)
return decoder_name, offset, args return decoder_name, offset, args
class FitsGzipDecoder(ImageFile.PyDecoder): class FitsGzipDecoder(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 assert self.fd is not None
value = gzip.decompress(self.fd.read()) value = gzip.decompress(self.fd.read())

View File

@ -241,7 +241,7 @@ class FpxImageFile(ImageFile.ImageFile):
self.ole.close() self.ole.close()
super().close() super().close()
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.ole.close() self.ole.close()
super().__exit__() super().__exit__()

View File

@ -29,8 +29,10 @@ import itertools
import math import math
import os import os
import subprocess import subprocess
import sys
from enum import IntEnum from enum import IntEnum
from functools import cached_property from functools import cached_property
from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union
from . import ( from . import (
Image, Image,
@ -45,6 +47,9 @@ from ._binary import i16le as i16
from ._binary import o8 from ._binary import o8
from ._binary import o16le as o16 from ._binary import o16le as o16
if TYPE_CHECKING:
from . import _imaging
class LoadingStrategy(IntEnum): class LoadingStrategy(IntEnum):
""".. versionadded:: 9.1.0""" """.. versionadded:: 9.1.0"""
@ -117,7 +122,7 @@ class GifImageFile(ImageFile.ImageFile):
self._seek(0) # get ready to read first frame self._seek(0) # get ready to read first frame
@property @property
def n_frames(self): def n_frames(self) -> int:
if self._n_frames is None: if self._n_frames is None:
current = self.tell() current = self.tell()
try: try:
@ -162,11 +167,11 @@ class GifImageFile(ImageFile.ImageFile):
msg = "no more images in GIF file" msg = "no more images in GIF file"
raise EOFError(msg) from e raise EOFError(msg) from e
def _seek(self, frame, update_image=True): def _seek(self, frame: int, update_image: bool = True) -> None:
if frame == 0: if frame == 0:
# rewind # rewind
self.__offset = 0 self.__offset = 0
self.dispose = None self.dispose: _imaging.ImagingCore | None = None
self.__frame = -1 self.__frame = -1
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)
self.disposal_method = 0 self.disposal_method = 0
@ -194,9 +199,9 @@ class GifImageFile(ImageFile.ImageFile):
msg = "no more images in GIF file" msg = "no more images in GIF file"
raise EOFError(msg) raise EOFError(msg)
palette = None palette: ImagePalette.ImagePalette | Literal[False] | None = None
info = {} info: dict[str, Any] = {}
frame_transparency = None frame_transparency = None
interlace = None interlace = None
frame_dispose_extent = None frame_dispose_extent = None
@ -212,7 +217,7 @@ class GifImageFile(ImageFile.ImageFile):
# #
s = self.fp.read(1) s = self.fp.read(1)
block = self.data() block = self.data()
if s[0] == 249: if s[0] == 249 and block is not None:
# #
# graphic control extension # graphic control extension
# #
@ -248,14 +253,14 @@ class GifImageFile(ImageFile.ImageFile):
info["comment"] = comment info["comment"] = comment
s = None s = None
continue continue
elif s[0] == 255 and frame == 0: elif s[0] == 255 and frame == 0 and block is not None:
# #
# application extension # application extension
# #
info["extension"] = block, self.fp.tell() info["extension"] = block, self.fp.tell()
if block[:11] == b"NETSCAPE2.0": if block[:11] == b"NETSCAPE2.0":
block = self.data() block = self.data()
if len(block) >= 3 and block[0] == 1: if block and len(block) >= 3 and block[0] == 1:
self.info["loop"] = i16(block, 1) self.info["loop"] = i16(block, 1)
while self.data(): while self.data():
pass pass
@ -336,60 +341,60 @@ class GifImageFile(ImageFile.ImageFile):
self._mode = "RGB" self._mode = "RGB"
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
def _rgb(color): def _rgb(color: int) -> tuple[int, int, int]:
if self._frame_palette: if self._frame_palette:
if color * 3 + 3 > len(self._frame_palette.palette): if color * 3 + 3 > len(self._frame_palette.palette):
color = 0 color = 0
color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
else: else:
color = (color, color, color) return (color, color, color)
return color
self.dispose = None
self.dispose_extent = frame_dispose_extent self.dispose_extent = frame_dispose_extent
try: if self.dispose_extent and self.disposal_method >= 2:
if self.disposal_method < 2: try:
# do not dispose or none specified if self.disposal_method == 2:
self.dispose = None # replace with background colour
elif self.disposal_method == 2:
# replace with background colour
# only dispose the extent in this frame
x0, y0, x1, y1 = self.dispose_extent
dispose_size = (x1 - x0, y1 - y0)
Image._decompression_bomb_check(dispose_size)
# by convention, attempt to use transparency first
dispose_mode = "P"
color = self.info.get("transparency", frame_transparency)
if color is not None:
if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGBA"
color = _rgb(color) + (0,)
else:
color = self.info.get("background", 0)
if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGB"
color = _rgb(color)
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
else:
# replace with previous contents
if self.im is not None:
# only dispose the extent in this frame # only dispose the extent in this frame
self.dispose = self._crop(self.im, self.dispose_extent)
elif frame_transparency is not None:
x0, y0, x1, y1 = self.dispose_extent x0, y0, x1, y1 = self.dispose_extent
dispose_size = (x1 - x0, y1 - y0) dispose_size = (x1 - x0, y1 - y0)
Image._decompression_bomb_check(dispose_size) Image._decompression_bomb_check(dispose_size)
# by convention, attempt to use transparency first
dispose_mode = "P" dispose_mode = "P"
color = frame_transparency color = self.info.get("transparency", frame_transparency)
if self.mode in ("RGB", "RGBA"): if color is not None:
dispose_mode = "RGBA" if self.mode in ("RGB", "RGBA"):
color = _rgb(frame_transparency) + (0,) dispose_mode = "RGBA"
color = _rgb(color) + (0,)
else:
color = self.info.get("background", 0)
if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGB"
color = _rgb(color)
self.dispose = Image.core.fill(dispose_mode, dispose_size, color) self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
except AttributeError: else:
pass # replace with previous contents
if self.im is not None:
# only dispose the extent in this frame
self.dispose = self._crop(self.im, self.dispose_extent)
elif frame_transparency is not None:
x0, y0, x1, y1 = self.dispose_extent
dispose_size = (x1 - x0, y1 - y0)
Image._decompression_bomb_check(dispose_size)
dispose_mode = "P"
color = frame_transparency
if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGBA"
color = _rgb(frame_transparency) + (0,)
self.dispose = Image.core.fill(
dispose_mode, dispose_size, color
)
except AttributeError:
pass
if interlace is not None: if interlace is not None:
transparency = -1 transparency = -1
@ -453,6 +458,8 @@ class GifImageFile(ImageFile.ImageFile):
frame_im = self.im.convert("RGBA") frame_im = self.im.convert("RGBA")
else: else:
frame_im = self.im.convert("RGB") frame_im = self.im.convert("RGB")
assert self.dispose_extent is not None
frame_im = self._crop(frame_im, self.dispose_extent) frame_im = self._crop(frame_im, self.dispose_extent)
self.im = self._prev_im self.im = self._prev_im
@ -498,7 +505,12 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
return im.convert("L") return im.convert("L")
def _normalize_palette(im, palette, info): _Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette]
def _normalize_palette(
im: Image.Image, palette: _Palette | None, info: dict[str, Any]
) -> Image.Image:
""" """
Normalizes the palette for image. Normalizes the palette for image.
- Sets the palette to the incoming palette, if provided. - Sets the palette to the incoming palette, if provided.
@ -526,8 +538,10 @@ def _normalize_palette(im, palette, info):
source_palette = bytearray(i // 3 for i in range(768)) source_palette = bytearray(i // 3 for i in range(768))
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
used_palette_colors: list[int] | None
if palette: if palette:
used_palette_colors = [] used_palette_colors = []
assert source_palette is not None
for i in range(0, len(source_palette), 3): for i in range(0, len(source_palette), 3):
source_color = tuple(source_palette[i : i + 3]) source_color = tuple(source_palette[i : i + 3])
index = im.palette.colors.get(source_color) index = im.palette.colors.get(source_color)
@ -558,7 +572,11 @@ def _normalize_palette(im, palette, info):
return im return im
def _write_single_frame(im, fp, palette): def _write_single_frame(
im: Image.Image,
fp: IO[bytes],
palette: _Palette | None,
) -> None:
im_out = _normalize_mode(im) im_out = _normalize_mode(im)
for k, v in im_out.info.items(): for k, v in im_out.info.items():
im.encoderinfo.setdefault(k, v) im.encoderinfo.setdefault(k, v)
@ -579,7 +597,9 @@ def _write_single_frame(im, fp, palette):
fp.write(b"\0") # end of image data fp.write(b"\0") # end of image data
def _getbbox(base_im, im_frame): def _getbbox(
base_im: Image.Image, im_frame: Image.Image
) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im):
im_frame = im_frame.convert("RGBA") im_frame = im_frame.convert("RGBA")
base_im = base_im.convert("RGBA") base_im = base_im.convert("RGBA")
@ -587,12 +607,20 @@ def _getbbox(base_im, im_frame):
return delta, delta.getbbox(alpha_only=False) return delta, delta.getbbox(alpha_only=False)
def _write_multiple_frames(im, fp, palette): class _Frame(NamedTuple):
im: Image.Image
bbox: tuple[int, int, int, int] | None
encoderinfo: dict[str, Any]
def _write_multiple_frames(
im: Image.Image, fp: IO[bytes], palette: _Palette | None
) -> bool:
duration = im.encoderinfo.get("duration") duration = im.encoderinfo.get("duration")
disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
im_frames = [] im_frames: list[_Frame] = []
previous_im = None previous_im: Image.Image | None = None
frame_count = 0 frame_count = 0
background_im = None background_im = None
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
@ -618,24 +646,22 @@ def _write_multiple_frames(im, fp, palette):
frame_count += 1 frame_count += 1
diff_frame = None diff_frame = None
if im_frames: if im_frames and previous_im:
# delta frame # delta frame
delta, bbox = _getbbox(previous_im, im_frame) delta, bbox = _getbbox(previous_im, im_frame)
if not bbox: if not bbox:
# This frame is identical to the previous frame # This frame is identical to the previous frame
if encoderinfo.get("duration"): if encoderinfo.get("duration"):
im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[ im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
"duration"
]
continue continue
if im_frames[-1]["encoderinfo"].get("disposal") == 2: if im_frames[-1].encoderinfo.get("disposal") == 2:
if background_im is None: if background_im is None:
color = im.encoderinfo.get( color = im.encoderinfo.get(
"transparency", im.info.get("transparency", (0, 0, 0)) "transparency", im.info.get("transparency", (0, 0, 0))
) )
background = _get_background(im_frame, color) background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background) background_im = Image.new("P", im_frame.size, background)
background_im.putpalette(im_frames[0]["im"].palette) background_im.putpalette(im_frames[0].im.palette)
bbox = _getbbox(background_im, im_frame)[1] bbox = _getbbox(background_im, im_frame)[1]
elif encoderinfo.get("optimize") and im_frame.mode != "1": elif encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo: if "transparency" not in encoderinfo:
@ -681,39 +707,39 @@ def _write_multiple_frames(im, fp, palette):
else: else:
bbox = None bbox = None
previous_im = im_frame previous_im = im_frame
im_frames.append( im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))
{"im": diff_frame or im_frame, "bbox": bbox, "encoderinfo": encoderinfo}
)
if len(im_frames) == 1: if len(im_frames) == 1:
if "duration" in im.encoderinfo: if "duration" in im.encoderinfo:
# Since multiple frames will not be written, use the combined duration # Since multiple frames will not be written, use the combined duration
im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"] im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
return return False
for frame_data in im_frames: for frame_data in im_frames:
im_frame = frame_data["im"] im_frame = frame_data.im
if not frame_data["bbox"]: if not frame_data.bbox:
# global header # global header
for s in _get_global_header(im_frame, frame_data["encoderinfo"]): for s in _get_global_header(im_frame, frame_data.encoderinfo):
fp.write(s) fp.write(s)
offset = (0, 0) offset = (0, 0)
else: else:
# compress difference # compress difference
if not palette: if not palette:
frame_data["encoderinfo"]["include_color_table"] = True frame_data.encoderinfo["include_color_table"] = True
im_frame = im_frame.crop(frame_data["bbox"]) im_frame = im_frame.crop(frame_data.bbox)
offset = frame_data["bbox"][:2] offset = frame_data.bbox[:2]
_write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"]) _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
return True return True
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True) _save(im, fp, filename, save_all=True)
def _save(im, fp, filename, save_all=False): def _save(
im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
) -> None:
# header # header
if "palette" in im.encoderinfo or "palette" in im.info: if "palette" in im.encoderinfo or "palette" in im.info:
palette = im.encoderinfo.get("palette", im.info.get("palette")) palette = im.encoderinfo.get("palette", im.info.get("palette"))
@ -730,7 +756,7 @@ def _save(im, fp, filename, save_all=False):
fp.flush() fp.flush()
def get_interlace(im): def get_interlace(im: Image.Image) -> int:
interlace = im.encoderinfo.get("interlace", 1) interlace = im.encoderinfo.get("interlace", 1)
# workaround for @PIL153 # workaround for @PIL153
@ -740,7 +766,9 @@ def get_interlace(im):
return interlace return interlace
def _write_local_header(fp, im, offset, flags): def _write_local_header(
fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int
) -> None:
try: try:
transparency = im.encoderinfo["transparency"] transparency = im.encoderinfo["transparency"]
except KeyError: except KeyError:
@ -788,7 +816,7 @@ def _write_local_header(fp, im, offset, flags):
fp.write(o8(8)) # bits fp.write(o8(8)) # bits
def _save_netpbm(im, fp, filename): def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# Unused by default. # Unused by default.
# To use, uncomment the register_save call at the end of the file. # To use, uncomment the register_save call at the end of the file.
# #
@ -819,6 +847,7 @@ def _save_netpbm(im, fp, filename):
) )
# Allow ppmquant to receive SIGPIPE if ppmtogif exits # Allow ppmquant to receive SIGPIPE if ppmtogif exits
assert quant_proc.stdout is not None
quant_proc.stdout.close() quant_proc.stdout.close()
retcode = quant_proc.wait() retcode = quant_proc.wait()
@ -840,7 +869,7 @@ def _save_netpbm(im, fp, filename):
_FORCE_OPTIMIZE = False _FORCE_OPTIMIZE = False
def _get_optimize(im, info): def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
""" """
Palette optimization is a potentially expensive operation. Palette optimization is a potentially expensive operation.
@ -884,6 +913,7 @@ def _get_optimize(im, info):
and current_palette_size > 2 and current_palette_size > 2
): ):
return used_palette_colors return used_palette_colors
return None
def _get_color_table_size(palette_bytes: bytes) -> int: def _get_color_table_size(palette_bytes: bytes) -> int:
@ -924,7 +954,10 @@ def _get_palette_bytes(im: Image.Image) -> bytes:
return im.palette.palette if im.palette else b"" return im.palette.palette if im.palette else b""
def _get_background(im, info_background): def _get_background(
im: Image.Image,
info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,
) -> int:
background = 0 background = 0
if info_background: if info_background:
if isinstance(info_background, tuple): if isinstance(info_background, tuple):
@ -947,7 +980,7 @@ def _get_background(im, info_background):
return background return background
def _get_global_header(im, info): def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
"""Return a list of strings representing a GIF header""" """Return a list of strings representing a GIF header"""
# Header Block # Header Block
@ -1009,7 +1042,12 @@ def _get_global_header(im, info):
return header return header
def _write_frame_data(fp, im_frame, offset, params): def _write_frame_data(
fp: IO[bytes],
im_frame: Image.Image,
offset: tuple[int, int],
params: dict[str, Any],
) -> None:
try: try:
im_frame.encoderinfo = params im_frame.encoderinfo = params
@ -1029,7 +1067,9 @@ def _write_frame_data(fp, im_frame, offset, params):
# Legacy GIF utilities # Legacy GIF utilities
def getheader(im, palette=None, info=None): def getheader(
im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None
) -> tuple[list[bytes], list[int] | None]:
""" """
Legacy Method to get Gif data from image. Legacy Method to get Gif data from image.
@ -1041,11 +1081,11 @@ def getheader(im, palette=None, info=None):
:returns: tuple of(list of header items, optimized palette) :returns: tuple of(list of header items, optimized palette)
""" """
used_palette_colors = _get_optimize(im, info)
if info is None: if info is None:
info = {} info = {}
used_palette_colors = _get_optimize(im, info)
if "background" not in info and "background" in im.info: if "background" not in info and "background" in im.info:
info["background"] = im.info["background"] info["background"] = im.info["background"]
@ -1057,7 +1097,9 @@ def getheader(im, palette=None, info=None):
return header, used_palette_colors return header, used_palette_colors
def getdata(im, offset=(0, 0), **params): def getdata(
im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
) -> list[bytes]:
""" """
Legacy Method Legacy Method
@ -1074,12 +1116,23 @@ def getdata(im, offset=(0, 0), **params):
:returns: List of bytes containing GIF encoded frame data :returns: List of bytes containing GIF encoded frame data
""" """
from io import BytesIO
class Collector: class Collector(BytesIO):
data = [] data = []
def write(self, data): if sys.version_info >= (3, 12):
self.data.append(data) from collections.abc import Buffer
def write(self, data: Buffer) -> int:
self.data.append(data)
return len(data)
else:
def write(self, data: Any) -> int:
self.data.append(data)
return len(data)
im.load() # make sure raster data is available im.load() # make sure raster data is available

View File

@ -21,6 +21,7 @@ See the GIMP distribution for more information.)
from __future__ import annotations from __future__ import annotations
from math import log, pi, sin, sqrt from math import log, pi, sin, sqrt
from typing import IO, Callable
from ._binary import o8 from ._binary import o8
@ -28,7 +29,7 @@ EPSILON = 1e-10
"""""" # Enable auto-doc for data member """""" # Enable auto-doc for data member
def linear(middle, pos): def linear(middle: float, pos: float) -> float:
if pos <= middle: if pos <= middle:
if middle < EPSILON: if middle < EPSILON:
return 0.0 return 0.0
@ -43,19 +44,19 @@ def linear(middle, pos):
return 0.5 + 0.5 * pos / middle return 0.5 + 0.5 * pos / middle
def curved(middle, pos): def curved(middle: float, pos: float) -> float:
return pos ** (log(0.5) / log(max(middle, EPSILON))) return pos ** (log(0.5) / log(max(middle, EPSILON)))
def sine(middle, pos): def sine(middle: float, pos: float) -> float:
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
def sphere_increasing(middle, pos): def sphere_increasing(middle: float, pos: float) -> float:
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
def sphere_decreasing(middle, pos): def sphere_decreasing(middle: float, pos: float) -> float:
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
@ -64,9 +65,22 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
class GradientFile: class GradientFile:
gradient = None gradient: (
list[
tuple[
float,
float,
float,
list[float],
list[float],
Callable[[float, float], float],
]
]
| None
) = None
def getpalette(self, entries=256): def getpalette(self, entries: int = 256) -> tuple[bytes, str]:
assert self.gradient is not None
palette = [] palette = []
ix = 0 ix = 0
@ -101,7 +115,7 @@ class GradientFile:
class GimpGradientFile(GradientFile): class GimpGradientFile(GradientFile):
"""File handler for GIMP's gradient format.""" """File handler for GIMP's gradient format."""
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
if fp.readline()[:13] != b"GIMP Gradient": if fp.readline()[:13] != b"GIMP Gradient":
msg = "not a GIMP gradient file" msg = "not a GIMP gradient file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -114,7 +128,7 @@ class GimpGradientFile(GradientFile):
count = int(line) count = int(line)
gradient = [] self.gradient = []
for i in range(count): for i in range(count):
s = fp.readline().split() s = fp.readline().split()
@ -132,6 +146,4 @@ class GimpGradientFile(GradientFile):
msg = "cannot handle HSV colour space" msg = "cannot handle HSV colour space"
raise OSError(msg) raise OSError(msg)
gradient.append((x0, x1, xm, rgb0, rgb1, segment)) self.gradient.append((x0, x1, xm, rgb0, rgb1, segment))
self.gradient = gradient

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: ImageFile.StubHandler) -> None: def register_handler(handler: ImageFile.StubHandler | None) -> None:
""" """
Install application-specific GRIB image handler. Install application-specific GRIB image handler.
@ -58,7 +60,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
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 = "GRIB save handler not installed" msg = "GRIB save handler not installed"
raise OSError(msg) raise OSError(msg)

View File

@ -17,7 +17,7 @@ from . import Image, ImageFile
_handler = None _handler = None
def register_handler(handler: ImageFile.StubHandler) -> None: def register_handler(handler: ImageFile.StubHandler | None) -> None:
""" """
Install application-specific HDF5 image handler. Install application-specific HDF5 image handler.
@ -60,7 +60,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
return _handler 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"): if _handler is None or not hasattr(_handler, "save"):
msg = "HDF5 save handler not installed" msg = "HDF5 save handler not installed"
raise OSError(msg) raise OSError(msg)

View File

@ -22,6 +22,7 @@ import io
import os import os
import struct import struct
import sys import sys
from typing import IO
from . import Image, ImageFile, PngImagePlugin, features from . import Image, ImageFile, PngImagePlugin, features
@ -312,7 +313,7 @@ class IcnsImageFile(ImageFile.ImageFile):
return px return px
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
""" """
Saves the image as a series of PNG files, Saves the image as a series of PNG files,
that are then combined into a .icns file. that are then combined into a .icns file.
@ -346,29 +347,27 @@ def _save(im, fp, filename):
entries = [] entries = []
for type, size in sizes.items(): for type, size in sizes.items():
stream = size_streams[size] stream = size_streams[size]
entries.append( entries.append((type, HEADERSIZE + len(stream), stream))
{"type": type, "size": HEADERSIZE + len(stream), "stream": stream}
)
# Header # Header
fp.write(MAGIC) fp.write(MAGIC)
file_length = HEADERSIZE # Header file_length = HEADERSIZE # Header
file_length += HEADERSIZE + 8 * len(entries) # TOC file_length += HEADERSIZE + 8 * len(entries) # TOC
file_length += sum(entry["size"] for entry in entries) file_length += sum(entry[1] for entry in entries)
fp.write(struct.pack(">i", file_length)) fp.write(struct.pack(">i", file_length))
# TOC # TOC
fp.write(b"TOC ") fp.write(b"TOC ")
fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
for entry in entries: for entry in entries:
fp.write(entry["type"]) fp.write(entry[0])
fp.write(struct.pack(">i", entry["size"])) fp.write(struct.pack(">i", entry[1]))
# Data # Data
for entry in entries: for entry in entries:
fp.write(entry["type"]) fp.write(entry[0])
fp.write(struct.pack(">i", entry["size"])) fp.write(struct.pack(">i", entry[1]))
fp.write(entry["stream"]) fp.write(entry[2])
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()

View File

@ -25,6 +25,7 @@ from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO
from math import ceil, log from math import ceil, log
from typing import IO
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16 from ._binary import i16le as i16
@ -39,7 +40,7 @@ from ._binary import o32le as o32
_MAGIC = b"\0\0\1\0" _MAGIC = b"\0\0\1\0"
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(_MAGIC) # (2+2) fp.write(_MAGIC) # (2+2)
bmp = im.encoderinfo.get("bitmap_format") == "bmp" bmp = im.encoderinfo.get("bitmap_format") == "bmp"
sizes = im.encoderinfo.get( sizes = im.encoderinfo.get(
@ -194,7 +195,7 @@ class IcoFile:
""" """
return self.frame(self.getentryindex(size, bpp)) return self.frame(self.getentryindex(size, bpp))
def frame(self, idx): def frame(self, idx: int) -> Image.Image:
""" """
Get an image from frame idx Get an image from frame idx
""" """
@ -205,6 +206,7 @@ class IcoFile:
data = self.buf.read(8) data = self.buf.read(8)
self.buf.seek(header["offset"]) self.buf.seek(header["offset"])
im: Image.Image
if data[:8] == PngImagePlugin._MAGIC: if data[:8] == PngImagePlugin._MAGIC:
# png frame # png frame
im = PngImagePlugin.PngImageFile(self.buf) im = PngImagePlugin.PngImageFile(self.buf)

View File

@ -28,6 +28,7 @@ from __future__ import annotations
import os import os
import re import re
from typing import IO, Any
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
@ -103,7 +104,7 @@ for j in range(2, 33):
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
def number(s): def number(s: Any) -> float:
try: try:
return int(s) return int(s)
except ValueError: except ValueError:
@ -325,7 +326,7 @@ SAVE = {
} }
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
image_type, rawmode = SAVE[im.mode] image_type, rawmode = SAVE[im.mode]
except KeyError as e: except KeyError as e:
@ -340,6 +341,8 @@ def _save(im, fp, filename):
# or: SyntaxError("not an IM file") # or: SyntaxError("not an IM file")
# 8 characters are used for "Name: " and "\r\n" # 8 characters are used for "Name: " and "\r\n"
# Keep just the filename, ditch the potentially overlong path # Keep just the filename, ditch the potentially overlong path
if isinstance(filename, bytes):
filename = filename.decode("ascii")
name, ext = os.path.splitext(os.path.basename(filename)) name, ext = os.path.splitext(os.path.basename(filename))
name = "".join([name[: 92 - len(ext)], ext]) name = "".join([name[: 92 - len(ext)], ext])

View File

@ -41,7 +41,7 @@ import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping
from enum import IntEnum from enum import IntEnum
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, Tuple, cast
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -410,7 +410,9 @@ def init() -> bool:
# Codec factories (used by tobytes/frombytes and ImageFile.load) # Codec factories (used by tobytes/frombytes and ImageFile.load)
def _getdecoder(mode, decoder_name, args, extra=()): def _getdecoder(
mode: str, decoder_name: str, args: Any, extra: tuple[Any, ...] = ()
) -> core.ImagingDecoder | ImageFile.PyDecoder:
# tweak arguments # tweak arguments
if args is None: if args is None:
args = () args = ()
@ -433,7 +435,9 @@ def _getdecoder(mode, decoder_name, args, extra=()):
return decoder(mode, *args + extra) return decoder(mode, *args + extra)
def _getencoder(mode, encoder_name, args, extra=()): def _getencoder(
mode: str, encoder_name: str, args: Any, extra: tuple[Any, ...] = ()
) -> core.ImagingEncoder | ImageFile.PyEncoder:
# tweak arguments # tweak arguments
if args is None: if args is None:
args = () args = ()
@ -503,6 +507,12 @@ def _getscaleoffset(expr):
# Implementation wrapper # Implementation wrapper
class SupportsGetData(Protocol):
def getdata(
self,
) -> tuple[Transform, Sequence[int]]: ...
class Image: class Image:
""" """
This class represents an image object. To create This class represents an image object. To create
@ -544,10 +554,10 @@ class Image:
return self._size return self._size
@property @property
def mode(self): def mode(self) -> str:
return self._mode return self._mode
def _new(self, im) -> Image: def _new(self, im: core.ImagingCore) -> Image:
new = Image() new = Image()
new.im = im new.im = im
new._mode = im.mode new._mode = im.mode
@ -620,7 +630,7 @@ class Image:
self.load() self.load()
def _dump( def _dump(
self, file: str | None = None, format: str | None = None, **options self, file: str | None = None, format: str | None = None, **options: Any
) -> str: ) -> str:
suffix = "" suffix = ""
if format: if format:
@ -643,10 +653,12 @@ class Image:
return filename return filename
def __eq__(self, other): def __eq__(self, other: object) -> bool:
if self.__class__ is not other.__class__:
return False
assert isinstance(other, Image)
return ( return (
self.__class__ is other.__class__ self.mode == other.mode
and self.mode == other.mode
and self.size == other.size and self.size == other.size
and self.info == other.info and self.info == other.info
and self.getpalette() == other.getpalette() and self.getpalette() == other.getpalette()
@ -679,7 +691,7 @@ class Image:
) )
) )
def _repr_image(self, image_format, **kwargs): def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None:
"""Helper function for iPython display hook. """Helper function for iPython display hook.
:param image_format: Image format. :param image_format: Image format.
@ -692,14 +704,14 @@ class Image:
return None return None
return b.getvalue() return b.getvalue()
def _repr_png_(self): def _repr_png_(self) -> bytes | None:
"""iPython display hook support for PNG format. """iPython display hook support for PNG format.
:returns: PNG version of the image as bytes :returns: PNG version of the image as bytes
""" """
return self._repr_image("PNG", compress_level=1) return self._repr_image("PNG", compress_level=1)
def _repr_jpeg_(self): def _repr_jpeg_(self) -> bytes | None:
"""iPython display hook support for JPEG format. """iPython display hook support for JPEG format.
:returns: JPEG version of the image as bytes :returns: JPEG version of the image as bytes
@ -746,7 +758,7 @@ class Image:
self.putpalette(palette) self.putpalette(palette)
self.frombytes(data) self.frombytes(data)
def tobytes(self, encoder_name: str = "raw", *args) -> bytes: def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes:
""" """
Return image as a bytes object. Return image as a bytes object.
@ -768,12 +780,13 @@ class Image:
:returns: A :py:class:`bytes` object. :returns: A :py:class:`bytes` object.
""" """
# may pass tuple instead of argument list encoder_args: Any = args
if len(args) == 1 and isinstance(args[0], tuple): if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple):
args = args[0] # may pass tuple instead of argument list
encoder_args = encoder_args[0]
if encoder_name == "raw" and args == (): if encoder_name == "raw" and encoder_args == ():
args = self.mode encoder_args = self.mode
self.load() self.load()
@ -781,7 +794,7 @@ class Image:
return b"" return b""
# unpack data # unpack data
e = _getencoder(self.mode, encoder_name, args) e = _getencoder(self.mode, encoder_name, encoder_args)
e.setimage(self.im) e.setimage(self.im)
bufsize = max(65536, self.size[0] * 4) # see RawEncode.c bufsize = max(65536, self.size[0] * 4) # see RawEncode.c
@ -824,7 +837,9 @@ class Image:
] ]
) )
def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: def frombytes(
self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any
) -> None:
""" """
Loads this image with pixel data from a bytes object. Loads this image with pixel data from a bytes object.
@ -835,16 +850,17 @@ class Image:
if self.width == 0 or self.height == 0: if self.width == 0 or self.height == 0:
return return
# may pass tuple instead of argument list decoder_args: Any = args
if len(args) == 1 and isinstance(args[0], tuple): if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
args = args[0] # may pass tuple instead of argument list
decoder_args = decoder_args[0]
# default format # default format
if decoder_name == "raw" and args == (): if decoder_name == "raw" and decoder_args == ():
args = self.mode decoder_args = self.mode
# unpack data # unpack data
d = _getdecoder(self.mode, decoder_name, args) d = _getdecoder(self.mode, decoder_name, decoder_args)
d.setimage(self.im) d.setimage(self.im)
s = d.decode(data) s = d.decode(data)
@ -988,9 +1004,11 @@ class Image:
if has_transparency and self.im.bands == 3: if has_transparency and self.im.bands == 3:
transparency = new_im.info["transparency"] transparency = new_im.info["transparency"]
def convert_transparency(m, v): def convert_transparency(
v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 m: tuple[float, ...], v: tuple[int, int, int]
return max(0, min(255, int(v))) ) -> int:
value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
return max(0, min(255, int(value)))
if mode == "L": if mode == "L":
transparency = convert_transparency(matrix, transparency) transparency = convert_transparency(matrix, transparency)
@ -1242,7 +1260,7 @@ class Image:
__copy__ = copy __copy__ = copy
def crop(self, box: tuple[int, int, int, int] | None = None) -> Image: def crop(self, box: tuple[float, float, float, float] | None = None) -> Image:
""" """
Returns a rectangular region from this image. The box is a Returns a rectangular region from this image. The box is a
4-tuple defining the left, upper, right, and lower pixel 4-tuple defining the left, upper, right, and lower pixel
@ -1268,7 +1286,9 @@ class Image:
self.load() self.load()
return self._new(self._crop(self.im, box)) return self._new(self._crop(self.im, box))
def _crop(self, im, box): def _crop(
self, im: core.ImagingCore, box: tuple[float, float, float, float]
) -> core.ImagingCore:
""" """
Returns a rectangular region from the core image object im. Returns a rectangular region from the core image object im.
@ -1289,7 +1309,7 @@ class Image:
return im.crop((x0, y0, x1, y1)) return im.crop((x0, y0, x1, y1))
def draft( def draft(
self, mode: str, size: tuple[int, int] self, mode: str | None, size: tuple[int, int]
) -> tuple[str, tuple[int, int, float, float]] | None: ) -> tuple[str, tuple[int, int, float, float]] | None:
""" """
Configures the image file loader so it returns a version of the Configures the image file loader so it returns a version of the
@ -1359,7 +1379,7 @@ class Image:
""" """
return ImageMode.getmode(self.mode).bands return ImageMode.getmode(self.mode).bands
def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None:
""" """
Calculates the bounding box of the non-zero regions in the Calculates the bounding box of the non-zero regions in the
image. image.
@ -1447,7 +1467,7 @@ class Image:
:returns: XMP tags in a dictionary. :returns: XMP tags in a dictionary.
""" """
def get_name(tag): def get_name(tag: str) -> str:
return re.sub("^{[^}]+}", "", tag) return re.sub("^{[^}]+}", "", tag)
def get_value(element): def get_value(element):
@ -1549,7 +1569,11 @@ class Image:
fp = io.BytesIO(data) fp = io.BytesIO(data)
with open(fp) as im: with open(fp) as im:
if thumbnail_offset is None: from . import TiffImagePlugin
if thumbnail_offset is None and isinstance(
im, TiffImagePlugin.TiffImageFile
):
im._frame_pos = [ifd_offset] im._frame_pos = [ifd_offset]
im._seek(0) im._seek(0)
im.load() im.load()
@ -1717,7 +1741,12 @@ class Image:
return self.im.entropy(extrema) return self.im.entropy(extrema)
return self.im.entropy() return self.im.entropy()
def paste(self, im, box=None, mask=None) -> None: def paste(
self,
im: Image | str | float | tuple[float, ...],
box: tuple[int, int, int, int] | tuple[int, int] | None = None,
mask: Image | None = None,
) -> None:
""" """
Pastes another image into this image. The box argument is either Pastes another image into this image. The box argument is either
a 2-tuple giving the upper left corner, a 4-tuple defining the a 2-tuple giving the upper left corner, a 4-tuple defining the
@ -1745,7 +1774,7 @@ class Image:
See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to
combine images with respect to their alpha channels. combine images with respect to their alpha channels.
:param im: Source image or pixel value (integer or tuple). :param im: Source image or pixel value (integer, float or tuple).
:param box: An optional 4-tuple giving the region to paste into. :param box: An optional 4-tuple giving the region to paste into.
If a 2-tuple is used instead, it's treated as the upper left If a 2-tuple is used instead, it's treated as the upper left
corner. If omitted or None, the source is pasted into the corner. If omitted or None, the source is pasted into the
@ -1798,7 +1827,9 @@ class Image:
else: else:
self.im.paste(im, box) self.im.paste(im, box)
def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): def alpha_composite(
self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0)
) -> None:
"""'In-place' analog of Image.alpha_composite. Composites an image """'In-place' analog of Image.alpha_composite. Composites an image
onto this image. onto this image.
@ -1813,32 +1844,35 @@ class Image:
""" """
if not isinstance(source, (list, tuple)): if not isinstance(source, (list, tuple)):
msg = "Source must be a tuple" msg = "Source must be a list or tuple"
raise ValueError(msg) raise ValueError(msg)
if not isinstance(dest, (list, tuple)): if not isinstance(dest, (list, tuple)):
msg = "Destination must be a tuple" msg = "Destination must be a list or tuple"
raise ValueError(msg) raise ValueError(msg)
if len(source) not in (2, 4):
msg = "Source must be a 2 or 4-tuple" if len(source) == 4:
overlay_crop_box = tuple(source)
elif len(source) == 2:
overlay_crop_box = tuple(source) + im.size
else:
msg = "Source must be a sequence of length 2 or 4"
raise ValueError(msg) raise ValueError(msg)
if not len(dest) == 2: if not len(dest) == 2:
msg = "Destination must be a 2-tuple" msg = "Destination must be a sequence of length 2"
raise ValueError(msg) raise ValueError(msg)
if min(source) < 0: if min(source) < 0:
msg = "Source must be non-negative" msg = "Source must be non-negative"
raise ValueError(msg) raise ValueError(msg)
if len(source) == 2: # over image, crop if it's not the whole image.
source = source + im.size if overlay_crop_box == (0, 0) + im.size:
# over image, crop if it's not the whole thing.
if source == (0, 0) + im.size:
overlay = im overlay = im
else: else:
overlay = im.crop(source) overlay = im.crop(overlay_crop_box)
# target for the paste # target for the paste
box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) box = tuple(dest) + (dest[0] + overlay.width, dest[1] + overlay.height)
# destination image. don't copy if we're using the whole image. # destination image. don't copy if we're using the whole image.
if box == (0, 0) + self.size: if box == (0, 0) + self.size:
@ -1849,7 +1883,11 @@ class Image:
result = alpha_composite(background, overlay) result = alpha_composite(background, overlay)
self.paste(result, box) self.paste(result, box)
def point(self, lut, mode: str | None = None) -> Image: def point(
self,
lut: Sequence[float] | Callable[[int], float] | ImagePointHandler,
mode: str | None = None,
) -> Image:
""" """
Maps this image through a lookup table or function. Maps this image through a lookup table or function.
@ -1886,7 +1924,9 @@ class Image:
scale, offset = _getscaleoffset(lut) scale, offset = _getscaleoffset(lut)
return self._new(self.im.point_transform(scale, offset)) return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table # for other modes, convert the function to a table
lut = [lut(i) for i in range(256)] * self.im.bands flatLut = [lut(i) for i in range(256)] * self.im.bands
else:
flatLut = lut
if self.mode == "F": if self.mode == "F":
# FIXME: _imaging returns a confusing error message for this case # FIXME: _imaging returns a confusing error message for this case
@ -1894,8 +1934,8 @@ class Image:
raise ValueError(msg) raise ValueError(msg)
if mode != "F": if mode != "F":
lut = [round(i) for i in lut] flatLut = [round(i) for i in flatLut]
return self._new(self.im.point(lut, mode)) return self._new(self.im.point(flatLut, mode))
def putalpha(self, alpha): def putalpha(self, alpha):
""" """
@ -2154,7 +2194,13 @@ class Image:
min(self.size[1], math.ceil(box[3] + support_y)), min(self.size[1], math.ceil(box[3] + support_y)),
) )
def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: def resize(
self,
size: tuple[int, int],
resample: int | None = None,
box: tuple[float, float, float, float] | None = None,
reducing_gap: float | None = None,
) -> Image:
""" """
Returns a resized copy of this image. Returns a resized copy of this image.
@ -2219,13 +2265,9 @@ class Image:
msg = "reducing_gap must be 1.0 or greater" msg = "reducing_gap must be 1.0 or greater"
raise ValueError(msg) raise ValueError(msg)
size = tuple(size)
self.load() self.load()
if box is None: if box is None:
box = (0, 0) + self.size box = (0, 0) + self.size
else:
box = tuple(box)
if self.size == size and box == (0, 0) + self.size: if self.size == size and box == (0, 0) + self.size:
return self.copy() return self.copy()
@ -2260,7 +2302,11 @@ class Image:
return self._new(self.im.resize(size, resample, box)) return self._new(self.im.resize(size, resample, box))
def reduce(self, factor, box=None): def reduce(
self,
factor: int | tuple[int, int],
box: tuple[int, int, int, int] | None = None,
) -> Image:
""" """
Returns a copy of the image reduced ``factor`` times. Returns a copy of the image reduced ``factor`` times.
If the size of the image is not dividable by ``factor``, If the size of the image is not dividable by ``factor``,
@ -2278,8 +2324,6 @@ class Image:
if box is None: if box is None:
box = (0, 0) + self.size box = (0, 0) + self.size
else:
box = tuple(box)
if factor == (1, 1) and box == (0, 0) + self.size: if factor == (1, 1) and box == (0, 0) + self.size:
return self.copy() return self.copy()
@ -2295,13 +2339,13 @@ class Image:
def rotate( def rotate(
self, self,
angle, angle: float,
resample=Resampling.NEAREST, resample: Resampling = Resampling.NEAREST,
expand=0, expand: int | bool = False,
center=None, center: tuple[int, int] | None = None,
translate=None, translate: tuple[int, int] | None = None,
fillcolor=None, fillcolor: float | tuple[float, ...] | str | None = None,
): ) -> Image:
""" """
Returns a rotated copy of this image. This method returns a Returns a rotated copy of this image. This method returns a
copy of this image, rotated the given number of degrees counter copy of this image, rotated the given number of degrees counter
@ -2463,7 +2507,7 @@ class Image:
save_all = params.pop("save_all", False) save_all = params.pop("save_all", False)
self.encoderinfo = params self.encoderinfo = params
self.encoderconfig = () self.encoderconfig: tuple[Any, ...] = ()
preinit() preinit()
@ -2608,7 +2652,12 @@ class Image:
""" """
return 0 return 0
def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): def thumbnail(
self,
size: tuple[float, float],
resample: Resampling = Resampling.BICUBIC,
reducing_gap: float = 2.0,
) -> None:
""" """
Make this image into a thumbnail. This method modifies the Make this image into a thumbnail. This method modifies the
image to contain a thumbnail version of itself, no larger than image to contain a thumbnail version of itself, no larger than
@ -2669,20 +2718,24 @@ class Image:
box = None box = None
if reducing_gap is not None: if reducing_gap is not None:
size = preserve_aspect_ratio() preserved_size = preserve_aspect_ratio()
if size is None: if preserved_size is None:
return return
size = preserved_size
res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) res = self.draft(
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
)
if res is not None: if res is not None:
box = res[1] box = res[1]
if box is None: if box is None:
self.load() self.load()
# load() may have changed the size of the image # load() may have changed the size of the image
size = preserve_aspect_ratio() preserved_size = preserve_aspect_ratio()
if size is None: if preserved_size is None:
return return
size = preserved_size
if self.size != size: if self.size != size:
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
@ -2698,12 +2751,12 @@ class Image:
# instead of bloating the method docs, add a separate chapter. # instead of bloating the method docs, add a separate chapter.
def transform( def transform(
self, self,
size, size: tuple[int, int],
method, method: Transform | ImageTransformHandler | SupportsGetData,
data=None, data: Sequence[Any] | None = None,
resample=Resampling.NEAREST, resample: int = Resampling.NEAREST,
fill=1, fill: int = 1,
fillcolor=None, fillcolor: float | tuple[float, ...] | str | None = None,
) -> Image: ) -> Image:
""" """
Transforms this image. This method creates a new image with the Transforms this image. This method creates a new image with the
@ -2867,7 +2920,7 @@ class Image:
if image.mode in ("1", "P"): if image.mode in ("1", "P"):
resample = Resampling.NEAREST resample = Resampling.NEAREST
self.im.transform2(box, image.im, method, data, resample, fill) self.im.transform(box, image.im, method, data, resample, fill)
def transpose(self, method: Transpose) -> Image: def transpose(self, method: Transpose) -> Image:
""" """
@ -2883,7 +2936,7 @@ class Image:
self.load() self.load()
return self._new(self.im.transpose(method)) return self._new(self.im.transpose(method))
def effect_spread(self, distance): def effect_spread(self, distance: int) -> Image:
""" """
Randomly spread pixels in an image. Randomly spread pixels in an image.
@ -2937,7 +2990,7 @@ class ImageTransformHandler:
self, self,
size: tuple[int, int], size: tuple[int, int],
image: Image, image: Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]], **options: Any,
) -> Image: ) -> Image:
pass pass
@ -2949,35 +3002,35 @@ class ImageTransformHandler:
# Debugging # Debugging
def _wedge(): def _wedge() -> Image:
"""Create grayscale wedge (for debugging only)""" """Create grayscale wedge (for debugging only)"""
return Image()._new(core.wedge("L")) return Image()._new(core.wedge("L"))
def _check_size(size): def _check_size(size: Any) -> None:
""" """
Common check to enforce type and sanity check on size tuples Common check to enforce type and sanity check on size tuples
:param size: Should be a 2 tuple of (width, height) :param size: Should be a 2 tuple of (width, height)
:returns: True, or raises a ValueError :returns: None, or raises a ValueError
""" """
if not isinstance(size, (list, tuple)): if not isinstance(size, (list, tuple)):
msg = "Size must be a tuple" msg = "Size must be a list or tuple"
raise ValueError(msg) raise ValueError(msg)
if len(size) != 2: if len(size) != 2:
msg = "Size must be a tuple of length 2" msg = "Size must be a sequence of length 2"
raise ValueError(msg) raise ValueError(msg)
if size[0] < 0 or size[1] < 0: if size[0] < 0 or size[1] < 0:
msg = "Width and height must be >= 0" msg = "Width and height must be >= 0"
raise ValueError(msg) raise ValueError(msg)
return True
def new( def new(
mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0 mode: str,
size: tuple[int, int] | list[int],
color: float | tuple[float, ...] | str | None = 0,
) -> Image: ) -> Image:
""" """
Creates a new image with the given mode and size. Creates a new image with the given mode and size.
@ -3011,16 +3064,28 @@ def new(
color = ImageColor.getcolor(color, mode) color = ImageColor.getcolor(color, mode)
im = Image() im = Image()
if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]: if (
# RGB or RGBA value for a P image mode == "P"
from . import ImagePalette and isinstance(color, (list, tuple))
and all(isinstance(i, int) for i in color)
):
color_ints: tuple[int, ...] = cast(Tuple[int, ...], tuple(color))
if len(color_ints) == 3 or len(color_ints) == 4:
# RGB or RGBA value for a P image
from . import ImagePalette
im.palette = ImagePalette.ImagePalette() im.palette = ImagePalette.ImagePalette()
color = im.palette.getcolor(color) color = im.palette.getcolor(color_ints)
return im._new(core.fill(mode, size, color)) return im._new(core.fill(mode, size, color))
def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: def frombytes(
mode: str,
size: tuple[int, int],
data: bytes | bytearray,
decoder_name: str = "raw",
*args: Any,
) -> Image:
""" """
Creates a copy of an image memory from pixel data in a buffer. Creates a copy of an image memory from pixel data in a buffer.
@ -3048,18 +3113,21 @@ def frombytes(mode, size, data, decoder_name="raw", *args) -> Image:
im = new(mode, size) im = new(mode, size)
if im.width != 0 and im.height != 0: if im.width != 0 and im.height != 0:
# may pass tuple instead of argument list decoder_args: Any = args
if len(args) == 1 and isinstance(args[0], tuple): if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
args = args[0] # may pass tuple instead of argument list
decoder_args = decoder_args[0]
if decoder_name == "raw" and args == (): if decoder_name == "raw" and decoder_args == ():
args = mode decoder_args = mode
im.frombytes(data, decoder_name, args) im.frombytes(data, decoder_name, decoder_args)
return im return im
def frombuffer(mode, size, data, decoder_name="raw", *args) -> Image: def frombuffer(
mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any
) -> Image:
""" """
Creates an image memory referencing pixel data in a byte buffer. Creates an image memory referencing pixel data in a byte buffer.
@ -3516,7 +3584,7 @@ def merge(mode: str, bands: Sequence[Image]) -> Image:
def register_open( def register_open(
id, id: str,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
accept: Callable[[bytes], bool | str] | None = None, accept: Callable[[bytes], bool | str] | None = None,
) -> None: ) -> None:
@ -3550,7 +3618,9 @@ def register_mime(id: str, mimetype: str) -> None:
MIME[id.upper()] = mimetype MIME[id.upper()] = mimetype
def register_save(id: str, driver) -> None: def register_save(
id: str, driver: Callable[[Image, IO[bytes], str | bytes], None]
) -> None:
""" """
Registers an image save function. This function should not be Registers an image save function. This function should not be
used in application code. used in application code.
@ -3561,7 +3631,9 @@ def register_save(id: str, driver) -> None:
SAVE[id.upper()] = driver SAVE[id.upper()] = driver
def register_save_all(id, driver) -> None: def register_save_all(
id: str, driver: Callable[[Image, IO[bytes], str | bytes], None]
) -> None:
""" """
Registers an image function to save all the frames Registers an image function to save all the frames
of a multiframe format. This function should not be of a multiframe format. This function should not be
@ -3573,7 +3645,7 @@ def register_save_all(id, driver) -> None:
SAVE_ALL[id.upper()] = driver SAVE_ALL[id.upper()] = driver
def register_extension(id, extension) -> None: def register_extension(id: str, extension: str) -> None:
""" """
Registers an image extension. This function should not be Registers an image extension. This function should not be
used in application code. used in application code.
@ -3584,7 +3656,7 @@ def register_extension(id, extension) -> None:
EXTENSION[extension.lower()] = id.upper() EXTENSION[extension.lower()] = id.upper()
def register_extensions(id, extensions) -> None: def register_extensions(id: str, extensions: list[str]) -> None:
""" """
Registers image extensions. This function should not be Registers image extensions. This function should not be
used in application code. used in application code.
@ -3596,7 +3668,7 @@ def register_extensions(id, extensions) -> None:
register_extension(id, extension) register_extension(id, extension)
def registered_extensions(): def registered_extensions() -> dict[str, str]:
""" """
Returns a dictionary containing all file extensions belonging Returns a dictionary containing all file extensions belonging
to registered plugins to registered plugins
@ -3635,7 +3707,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None:
# Simple display support. # Simple display support.
def _show(image, **options) -> None: def _show(image: Image, **options: Any) -> None:
from . import ImageShow from . import ImageShow
ImageShow.show(image, **options) ImageShow.show(image, **options)
@ -3645,7 +3717,9 @@ def _show(image, **options) -> None:
# Effects # Effects
def effect_mandelbrot(size, extent, quality): def effect_mandelbrot(
size: tuple[int, int], extent: tuple[float, float, float, float], quality: int
) -> Image:
""" """
Generate a Mandelbrot set covering the given extent. Generate a Mandelbrot set covering the given extent.
@ -3658,7 +3732,7 @@ def effect_mandelbrot(size, extent, quality):
return Image()._new(core.effect_mandelbrot(size, extent, quality)) return Image()._new(core.effect_mandelbrot(size, extent, quality))
def effect_noise(size, sigma): def effect_noise(size: tuple[int, int], sigma: float) -> Image:
""" """
Generate Gaussian noise centered around 128. Generate Gaussian noise centered around 128.
@ -3669,7 +3743,7 @@ def effect_noise(size, sigma):
return Image()._new(core.effect_noise(size, sigma)) return Image()._new(core.effect_noise(size, sigma))
def linear_gradient(mode): def linear_gradient(mode: str) -> Image:
""" """
Generate 256x256 linear gradient from black to white, top to bottom. Generate 256x256 linear gradient from black to white, top to bottom.
@ -3678,7 +3752,7 @@ def linear_gradient(mode):
return Image()._new(core.linear_gradient(mode)) return Image()._new(core.linear_gradient(mode))
def radial_gradient(mode): def radial_gradient(mode: str) -> Image:
""" """
Generate 256x256 radial gradient from black to white, centre to edge. Generate 256x256 radial gradient from black to white, centre to edge.
@ -3691,19 +3765,18 @@ def radial_gradient(mode):
# Resources # Resources
def _apply_env_variables(env=None) -> None: def _apply_env_variables(env: dict[str, str] | None = None) -> None:
if env is None: env_dict = env if env is not None else os.environ
env = os.environ
for var_name, setter in [ for var_name, setter in [
("PILLOW_ALIGNMENT", core.set_alignment), ("PILLOW_ALIGNMENT", core.set_alignment),
("PILLOW_BLOCK_SIZE", core.set_block_size), ("PILLOW_BLOCK_SIZE", core.set_block_size),
("PILLOW_BLOCKS_MAX", core.set_blocks_max), ("PILLOW_BLOCKS_MAX", core.set_blocks_max),
]: ]:
if var_name not in env: if var_name not in env_dict:
continue continue
var = env[var_name].lower() var = env_dict[var_name].lower()
units = 1 units = 1
for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]:
@ -3712,13 +3785,13 @@ def _apply_env_variables(env=None) -> None:
var = var[: -len(postfix)] var = var[: -len(postfix)]
try: try:
var = int(var) * units var_int = int(var) * units
except ValueError: except ValueError:
warnings.warn(f"{var_name} is not int") warnings.warn(f"{var_name} is not int")
continue continue
try: try:
setter(var) setter(var_int)
except ValueError as e: except ValueError as e:
warnings.warn(f"{var_name}: {e}") warnings.warn(f"{var_name}: {e}")

View File

@ -25,7 +25,7 @@ from . import Image
@lru_cache @lru_cache
def getrgb(color): def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]:
""" """
Convert a color string to an RGB or RGBA tuple. If the string cannot be Convert a color string to an RGB or RGBA tuple. If the string cannot be
parsed, this function raises a :py:exc:`ValueError` exception. parsed, this function raises a :py:exc:`ValueError` exception.
@ -44,8 +44,10 @@ def getrgb(color):
if rgb: if rgb:
if isinstance(rgb, tuple): if isinstance(rgb, tuple):
return rgb return rgb
colormap[color] = rgb = getrgb(rgb) rgb_tuple = getrgb(rgb)
return rgb assert len(rgb_tuple) == 3
colormap[color] = rgb_tuple
return rgb_tuple
# check for known string formats # check for known string formats
if re.match("#[a-f0-9]{3}$", color): if re.match("#[a-f0-9]{3}$", color):
@ -88,15 +90,15 @@ def getrgb(color):
if m: if m:
from colorsys import hls_to_rgb from colorsys import hls_to_rgb
rgb = hls_to_rgb( rgb_floats = hls_to_rgb(
float(m.group(1)) / 360.0, float(m.group(1)) / 360.0,
float(m.group(3)) / 100.0, float(m.group(3)) / 100.0,
float(m.group(2)) / 100.0, float(m.group(2)) / 100.0,
) )
return ( return (
int(rgb[0] * 255 + 0.5), int(rgb_floats[0] * 255 + 0.5),
int(rgb[1] * 255 + 0.5), int(rgb_floats[1] * 255 + 0.5),
int(rgb[2] * 255 + 0.5), int(rgb_floats[2] * 255 + 0.5),
) )
m = re.match( m = re.match(
@ -105,15 +107,15 @@ def getrgb(color):
if m: if m:
from colorsys import hsv_to_rgb from colorsys import hsv_to_rgb
rgb = hsv_to_rgb( rgb_floats = hsv_to_rgb(
float(m.group(1)) / 360.0, float(m.group(1)) / 360.0,
float(m.group(2)) / 100.0, float(m.group(2)) / 100.0,
float(m.group(3)) / 100.0, float(m.group(3)) / 100.0,
) )
return ( return (
int(rgb[0] * 255 + 0.5), int(rgb_floats[0] * 255 + 0.5),
int(rgb[1] * 255 + 0.5), int(rgb_floats[1] * 255 + 0.5),
int(rgb[2] * 255 + 0.5), int(rgb_floats[2] * 255 + 0.5),
) )
m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
@ -124,7 +126,7 @@ def getrgb(color):
@lru_cache @lru_cache
def getcolor(color, mode: str) -> tuple[int, ...]: def getcolor(color: str, mode: str) -> int | tuple[int, ...]:
""" """
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
@ -136,33 +138,34 @@ def getcolor(color, mode: str) -> tuple[int, ...]:
:param color: A color string :param color: A color string
:param mode: Convert result to this mode :param mode: Convert result to this mode
:return: ``(graylevel[, alpha]) or (red, green, blue[, alpha])`` :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])``
""" """
# same as getrgb, but converts the result to the given mode # same as getrgb, but converts the result to the given mode
color, alpha = getrgb(color), 255 rgb, alpha = getrgb(color), 255
if len(color) == 4: if len(rgb) == 4:
color, alpha = color[:3], color[3] alpha = rgb[3]
rgb = rgb[:3]
if mode == "HSV": if mode == "HSV":
from colorsys import rgb_to_hsv from colorsys import rgb_to_hsv
r, g, b = color r, g, b = rgb
h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255)
return int(h * 255), int(s * 255), int(v * 255) return int(h * 255), int(s * 255), int(v * 255)
elif Image.getmodebase(mode) == "L": elif Image.getmodebase(mode) == "L":
r, g, b = color r, g, b = rgb
# ITU-R Recommendation 601-2 for nonlinear RGB # ITU-R Recommendation 601-2 for nonlinear RGB
# scaled to 24 bits to match the convert's implementation. # scaled to 24 bits to match the convert's implementation.
color = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16
if mode[-1] == "A": if mode[-1] == "A":
return color, alpha return graylevel, alpha
else: return graylevel
if mode[-1] == "A": elif mode[-1] == "A":
return color + (alpha,) return rgb + (alpha,)
return color return rgb
colormap = { colormap: dict[str, str | tuple[int, int, int]] = {
# X11 colour table from https://drafts.csswg.org/css-color-4/, with # X11 colour table from https://drafts.csswg.org/css-color-4/, with
# gray/grey spelling issues fixed. This is a superset of HTML 4.0 # gray/grey spelling issues fixed. This is a superset of HTML 4.0
# colour names used in CSS 1. # colour names used in CSS 1.

View File

@ -34,11 +34,16 @@ from __future__ import annotations
import math import math
import numbers import numbers
import struct import struct
from typing import TYPE_CHECKING, Sequence, cast from types import ModuleType
from typing import TYPE_CHECKING, AnyStr, Sequence, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._deprecate import deprecate
from ._typing import Coords from ._typing import Coords
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont
""" """
A simple 2D drawing interface for PIL images. A simple 2D drawing interface for PIL images.
<p> <p>
@ -92,10 +97,9 @@ class ImageDraw:
self.fontmode = "L" # aliasing is okay for other modes self.fontmode = "L" # aliasing is okay for other modes
self.fill = False self.fill = False
if TYPE_CHECKING: def getfont(
from . import ImageFont self,
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
def getfont(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
""" """
Get the current default font. Get the current default font.
@ -120,14 +124,15 @@ class ImageDraw:
self.font = ImageFont.load_default() self.font = ImageFont.load_default()
return self.font return self.font
def _getfont(self, font_size: float | None): def _getfont(
self, font_size: float | None
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
if font_size is not None: if font_size is not None:
from . import ImageFont from . import ImageFont
font = ImageFont.load_default(font_size) return ImageFont.load_default(font_size)
else: else:
font = self.getfont() return self.getfont()
return font
def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: def _getink(self, ink, fill=None) -> tuple[int | None, int | None]:
if ink is None and fill is None: if ink is None and fill is None:
@ -216,7 +221,9 @@ class ImageDraw:
# This is a straight line, so no joint is required # This is a straight line, so no joint is required
continue continue
def coord_at_angle(coord, angle): def coord_at_angle(
coord: Sequence[float], angle: float
) -> tuple[float, float]:
x, y = coord x, y = coord
angle -= 90 angle -= 90
distance = width / 2 - 1 distance = width / 2 - 1
@ -460,15 +467,13 @@ class ImageDraw:
right[3] -= r + 1 right[3] -= r + 1
self.draw.draw_rectangle(right, ink, 1) self.draw.draw_rectangle(right, ink, 1)
def _multiline_check(self, text) -> bool: def _multiline_check(self, text: AnyStr) -> bool:
split_character = "\n" if isinstance(text, str) else b"\n" split_character = "\n" if isinstance(text, str) else b"\n"
return split_character in text return split_character in text
def _multiline_split(self, text) -> list[str | bytes]: def _multiline_split(self, text: AnyStr) -> list[AnyStr]:
split_character = "\n" if isinstance(text, str) else b"\n" return text.split("\n" if isinstance(text, str) else b"\n")
return text.split(split_character)
def _multiline_spacing(self, font, spacing, stroke_width): def _multiline_spacing(self, font, spacing, stroke_width):
return ( return (
@ -479,10 +484,15 @@ class ImageDraw:
def text( def text(
self, self,
xy, xy: tuple[float, float],
text, text: str,
fill=None, fill=None,
font=None, font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
anchor=None, anchor=None,
spacing=4, spacing=4,
align="left", align="left",
@ -536,7 +546,7 @@ class ImageDraw:
coord.append(int(xy[i])) coord.append(int(xy[i]))
start.append(math.modf(xy[i])[0]) start.append(math.modf(xy[i])[0])
try: try:
mask, offset = font.getmask2( mask, offset = font.getmask2( # type: ignore[union-attr,misc]
text, text,
mode, mode,
direction=direction, direction=direction,
@ -552,7 +562,7 @@ class ImageDraw:
coord = [coord[0] + offset[0], coord[1] + offset[1]] coord = [coord[0] + offset[0], coord[1] + offset[1]]
except AttributeError: except AttributeError:
try: try:
mask = font.getmask( mask = font.getmask( # type: ignore[misc]
text, text,
mode, mode,
direction, direction,
@ -601,10 +611,15 @@ class ImageDraw:
def multiline_text( def multiline_text(
self, self,
xy, xy: tuple[float, float],
text, text: str,
fill=None, fill=None,
font=None, font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
anchor=None, anchor=None,
spacing=4, spacing=4,
align="left", align="left",
@ -634,7 +649,7 @@ class ImageDraw:
font = self._getfont(font_size) font = self._getfont(font_size)
widths = [] widths = []
max_width = 0 max_width: float = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width) line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines: for line in lines:
@ -688,15 +703,20 @@ class ImageDraw:
def textlength( def textlength(
self, self,
text, text: str,
font=None, font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
direction=None, direction=None,
features=None, features=None,
language=None, language=None,
embedded_color=False, embedded_color=False,
*, *,
font_size=None, font_size=None,
): ) -> float:
"""Get the length of a given string, in pixels with 1/64 precision.""" """Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text): if self._multiline_check(text):
msg = "can't measure length of multiline text" msg = "can't measure length of multiline text"
@ -788,7 +808,7 @@ class ImageDraw:
font = self._getfont(font_size) font = self._getfont(font_size)
widths = [] widths = []
max_width = 0 max_width: float = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width) line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines: for line in lines:
@ -860,7 +880,7 @@ class ImageDraw:
return bbox return bbox
def Draw(im, mode: str | None = None) -> ImageDraw: def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
""" """
A simple 2D drawing interface for PIL images. A simple 2D drawing interface for PIL images.
@ -872,7 +892,7 @@ def Draw(im, mode: str | None = None) -> ImageDraw:
defaults to the mode of the image. defaults to the mode of the image.
""" """
try: try:
return im.getdraw(mode) return getattr(im, "getdraw")(mode)
except AttributeError: except AttributeError:
return ImageDraw(im, mode) return ImageDraw(im, mode)
@ -884,28 +904,20 @@ except AttributeError:
Outline = None Outline = None
def getdraw(im=None, hints=None): def getdraw(
im: Image.Image | None = None, hints: list[str] | None = None
) -> tuple[ImageDraw2.Draw | None, ModuleType]:
""" """
(Experimental) A more advanced 2D drawing interface for PIL images,
based on the WCK interface.
:param im: The image to draw in. :param im: The image to draw in.
:param hints: An optional list of hints. :param hints: An optional list of hints. Deprecated.
:returns: A (drawing context, drawing resource factory) tuple. :returns: A (drawing context, drawing resource factory) tuple.
""" """
# FIXME: this needs more work! if hints is not None:
# FIXME: come up with a better 'hints' scheme. deprecate("'hints' parameter", 12)
handler = None from . import ImageDraw2
if not hints or "nicest" in hints:
try: draw = ImageDraw2.Draw(im) if im is not None else None
from . import _imagingagg as handler return draw, ImageDraw2
except ImportError:
pass
if handler is None:
from . import ImageDraw2 as handler
if im:
im = handler.Draw(im)
return im, handler
def floodfill( def floodfill(
@ -1093,11 +1105,13 @@ def _compute_regular_polygon_vertices(
return [_compute_polygon_vertex(angle) for angle in angles] return [_compute_polygon_vertex(angle) for angle in angles]
def _color_diff(color1, color2: float | tuple[int, ...]) -> float: def _color_diff(
color1: float | tuple[int, ...], color2: float | tuple[int, ...]
) -> float:
""" """
Uses 1-norm distance to calculate difference between two values. Uses 1-norm distance to calculate difference between two values.
""" """
if isinstance(color2, tuple): first = color1 if isinstance(color1, tuple) else (color1,)
return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2))) second = color2 if isinstance(color2, tuple) else (color2,)
else:
return abs(color1 - color2) return sum(abs(first[i] - second[i]) for i in range(0, len(second)))

View File

@ -30,7 +30,7 @@ from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
class Pen: class Pen:
"""Stores an outline color and width.""" """Stores an outline color and width."""
def __init__(self, color, width=1, opacity=255): def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None:
self.color = ImageColor.getrgb(color) self.color = ImageColor.getrgb(color)
self.width = width self.width = width
@ -38,7 +38,7 @@ class Pen:
class Brush: class Brush:
"""Stores a fill color""" """Stores a fill color"""
def __init__(self, color, opacity=255): def __init__(self, color: str, opacity: int = 255) -> None:
self.color = ImageColor.getrgb(color) self.color = ImageColor.getrgb(color)
@ -63,7 +63,7 @@ class Draw:
self.image = image self.image = image
self.transform = None self.transform = None
def flush(self): def flush(self) -> Image.Image:
return self.image return self.image
def render(self, op, xy, pen, brush=None): def render(self, op, xy, pen, brush=None):

View File

@ -487,7 +487,7 @@ class Parser:
def __enter__(self): def __enter__(self):
return self return self
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.close() self.close()
def close(self): def close(self):
@ -763,7 +763,7 @@ class PyEncoder(PyCodec):
def pushes_fd(self): def pushes_fd(self):
return self._pushes_fd return self._pushes_fd
def encode(self, bufsize): def encode(self, bufsize: int) -> tuple[int, int, bytes]:
""" """
Override to perform the encoding process. Override to perform the encoding process.

View File

@ -33,11 +33,16 @@ import sys
import warnings import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from typing import BinaryIO from typing import IO, TYPE_CHECKING, Any, BinaryIO
from . import Image from . import Image
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
from ._util import is_directory, is_path from ._util import is_path
if TYPE_CHECKING:
from . import ImageFile
from ._imaging import ImagingFont
from ._imagingft import Font
class Layout(IntEnum): class Layout(IntEnum):
@ -56,7 +61,7 @@ except ImportError as ex:
core = DeferredError.new(ex) core = DeferredError.new(ex)
def _string_length_check(text): def _string_length_check(text: str | bytes | bytearray) -> None:
if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
msg = "too many characters in string" msg = "too many characters in string"
raise ValueError(msg) raise ValueError(msg)
@ -81,9 +86,11 @@ def _string_length_check(text):
class ImageFont: class ImageFont:
"""PIL font wrapper""" """PIL font wrapper"""
def _load_pilfont(self, filename): font: ImagingFont
def _load_pilfont(self, filename: str) -> None:
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
image = None image: ImageFile.ImageFile | None = None
for ext in (".png", ".gif", ".pbm"): for ext in (".png", ".gif", ".pbm"):
if image: if image:
image.close() image.close()
@ -106,7 +113,7 @@ class ImageFont:
self._load_pilfont_data(fp, image) self._load_pilfont_data(fp, image)
image.close() image.close()
def _load_pilfont_data(self, file, image): def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
# read PILfont header # read PILfont header
if file.readline() != b"PILfont\n": if file.readline() != b"PILfont\n":
msg = "Not a PILfont file" msg = "Not a PILfont file"
@ -153,7 +160,9 @@ class ImageFont:
Image._decompression_bomb_check(self.font.getsize(text)) Image._decompression_bomb_check(self.font.getsize(text))
return self.font.getmask(text, mode) return self.font.getmask(text, mode)
def getbbox(self, text, *args, **kwargs): def getbbox(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
) -> tuple[int, int, int, int]:
""" """
Returns bounding box (in pixels) of given text. Returns bounding box (in pixels) of given text.
@ -167,7 +176,9 @@ class ImageFont:
width, height = self.font.getsize(text) width, height = self.font.getsize(text)
return 0, 0, width, height return 0, 0, width, height
def getlength(self, text, *args, **kwargs): def getlength(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
) -> int:
""" """
Returns length (in pixels) of given text. Returns length (in pixels) of given text.
This is the amount by which following text should be offset. This is the amount by which following text should be offset.
@ -187,6 +198,8 @@ class ImageFont:
class FreeTypeFont: class FreeTypeFont:
"""FreeType font wrapper (requires _imagingft service)""" """FreeType font wrapper (requires _imagingft service)"""
font: Font
def __init__( def __init__(
self, self,
font: StrOrBytesPath | BinaryIO | None = None, font: StrOrBytesPath | BinaryIO | None = None,
@ -250,7 +263,7 @@ class FreeTypeFont:
path, size, index, encoding, layout_engine = state path, size, index, encoding, layout_engine = state
self.__init__(path, size, index, encoding, layout_engine) self.__init__(path, size, index, encoding, layout_engine)
def getname(self): def getname(self) -> tuple[str | None, str | None]:
""" """
:return: A tuple of the font family (e.g. Helvetica) and the font style :return: A tuple of the font family (e.g. Helvetica) and the font style
(e.g. Bold) (e.g. Bold)
@ -265,7 +278,9 @@ class FreeTypeFont:
""" """
return self.font.ascent, self.font.descent return self.font.ascent, self.font.descent
def getlength(self, text, mode="", direction=None, features=None, language=None): def getlength(
self, text: str, mode="", direction=None, features=None, language=None
) -> float:
""" """
Returns length (in pixels with 1/64 precision) of given text when rendered Returns length (in pixels with 1/64 precision) of given text when rendered
in font with provided direction, features, and language. in font with provided direction, features, and language.
@ -339,14 +354,14 @@ class FreeTypeFont:
def getbbox( def getbbox(
self, self,
text, text: str,
mode="", mode: str = "",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
anchor=None, anchor: str | None = None,
): ) -> tuple[float, float, float, float]:
""" """
Returns bounding box (in pixels) of given text relative to given anchor Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language. when rendered in font with provided direction, features, and language.
@ -496,7 +511,7 @@ class FreeTypeFont:
def getmask2( def getmask2(
self, self,
text, text: str,
mode="", mode="",
direction=None, direction=None,
features=None, features=None,
@ -666,10 +681,11 @@ class FreeTypeFont:
msg = "FreeType 2.9.1 or greater is required" msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e raise NotImplementedError(msg) from e
for axis in axes: for axis in axes:
axis["name"] = axis["name"].replace(b"\x00", b"") if axis["name"]:
axis["name"] = axis["name"].replace(b"\x00", b"")
return axes return axes
def set_variation_by_axes(self, axes): def set_variation_by_axes(self, axes: list[float]) -> None:
""" """
:param axes: A list of values for each axis. :param axes: A list of values for each axis.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
@ -714,14 +730,14 @@ class TransposedFont:
return 0, 0, height, width return 0, 0, height, width
return 0, 0, width, height return 0, 0, width, height
def getlength(self, text, *args, **kwargs): def getlength(self, text: str, *args, **kwargs) -> float:
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
msg = "text length is undefined for text rotated by 90 or 270 degrees" msg = "text length is undefined for text rotated by 90 or 270 degrees"
raise ValueError(msg) raise ValueError(msg)
return self.font.getlength(text, *args, **kwargs) return self.font.getlength(text, *args, **kwargs)
def load(filename): def load(filename: str) -> ImageFont:
""" """
Load a font file. This function loads a font object from the given Load a font file. This function loads a font object from the given
bitmap font file, and returns the corresponding font object. bitmap font file, and returns the corresponding font object.
@ -735,7 +751,13 @@ def load(filename):
return f return f
def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): def truetype(
font: StrOrBytesPath | BinaryIO | None = None,
size: float = 10,
index: int = 0,
encoding: str = "",
layout_engine: Layout | None = None,
) -> FreeTypeFont:
""" """
Load a TrueType or OpenType font from a file or file-like object, Load a TrueType or OpenType font from a file or file-like object,
and create a font object. and create a font object.
@ -796,7 +818,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
:exception ValueError: If the font size is not greater than zero. :exception ValueError: If the font size is not greater than zero.
""" """
def freetype(font): def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont:
return FreeTypeFont(font, size, index, encoding, layout_engine) return FreeTypeFont(font, size, index, encoding, layout_engine)
try: try:
@ -846,7 +868,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
raise raise
def load_path(filename): def load_path(filename: str | bytes) -> ImageFont:
""" """
Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a
bitmap font along the Python path. bitmap font along the Python path.
@ -855,14 +877,13 @@ def load_path(filename):
:return: A font object. :return: A font object.
:exception OSError: If the file could not be read. :exception OSError: If the file could not be read.
""" """
if not isinstance(filename, str):
filename = filename.decode("utf-8")
for directory in sys.path: for directory in sys.path:
if is_directory(directory): try:
if not isinstance(filename, str): return load(os.path.join(directory, filename))
filename = filename.decode("utf-8") except OSError:
try: pass
return load(os.path.join(directory, filename))
except OSError:
pass
msg = "cannot find font file" msg = "cannot find font file"
raise OSError(msg) raise OSError(msg)
@ -881,6 +902,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
:return: A font object. :return: A font object.
""" """
f: FreeTypeFont | ImageFont
if core.__class__.__name__ == "module" or size is not None: if core.__class__.__name__ == "module" or size is not None:
f = truetype( f = truetype(
BytesIO( BytesIO(

View File

@ -497,7 +497,7 @@ def expand(
color = _color(fill, image.mode) color = _color(fill, image.mode)
if image.palette: if image.palette:
palette = ImagePalette.ImagePalette(palette=image.getpalette()) palette = ImagePalette.ImagePalette(palette=image.getpalette())
if isinstance(color, tuple): if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
color = palette.getcolor(color) color = palette.getcolor(color)
else: else:
palette = None palette = None

View File

@ -18,10 +18,13 @@
from __future__ import annotations from __future__ import annotations
import array import array
from typing import IO, Sequence from typing import IO, TYPE_CHECKING, Sequence
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
if TYPE_CHECKING:
from . import Image
class ImagePalette: class ImagePalette:
""" """
@ -51,7 +54,7 @@ class ImagePalette:
self._palette = palette self._palette = palette
@property @property
def colors(self): def colors(self) -> dict[tuple[int, int, int] | tuple[int, int, int, int], int]:
if self._colors is None: if self._colors is None:
mode_len = len(self.mode) mode_len = len(self.mode)
self._colors = {} self._colors = {}
@ -63,7 +66,9 @@ class ImagePalette:
return self._colors return self._colors
@colors.setter @colors.setter
def colors(self, colors): def colors(
self, colors: dict[tuple[int, int, int] | tuple[int, int, int, int], int]
) -> None:
self._colors = colors self._colors = colors
def copy(self) -> ImagePalette: def copy(self) -> ImagePalette:
@ -104,11 +109,13 @@ class ImagePalette:
# Declare tostring as an alias for tobytes # Declare tostring as an alias for tobytes
tostring = tobytes tostring = tobytes
def _new_color_index(self, image=None, e=None): def _new_color_index(
self, image: Image.Image | None = None, e: Exception | None = None
) -> int:
if not isinstance(self.palette, bytearray): if not isinstance(self.palette, bytearray):
self._palette = bytearray(self.palette) self._palette = bytearray(self.palette)
index = len(self.palette) // 3 index = len(self.palette) // 3
special_colors = () special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
if image: if image:
special_colors = ( special_colors = (
image.info.get("background"), image.info.get("background"),
@ -128,7 +135,11 @@ class ImagePalette:
raise ValueError(msg) from e raise ValueError(msg) from e
return index return index
def getcolor(self, color, image=None) -> int: def getcolor(
self,
color: tuple[int, int, int] | tuple[int, int, int, int],
image: Image.Image | None = None,
) -> int:
"""Given an rgb tuple, allocate palette entry. """Given an rgb tuple, allocate palette entry.
.. warning:: This method is experimental. .. warning:: This method is experimental.
@ -163,7 +174,7 @@ class ImagePalette:
self.dirty = 1 self.dirty = 1
return index return index
else: else:
msg = f"unknown color specifier: {repr(color)}" msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable]
raise ValueError(msg) raise ValueError(msg)
def save(self, fp: str | IO[str]) -> None: def save(self, fp: str | IO[str]) -> None:

View File

@ -37,7 +37,7 @@ from . import Image
_pilbitmap_ok = None _pilbitmap_ok = None
def _pilbitmap_check(): def _pilbitmap_check() -> int:
global _pilbitmap_ok global _pilbitmap_ok
if _pilbitmap_ok is None: if _pilbitmap_ok is None:
try: try:
@ -162,7 +162,7 @@ class PhotoImage:
""" """
return self.__size[1] return self.__size[1]
def paste(self, im): def paste(self, im: Image.Image) -> None:
""" """
Paste a PIL image into the photo image. Note that this can Paste a PIL image into the photo image. Note that this can
be very slow if the photo image is displayed. be very slow if the photo image is displayed.
@ -254,7 +254,7 @@ class BitmapImage:
return str(self.__photo) return str(self.__photo)
def getimage(photo): def getimage(photo: PhotoImage) -> Image.Image:
"""Copies the contents of a PhotoImage to a PIL image memory.""" """Copies the contents of a PhotoImage to a PIL image memory."""
im = Image.new("RGBA", (photo.width(), photo.height())) im = Image.new("RGBA", (photo.width(), photo.height()))
block = im.im block = im.im

View File

@ -14,7 +14,7 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import Sequence from typing import Any, Sequence
from . import Image from . import Image
@ -34,7 +34,7 @@ class Transform(Image.ImageTransformHandler):
self, self,
size: tuple[int, int], size: tuple[int, int],
image: Image.Image, image: Image.Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]], **options: Any,
) -> Image.Image: ) -> Image.Image:
"""Perform the transform. Called from :py:meth:`.Image.transform`.""" """Perform the transform. Called from :py:meth:`.Image.transform`."""
# can be overridden # can be overridden

View File

@ -18,6 +18,7 @@ from __future__ import annotations
import io import io
import os import os
import struct import struct
from typing import IO, Tuple, cast
from . import Image, ImageFile, ImagePalette, _binary from . import Image, ImageFile, ImagePalette, _binary
@ -58,7 +59,7 @@ class BoxReader:
self.remaining_in_box -= num_bytes self.remaining_in_box -= num_bytes
return data return data
def read_fields(self, field_format): def read_fields(self, field_format: str) -> tuple[int | bytes, ...]:
size = struct.calcsize(field_format) size = struct.calcsize(field_format)
data = self._read_bytes(size) data = self._read_bytes(size)
return struct.unpack(field_format, data) return struct.unpack(field_format, data)
@ -81,9 +82,9 @@ class BoxReader:
self.remaining_in_box = -1 self.remaining_in_box = -1
# Read the length and type of the next box # Read the length and type of the next box
lbox, tbox = self.read_fields(">I4s") lbox, tbox = cast(Tuple[int, bytes], self.read_fields(">I4s"))
if lbox == 1: if lbox == 1:
lbox = self.read_fields(">Q")[0] lbox = cast(int, self.read_fields(">Q")[0])
hlen = 16 hlen = 16
else: else:
hlen = 8 hlen = 8
@ -121,17 +122,18 @@ def _parse_codestream(fp):
elif csiz == 4: elif csiz == 4:
mode = "RGBA" mode = "RGBA"
else: else:
mode = None mode = ""
return size, mode return size, mode
def _res_to_dpi(num, denom, exp): def _res_to_dpi(num: int, denom: int, exp: int) -> float | None:
"""Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
calculated as (num / denom) * 10^exp and stored in dots per meter, calculated as (num / denom) * 10^exp and stored in dots per meter,
to floating-point dots per inch.""" to floating-point dots per inch."""
if denom != 0: if denom == 0:
return (254 * num * (10**exp)) / (10000 * denom) return None
return (254 * num * (10**exp)) / (10000 * denom)
def _parse_jp2_header(fp): def _parse_jp2_header(fp):
@ -235,7 +237,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
msg = "not a JPEG 2000 file" msg = "not a JPEG 2000 file"
raise SyntaxError(msg) raise SyntaxError(msg)
if self.size is None or self.mode is None: if self.size is None or not self.mode:
msg = "unable to determine size/mode" msg = "unable to determine size/mode"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -328,11 +330,13 @@ def _accept(prefix: bytes) -> bool:
# Save support # Save support
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# Get the keyword arguments # Get the keyword arguments
info = im.encoderinfo info = im.encoderinfo
if filename.endswith(".j2k") or info.get("no_jp2", False): if isinstance(filename, str):
filename = filename.encode()
if filename.endswith(b".j2k") or info.get("no_jp2", False):
kind = "j2k" kind = "j2k"
else: else:
kind = "jp2" kind = "jp2"

View File

@ -42,7 +42,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
import warnings import warnings
from typing import Any from typing import IO, Any
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -428,7 +428,7 @@ class JpegImageFile(ImageFile.ImageFile):
return s return s
def draft( def draft(
self, mode: str, size: tuple[int, int] self, mode: str | None, size: tuple[int, int]
) -> tuple[str, tuple[int, int, float, float]] | None: ) -> tuple[str, tuple[int, int, float, float]] | None:
if len(self.tile) != 1: if len(self.tile) != 1:
return None return None
@ -631,7 +631,7 @@ def get_sampling(im):
return samplings.get(sampling, -1) return samplings.get(sampling, -1)
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.width == 0 or im.height == 0: if im.width == 0 or im.height == 0:
msg = "cannot write empty image as JPEG" msg = "cannot write empty image as JPEG"
raise ValueError(msg) raise ValueError(msg)
@ -814,7 +814,7 @@ def _save(im, fp, filename):
ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize)
def _save_cjpeg(im, fp, filename): def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# ALTERNATIVE: handle JPEGs via the IJG command line utilities. # ALTERNATIVE: handle JPEGs via the IJG command line utilities.
tempfile = im._dump() tempfile = im._dump()
subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) subprocess.check_call(["cjpeg", "-outfile", filename, tempfile])

View File

@ -63,7 +63,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
msg = "not an MIC file; no image entries" msg = "not an MIC file; no image entries"
raise SyntaxError(msg) raise SyntaxError(msg)
self.frame = None self.frame = -1
self._n_frames = len(self.images) self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1 self.is_animated = self._n_frames > 1
@ -85,7 +85,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.frame = frame self.frame = frame
def tell(self): def tell(self) -> int:
return self.frame return self.frame
def close(self) -> None: def close(self) -> None:
@ -93,7 +93,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.ole.close() self.ole.close()
super().close() super().close()
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.__fp.close() self.__fp.close()
self.ole.close() self.ole.close()
super().__exit__() super().__exit__()

View File

@ -22,6 +22,7 @@ from __future__ import annotations
import itertools import itertools
import os import os
import struct import struct
from typing import IO
from . import ( from . import (
Image, Image,
@ -32,23 +33,18 @@ from . import (
from ._binary import o32le from ._binary import o32le
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
JpegImagePlugin._save(im, fp, filename) JpegImagePlugin._save(im, fp, filename)
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
append_images = im.encoderinfo.get("append_images", []) append_images = im.encoderinfo.get("append_images", [])
if not append_images: if not append_images and not getattr(im, "is_animated", False):
try: _save(im, fp, filename)
animated = im.is_animated return
except AttributeError:
animated = False
if not animated:
_save(im, fp, filename)
return
mpf_offset = 28 mpf_offset = 28
offsets = [] offsets: list[int] = []
for imSequence in itertools.chain([im], append_images): for imSequence in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(imSequence): for im_frame in ImageSequence.Iterator(imSequence):
if not offsets: if not offsets:

View File

@ -164,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder)
# write MSP files (uncompressed only) # write MSP files (uncompressed only)
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 != "1": if im.mode != "1":
msg = f"cannot write mode {im.mode} as MSP" msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg) raise OSError(msg)

View File

@ -138,7 +138,7 @@ class PSDraw:
sx = x / im.size[0] sx = x / im.size[0]
sy = y / im.size[1] sy = y / im.size[1]
self.fp.write(b"%f %f scale\n" % (sx, sy)) self.fp.write(b"%f %f scale\n" % (sx, sy))
EpsImagePlugin._save(im, self.fp, None, 0) EpsImagePlugin._save(im, self.fp, "", 0)
self.fp.write(b"\ngrestore\n") self.fp.write(b"\ngrestore\n")

View File

@ -14,6 +14,8 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import IO
from ._binary import o8 from ._binary import o8
@ -22,8 +24,8 @@ class PaletteFile:
rawmode = "RGB" rawmode = "RGB"
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
self.palette = [(i, i, i) for i in range(256)] palette = [o8(i) * 3 for i in range(256)]
while True: while True:
s = fp.readline() s = fp.readline()
@ -44,9 +46,9 @@ class PaletteFile:
g = b = r g = b = r
if 0 <= i <= 255: if 0 <= i <= 255:
self.palette[i] = o8(r) + o8(g) + o8(b) palette[i] = o8(r) + o8(g) + o8(b)
self.palette = b"".join(self.palette) self.palette = b"".join(palette)
def getpalette(self) -> tuple[bytes, str]: def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode return self.palette, self.rawmode

View File

@ -8,6 +8,8 @@
## ##
from __future__ import annotations from __future__ import annotations
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import o8 from ._binary import o8
from ._binary import o16be as o16b from ._binary import o16be as o16b
@ -82,10 +84,10 @@ _Palm8BitColormapValues = (
# so build a prototype image to be used for palette resampling # so build a prototype image to be used for palette resampling
def build_prototype_image(): def build_prototype_image() -> Image.Image:
image = Image.new("L", (1, len(_Palm8BitColormapValues))) image = Image.new("L", (1, len(_Palm8BitColormapValues)))
image.putdata(list(range(len(_Palm8BitColormapValues)))) image.putdata(list(range(len(_Palm8BitColormapValues))))
palettedata = () palettedata: tuple[int, ...] = ()
for colormapValue in _Palm8BitColormapValues: for colormapValue in _Palm8BitColormapValues:
palettedata += colormapValue palettedata += colormapValue
palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues))
@ -112,7 +114,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00}
# (Internal) Image save plugin for the Palm format. # (Internal) Image save plugin for the Palm format.
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":
# we assume this is a color Palm image with the standard colormap, # we assume this is a color Palm image with the standard colormap,
# unless the "info" dict has a "custom-colormap" field # unless the "info" dict has a "custom-colormap" field
@ -127,21 +129,22 @@ def _save(im, fp, filename):
# and invert it because # and invert it because
# Palm does grayscale from white (0) to black (1) # Palm does grayscale from white (0) to black (1)
bpp = im.encoderinfo["bpp"] bpp = im.encoderinfo["bpp"]
im = im.point( maxval = (1 << bpp) - 1
lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift) shift = 8 - bpp
) im = im.point(lambda x: maxval - (x >> shift))
elif im.info.get("bpp") in (1, 2, 4): elif im.info.get("bpp") in (1, 2, 4):
# here we assume that even though the inherent mode is 8-bit grayscale, # here we assume that even though the inherent mode is 8-bit grayscale,
# only the lower bpp bits are significant. # only the lower bpp bits are significant.
# We invert them to match the Palm. # We invert them to match the Palm.
bpp = im.info["bpp"] bpp = im.info["bpp"]
im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) maxval = (1 << bpp) - 1
im = im.point(lambda x: maxval - (x & maxval))
else: else:
msg = f"cannot write mode {im.mode} as Palm" msg = f"cannot write mode {im.mode} as Palm"
raise OSError(msg) raise OSError(msg)
# we ignore the palette here # we ignore the palette here
im.mode = "P" im._mode = "P"
rawmode = f"P;{bpp}" rawmode = f"P;{bpp}"
version = 1 version = 1

View File

@ -144,7 +144,7 @@ SAVE = {
} }
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
version, bits, planes, rawmode = SAVE[im.mode] version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e: except KeyError as e:

View File

@ -25,6 +25,7 @@ import io
import math import math
import os import os
import time import time
from typing import IO
from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
@ -39,7 +40,7 @@ from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
# 5. page contents # 5. page contents
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True) _save(im, fp, filename, save_all=True)

View File

@ -76,7 +76,7 @@ class PdfFormatError(RuntimeError):
pass pass
def check_format_condition(condition, error_message): def check_format_condition(condition: bool, error_message: str) -> None:
if not condition: if not condition:
raise PdfFormatError(error_message) raise PdfFormatError(error_message)
@ -93,12 +93,11 @@ class IndirectReference(IndirectReferenceTuple):
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
return self.__str__().encode("us-ascii") return self.__str__().encode("us-ascii")
def __eq__(self, other): def __eq__(self, other: object) -> bool:
return ( if self.__class__ is not other.__class__:
other.__class__ is self.__class__ return False
and other.object_id == self.object_id assert isinstance(other, IndirectReference)
and other.generation == self.generation return other.object_id == self.object_id and other.generation == self.generation
)
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return not (self == other)
@ -405,9 +404,8 @@ class PdfParser:
def __enter__(self) -> PdfParser: def __enter__(self) -> PdfParser:
return self return self
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, *args: object) -> None:
self.close() self.close()
return False # do not suppress exceptions
def start_writing(self) -> None: def start_writing(self) -> None:
self.close_buf() self.close_buf()

View File

@ -39,7 +39,7 @@ import struct
import warnings import warnings
import zlib import zlib
from enum import IntEnum from enum import IntEnum
from typing import IO, Any from typing import IO, TYPE_CHECKING, Any, NoReturn
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -48,6 +48,9 @@ from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from ._binary import o32be as o32 from ._binary import o32be as o32
if TYPE_CHECKING:
from . import _imaging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
is_cid = re.compile(rb"\w\w\w\w").match is_cid = re.compile(rb"\w\w\w\w").match
@ -178,7 +181,7 @@ class ChunkStream:
def __enter__(self) -> ChunkStream: def __enter__(self) -> ChunkStream:
return self return self
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.close() self.close()
def close(self) -> None: def close(self) -> None:
@ -249,6 +252,9 @@ class iTXt(str):
""" """
lang: str | bytes | None
tkey: str | bytes | None
@staticmethod @staticmethod
def __new__(cls, text, lang=None, tkey=None): def __new__(cls, text, lang=None, tkey=None):
""" """
@ -270,10 +276,10 @@ class PngInfo:
""" """
def __init__(self): def __init__(self) -> None:
self.chunks = [] self.chunks: list[tuple[bytes, bytes, bool]] = []
def add(self, cid, data, after_idat=False): def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None:
"""Appends an arbitrary chunk. Use with caution. """Appends an arbitrary chunk. Use with caution.
:param cid: a byte string, 4 bytes long. :param cid: a byte string, 4 bytes long.
@ -283,12 +289,16 @@ class PngInfo:
""" """
chunk = [cid, data] self.chunks.append((cid, data, after_idat))
if after_idat:
chunk.append(True)
self.chunks.append(tuple(chunk))
def add_itxt(self, key, value, lang="", tkey="", zip=False): def add_itxt(
self,
key: str | bytes,
value: str | bytes,
lang: str | bytes = "",
tkey: str | bytes = "",
zip: bool = False,
) -> None:
"""Appends an iTXt chunk. """Appends an iTXt chunk.
:param key: latin-1 encodable text key name :param key: latin-1 encodable text key name
@ -316,7 +326,9 @@ class PngInfo:
else: else:
self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value)
def add_text(self, key, value, zip=False): def add_text(
self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False
) -> None:
"""Appends a text chunk. """Appends a text chunk.
:param key: latin-1 encodable text key name :param key: latin-1 encodable text key name
@ -326,7 +338,13 @@ class PngInfo:
""" """
if isinstance(value, iTXt): if isinstance(value, iTXt):
return self.add_itxt(key, value, value.lang, value.tkey, zip=zip) return self.add_itxt(
key,
value,
value.lang if value.lang is not None else b"",
value.tkey if value.tkey is not None else b"",
zip=zip,
)
# The tEXt chunk stores latin-1 text # The tEXt chunk stores latin-1 text
if not isinstance(value, bytes): if not isinstance(value, bytes):
@ -434,7 +452,7 @@ class PngStream(ChunkStream):
raise SyntaxError(msg) raise SyntaxError(msg)
return s return s
def chunk_IDAT(self, pos, length): def chunk_IDAT(self, pos: int, length: int) -> NoReturn:
# image data # image data
if "bbox" in self.im_info: if "bbox" in self.im_info:
tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)]
@ -447,7 +465,7 @@ class PngStream(ChunkStream):
msg = "image data found" msg = "image data found"
raise EOFError(msg) raise EOFError(msg)
def chunk_IEND(self, pos, length): def chunk_IEND(self, pos: int, length: int) -> NoReturn:
msg = "end of PNG image" msg = "end of PNG image"
raise EOFError(msg) raise EOFError(msg)
@ -823,7 +841,10 @@ class PngImageFile(ImageFile.ImageFile):
msg = "no more images in APNG file" msg = "no more images in APNG file"
raise EOFError(msg) from e raise EOFError(msg) from e
def _seek(self, frame, rewind=False): def _seek(self, frame: int, rewind: bool = False) -> None:
assert self.png is not None
self.dispose: _imaging.ImagingCore | None
if frame == 0: if frame == 0:
if rewind: if rewind:
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)
@ -908,14 +929,14 @@ class PngImageFile(ImageFile.ImageFile):
if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
self.dispose_op = Disposal.OP_BACKGROUND self.dispose_op = Disposal.OP_BACKGROUND
self.dispose = None
if self.dispose_op == Disposal.OP_PREVIOUS: if self.dispose_op == Disposal.OP_PREVIOUS:
self.dispose = self._prev_im.copy() if self._prev_im:
self.dispose = self._crop(self.dispose, self.dispose_extent) self.dispose = self._prev_im.copy()
self.dispose = self._crop(self.dispose, self.dispose_extent)
elif self.dispose_op == Disposal.OP_BACKGROUND: elif self.dispose_op == Disposal.OP_BACKGROUND:
self.dispose = Image.core.fill(self.mode, self.size) self.dispose = Image.core.fill(self.mode, self.size)
self.dispose = self._crop(self.dispose, self.dispose_extent) self.dispose = self._crop(self.dispose, self.dispose_extent)
else:
self.dispose = None
def tell(self) -> int: def tell(self) -> int:
return self.__frame return self.__frame
@ -1028,7 +1049,7 @@ class PngImageFile(ImageFile.ImageFile):
return None return None
return self.getexif()._get_merged_dict() return self.getexif()._get_merged_dict()
def getexif(self): def getexif(self) -> Image.Exif:
if "exif" not in self.info: if "exif" not in self.info:
self.load() self.load()
@ -1223,7 +1244,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
seq_num = fdat_chunks.seq_num seq_num = fdat_chunks.seq_num
def _save_all(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True) _save(im, fp, filename, save_all=True)
@ -1335,7 +1356,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, cid, data) chunk(fp, cid, data)
elif cid[1:2].islower(): elif cid[1:2].islower():
# Private chunk # Private chunk
after_idat = info_chunk[2:3] after_idat = len(info_chunk) == 3 and info_chunk[2]
if not after_idat: if not after_idat:
chunk(fp, cid, data) chunk(fp, cid, data)
@ -1414,7 +1435,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
cid, data = info_chunk[:2] cid, data = info_chunk[:2]
if cid[1:2].islower(): if cid[1:2].islower():
# Private chunk # Private chunk
after_idat = info_chunk[2:3] after_idat = len(info_chunk) == 3 and info_chunk[2]
if after_idat: if after_idat:
chunk(fp, cid, data) chunk(fp, cid, data)

View File

@ -328,7 +328,7 @@ class PpmDecoder(ImageFile.PyDecoder):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
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 == "1": if im.mode == "1":
rawmode, head = "1;I", b"P4" rawmode, head = "1;I", b"P4"
elif im.mode == "L": elif im.mode == "L":

View File

@ -37,6 +37,8 @@ class QoiImageFile(ImageFile.ImageFile):
class QoiDecoder(ImageFile.PyDecoder): class QoiDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
_previous_pixel: bytes | bytearray | None = None
_previously_seen_pixels: dict[int, bytes | bytearray] = {}
def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
self._previous_pixel = value self._previous_pixel = value
@ -45,9 +47,10 @@ class QoiDecoder(ImageFile.PyDecoder):
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
self._previously_seen_pixels[hash_value] = value self._previously_seen_pixels[hash_value] = value
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
self._previously_seen_pixels = {} self._previously_seen_pixels = {}
self._previous_pixel = None
self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) self._add_to_previous_pixels(bytearray((0, 0, 0, 255)))
data = bytearray() data = bytearray()
@ -55,7 +58,8 @@ class QoiDecoder(ImageFile.PyDecoder):
dest_length = self.state.xsize * self.state.ysize * bands dest_length = self.state.xsize * self.state.ysize * bands
while len(data) < dest_length: while len(data) < dest_length:
byte = self.fd.read(1)[0] byte = self.fd.read(1)[0]
if byte == 0b11111110: # QOI_OP_RGB value: bytes | bytearray
if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB
value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] value = bytearray(self.fd.read(3)) + self._previous_pixel[3:]
elif byte == 0b11111111: # QOI_OP_RGBA elif byte == 0b11111111: # QOI_OP_RGBA
value = self.fd.read(4) value = self.fd.read(4)
@ -66,7 +70,7 @@ class QoiDecoder(ImageFile.PyDecoder):
value = self._previously_seen_pixels.get( value = self._previously_seen_pixels.get(
op_index, bytearray((0, 0, 0, 0)) op_index, bytearray((0, 0, 0, 0))
) )
elif op == 1: # QOI_OP_DIFF elif op == 1 and self._previous_pixel: # QOI_OP_DIFF
value = bytearray( value = bytearray(
( (
(self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
@ -77,7 +81,7 @@ class QoiDecoder(ImageFile.PyDecoder):
self._previous_pixel[3], self._previous_pixel[3],
) )
) )
elif op == 2: # QOI_OP_LUMA elif op == 2 and self._previous_pixel: # QOI_OP_LUMA
second_byte = self.fd.read(1)[0] second_byte = self.fd.read(1)[0]
diff_green = (byte & 0b00111111) - 32 diff_green = (byte & 0b00111111) - 32
diff_red = ((second_byte & 0b11110000) >> 4) - 8 diff_red = ((second_byte & 0b11110000) >> 4) - 8
@ -90,7 +94,7 @@ class QoiDecoder(ImageFile.PyDecoder):
) )
) )
value += self._previous_pixel[3:] value += self._previous_pixel[3:]
elif op == 3: # QOI_OP_RUN elif op == 3 and self._previous_pixel: # QOI_OP_RUN
run_length = (byte & 0b00111111) + 1 run_length = (byte & 0b00111111) + 1
value = self._previous_pixel value = self._previous_pixel
if bands == 3: if bands == 3:

View File

@ -125,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile):
] ]
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"}: if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode" msg = "Unsupported SGI image mode"
raise ValueError(msg) raise ValueError(msg)
@ -171,8 +171,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
# Maximum Byte value (255 = 8bits per pixel) # Maximum Byte value (255 = 8bits per pixel)
pinmax = 255 pinmax = 255
# Image name (79 characters max, truncated below in write) # Image name (79 characters max, truncated below in write)
filename = os.path.basename(filename) img_name = os.path.splitext(os.path.basename(filename))[0]
img_name = os.path.splitext(filename)[0].encode("ascii", "ignore") if isinstance(img_name, str):
img_name = img_name.encode("ascii", "ignore")
# Standard representation of pixel in the file # Standard representation of pixel in the file
colormap = 0 colormap = 0
fp.write(struct.pack(">h", magic_number)) fp.write(struct.pack(">h", magic_number))

View File

@ -37,7 +37,7 @@ from __future__ import annotations
import os import os
import struct import struct
import sys import sys
from typing import TYPE_CHECKING from typing import IO, TYPE_CHECKING
from . import Image, ImageFile from . import Image, ImageFile
@ -263,7 +263,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]:
return [struct.pack("f", v) for v in hdr] return [struct.pack("f", v) for v in hdr]
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode[0] != "F": if im.mode[0] != "F":
im = im.convert("F") im = im.convert("F")
@ -279,9 +279,10 @@ def _save(im, fp, filename):
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
def _save_spider(im, fp, filename): def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# get the filename extension and register it with Image # get the filename extension and register it with Image
ext = os.path.splitext(filename)[1] filename_ext = os.path.splitext(filename)[1]
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
Image.register_extension(SpiderImageFile.format, ext) Image.register_extension(SpiderImageFile.format, ext)
_save(im, fp, filename) _save(im, fp, filename)

View File

@ -16,7 +16,6 @@
from __future__ import annotations from __future__ import annotations
import io import io
from types import TracebackType
from . import ContainerIO from . import ContainerIO
@ -61,12 +60,7 @@ class TarIO(ContainerIO.ContainerIO[bytes]):
def __enter__(self) -> TarIO: def __enter__(self) -> TarIO:
return self return self
def __exit__( def __exit__(self, *args: object) -> None:
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.close() self.close()
def close(self) -> None: def close(self) -> None:

View File

@ -178,7 +178,7 @@ SAVE = {
} }
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode] rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e: except KeyError as e:

View File

@ -50,7 +50,7 @@ import warnings
from collections.abc import MutableMapping from collections.abc import MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational from numbers import Number, Rational
from typing import TYPE_CHECKING, Any, Callable from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -384,10 +384,10 @@ class IFDRational(Rational):
def __repr__(self) -> str: def __repr__(self) -> str:
return str(float(self._val)) return str(float(self._val))
def __hash__(self): def __hash__(self) -> int:
return self._val.__hash__() return self._val.__hash__()
def __eq__(self, other): def __eq__(self, other: object) -> bool:
val = self._val val = self._val
if isinstance(other, IFDRational): if isinstance(other, IFDRational):
other = other._val other = other._val
@ -551,7 +551,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
_load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {}
_write_dispatch: dict[int, Callable[..., Any]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {}
def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): def __init__(
self,
ifh: bytes = b"II\052\0\0\0\0\0",
prefix: bytes | None = None,
group: int | None = None,
) -> None:
"""Initialize an ImageFileDirectory. """Initialize an ImageFileDirectory.
To construct an ImageFileDirectory from a real file, pass the 8-byte To construct an ImageFileDirectory from a real file, pass the 8-byte
@ -575,7 +580,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise SyntaxError(msg) raise SyntaxError(msg)
self._bigtiff = ifh[2] == 43 self._bigtiff = ifh[2] == 43
self.group = group self.group = group
self.tagtype = {} self.tagtype: dict[int, int] = {}
""" Dictionary of tag types """ """ Dictionary of tag types """
self.reset() self.reset()
(self.next,) = ( (self.next,) = (
@ -587,18 +592,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
offset = property(lambda self: self._offset) offset = property(lambda self: self._offset)
@property @property
def legacy_api(self): def legacy_api(self) -> bool:
return self._legacy_api return self._legacy_api
@legacy_api.setter @legacy_api.setter
def legacy_api(self, value): def legacy_api(self, value: bool) -> NoReturn:
msg = "Not allowing setting of legacy api" msg = "Not allowing setting of legacy api"
raise Exception(msg) raise Exception(msg)
def reset(self): def reset(self) -> None:
self._tags_v1 = {} # will remain empty if legacy_api is false self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false
self._tags_v2 = {} # main tag storage self._tags_v2: dict[int, Any] = {} # main tag storage
self._tagdata = {} self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None self._next = None
self._offset = None self._offset = None
@ -717,7 +722,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
# Unspec'd, and length > 1 # Unspec'd, and length > 1
dest[tag] = values dest[tag] = values
def __delitem__(self, tag): def __delitem__(self, tag: int) -> None:
self._tags_v2.pop(tag, None) self._tags_v2.pop(tag, None)
self._tags_v1.pop(tag, None) self._tags_v1.pop(tag, None)
self._tagdata.pop(tag, None) self._tagdata.pop(tag, None)
@ -1106,7 +1111,7 @@ class TiffImageFile(ImageFile.ImageFile):
super().__init__(fp, filename) super().__init__(fp, filename)
def _open(self): def _open(self) -> None:
"""Open the first image in a TIFF file""" """Open the first image in a TIFF file"""
# Header # Header
@ -1123,8 +1128,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.__first = self.__next = self.tag_v2.next self.__first = self.__next = self.tag_v2.next
self.__frame = -1 self.__frame = -1
self._fp = self.fp self._fp = self.fp
self._frame_pos = [] self._frame_pos: list[int] = []
self._n_frames = None self._n_frames: int | None = None
logger.debug("*** TiffImageFile._open ***") logger.debug("*** TiffImageFile._open ***")
logger.debug("- __first: %s", self.__first) logger.debug("- __first: %s", self.__first)
@ -1990,13 +1995,12 @@ class AppendingTiffWriter:
self.finalize() self.finalize()
self.setup() self.setup()
def __enter__(self): def __enter__(self) -> AppendingTiffWriter:
return self return self
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, *args: object) -> None:
if self.close_fp: if self.close_fp:
self.close() self.close()
return False
def tell(self) -> int: def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage return self.f.tell() - self.offsetOfNewPage
@ -2018,7 +2022,7 @@ class AppendingTiffWriter:
self.f.write(bytes(pad_bytes)) self.f.write(bytes(pad_bytes))
self.offsetOfNewPage = self.f.tell() self.offsetOfNewPage = self.f.tell()
def setEndian(self, endian): def setEndian(self, endian: str) -> None:
self.endian = endian self.endian = endian
self.longFmt = f"{self.endian}L" self.longFmt = f"{self.endian}L"
self.shortFmt = f"{self.endian}H" self.shortFmt = f"{self.endian}H"
@ -2035,45 +2039,45 @@ class AppendingTiffWriter:
num_tags = self.readShort() num_tags = self.readShort()
self.f.seek(num_tags * 12, os.SEEK_CUR) self.f.seek(num_tags * 12, os.SEEK_CUR)
def write(self, data): def write(self, data: bytes) -> int | None:
return self.f.write(data) return self.f.write(data)
def readShort(self): def readShort(self) -> int:
(value,) = struct.unpack(self.shortFmt, self.f.read(2)) (value,) = struct.unpack(self.shortFmt, self.f.read(2))
return value return value
def readLong(self): def readLong(self) -> int:
(value,) = struct.unpack(self.longFmt, self.f.read(4)) (value,) = struct.unpack(self.longFmt, self.f.read(4))
return value return value
def rewriteLastShortToLong(self, value): def rewriteLastShortToLong(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg) raise RuntimeError(msg)
def rewriteLastShort(self, value): def rewriteLastShort(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2: if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2" msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg) raise RuntimeError(msg)
def rewriteLastLong(self, value): def rewriteLastLong(self, value: int) -> None:
self.f.seek(-4, os.SEEK_CUR) self.f.seek(-4, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg) raise RuntimeError(msg)
def writeShort(self, value): def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2: if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2" msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg) raise RuntimeError(msg)
def writeLong(self, value): def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
@ -2092,9 +2096,9 @@ class AppendingTiffWriter:
field_size = self.fieldSizes[field_type] field_size = self.fieldSizes[field_type]
total_size = field_size * count total_size = field_size * count
is_local = total_size <= 4 is_local = total_size <= 4
offset: int | None
if not is_local: if not is_local:
offset = self.readLong() offset = self.readLong() + self.offsetOfNewPage
offset += self.offsetOfNewPage
self.rewriteLastLong(offset) self.rewriteLastLong(offset)
if tag in self.Tags: if tag in self.Tags:
@ -2118,7 +2122,9 @@ class AppendingTiffWriter:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(4, os.SEEK_CUR)
def fixOffsets(self, count, isShort=False, isLong=False): def fixOffsets(
self, count: int, isShort: bool = False, isLong: bool = False
) -> None:
if not isShort and not isLong: if not isShort and not isLong:
msg = "offset is neither short nor long" msg = "offset is neither short nor long"
raise RuntimeError(msg) raise RuntimeError(msg)
@ -2144,7 +2150,7 @@ class AppendingTiffWriter:
self.rewriteLastLong(offset) self.rewriteLastLong(offset)
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
encoderconfig = im.encoderconfig encoderconfig = im.encoderconfig
append_images = list(encoderinfo.get("append_images", [])) append_images = list(encoderinfo.get("append_images", []))

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
from typing import Any from typing import IO, Any
from . import Image, ImageFile from . import Image, ImageFile
@ -173,7 +173,7 @@ class WebPImageFile(ImageFile.ImageFile):
return self.__logical_frame return self.__logical_frame
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
append_images = list(encoderinfo.get("append_images", [])) append_images = list(encoderinfo.get("append_images", []))
@ -186,7 +186,7 @@ def _save_all(im, fp, filename):
_save(im, fp, filename) _save(im, fp, filename)
return return
background = (0, 0, 0, 0) background: int | tuple[int, ...] = (0, 0, 0, 0)
if "background" in encoderinfo: if "background" in encoderinfo:
background = encoderinfo["background"] background = encoderinfo["background"]
elif "background" in im.info: elif "background" in im.info:
@ -316,7 +316,7 @@ def _save_all(im, fp, filename):
fp.write(data) fp.write(data)
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
lossless = im.encoderinfo.get("lossless", False) lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
alpha_quality = im.encoderinfo.get("alpha_quality", 100) alpha_quality = im.encoderinfo.get("alpha_quality", 100)

View File

@ -20,6 +20,8 @@
# http://wvware.sourceforge.net/caolan/ora-wmf.html # http://wvware.sourceforge.net/caolan/ora-wmf.html
from __future__ import annotations from __future__ import annotations
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16le as word from ._binary import i16le as word
from ._binary import si16le as short from ._binary import si16le as short
@ -28,7 +30,7 @@ from ._binary import si32le as _long
_handler = None _handler = None
def register_handler(handler: ImageFile.StubHandler) -> None: def register_handler(handler: ImageFile.StubHandler | None) -> None:
""" """
Install application-specific WMF image handler. Install application-specific WMF image handler.
@ -161,7 +163,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
return super().load() return super().load()
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 = "WMF save handler not installed" msg = "WMF save handler not installed"
raise OSError(msg) raise OSError(msg)

View File

@ -70,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile):
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] self.tile = [("xbm", (0, 0) + self.size, m.end(), None)]
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 != "1": if im.mode != "1":
msg = f"cannot write mode {im.mode} as XBM" msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg) raise OSError(msg)

View File

@ -1,3 +1,22 @@
from typing import Any from typing import Any
class ImagingCore:
def __getattr__(self, name: str) -> Any: ...
class ImagingFont:
def __getattr__(self, name: str) -> Any: ...
class ImagingDraw:
def __getattr__(self, name: str) -> Any: ...
class PixelAccess:
def __getattr__(self, name: str) -> Any: ...
class ImagingDecoder:
def __getattr__(self, name: str) -> Any: ...
class ImagingEncoder:
def __getattr__(self, name: str) -> Any: ...
def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ...
def __getattr__(name: str) -> Any: ... def __getattr__(name: str) -> Any: ...

View File

@ -1,3 +1,69 @@
from typing import Any from typing import Any, TypedDict
from . import _imaging
class _Axis(TypedDict):
minimum: int | None
default: int | None
maximum: int | None
name: bytes | None
class Font:
@property
def family(self) -> str | None: ...
@property
def style(self) -> str | None: ...
@property
def ascent(self) -> int: ...
@property
def descent(self) -> int: ...
@property
def height(self) -> int: ...
@property
def x_ppem(self) -> int: ...
@property
def y_ppem(self) -> int: ...
@property
def glyphs(self) -> int: ...
def render(
self,
string: str,
fill,
mode=...,
dir=...,
features=...,
lang=...,
stroke_width=...,
anchor=...,
foreground_ink_long=...,
x_start=...,
y_start=...,
/,
) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ...
def getsize(
self,
string: str | bytes | bytearray,
mode=...,
dir=...,
features=...,
lang=...,
anchor=...,
/,
) -> tuple[tuple[int, int], tuple[int, int]]: ...
def getlength(
self, string: str, mode=..., dir=..., features=..., lang=..., /
) -> float: ...
def getvarnames(self) -> list[bytes]: ...
def getvaraxes(self) -> list[_Axis] | None: ...
def setvarname(self, instance_index: int, /) -> None: ...
def setvaraxes(self, axes: list[float], /) -> None: ...
def getfont(
filename: str | bytes,
size: float,
index=...,
encoding=...,
font_bytes=...,
layout_engine=...,
) -> Font: ...
def __getattr__(name: str) -> Any: ... def __getattr__(name: str) -> Any: ...

View File

@ -4,6 +4,7 @@ import collections
import os import os
import sys import sys
import warnings import warnings
from typing import IO
import PIL import PIL
@ -223,7 +224,7 @@ def get_supported() -> list[str]:
return ret return ret
def pilinfo(out=None, supported_formats=True): def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
""" """
Prints information about this installation of Pillow. Prints information about this installation of Pillow.
This function can be called with ``python3 -m PIL``. This function can be called with ``python3 -m PIL``.
@ -244,9 +245,9 @@ def pilinfo(out=None, supported_formats=True):
print("-" * 68, file=out) print("-" * 68, file=out)
print(f"Pillow {PIL.__version__}", file=out) print(f"Pillow {PIL.__version__}", file=out)
py_version = sys.version.splitlines() py_version_lines = sys.version.splitlines()
print(f"Python {py_version[0].strip()}", file=out) print(f"Python {py_version_lines[0].strip()}", file=out)
for py_version in py_version[1:]: for py_version in py_version_lines[1:]:
print(f" {py_version.strip()}", file=out) print(f" {py_version.strip()}", file=out)
print("-" * 68, file=out) print("-" * 68, file=out)
print(f"Python executable is {sys.executable or 'unknown'}", file=out) print(f"Python executable is {sys.executable or 'unknown'}", file=out)
@ -282,9 +283,12 @@ def pilinfo(out=None, supported_formats=True):
("xcb", "XCB (X protocol)"), ("xcb", "XCB (X protocol)"),
]: ]:
if check(name): if check(name):
if name == "jpg" and check_feature("libjpeg_turbo"): v: str | None = None
v = "libjpeg-turbo " + version_feature("libjpeg_turbo") if name == "jpg":
else: libjpeg_turbo_version = version_feature("libjpeg_turbo")
if libjpeg_turbo_version is not None:
v = "libjpeg-turbo " + libjpeg_turbo_version
if v is None:
v = version(name) v = version(name)
if v is not None: if v is not None:
version_static = name in ("pil", "jpg") version_static = name in ("pil", "jpg")

View File

@ -2028,7 +2028,7 @@ im_setmode(ImagingObject *self, PyObject *args) {
} }
static PyObject * static PyObject *
_transform2(ImagingObject *self, PyObject *args) { _transform(ImagingObject *self, PyObject *args) {
static const char *wrong_number = "wrong number of matrix entries"; static const char *wrong_number = "wrong number of matrix entries";
Imaging imOut; Imaging imOut;
@ -3647,7 +3647,7 @@ static struct PyMethodDef methods[] = {
{"resize", (PyCFunction)_resize, METH_VARARGS}, {"resize", (PyCFunction)_resize, METH_VARARGS},
{"reduce", (PyCFunction)_reduce, METH_VARARGS}, {"reduce", (PyCFunction)_reduce, METH_VARARGS},
{"transpose", (PyCFunction)_transpose, METH_VARARGS}, {"transpose", (PyCFunction)_transpose, METH_VARARGS},
{"transform2", (PyCFunction)_transform2, METH_VARARGS}, {"transform", (PyCFunction)_transform, METH_VARARGS},
{"isblock", (PyCFunction)_isblock, METH_NOARGS}, {"isblock", (PyCFunction)_isblock, METH_NOARGS},

View File

@ -112,12 +112,12 @@ ARCHITECTURES = {
V = { V = {
"BROTLI": "1.1.0", "BROTLI": "1.1.0",
"FREETYPE": "2.13.2", "FREETYPE": "2.13.2",
"FRIBIDI": "1.0.13", "FRIBIDI": "1.0.15",
"HARFBUZZ": "8.4.0", "HARFBUZZ": "8.5.0",
"JPEGTURBO": "3.0.2", "JPEGTURBO": "3.0.3",
"LCMS2": "2.16", "LCMS2": "2.16",
"LIBPNG": "1.6.43", "LIBPNG": "1.6.43",
"LIBWEBP": "1.3.2", "LIBWEBP": "1.4.0",
"OPENJPEG": "2.5.2", "OPENJPEG": "2.5.2",
"TIFF": "4.6.0", "TIFF": "4.6.0",
"XZ": "5.4.5", "XZ": "5.4.5",