tuple[int, int]:
try:
self._read_blp_header()
self._load()
@@ -284,7 +288,12 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
raise OSError(msg) from e
return -1, 0
- def _read_blp_header(self):
+ @abc.abstractmethod
+ def _load(self) -> None:
+ pass
+
+ def _read_blp_header(self) -> None:
+ assert self.fd is not None
self.fd.seek(4)
(self._blp_compression,) = struct.unpack(" bytes:
+ assert self.fd is not None
return ImageFile._safe_read(self.fd, length)
- def _read_palette(self):
+ def _read_palette(self) -> list[tuple[int, int, int, int]]:
ret = []
for i in range(256):
try:
@@ -316,7 +326,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a))
return ret
- def _read_bgra(self, palette):
+ def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
@@ -325,7 +335,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
except struct.error:
break
b, g, r, a = palette[offset]
- d = (r, g, b)
+ d: tuple[int, ...] = (r, g, b)
if self._blp_alpha_depth:
d += (a,)
data.extend(d)
@@ -349,29 +359,30 @@ class BLP1Decoder(_BLPBaseDecoder):
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
raise BLPFormatError(msg)
- def _decode_jpeg_stream(self):
+ def _decode_jpeg_stream(self) -> None:
from .JpegImagePlugin import JpegImageFile
(jpeg_header_size,) = struct.unpack(" None:
palette = self._read_palette()
+ assert self.fd is not None
self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1:
@@ -420,6 +431,7 @@ class BLPEncoder(ImageFile.PyEncoder):
def _write_palette(self) -> bytes:
data = b""
+ assert self.im is not None
palette = self.im.getpalette("RGBA", "RGBA")
for i in range(len(palette) // 4):
r, g, b, a = palette[i * 4 : (i + 1) * 4]
@@ -428,12 +440,13 @@ class BLPEncoder(ImageFile.PyEncoder):
data += b"\x00" * 4
return data
- def encode(self, bufsize):
+ def encode(self, bufsize: int) -> tuple[int, int, bytes]:
palette_data = self._write_palette()
offset = 20 + 16 * 4 * 2 + len(palette_data)
data = struct.pack("<16I", offset, *((0,) * 15))
+ assert self.im is not None
w, h = self.im.size
data += struct.pack("<16I", w * h, *((0,) * 15))
@@ -446,7 +459,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode != "P":
msg = "Unsupported BLP image mode"
raise ValueError(msg)
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index c5d1cd40d..48bdd9830 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -25,6 +25,7 @@
from __future__ import annotations
import os
+from typing import IO, Any
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@@ -52,7 +53,7 @@ def _accept(prefix: bytes) -> bool:
return prefix[:2] == b"BM"
-def _dib_accept(prefix):
+def _dib_accept(prefix: bytes) -> bool:
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
@@ -71,16 +72,20 @@ class BmpImageFile(ImageFile.ImageFile):
for k, v in COMPRESSIONS.items():
vars()[k] = v
- def _bitmap(self, header=0, offset=0):
+ def _bitmap(self, header: int = 0, offset: int = 0) -> None:
"""Read relevant info about the BMP"""
read, seek = self.fp.read, self.fp.seek
if header:
seek(header)
# read bmp header size @offset 14 (this is part of the header size)
- file_info = {"header_size": i32(read(4)), "direction": -1}
+ file_info: dict[str, bool | int | tuple[int, ...]] = {
+ "header_size": i32(read(4)),
+ "direction": -1,
+ }
# -------------------- If requested, read header at a specific position
# read the rest of the bmp header, without its size
+ assert isinstance(file_info["header_size"], int)
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
# ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
@@ -91,7 +96,7 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["height"] = i16(header_data, 2)
file_info["planes"] = i16(header_data, 4)
file_info["bits"] = i16(header_data, 6)
- file_info["compression"] = self.RAW
+ file_info["compression"] = self.COMPRESSIONS["RAW"]
file_info["palette_padding"] = 3
# --------------------------------------------- Windows Bitmap v3 to v5
@@ -121,8 +126,9 @@ class BmpImageFile(ImageFile.ImageFile):
)
file_info["colors"] = i32(header_data, 28)
file_info["palette_padding"] = 4
+ assert isinstance(file_info["pixels_per_meter"], tuple)
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
- if file_info["compression"] == self.BITFIELDS:
+ if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
masks = ["r_mask", "g_mask", "b_mask"]
if len(header_data) >= 48:
if len(header_data) >= 52:
@@ -143,6 +149,10 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["a_mask"] = 0x0
for mask in masks:
file_info[mask] = i32(read(4))
+ assert isinstance(file_info["r_mask"], int)
+ assert isinstance(file_info["g_mask"], int)
+ assert isinstance(file_info["b_mask"], int)
+ assert isinstance(file_info["a_mask"], int)
file_info["rgb_mask"] = (
file_info["r_mask"],
file_info["g_mask"],
@@ -163,24 +173,26 @@ class BmpImageFile(ImageFile.ImageFile):
self._size = file_info["width"], file_info["height"]
# ------- If color count was not found in the header, compute from bits
+ assert isinstance(file_info["bits"], int)
file_info["colors"] = (
file_info["colors"]
if file_info.get("colors", 0)
else (1 << file_info["bits"])
)
+ assert isinstance(file_info["colors"], int)
if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
offset += 4 * file_info["colors"]
# ---------------------- Check bit depth for unusual unsupported values
- self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
- if self.mode is None:
+ self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", ""))
+ if not self.mode:
msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
raise OSError(msg)
# ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw"
- if file_info["compression"] == self.BITFIELDS:
- SUPPORTED = {
+ if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
+ SUPPORTED: dict[int, list[tuple[int, ...]]] = {
32: [
(0xFF0000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
@@ -212,12 +224,14 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["bits"] == 32
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
):
+ assert isinstance(file_info["rgba_mask"], tuple)
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
self._mode = "RGBA" if "A" in raw_mode else self.mode
elif (
file_info["bits"] in (24, 16)
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
):
+ assert isinstance(file_info["rgb_mask"], tuple)
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
else:
msg = "Unsupported BMP bitfields layout"
@@ -225,10 +239,13 @@ class BmpImageFile(ImageFile.ImageFile):
else:
msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
- elif file_info["compression"] == self.RAW:
+ elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self._mode = "BGRA", "RGBA"
- elif file_info["compression"] in (self.RLE8, self.RLE4):
+ elif file_info["compression"] in (
+ self.COMPRESSIONS["RLE8"],
+ self.COMPRESSIONS["RLE4"],
+ ):
decoder_name = "bmp_rle"
else:
msg = f"Unsupported BMP compression ({file_info['compression']})"
@@ -241,6 +258,7 @@ class BmpImageFile(ImageFile.ImageFile):
msg = f"Unsupported BMP Palette size ({file_info['colors']})"
raise OSError(msg)
else:
+ assert isinstance(file_info["palette_padding"], int)
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
grayscale = True
@@ -268,10 +286,11 @@ class BmpImageFile(ImageFile.ImageFile):
# ---------------------------- Finally set the tile data for the plugin
self.info["compression"] = file_info["compression"]
- args = [raw_mode]
+ args: list[Any] = [raw_mode]
if decoder_name == "bmp_rle":
- args.append(file_info["compression"] == self.RLE4)
+ args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"])
else:
+ assert isinstance(file_info["width"], int)
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
args.append(file_info["direction"])
self.tile = [
@@ -300,7 +319,8 @@ class BmpImageFile(ImageFile.ImageFile):
class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
+ assert self.fd is not None
rle4 = self.args[1]
data = bytearray()
x = 0
@@ -394,11 +414,13 @@ SAVE = {
}
-def _dib_save(im, fp, filename):
+def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, False)
-def _save(im, fp, filename, bitmap_header=True):
+def _save(
+ im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
+) -> None:
try:
rawmode, bits, colors = SAVE[im.mode]
except KeyError as e:
diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py
index 271db7258..0ee2f653b 100644
--- a/src/PIL/BufrStubImagePlugin.py
+++ b/src/PIL/BufrStubImagePlugin.py
@@ -10,12 +10,14 @@
#
from __future__ import annotations
+from typing import IO
+
from . import Image, ImageFile
_handler = None
-def register_handler(handler):
+def register_handler(handler: ImageFile.StubHandler | None) -> None:
"""
Install application-specific BUFR image handler.
@@ -54,11 +56,11 @@ class BufrStubImageFile(ImageFile.StubImageFile):
if loader:
loader.open(self)
- def _load(self):
+ def _load(self) -> ImageFile.StubHandler | None:
return _handler
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"):
msg = "BUFR save handler not installed"
raise OSError(msg)
diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py
index 1c455b032..f67f27d73 100644
--- a/src/PIL/DcxImagePlugin.py
+++ b/src/PIL/DcxImagePlugin.py
@@ -42,7 +42,7 @@ class DcxImageFile(PcxImageFile):
format_description = "Intel DCX"
_close_exclusive_fp_after_loading = False
- def _open(self):
+ def _open(self) -> None:
# Header
s = self.fp.read(4)
if not _accept(s):
@@ -58,7 +58,7 @@ class DcxImageFile(PcxImageFile):
self._offset.append(offset)
self._fp = self.fp
- self.frame = None
+ self.frame = -1
self.n_frames = len(self._offset)
self.is_animated = self.n_frames > 1
self.seek(0)
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index 1575f2d88..a57e4aea2 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -16,6 +16,7 @@ import io
import struct
import sys
from enum import IntEnum, IntFlag
+from typing import IO
from . import Image, ImageFile, ImagePalette
from ._binary import i32le as i32
@@ -379,6 +380,7 @@ class DdsImageFile(ImageFile.ImageFile):
elif pfflags & DDPF.PALETTEINDEXED8:
self._mode = "P"
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
+ self.palette.mode = "RGBA"
elif pfflags & DDPF.FOURCC:
offset = header_size + 4
if fourcc == D3DFMT.DXT1:
@@ -479,7 +481,8 @@ class DdsImageFile(ImageFile.ImageFile):
class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
+ assert self.fd is not None
bitcount, masks = self.args
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
@@ -510,7 +513,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
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"):
msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg)
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 5a44baa49..59bb8594d 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -27,10 +27,10 @@ import re
import subprocess
import sys
import tempfile
+from typing import IO
from . import Image, ImageFile
from ._binary import i32le as i32
-from ._deprecate import deprecate
# --------------------------------------------------------------------
@@ -158,43 +158,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
return im
-class PSFile:
- """
- Wrapper for bytesio object that treats either CR or LF as end of line.
- This class is no longer used internally, but kept for backwards compatibility.
- """
-
- def __init__(self, fp):
- deprecate(
- "PSFile",
- 11,
- action="If you need the functionality of this class "
- "you will need to implement it yourself.",
- )
- self.fp = fp
- self.char = None
-
- def seek(self, offset, whence=io.SEEK_SET):
- self.char = None
- self.fp.seek(offset, whence)
-
- def readline(self) -> str:
- s = [self.char or b""]
- self.char = None
-
- c = self.fp.read(1)
- while (c not in b"\r\n") and len(c):
- s.append(c)
- c = self.fp.read(1)
-
- self.char = self.fp.read(1)
- # line endings can be 1 or 2 of \r \n, in either order
- if self.char in b"\r\n":
- self.char = None
-
- return b"".join(s).decode("latin-1")
-
-
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
@@ -228,7 +191,12 @@ class EpsImageFile(ImageFile.ImageFile):
reading_trailer_comments = False
trailer_reached = False
- def check_required_header_comments():
+ def check_required_header_comments() -> None:
+ """
+ The EPS specification requires that some headers exist.
+ This should be checked when the header comments formally end,
+ when image data starts, or when the file ends, whichever comes first.
+ """
if "PS-Adobe" not in self.info:
msg = 'EPS header missing "%!PS-Adobe" comment'
raise SyntaxError(msg)
@@ -236,7 +204,7 @@ class EpsImageFile(ImageFile.ImageFile):
msg = 'EPS header missing "%%BoundingBox" comment'
raise SyntaxError(msg)
- def _read_comment(s):
+ def _read_comment(s: str) -> bool:
nonlocal reading_trailer_comments
try:
m = split.match(s)
@@ -244,33 +212,33 @@ class EpsImageFile(ImageFile.ImageFile):
msg = "not an EPS file"
raise SyntaxError(msg) from e
- if m:
- k, v = m.group(1, 2)
- self.info[k] = v
- if k == "BoundingBox":
- if v == "(atend)":
- reading_trailer_comments = True
- elif not self._size or (
- trailer_reached and reading_trailer_comments
- ):
- try:
- # Note: The DSC spec says that BoundingBox
- # fields should be integers, but some drivers
- # put floating point values there anyway.
- box = [int(float(i)) for i in v.split()]
- self._size = box[2] - box[0], box[3] - box[1]
- self.tile = [
- ("eps", (0, 0) + self.size, offset, (length, box))
- ]
- except Exception:
- pass
- return True
+ if not m:
+ return False
+
+ k, v = m.group(1, 2)
+ self.info[k] = v
+ if k == "BoundingBox":
+ if v == "(atend)":
+ reading_trailer_comments = True
+ elif not self._size or (trailer_reached and reading_trailer_comments):
+ try:
+ # Note: The DSC spec says that BoundingBox
+ # fields should be integers, but some drivers
+ # put floating point values there anyway.
+ box = [int(float(i)) for i in v.split()]
+ self._size = box[2] - box[0], box[3] - box[1]
+ self.tile = [("eps", (0, 0) + self.size, offset, (length, box))]
+ except Exception:
+ pass
+ return True
while True:
byte = self.fp.read(1)
if byte == b"":
# if we didn't read a byte we must be at the end of the file
if bytes_read == 0:
+ if reading_header_comments:
+ check_required_header_comments()
break
elif byte in b"\r\n":
# if we read a line ending character, ignore it and parse what
@@ -366,13 +334,11 @@ class EpsImageFile(ImageFile.ImageFile):
trailer_reached = True
bytes_read = 0
- check_required_header_comments()
-
if not self._size:
msg = "cannot determine EPS bounding box"
raise OSError(msg)
- def _find_offset(self, fp):
+ def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
s = fp.read(4)
if s == b"%!PS":
@@ -395,7 +361,9 @@ class EpsImageFile(ImageFile.ImageFile):
return length, offset
- def load(self, scale=1, transparency=False):
+ def load(
+ self, scale: int = 1, transparency: bool = False
+ ) -> Image.core.PixelAccess | None:
# Load EPS via Ghostscript
if self.tile:
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
@@ -413,7 +381,7 @@ class EpsImageFile(ImageFile.ImageFile):
# --------------------------------------------------------------------
-def _save(im, fp, filename, eps=1):
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
"""EPS Writer for the Python Imaging Library."""
# make sure image data is available
diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index 071918925..4846054b1 100644
--- a/src/PIL/FitsImagePlugin.py
+++ b/src/PIL/FitsImagePlugin.py
@@ -115,14 +115,18 @@ class FitsImageFile(ImageFile.ImageFile):
elif number_of_bits in (-32, -64):
self._mode = "F"
- args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,)
+ args: tuple[str | int, ...]
+ if decoder_name == "raw":
+ args = (self.mode, 0, -1)
+ else:
+ args = (number_of_bits,)
return decoder_name, offset, args
class FitsGzipDecoder(ImageFile.PyDecoder):
_pulls_fd = True
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
value = gzip.decompress(self.fd.read())
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index dceb83927..52d1fce31 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -45,7 +45,7 @@ class FliImageFile(ImageFile.ImageFile):
format_description = "Autodesk FLI/FLC Animation"
_close_exclusive_fp_after_loading = False
- def _open(self):
+ def _open(self) -> None:
# HEAD
s = self.fp.read(128)
if not (_accept(s) and s[20:22] == b"\x00\x00"):
@@ -83,7 +83,7 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF1FA:
# look for palette chunk
number_of_subchunks = i16(s, 6)
- chunk_size = None
+ chunk_size: int | None = None
for _ in range(number_of_subchunks):
if chunk_size is not None:
self.fp.seek(chunk_size - 6, os.SEEK_CUR)
@@ -96,8 +96,9 @@ class FliImageFile(ImageFile.ImageFile):
if not chunk_size:
break
- palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
- self.palette = ImagePalette.raw("RGB", b"".join(palette))
+ self.palette = ImagePalette.raw(
+ "RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette)
+ )
# set things up to decode first frame
self.__frame = -1
@@ -105,7 +106,7 @@ class FliImageFile(ImageFile.ImageFile):
self.__rewind = self.fp.tell()
self.seek(0)
- def _palette(self, palette, shift):
+ def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None:
# load palette
i = 0
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index 4ba93bb39..386e37233 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -53,7 +53,7 @@ class FpxImageFile(ImageFile.ImageFile):
format = "FPX"
format_description = "FlashPix"
- def _open(self):
+ def _open(self) -> None:
#
# read the OLE directory and see if this is a likely
# to be a FlashPix file
@@ -64,13 +64,14 @@ class FpxImageFile(ImageFile.ImageFile):
msg = "not an FPX file; invalid OLE file"
raise SyntaxError(msg) from e
- if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
+ root = self.ole.root
+ if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
msg = "not an FPX file; bad root CLSID"
raise SyntaxError(msg)
self._open_index(1)
- def _open_index(self, index=1):
+ def _open_index(self, index: int = 1) -> None:
#
# get the Image Contents Property Set
@@ -85,7 +86,7 @@ class FpxImageFile(ImageFile.ImageFile):
size = max(self.size)
i = 1
while size > 64:
- size = size / 2
+ size = size // 2
i += 1
self.maxid = i - 1
@@ -99,8 +100,7 @@ class FpxImageFile(ImageFile.ImageFile):
s = prop[0x2000002 | id]
- bands = i32(s, 4)
- if bands > 4:
+ if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4:
msg = "Invalid number of bands"
raise OSError(msg)
@@ -118,7 +118,7 @@ class FpxImageFile(ImageFile.ImageFile):
self._open_subimage(1, self.maxid)
- def _open_subimage(self, index=1, subimage=0):
+ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
#
# setup tile descriptors for a given subimage
@@ -231,7 +231,7 @@ class FpxImageFile(ImageFile.ImageFile):
self._fp = self.fp
self.fp = None
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if not self.fp:
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
@@ -241,7 +241,7 @@ class FpxImageFile(ImageFile.ImageFile):
self.ole.close()
super().close()
- def __exit__(self, *args):
+ def __exit__(self, *args: object) -> None:
self.ole.close()
super().__exit__()
diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py
index 93e89b1e6..3c8feea5f 100644
--- a/src/PIL/GbrImagePlugin.py
+++ b/src/PIL/GbrImagePlugin.py
@@ -88,7 +88,7 @@ class GbrImageFile(ImageFile.ImageFile):
# Data is an uncompressed block of w * h * bytes/pixel
self._data_size = width * height * color_depth
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if not self.im:
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self._data_size))
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 962a92834..bf74f9356 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -29,8 +29,10 @@ import itertools
import math
import os
import subprocess
+import sys
from enum import IntEnum
from functools import cached_property
+from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union
from . import (
Image,
@@ -45,6 +47,9 @@ from ._binary import i16le as i16
from ._binary import o8
from ._binary import o16le as o16
+if TYPE_CHECKING:
+ from . import _imaging
+
class LoadingStrategy(IntEnum):
""".. versionadded:: 9.1.0"""
@@ -117,7 +122,7 @@ class GifImageFile(ImageFile.ImageFile):
self._seek(0) # get ready to read first frame
@property
- def n_frames(self):
+ def n_frames(self) -> int:
if self._n_frames is None:
current = self.tell()
try:
@@ -162,11 +167,11 @@ class GifImageFile(ImageFile.ImageFile):
msg = "no more images in GIF file"
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:
# rewind
self.__offset = 0
- self.dispose = None
+ self.dispose: _imaging.ImagingCore | None = None
self.__frame = -1
self._fp.seek(self.__rewind)
self.disposal_method = 0
@@ -194,9 +199,9 @@ class GifImageFile(ImageFile.ImageFile):
msg = "no more images in GIF file"
raise EOFError(msg)
- palette = None
+ palette: ImagePalette.ImagePalette | Literal[False] | None = None
- info = {}
+ info: dict[str, Any] = {}
frame_transparency = None
interlace = None
frame_dispose_extent = None
@@ -212,7 +217,7 @@ class GifImageFile(ImageFile.ImageFile):
#
s = self.fp.read(1)
block = self.data()
- if s[0] == 249:
+ if s[0] == 249 and block is not None:
#
# graphic control extension
#
@@ -248,14 +253,14 @@ class GifImageFile(ImageFile.ImageFile):
info["comment"] = comment
s = None
continue
- elif s[0] == 255 and frame == 0:
+ elif s[0] == 255 and frame == 0 and block is not None:
#
# application extension
#
info["extension"] = block, self.fp.tell()
if block[:11] == b"NETSCAPE2.0":
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)
while self.data():
pass
@@ -326,7 +331,6 @@ class GifImageFile(ImageFile.ImageFile):
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
or palette
):
- self.pyaccess = None
if "transparency" in self.info:
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
@@ -336,60 +340,60 @@ class GifImageFile(ImageFile.ImageFile):
self._mode = "RGB"
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 color * 3 + 3 > len(self._frame_palette.palette):
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:
- color = (color, color, color)
- return color
+ return (color, color, color)
+ self.dispose = None
self.dispose_extent = frame_dispose_extent
- try:
- if self.disposal_method < 2:
- # do not dispose or none specified
- self.dispose = None
- elif self.disposal_method == 2:
- # replace with background colour
+ if self.dispose_extent and self.disposal_method >= 2:
+ try:
+ if 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
- 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)
+
+ # by convention, attempt to use transparency first
dispose_mode = "P"
- color = frame_transparency
- if self.mode in ("RGB", "RGBA"):
- dispose_mode = "RGBA"
- color = _rgb(frame_transparency) + (0,)
+ 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)
- except AttributeError:
- pass
+ else:
+ # 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:
transparency = -1
@@ -428,7 +432,7 @@ class GifImageFile(ImageFile.ImageFile):
self._prev_im = self.im
if self._frame_palette:
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
- self.im.putpalette(*self._frame_palette.getdata())
+ self.im.putpalette("RGB", *self._frame_palette.getdata())
else:
self.im = None
self._mode = temp_mode
@@ -453,6 +457,8 @@ class GifImageFile(ImageFile.ImageFile):
frame_im = self.im.convert("RGBA")
else:
frame_im = self.im.convert("RGB")
+
+ assert self.dispose_extent is not None
frame_im = self._crop(frame_im, self.dispose_extent)
self.im = self._prev_im
@@ -498,7 +504,12 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
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.
- Sets the palette to the incoming palette, if provided.
@@ -526,8 +537,10 @@ def _normalize_palette(im, palette, info):
source_palette = bytearray(i // 3 for i in range(768))
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
+ used_palette_colors: list[int] | None
if palette:
used_palette_colors = []
+ assert source_palette is not None
for i in range(0, len(source_palette), 3):
source_color = tuple(source_palette[i : i + 3])
index = im.palette.colors.get(source_color)
@@ -558,7 +571,11 @@ def _normalize_palette(im, palette, info):
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)
for k, v in im_out.info.items():
im.encoderinfo.setdefault(k, v)
@@ -579,7 +596,9 @@ def _write_single_frame(im, fp, palette):
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):
im_frame = im_frame.convert("RGBA")
base_im = base_im.convert("RGBA")
@@ -587,12 +606,20 @@ def _getbbox(base_im, im_frame):
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")
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
- im_frames = []
- previous_im = None
+ im_frames: list[_Frame] = []
+ previous_im: Image.Image | None = None
frame_count = 0
background_im = None
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
@@ -618,24 +645,22 @@ def _write_multiple_frames(im, fp, palette):
frame_count += 1
diff_frame = None
- if im_frames:
+ if im_frames and previous_im:
# delta frame
delta, bbox = _getbbox(previous_im, im_frame)
if not bbox:
# This frame is identical to the previous frame
if encoderinfo.get("duration"):
- im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[
- "duration"
- ]
+ im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
continue
- if im_frames[-1]["encoderinfo"].get("disposal") == 2:
+ if im_frames[-1].encoderinfo.get("disposal") == 2:
if background_im is None:
color = im.encoderinfo.get(
"transparency", im.info.get("transparency", (0, 0, 0))
)
background = _get_background(im_frame, color)
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]
elif encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo:
@@ -681,39 +706,39 @@ def _write_multiple_frames(im, fp, palette):
else:
bbox = None
previous_im = im_frame
- im_frames.append(
- {"im": diff_frame or im_frame, "bbox": bbox, "encoderinfo": encoderinfo}
- )
+ im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))
if len(im_frames) == 1:
if "duration" in im.encoderinfo:
# Since multiple frames will not be written, use the combined duration
- im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"]
- return
+ im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
+ return False
for frame_data in im_frames:
- im_frame = frame_data["im"]
- if not frame_data["bbox"]:
+ im_frame = frame_data.im
+ if not frame_data.bbox:
# 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)
offset = (0, 0)
else:
# compress difference
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"])
- offset = frame_data["bbox"][:2]
- _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
+ im_frame = im_frame.crop(frame_data.bbox)
+ offset = frame_data.bbox[:2]
+ _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
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)
-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
if "palette" in im.encoderinfo or "palette" in im.info:
palette = im.encoderinfo.get("palette", im.info.get("palette"))
@@ -730,7 +755,7 @@ def _save(im, fp, filename, save_all=False):
fp.flush()
-def get_interlace(im):
+def get_interlace(im: Image.Image) -> int:
interlace = im.encoderinfo.get("interlace", 1)
# workaround for @PIL153
@@ -740,7 +765,9 @@ def get_interlace(im):
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:
transparency = im.encoderinfo["transparency"]
except KeyError:
@@ -788,7 +815,7 @@ def _write_local_header(fp, im, offset, flags):
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.
# To use, uncomment the register_save call at the end of the file.
#
@@ -819,6 +846,7 @@ def _save_netpbm(im, fp, filename):
)
# Allow ppmquant to receive SIGPIPE if ppmtogif exits
+ assert quant_proc.stdout is not None
quant_proc.stdout.close()
retcode = quant_proc.wait()
@@ -840,7 +868,7 @@ def _save_netpbm(im, fp, filename):
_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.
@@ -884,6 +912,7 @@ def _get_optimize(im, info):
and current_palette_size > 2
):
return used_palette_colors
+ return None
def _get_color_table_size(palette_bytes: bytes) -> int:
@@ -924,7 +953,10 @@ def _get_palette_bytes(im: Image.Image) -> bytes:
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
if info_background:
if isinstance(info_background, tuple):
@@ -947,7 +979,7 @@ def _get_background(im, info_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"""
# Header Block
@@ -1009,7 +1041,12 @@ def _get_global_header(im, info):
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:
im_frame.encoderinfo = params
@@ -1029,7 +1066,9 @@ def _write_frame_data(fp, im_frame, offset, params):
# 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.
@@ -1041,11 +1080,11 @@ def getheader(im, palette=None, info=None):
:returns: tuple of(list of header items, optimized palette)
"""
- used_palette_colors = _get_optimize(im, info)
-
if info is None:
info = {}
+ used_palette_colors = _get_optimize(im, info)
+
if "background" not in info and "background" in im.info:
info["background"] = im.info["background"]
@@ -1057,7 +1096,9 @@ def getheader(im, palette=None, info=None):
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
@@ -1074,12 +1115,23 @@ def getdata(im, offset=(0, 0), **params):
:returns: List of bytes containing GIF encoded frame data
"""
+ from io import BytesIO
- class Collector:
+ class Collector(BytesIO):
data = []
- def write(self, data):
- self.data.append(data)
+ if sys.version_info >= (3, 12):
+ 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
diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py
index 2d8c78ea9..220eac57e 100644
--- a/src/PIL/GimpGradientFile.py
+++ b/src/PIL/GimpGradientFile.py
@@ -21,6 +21,7 @@ See the GIMP distribution for more information.)
from __future__ import annotations
from math import log, pi, sin, sqrt
+from typing import IO, Callable
from ._binary import o8
@@ -28,7 +29,7 @@ EPSILON = 1e-10
"""""" # Enable auto-doc for data member
-def linear(middle, pos):
+def linear(middle: float, pos: float) -> float:
if pos <= middle:
if middle < EPSILON:
return 0.0
@@ -43,19 +44,19 @@ def linear(middle, pos):
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)))
-def sine(middle, pos):
+def sine(middle: float, pos: float) -> float:
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)
-def sphere_decreasing(middle, pos):
+def sphere_decreasing(middle: float, pos: float) -> float:
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
@@ -64,9 +65,22 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
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 = []
ix = 0
@@ -101,7 +115,7 @@ class GradientFile:
class GimpGradientFile(GradientFile):
"""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":
msg = "not a GIMP gradient file"
raise SyntaxError(msg)
@@ -114,7 +128,7 @@ class GimpGradientFile(GradientFile):
count = int(line)
- gradient = []
+ self.gradient = []
for i in range(count):
s = fp.readline().split()
@@ -132,6 +146,4 @@ class GimpGradientFile(GradientFile):
msg = "cannot handle HSV colour space"
raise OSError(msg)
- gradient.append((x0, x1, xm, rgb0, rgb1, segment))
-
- self.gradient = gradient
+ self.gradient.append((x0, x1, xm, rgb0, rgb1, segment))
diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py
index 2274f1a8b..4cad0ebee 100644
--- a/src/PIL/GimpPaletteFile.py
+++ b/src/PIL/GimpPaletteFile.py
@@ -16,6 +16,7 @@
from __future__ import annotations
import re
+from typing import IO
from ._binary import o8
@@ -25,8 +26,8 @@ class GimpPaletteFile:
rawmode = "RGB"
- def __init__(self, fp):
- self.palette = [o8(i) * 3 for i in range(256)]
+ def __init__(self, fp: IO[bytes]) -> None:
+ palette = [o8(i) * 3 for i in range(256)]
if fp.readline()[:12] != b"GIMP Palette":
msg = "not a GIMP palette file"
@@ -49,9 +50,9 @@ class GimpPaletteFile:
msg = "bad palette entry"
raise ValueError(msg)
- self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
+ palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
- self.palette = b"".join(self.palette)
+ self.palette = b"".join(palette)
def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode
diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py
index 13bdfa616..e9aa084b2 100644
--- a/src/PIL/GribStubImagePlugin.py
+++ b/src/PIL/GribStubImagePlugin.py
@@ -10,12 +10,14 @@
#
from __future__ import annotations
+from typing import IO
+
from . import Image, ImageFile
_handler = None
-def register_handler(handler):
+def register_handler(handler: ImageFile.StubHandler | None) -> None:
"""
Install application-specific GRIB image handler.
@@ -54,11 +56,11 @@ class GribStubImageFile(ImageFile.StubImageFile):
if loader:
loader.open(self)
- def _load(self):
+ def _load(self) -> ImageFile.StubHandler | None:
return _handler
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"):
msg = "GRIB save handler not installed"
raise OSError(msg)
diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py
index afbfd1639..cc9e73deb 100644
--- a/src/PIL/Hdf5StubImagePlugin.py
+++ b/src/PIL/Hdf5StubImagePlugin.py
@@ -10,12 +10,14 @@
#
from __future__ import annotations
+from typing import IO
+
from . import Image, ImageFile
_handler = None
-def register_handler(handler):
+def register_handler(handler: ImageFile.StubHandler | None) -> None:
"""
Install application-specific HDF5 image handler.
@@ -54,11 +56,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
if loader:
loader.open(self)
- def _load(self):
+ def _load(self) -> ImageFile.StubHandler | None:
return _handler
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"):
msg = "HDF5 save handler not installed"
raise OSError(msg)
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index 0a86ba883..8729f7643 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -22,6 +22,7 @@ import io
import os
import struct
import sys
+from typing import IO
from . import Image, ImageFile, PngImagePlugin, features
@@ -33,11 +34,13 @@ MAGIC = b"icns"
HEADERSIZE = 8
-def nextheader(fobj):
+def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]:
return struct.unpack(">4sI", fobj.read(HEADERSIZE))
-def read_32t(fobj, start_length, size):
+def read_32t(
+ fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
+) -> dict[str, Image.Image]:
# The 128x128 icon seems to have an extra header for some reason.
(start, length) = start_length
fobj.seek(start)
@@ -48,7 +51,9 @@ def read_32t(fobj, start_length, size):
return read_32(fobj, (start + 4, length - 4), size)
-def read_32(fobj, start_length, size):
+def read_32(
+ fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
+) -> dict[str, Image.Image]:
"""
Read a 32bit RGB icon resource. Seems to be either uncompressed or
an RLE packbits-like scheme.
@@ -71,14 +76,14 @@ def read_32(fobj, start_length, size):
byte = fobj.read(1)
if not byte:
break
- byte = byte[0]
- if byte & 0x80:
- blocksize = byte - 125
+ byte_int = byte[0]
+ if byte_int & 0x80:
+ blocksize = byte_int - 125
byte = fobj.read(1)
for i in range(blocksize):
data.append(byte)
else:
- blocksize = byte + 1
+ blocksize = byte_int + 1
data.append(fobj.read(blocksize))
bytesleft -= blocksize
if bytesleft <= 0:
@@ -91,7 +96,9 @@ def read_32(fobj, start_length, size):
return {"RGB": im}
-def read_mk(fobj, start_length, size):
+def read_mk(
+ fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
+) -> dict[str, Image.Image]:
# Alpha masks seem to be uncompressed
start = start_length[0]
fobj.seek(start)
@@ -101,10 +108,14 @@ def read_mk(fobj, start_length, size):
return {"A": band}
-def read_png_or_jpeg2000(fobj, start_length, size):
+def read_png_or_jpeg2000(
+ fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
+) -> dict[str, Image.Image]:
(start, length) = start_length
fobj.seek(start)
sig = fobj.read(12)
+
+ im: Image.Image
if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a":
fobj.seek(start)
im = PngImagePlugin.PngImageFile(fobj)
@@ -163,12 +174,12 @@ class IcnsFile:
],
}
- def __init__(self, fobj):
+ def __init__(self, fobj: IO[bytes]) -> None:
"""
fobj is a file-like object as an icns resource
"""
# signature : (start, length)
- self.dct = dct = {}
+ self.dct = {}
self.fobj = fobj
sig, filesize = nextheader(fobj)
if not _accept(sig):
@@ -182,11 +193,11 @@ class IcnsFile:
raise SyntaxError(msg)
i += HEADERSIZE
blocksize -= HEADERSIZE
- dct[sig] = (i, blocksize)
+ self.dct[sig] = (i, blocksize)
fobj.seek(blocksize, io.SEEK_CUR)
i += blocksize
- def itersizes(self):
+ def itersizes(self) -> list[tuple[int, int, int]]:
sizes = []
for size, fmts in self.SIZES.items():
for fmt, reader in fmts:
@@ -195,14 +206,14 @@ class IcnsFile:
break
return sizes
- def bestsize(self):
+ def bestsize(self) -> tuple[int, int, int]:
sizes = self.itersizes()
if not sizes:
msg = "No 32bit icon resources found"
raise SyntaxError(msg)
return max(sizes)
- def dataforsize(self, size):
+ def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]:
"""
Get an icon resource as {channel: array}. Note that
the arrays are bottom-up like windows bitmaps and will likely
@@ -215,18 +226,20 @@ class IcnsFile:
dct.update(reader(self.fobj, desc, size))
return dct
- def getimage(self, size=None):
+ def getimage(
+ self, size: tuple[int, int] | tuple[int, int, int] | None = None
+ ) -> Image.Image:
if size is None:
size = self.bestsize()
- if len(size) == 2:
+ elif len(size) == 2:
size = (size[0], size[1], 1)
channels = self.dataforsize(size)
- im = channels.get("RGBA", None)
+ im = channels.get("RGBA")
if im:
return im
- im = channels.get("RGB").copy()
+ im = channels["RGB"].copy()
try:
im.putalpha(channels["A"])
except KeyError:
@@ -267,7 +280,7 @@ class IcnsImageFile(ImageFile.ImageFile):
return self._size
@size.setter
- def size(self, value):
+ def size(self, value) -> None:
info_size = value
if info_size not in self.info["sizes"] and len(info_size) == 2:
info_size = (info_size[0], info_size[1], 1)
@@ -286,7 +299,7 @@ class IcnsImageFile(ImageFile.ImageFile):
raise ValueError(msg)
self._size = value
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if len(self.size) == 3:
self.best_size = self.size
self.size = (
@@ -312,7 +325,7 @@ class IcnsImageFile(ImageFile.ImageFile):
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,
that are then combined into a .icns file.
@@ -346,29 +359,27 @@ def _save(im, fp, filename):
entries = []
for type, size in sizes.items():
stream = size_streams[size]
- entries.append(
- {"type": type, "size": HEADERSIZE + len(stream), "stream": stream}
- )
+ entries.append((type, HEADERSIZE + len(stream), stream))
# Header
fp.write(MAGIC)
file_length = HEADERSIZE # Header
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))
# TOC
fp.write(b"TOC ")
fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
for entry in entries:
- fp.write(entry["type"])
- fp.write(struct.pack(">i", entry["size"]))
+ fp.write(entry[0])
+ fp.write(struct.pack(">i", entry[1]))
# Data
for entry in entries:
- fp.write(entry["type"])
- fp.write(struct.pack(">i", entry["size"]))
- fp.write(entry["stream"])
+ fp.write(entry[0])
+ fp.write(struct.pack(">i", entry[1]))
+ fp.write(entry[2])
if hasattr(fp, "flush"):
fp.flush()
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index cea093f9c..650f5e4f1 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -25,6 +25,7 @@ from __future__ import annotations
import warnings
from io import BytesIO
from math import ceil, log
+from typing import IO
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16
@@ -39,7 +40,7 @@ from ._binary import o32le as o32
_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)
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
sizes = im.encoderinfo.get(
@@ -119,7 +120,7 @@ def _accept(prefix: bytes) -> bool:
class IcoFile:
- def __init__(self, buf):
+ def __init__(self, buf) -> None:
"""
Parse image from file-like object containing ico file data
"""
@@ -176,25 +177,25 @@ class IcoFile:
# ICO images are usually squares
self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True)
- def sizes(self):
+ def sizes(self) -> set[tuple[int, int]]:
"""
Get a list of all available icon sizes and color depths.
"""
return {(h["width"], h["height"]) for h in self.entry}
- def getentryindex(self, size, bpp=False):
+ def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
for i, h in enumerate(self.entry):
if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
return i
return 0
- def getimage(self, size, bpp=False):
+ def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image:
"""
Get an image from the icon
"""
return self.frame(self.getentryindex(size, bpp))
- def frame(self, idx):
+ def frame(self, idx: int) -> Image.Image:
"""
Get an image from frame idx
"""
@@ -205,6 +206,7 @@ class IcoFile:
data = self.buf.read(8)
self.buf.seek(header["offset"])
+ im: Image.Image
if data[:8] == PngImagePlugin._MAGIC:
# png frame
im = PngImagePlugin.PngImageFile(self.buf)
@@ -319,7 +321,7 @@ class IcoImageFile(ImageFile.ImageFile):
raise ValueError(msg)
self._size = value
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if self.im is not None and self.im.size == self.size:
# Already loaded
return Image.Image.load(self)
@@ -327,7 +329,6 @@ class IcoImageFile(ImageFile.ImageFile):
# if tile is PNG, it won't really be loaded yet
im.load()
self.im = im.im
- self.pyaccess = None
self._mode = im.mode
if im.palette:
self.palette = im.palette
@@ -340,6 +341,7 @@ class IcoImageFile(ImageFile.ImageFile):
self.info["sizes"] = set(sizes)
self.size = im.size
+ return None
def load_seek(self, pos: int) -> None:
# Flag the ImageFile.Parser so that it
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 8e949ebaf..2fb7ecd52 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -28,6 +28,7 @@ from __future__ import annotations
import os
import re
+from typing import IO, Any
from . import Image, ImageFile, ImagePalette
@@ -78,7 +79,7 @@ OPEN = {
"LA image": ("LA", "LA;L"),
"PA image": ("LA", "PA;L"),
"RGBA image": ("RGBA", "RGBA;L"),
- "RGBX image": ("RGBX", "RGBX;L"),
+ "RGBX image": ("RGB", "RGBX;L"),
"CMYK image": ("CMYK", "CMYK;L"),
"YCC image": ("YCbCr", "YCbCr;L"),
}
@@ -103,7 +104,7 @@ for j in range(2, 33):
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
-def number(s):
+def number(s: Any) -> float:
try:
return int(s)
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:
image_type, rawmode = SAVE[im.mode]
except KeyError as e:
@@ -340,6 +341,8 @@ def _save(im, fp, filename):
# or: SyntaxError("not an IM file")
# 8 characters are used for "Name: " and "\r\n"
# 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 = "".join([name[: 92 - len(ext)], ext])
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 958b95e3b..565abe71d 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -38,10 +38,17 @@ import struct
import sys
import tempfile
import warnings
-from collections.abc import Callable, MutableMapping
+from collections.abc import Callable, MutableMapping, Sequence
from enum import IntEnum
from types import ModuleType
-from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Literal,
+ Protocol,
+ cast,
+)
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
@@ -56,7 +63,6 @@ from . import (
)
from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
-from ._typing import StrOrBytesPath, TypeGuard
from ._util import DeferredError, is_path
ElementTree: ModuleType | None
@@ -76,6 +82,8 @@ class DecompressionBombError(Exception):
pass
+WARN_POSSIBLE_FORMATS: bool = False
+
# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image
MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3)
@@ -114,14 +122,6 @@ except ImportError as v:
raise
-USE_CFFI_ACCESS = False
-cffi: ModuleType | None
-try:
- import cffi
-except ImportError:
- cffi = None
-
-
def isImageType(t: Any) -> TypeGuard[Image]:
"""
Checks if an object is an image object.
@@ -218,7 +218,8 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries
if TYPE_CHECKING:
- from . import ImageFile
+ from . import ImageFile, ImagePalette
+ from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = []
OPEN: dict[
str,
@@ -410,7 +411,9 @@ def init() -> bool:
# Codec factories (used by tobytes/frombytes and ImageFile.load)
-def _getdecoder(mode, decoder_name, args, extra=()):
+def _getdecoder(
+ mode: str, decoder_name: str, args: Any, extra: tuple[Any, ...] = ()
+) -> core.ImagingDecoder | ImageFile.PyDecoder:
# tweak arguments
if args is None:
args = ()
@@ -433,7 +436,9 @@ def _getdecoder(mode, decoder_name, args, extra=()):
return decoder(mode, *args + extra)
-def _getencoder(mode, encoder_name, args, extra=()):
+def _getencoder(
+ mode: str, encoder_name: str, args: Any, extra: tuple[Any, ...] = ()
+) -> core.ImagingEncoder | ImageFile.PyEncoder:
# tweak arguments
if args is None:
args = ()
@@ -503,6 +508,12 @@ def _getscaleoffset(expr):
# Implementation wrapper
+class SupportsGetData(Protocol):
+ def getdata(
+ self,
+ ) -> tuple[Transform, Sequence[int]]: ...
+
+
class Image:
"""
This class represents an image object. To create
@@ -528,7 +539,6 @@ class Image:
self.palette = None
self.info = {}
self.readonly = 0
- self.pyaccess = None
self._exif = None
@property
@@ -544,10 +554,10 @@ class Image:
return self._size
@property
- def mode(self):
+ def mode(self) -> str:
return self._mode
- def _new(self, im) -> Image:
+ def _new(self, im: core.ImagingCore) -> Image:
new = Image()
new.im = im
new._mode = im.mode
@@ -610,7 +620,6 @@ class Image:
def _copy(self) -> None:
self.load()
self.im = self.im.copy()
- self.pyaccess = None
self.readonly = 0
def _ensure_mutable(self) -> None:
@@ -620,7 +629,7 @@ class Image:
self.load()
def _dump(
- self, file: str | None = None, format: str | None = None, **options
+ self, file: str | None = None, format: str | None = None, **options: Any
) -> str:
suffix = ""
if format:
@@ -643,10 +652,12 @@ class Image:
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 (
- self.__class__ is other.__class__
- and self.mode == other.mode
+ self.mode == other.mode
and self.size == other.size
and self.info == other.info
and self.getpalette() == other.getpalette()
@@ -679,7 +690,7 @@ class Image:
)
)
- def _repr_image(self, image_format, **kwargs):
+ def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None:
"""Helper function for iPython display hook.
:param image_format: Image format.
@@ -692,14 +703,14 @@ class Image:
return None
return b.getvalue()
- def _repr_png_(self):
+ def _repr_png_(self) -> bytes | None:
"""iPython display hook support for PNG format.
:returns: PNG version of the image as bytes
"""
return self._repr_image("PNG", compress_level=1)
- def _repr_jpeg_(self):
+ def _repr_jpeg_(self) -> bytes | None:
"""iPython display hook support for JPEG format.
:returns: JPEG version of the image as bytes
@@ -746,7 +757,7 @@ class Image:
self.putpalette(palette)
self.frombytes(data)
- def tobytes(self, encoder_name: str = "raw", *args) -> bytes:
+ def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes:
"""
Return image as a bytes object.
@@ -768,12 +779,13 @@ class Image:
:returns: A :py:class:`bytes` object.
"""
- # may pass tuple instead of argument list
- if len(args) == 1 and isinstance(args[0], tuple):
- args = args[0]
+ encoder_args: Any = args
+ if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple):
+ # may pass tuple instead of argument list
+ encoder_args = encoder_args[0]
- if encoder_name == "raw" and args == ():
- args = self.mode
+ if encoder_name == "raw" and encoder_args == ():
+ encoder_args = self.mode
self.load()
@@ -781,7 +793,7 @@ class Image:
return b""
# unpack data
- e = _getencoder(self.mode, encoder_name, args)
+ e = _getencoder(self.mode, encoder_name, encoder_args)
e.setimage(self.im)
bufsize = max(65536, self.size[0] * 4) # see RawEncode.c
@@ -824,7 +836,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.
@@ -835,16 +849,17 @@ class Image:
if self.width == 0 or self.height == 0:
return
- # may pass tuple instead of argument list
- if len(args) == 1 and isinstance(args[0], tuple):
- args = args[0]
+ decoder_args: Any = args
+ if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
+ # may pass tuple instead of argument list
+ decoder_args = decoder_args[0]
# default format
- if decoder_name == "raw" and args == ():
- args = self.mode
+ if decoder_name == "raw" and decoder_args == ():
+ decoder_args = self.mode
# unpack data
- d = _getdecoder(self.mode, decoder_name, args)
+ d = _getdecoder(self.mode, decoder_name, decoder_args)
d.setimage(self.im)
s = d.decode(data)
@@ -855,7 +870,7 @@ class Image:
msg = "cannot decode image data"
raise ValueError(msg)
- def load(self):
+ def load(self) -> core.PixelAccess | None:
"""
Allocates storage for the image and loads the pixel data. In
normal cases, you don't need to call this method, since the
@@ -868,12 +883,12 @@ class Image:
operations. See :ref:`file-handling` for more information.
:returns: An image access object.
- :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
+ :rtype: :py:class:`.PixelAccess`
"""
if self.im is not None and self.palette and self.palette.dirty:
# realize palette
mode, arr = self.palette.getdata()
- self.im.putpalette(mode, arr)
+ self.im.putpalette(self.palette.mode, mode, arr)
self.palette.dirty = 0
self.palette.rawmode = None
if "transparency" in self.info and mode in ("LA", "PA"):
@@ -883,20 +898,13 @@ class Image:
self.im.putpalettealphas(self.info["transparency"])
self.palette.mode = "RGBA"
else:
- palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB"
- self.palette.mode = palette_mode
- self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
+ self.palette.palette = self.im.getpalette(
+ self.palette.mode, self.palette.mode
+ )
if self.im is not None:
- if cffi and USE_CFFI_ACCESS:
- if self.pyaccess:
- return self.pyaccess
- from . import PyAccess
-
- self.pyaccess = PyAccess.new(self, self.readonly)
- if self.pyaccess:
- return self.pyaccess
return self.im.pixel_access(self.readonly)
+ return None
def verify(self) -> None:
"""
@@ -988,9 +996,11 @@ class Image:
if has_transparency and self.im.bands == 3:
transparency = new_im.info["transparency"]
- def convert_transparency(m, v):
- v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
- return max(0, min(255, int(v)))
+ def convert_transparency(
+ m: tuple[float, ...], v: tuple[int, int, int]
+ ) -> int:
+ value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
+ return max(0, min(255, int(value)))
if mode == "L":
transparency = convert_transparency(matrix, transparency)
@@ -1084,7 +1094,10 @@ class Image:
del new_im.info["transparency"]
if trns is not None:
try:
- new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
+ new_im.info["transparency"] = new_im.palette.getcolor(
+ cast(tuple[int, ...], trns), # trns was converted to RGB
+ new_im,
+ )
except Exception:
# if we can't make a transparent color, don't leave the old
# transparency hanging around to mess us up.
@@ -1132,7 +1145,7 @@ class Image:
# crash fail if we leave a bytes transparency in an rgb/l mode.
del new_im.info["transparency"]
if trns is not None:
- if new_im.mode == "P":
+ if new_im.mode == "P" and new_im.palette:
try:
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
except ValueError as e:
@@ -1150,9 +1163,9 @@ class Image:
def quantize(
self,
colors: int = 256,
- method: Quantize | None = None,
+ method: int | None = None,
kmeans: int = 0,
- palette=None,
+ palette: Image | None = None,
dither: Dither = Dither.FLOYDSTEINBERG,
) -> Image:
"""
@@ -1224,8 +1237,8 @@ class Image:
from . import ImagePalette
mode = im.im.getpalettemode()
- palette = im.im.getpalette(mode, mode)[: colors * len(mode)]
- im.palette = ImagePalette.ImagePalette(mode, palette)
+ palette_data = im.im.getpalette(mode, mode)[: colors * len(mode)]
+ im.palette = ImagePalette.ImagePalette(mode, palette_data)
return im
@@ -1242,7 +1255,7 @@ class Image:
__copy__ = copy
- def crop(self, box: tuple[int, int, int, int] | None = None) -> Image:
+ def crop(self, box: tuple[float, float, float, float] | None = None) -> Image:
"""
Returns a rectangular region from this image. The box is a
4-tuple defining the left, upper, right, and lower pixel
@@ -1268,7 +1281,9 @@ class Image:
self.load()
return self._new(self._crop(self.im, box))
- def _crop(self, im, box):
+ def _crop(
+ self, im: core.ImagingCore, box: tuple[float, float, float, float]
+ ) -> core.ImagingCore:
"""
Returns a rectangular region from the core image object im.
@@ -1289,7 +1304,7 @@ class Image:
return im.crop((x0, y0, x1, y1))
def draft(
- self, mode: str, size: tuple[int, int]
+ self, mode: str | None, size: tuple[int, int] | None
) -> tuple[str, tuple[int, int, float, float]] | None:
"""
Configures the image file loader so it returns a version of the
@@ -1359,7 +1374,7 @@ class Image:
"""
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
image.
@@ -1378,7 +1393,9 @@ class Image:
self.load()
return self.im.getbbox(alpha_only)
- def getcolors(self, maxcolors: int = 256):
+ def getcolors(
+ self, maxcolors: int = 256
+ ) -> list[tuple[int, tuple[int, ...]]] | list[tuple[int, float]] | None:
"""
Returns a list of colors used in this image.
@@ -1395,7 +1412,7 @@ class Image:
self.load()
if self.mode in ("1", "L", "P"):
h = self.im.histogram()
- out = [(h[i], i) for i in range(256) if h[i]]
+ out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]]
if len(out) > maxcolors:
return None
return out
@@ -1439,8 +1456,15 @@ class Image:
return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
return self.im.getextrema()
- def _getxmp(self, xmp_tags):
- def get_name(tag):
+ def getxmp(self) -> dict[str, Any]:
+ """
+ Returns a dictionary containing the XMP tags.
+ Requires defusedxml to be installed.
+
+ :returns: XMP tags in a dictionary.
+ """
+
+ def get_name(tag: str) -> str:
return re.sub("^{[^}]+}", "", tag)
def get_value(element):
@@ -1466,9 +1490,10 @@ class Image:
if ElementTree is None:
warnings.warn("XMP data cannot be read without defusedxml dependency")
return {}
- else:
- root = ElementTree.fromstring(xmp_tags)
- return {get_name(root.tag): get_value(root)}
+ if "xmp" not in self.info:
+ return {}
+ root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00"))
+ return {get_name(root.tag): get_value(root)}
def getexif(self) -> Exif:
"""
@@ -1511,7 +1536,7 @@ class Image:
self._exif._loaded = False
self.getexif()
- def get_child_images(self):
+ def get_child_images(self) -> list[ImageFile.ImageFile]:
child_images = []
exif = self.getexif()
ifds = []
@@ -1535,16 +1560,17 @@ class Image:
fp = self.fp
thumbnail_offset = ifd.get(513)
if thumbnail_offset is not None:
- try:
- thumbnail_offset += self._exif_offset
- except AttributeError:
- pass
+ thumbnail_offset += getattr(self, "_exif_offset", 0)
self.fp.seek(thumbnail_offset)
data = self.fp.read(ifd.get(514))
fp = io.BytesIO(data)
with open(fp) as im:
- if thumbnail_offset is None:
+ from . import TiffImagePlugin
+
+ if thumbnail_offset is None and isinstance(
+ im, TiffImagePlugin.TiffImageFile
+ ):
im._frame_pos = [ifd_offset]
im._seek(0)
im.load()
@@ -1604,7 +1630,7 @@ class Image:
or "transparency" in self.info
)
- def apply_transparency(self):
+ def apply_transparency(self) -> None:
"""
If a P mode image has a "transparency" key in the info dictionary,
remove the key and instead apply the transparency to the palette.
@@ -1616,6 +1642,7 @@ class Image:
from . import ImagePalette
palette = self.getpalette("RGBA")
+ assert palette is not None
transparency = self.info["transparency"]
if isinstance(transparency, bytes):
for i, alpha in enumerate(transparency):
@@ -1627,7 +1654,9 @@ class Image:
del self.info["transparency"]
- def getpixel(self, xy):
+ def getpixel(
+ self, xy: tuple[int, int] | list[int]
+ ) -> float | tuple[int, ...] | None:
"""
Returns the pixel value at a given position.
@@ -1638,8 +1667,6 @@ class Image:
"""
self.load()
- if self.pyaccess:
- return self.pyaccess.getpixel(xy)
return self.im.getpixel(tuple(xy))
def getprojection(self) -> tuple[list[int], list[int]]:
@@ -1711,7 +1738,12 @@ class Image:
return self.im.entropy(extrema)
return self.im.entropy()
- def paste(self, im, box=None, mask=None) -> None:
+ def paste(
+ self,
+ im: Image | str | float | tuple[float, ...],
+ box: Image | 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
a 2-tuple giving the upper left corner, a 4-tuple defining the
@@ -1739,7 +1771,7 @@ class Image:
See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to
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.
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
@@ -1751,10 +1783,14 @@ class Image:
:param mask: An optional mask image.
"""
- if isImageType(box) and mask is None:
+ if isImageType(box):
+ if mask is not None:
+ msg = "If using second argument as mask, third argument must be None"
+ raise ValueError(msg)
# abbreviated paste(im, mask) syntax
mask = box
box = None
+ assert not isinstance(box, Image)
if box is None:
box = (0, 0)
@@ -1792,7 +1828,9 @@ class Image:
else:
self.im.paste(im, box)
- def alpha_composite(self, im, dest=(0, 0), source=(0, 0)):
+ def alpha_composite(
+ self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0)
+ ) -> None:
"""'In-place' analog of Image.alpha_composite. Composites an image
onto this image.
@@ -1807,32 +1845,35 @@ class Image:
"""
if not isinstance(source, (list, tuple)):
- msg = "Source must be a tuple"
+ msg = "Source must be a list or tuple"
raise ValueError(msg)
if not isinstance(dest, (list, tuple)):
- msg = "Destination must be a tuple"
+ msg = "Destination must be a list or tuple"
raise ValueError(msg)
- if len(source) not in (2, 4):
- msg = "Source must be a 2 or 4-tuple"
+
+ if len(source) == 4:
+ overlay_crop_box = tuple(source)
+ elif len(source) == 2:
+ overlay_crop_box = tuple(source) + im.size
+ else:
+ msg = "Source must be a sequence of length 2 or 4"
raise ValueError(msg)
+
if not len(dest) == 2:
- msg = "Destination must be a 2-tuple"
+ msg = "Destination must be a sequence of length 2"
raise ValueError(msg)
if min(source) < 0:
msg = "Source must be non-negative"
raise ValueError(msg)
- if len(source) == 2:
- source = source + im.size
-
- # over image, crop if it's not the whole thing.
- if source == (0, 0) + im.size:
+ # over image, crop if it's not the whole image.
+ if overlay_crop_box == (0, 0) + im.size:
overlay = im
else:
- overlay = im.crop(source)
+ overlay = im.crop(overlay_crop_box)
# target for the paste
- box = dest + (dest[0] + overlay.width, dest[1] + overlay.height)
+ box = tuple(dest) + (dest[0] + overlay.width, dest[1] + overlay.height)
# destination image. don't copy if we're using the whole image.
if box == (0, 0) + self.size:
@@ -1843,7 +1884,11 @@ class Image:
result = alpha_composite(background, overlay)
self.paste(result, box)
- def point(self, lut, mode: str | None = None) -> Image:
+ def point(
+ self,
+ lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler,
+ mode: str | None = None,
+ ) -> Image:
"""
Maps this image through a lookup table or function.
@@ -1880,7 +1925,9 @@ class Image:
scale, offset = _getscaleoffset(lut)
return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table
- lut = [lut(i) for i in range(256)] * self.im.bands
+ flatLut = [lut(i) for i in range(256)] * self.im.bands
+ else:
+ flatLut = lut
if self.mode == "F":
# FIXME: _imaging returns a confusing error message for this case
@@ -1888,18 +1935,17 @@ class Image:
raise ValueError(msg)
if mode != "F":
- lut = [round(i) for i in lut]
- return self._new(self.im.point(lut, mode))
+ flatLut = [round(i) for i in flatLut]
+ return self._new(self.im.point(flatLut, mode))
- def putalpha(self, alpha):
+ def putalpha(self, alpha: Image | int) -> None:
"""
Adds or replaces the alpha layer in this image. If the image
does not have an alpha layer, it's converted to "LA" or "RGBA".
The new layer must be either "L" or "1".
:param alpha: The new alpha layer. This can either be an "L" or "1"
- image having the same size as this image, or an integer or
- other color value.
+ image having the same size as this image, or an integer.
"""
self._ensure_mutable()
@@ -1917,7 +1963,6 @@ class Image:
msg = "alpha channel could not be added"
raise ValueError(msg) from e # sanity check
self.im = im
- self.pyaccess = None
self._mode = self.im.mode
except KeyError as e:
msg = "illegal image mode"
@@ -1938,6 +1983,7 @@ class Image:
alpha = alpha.convert("L")
else:
# constant alpha
+ alpha = cast(int, alpha) # see python/typing#1013
try:
self.im.fillband(band, alpha)
except (AttributeError, ValueError):
@@ -1948,7 +1994,12 @@ class Image:
self.im.putband(alpha.im, band)
- def putdata(self, data, scale=1.0, offset=0.0):
+ def putdata(
+ self,
+ data: Sequence[float] | Sequence[Sequence[int]] | NumpyArray,
+ scale: float = 1.0,
+ offset: float = 0.0,
+ ) -> None:
"""
Copies pixel data from a flattened sequence object into the image. The
values should start at the upper left corner (0, 0), continue to the
@@ -1966,7 +2017,11 @@ class Image:
self.im.putdata(data, scale, offset)
- def putpalette(self, data, rawmode="RGB") -> None:
+ def putpalette(
+ self,
+ data: ImagePalette.ImagePalette | bytes | Sequence[int],
+ rawmode: str = "RGB",
+ ) -> None:
"""
Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image.
@@ -1998,10 +2053,12 @@ class Image:
palette = ImagePalette.raw(rawmode, data)
self._mode = "PA" if "A" in self.mode else "P"
self.palette = palette
- self.palette.mode = "RGB"
+ self.palette.mode = "RGBA" if "A" in rawmode else "RGB"
self.load() # install new palette
- def putpixel(self, xy, value):
+ def putpixel(
+ self, xy: tuple[int, int], value: float | tuple[int, ...] | list[int]
+ ) -> None:
"""
Modifies the pixel at the given position. The color is given as
a single numerical value for single-band images, and a tuple for
@@ -2027,9 +2084,6 @@ class Image:
self._copy()
self.load()
- if self.pyaccess:
- return self.pyaccess.putpixel(xy, value)
-
if (
self.mode in ("P", "PA")
and isinstance(value, (list, tuple))
@@ -2039,12 +2093,13 @@ class Image:
if self.mode == "PA":
alpha = value[3] if len(value) == 4 else 255
value = value[:3]
- value = self.palette.getcolor(value, self)
- if self.mode == "PA":
- value = (value, alpha)
+ palette_index = self.palette.getcolor(value, self)
+ value = (palette_index, alpha) if self.mode == "PA" else palette_index
return self.im.putpixel(xy, value)
- def remap_palette(self, dest_map, source_palette=None):
+ def remap_palette(
+ self, dest_map: list[int], source_palette: bytes | bytearray | None = None
+ ) -> Image:
"""
Rewrites the image to reorder the palette.
@@ -2113,7 +2168,7 @@ class Image:
# m_im.putpalette(mapping_palette, 'L') # converts to 'P'
# or just force it.
# UNDONE -- this is part of the general issue with palettes
- m_im.im.putpalette(palette_mode + ";L", m_im.palette.tobytes())
+ m_im.im.putpalette(palette_mode, palette_mode + ";L", m_im.palette.tobytes())
m_im = m_im.convert("L")
@@ -2146,11 +2201,17 @@ class Image:
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] | list[int] | NumpyArray,
+ 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.
- :param size: The requested size in pixels, as a 2-tuple:
+ :param size: The requested size in pixels, as a tuple or array:
(width, height).
:param resample: An optional resampling filter. This can be
one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
@@ -2211,14 +2272,11 @@ class Image:
msg = "reducing_gap must be 1.0 or greater"
raise ValueError(msg)
- size = tuple(size)
-
self.load()
if box is None:
box = (0, 0) + self.size
- else:
- box = tuple(box)
+ size = tuple(size)
if self.size == size and box == (0, 0) + self.size:
return self.copy()
@@ -2252,7 +2310,11 @@ class Image:
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.
If the size of the image is not dividable by ``factor``,
@@ -2270,8 +2332,6 @@ class Image:
if box is None:
box = (0, 0) + self.size
- else:
- box = tuple(box)
if factor == (1, 1) and box == (0, 0) + self.size:
return self.copy()
@@ -2287,13 +2347,13 @@ class Image:
def rotate(
self,
- angle,
- resample=Resampling.NEAREST,
- expand=0,
- center=None,
- translate=None,
- fillcolor=None,
- ):
+ angle: float,
+ resample: Resampling = Resampling.NEAREST,
+ expand: int | bool = False,
+ center: tuple[float, float] | None = None,
+ translate: tuple[int, int] | None = None,
+ fillcolor: float | tuple[float, ...] | str | None = None,
+ ) -> Image:
"""
Returns a rotated copy of this image. This method returns a
copy of this image, rotated the given number of degrees counter
@@ -2358,10 +2418,7 @@ class Image:
else:
post_trans = translate
if center is None:
- # FIXME These should be rounded to ints?
- rotn_center = (w / 2.0, h / 2.0)
- else:
- rotn_center = center
+ center = (w / 2, h / 2)
angle = -math.radians(angle)
matrix = [
@@ -2378,10 +2435,10 @@ class Image:
return a * x + b * y + c, d * x + e * y + f
matrix[2], matrix[5] = transform(
- -rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix
+ -center[0] - post_trans[0], -center[1] - post_trans[1], matrix
)
- matrix[2] += rotn_center[0]
- matrix[5] += rotn_center[1]
+ matrix[2] += center[0]
+ matrix[5] += center[1]
if expand:
# calculate output size
@@ -2455,7 +2512,7 @@ class Image:
save_all = params.pop("save_all", False)
self.encoderinfo = params
- self.encoderconfig = ()
+ self.encoderconfig: tuple[Any, ...] = ()
preinit()
@@ -2600,7 +2657,12 @@ class Image:
"""
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 | None = 2.0,
+ ) -> None:
"""
Make this image into a thumbnail. This method modifies the
image to contain a thumbnail version of itself, no larger than
@@ -2660,42 +2722,46 @@ class Image:
return x, y
box = None
+ final_size: tuple[int, int]
if reducing_gap is not None:
- size = preserve_aspect_ratio()
- if size is None:
+ preserved_size = preserve_aspect_ratio()
+ if preserved_size is None:
return
+ final_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:
box = res[1]
if box is None:
self.load()
# load() may have changed the size of the image
- size = preserve_aspect_ratio()
- if size is None:
+ preserved_size = preserve_aspect_ratio()
+ if preserved_size is None:
return
+ final_size = preserved_size
- if self.size != size:
- im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
+ if self.size != final_size:
+ im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)
self.im = im.im
- self._size = size
+ self._size = final_size
self._mode = self.im.mode
self.readonly = 0
- self.pyaccess = None
# FIXME: the different transform methods need further explanation
# instead of bloating the method docs, add a separate chapter.
def transform(
self,
- size,
- method,
- data=None,
- resample=Resampling.NEAREST,
- fill=1,
- fillcolor=None,
+ size: tuple[int, int],
+ method: Transform | ImageTransformHandler | SupportsGetData,
+ data: Sequence[Any] | None = None,
+ resample: int = Resampling.NEAREST,
+ fill: int = 1,
+ fillcolor: float | tuple[float, ...] | str | None = None,
) -> Image:
"""
Transforms this image. This method creates a new image with the
@@ -2859,7 +2925,7 @@ class Image:
if image.mode in ("1", "P"):
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:
"""
@@ -2875,7 +2941,7 @@ class Image:
self.load()
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.
@@ -2929,7 +2995,7 @@ class ImageTransformHandler:
self,
size: tuple[int, int],
image: Image,
- **options: dict[str, str | int | tuple[int, ...] | list[int]],
+ **options: Any,
) -> Image:
pass
@@ -2941,35 +3007,35 @@ class ImageTransformHandler:
# Debugging
-def _wedge():
+def _wedge() -> Image:
"""Create grayscale wedge (for debugging only)"""
return Image()._new(core.wedge("L"))
-def _check_size(size):
+def _check_size(size: Any) -> None:
"""
Common check to enforce type and sanity check on size tuples
:param size: Should be a 2 tuple of (width, height)
- :returns: True, or raises a ValueError
+ :returns: None, or raises a ValueError
"""
if not isinstance(size, (list, tuple)):
- msg = "Size must be a tuple"
+ msg = "Size must be a list or tuple"
raise ValueError(msg)
if len(size) != 2:
- msg = "Size must be a tuple of length 2"
+ msg = "Size must be a sequence of length 2"
raise ValueError(msg)
if size[0] < 0 or size[1] < 0:
msg = "Width and height must be >= 0"
raise ValueError(msg)
- return True
-
def new(
- mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0
+ mode: str,
+ size: tuple[int, int] | list[int],
+ color: float | tuple[float, ...] | str | None = 0,
) -> Image:
"""
Creates a new image with the given mode and size.
@@ -3003,16 +3069,28 @@ def new(
color = ImageColor.getcolor(color, mode)
im = Image()
- if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]:
- # RGB or RGBA value for a P image
- from . import ImagePalette
+ if (
+ mode == "P"
+ 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()
- color = im.palette.getcolor(color)
+ im.palette = ImagePalette.ImagePalette()
+ color = im.palette.getcolor(color_ints)
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.
@@ -3040,18 +3118,21 @@ def frombytes(mode, size, data, decoder_name="raw", *args) -> Image:
im = new(mode, size)
if im.width != 0 and im.height != 0:
- # may pass tuple instead of argument list
- if len(args) == 1 and isinstance(args[0], tuple):
- args = args[0]
+ decoder_args: Any = args
+ if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
+ # may pass tuple instead of argument list
+ decoder_args = decoder_args[0]
- if decoder_name == "raw" and args == ():
- args = mode
+ if decoder_name == "raw" and decoder_args == ():
+ decoder_args = mode
- im.frombytes(data, decoder_name, args)
+ im.frombytes(data, decoder_name, decoder_args)
return im
-def frombuffer(mode, 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.
@@ -3344,7 +3425,7 @@ def open(
preinit()
- accept_warnings: list[str] = []
+ warning_messages: list[str] = []
def _open_core(
fp: IO[bytes],
@@ -3360,17 +3441,15 @@ def open(
factory, accept = OPEN[i]
result = not accept or accept(prefix)
if isinstance(result, str):
- accept_warnings.append(result)
+ warning_messages.append(result)
elif result:
fp.seek(0)
im = factory(fp, filename)
_decompression_bomb_check(im.size)
return im
- except (SyntaxError, IndexError, TypeError, struct.error):
- # Leave disabled by default, spams the logs with image
- # opening failures that are entirely expected.
- # logger.debug("", exc_info=True)
- continue
+ except (SyntaxError, IndexError, TypeError, struct.error) as e:
+ if WARN_POSSIBLE_FORMATS:
+ warning_messages.append(i + " opening failed. " + str(e))
except BaseException:
if exclusive_fp:
fp.close()
@@ -3395,7 +3474,7 @@ def open(
if exclusive_fp:
fp.close()
- for message in accept_warnings:
+ for message in warning_messages:
warnings.warn(message)
msg = "cannot identify image file %r" % (filename if filename else fp)
raise UnidentifiedImageError(msg)
@@ -3460,7 +3539,7 @@ def composite(image1: Image, image2: Image, mask: Image) -> Image:
return image
-def eval(image, *args):
+def eval(image: Image, *args: Callable[[int], float]) -> Image:
"""
Applies the function (which should take one argument) to each pixel
in the given image. If the image has more than one band, the same
@@ -3508,7 +3587,7 @@ def merge(mode: str, bands: Sequence[Image]) -> Image:
def register_open(
- id,
+ id: str,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
accept: Callable[[bytes], bool | str] | None = None,
) -> None:
@@ -3542,7 +3621,9 @@ def register_mime(id: str, mimetype: str) -> None:
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
used in application code.
@@ -3553,7 +3634,9 @@ def register_save(id: str, driver) -> None:
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
of a multiframe format. This function should not be
@@ -3565,7 +3648,7 @@ def register_save_all(id, driver) -> None:
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
used in application code.
@@ -3576,7 +3659,7 @@ def register_extension(id, extension) -> None:
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
used in application code.
@@ -3588,7 +3671,7 @@ def register_extensions(id, extensions) -> None:
register_extension(id, extension)
-def registered_extensions():
+def registered_extensions() -> dict[str, str]:
"""
Returns a dictionary containing all file extensions belonging
to registered plugins
@@ -3627,7 +3710,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None:
# Simple display support.
-def _show(image, **options) -> None:
+def _show(image: Image, **options: Any) -> None:
from . import ImageShow
ImageShow.show(image, **options)
@@ -3637,7 +3720,9 @@ def _show(image, **options) -> None:
# 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.
@@ -3650,7 +3735,7 @@ def 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.
@@ -3661,7 +3746,7 @@ def 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.
@@ -3670,7 +3755,7 @@ def 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.
@@ -3683,19 +3768,18 @@ def radial_gradient(mode):
# Resources
-def _apply_env_variables(env=None) -> None:
- if env is None:
- env = os.environ
+def _apply_env_variables(env: dict[str, str] | None = None) -> None:
+ env_dict = env if env is not None else os.environ
for var_name, setter in [
("PILLOW_ALIGNMENT", core.set_alignment),
("PILLOW_BLOCK_SIZE", core.set_block_size),
("PILLOW_BLOCKS_MAX", core.set_blocks_max),
]:
- if var_name not in env:
+ if var_name not in env_dict:
continue
- var = env[var_name].lower()
+ var = env_dict[var_name].lower()
units = 1
for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]:
@@ -3704,13 +3788,13 @@ def _apply_env_variables(env=None) -> None:
var = var[: -len(postfix)]
try:
- var = int(var) * units
+ var_int = int(var) * units
except ValueError:
warnings.warn(f"{var_name} is not int")
continue
try:
- setter(var)
+ setter(var_int)
except ValueError as e:
warnings.warn(f"{var_name}: {e}")
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 5f5c5df54..ec10230f1 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -299,6 +299,31 @@ class ImageCmsTransform(Image.ImagePointHandler):
proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
flags: Flags = Flags.NONE,
):
+ supported_modes = (
+ "RGB",
+ "RGBA",
+ "RGBX",
+ "CMYK",
+ "I;16",
+ "I;16L",
+ "I;16B",
+ "YCbCr",
+ "LAB",
+ "L",
+ "1",
+ )
+ for mode in (input_mode, output_mode):
+ if mode not in supported_modes:
+ deprecate(
+ mode,
+ 12,
+ {
+ "L;16": "I;16 or I;16L",
+ "L:16B": "I;16B",
+ "YCCA": "YCbCr",
+ "YCC": "YCbCr",
+ }.get(mode),
+ )
if proof is None:
self.transform = core.buildTransform(
input.profile, output.profile, input_mode, output_mode, intent, flags
@@ -754,7 +779,7 @@ def applyTransform(
def createProfile(
- colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = -1
+ colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0
) -> core.CmsProfile:
"""
(pyCMS) Creates a profile.
@@ -777,7 +802,7 @@ def createProfile(
:param colorSpace: String, the color space of the profile you wish to
create.
Currently only "LAB", "XYZ", and "sRGB" are supported.
- :param colorTemp: Positive integer for the white point for the profile, in
+ :param colorTemp: Positive number for the white point for the profile, in
degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50
illuminant if omitted (5000k). colorTemp is ONLY applied to LAB
profiles, and is ignored for XYZ and sRGB.
@@ -1089,7 +1114,7 @@ def isIntentSupported(
raise PyCMSError(v) from v
-def versions() -> tuple[str, str, str, str]:
+def versions() -> tuple[str, str | None, str, str]:
"""
(pyCMS) Fetches versions.
"""
diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py
index 5fb80b753..9a15a8eb7 100644
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -25,7 +25,7 @@ from . import Image
@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
parsed, this function raises a :py:exc:`ValueError` exception.
@@ -44,8 +44,10 @@ def getrgb(color):
if rgb:
if isinstance(rgb, tuple):
return rgb
- colormap[color] = rgb = getrgb(rgb)
- return rgb
+ rgb_tuple = getrgb(rgb)
+ assert len(rgb_tuple) == 3
+ colormap[color] = rgb_tuple
+ return rgb_tuple
# check for known string formats
if re.match("#[a-f0-9]{3}$", color):
@@ -88,15 +90,15 @@ def getrgb(color):
if m:
from colorsys import hls_to_rgb
- rgb = hls_to_rgb(
+ rgb_floats = hls_to_rgb(
float(m.group(1)) / 360.0,
float(m.group(3)) / 100.0,
float(m.group(2)) / 100.0,
)
return (
- int(rgb[0] * 255 + 0.5),
- int(rgb[1] * 255 + 0.5),
- int(rgb[2] * 255 + 0.5),
+ int(rgb_floats[0] * 255 + 0.5),
+ int(rgb_floats[1] * 255 + 0.5),
+ int(rgb_floats[2] * 255 + 0.5),
)
m = re.match(
@@ -105,15 +107,15 @@ def getrgb(color):
if m:
from colorsys import hsv_to_rgb
- rgb = hsv_to_rgb(
+ rgb_floats = hsv_to_rgb(
float(m.group(1)) / 360.0,
float(m.group(2)) / 100.0,
float(m.group(3)) / 100.0,
)
return (
- int(rgb[0] * 255 + 0.5),
- int(rgb[1] * 255 + 0.5),
- int(rgb[2] * 255 + 0.5),
+ int(rgb_floats[0] * 255 + 0.5),
+ int(rgb_floats[1] * 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)
@@ -124,7 +126,7 @@ def getrgb(color):
@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
``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 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
- color, alpha = getrgb(color), 255
- if len(color) == 4:
- color, alpha = color[:3], color[3]
+ rgb, alpha = getrgb(color), 255
+ if len(rgb) == 4:
+ alpha = rgb[3]
+ rgb = rgb[:3]
if mode == "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)
return int(h * 255), int(s * 255), int(v * 255)
elif Image.getmodebase(mode) == "L":
- r, g, b = color
+ r, g, b = rgb
# ITU-R Recommendation 601-2 for nonlinear RGB
# 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":
- return color, alpha
- else:
- if mode[-1] == "A":
- return color + (alpha,)
- return color
+ return graylevel, alpha
+ return graylevel
+ elif mode[-1] == "A":
+ return rgb + (alpha,)
+ return rgb
-colormap = {
+colormap: dict[str, str | tuple[int, int, int]] = {
# 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
# colour names used in CSS 1.
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index 17c176430..2b3620e71 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -34,11 +34,26 @@ from __future__ import annotations
import math
import numbers
import struct
-from typing import TYPE_CHECKING, Sequence, cast
+from collections.abc import Sequence
+from types import ModuleType
+from typing import TYPE_CHECKING, AnyStr, Callable, Union, cast
from . import Image, ImageColor
+from ._deprecate import deprecate
from ._typing import Coords
+# experimental access to the outline API
+Outline: Callable[[], Image.core._Outline] | None
+try:
+ Outline = Image.core.outline
+except AttributeError:
+ Outline = None
+
+if TYPE_CHECKING:
+ from . import ImageDraw2, ImageFont
+
+_Ink = Union[float, tuple[int, ...], str]
+
"""
A simple 2D drawing interface for PIL images.
@@ -48,7 +63,9 @@ directly.
class ImageDraw:
- font = None
+ font: (
+ ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
+ ) = None
def __init__(self, im: Image.Image, mode: str | None = None) -> None:
"""
@@ -92,10 +109,9 @@ class ImageDraw:
self.fontmode = "L" # aliasing is okay for other modes
self.fill = False
- if TYPE_CHECKING:
- from . import ImageFont
-
- def getfont(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
+ def getfont(
+ self,
+ ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
"""
Get the current default font.
@@ -120,43 +136,57 @@ class ImageDraw:
self.font = ImageFont.load_default()
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:
from . import ImageFont
- font = ImageFont.load_default(font_size)
+ return ImageFont.load_default(font_size)
else:
- font = self.getfont()
- return font
+ return self.getfont()
- def _getink(self, ink, fill=None) -> tuple[int | None, int | None]:
+ def _getink(
+ self, ink: _Ink | None, fill: _Ink | None = None
+ ) -> tuple[int | None, int | None]:
+ result_ink = None
+ result_fill = None
if ink is None and fill is None:
if self.fill:
- fill = self.ink
+ result_fill = self.ink
else:
- ink = self.ink
+ result_ink = self.ink
else:
if ink is not None:
if isinstance(ink, str):
ink = ImageColor.getcolor(ink, self.mode)
if self.palette and not isinstance(ink, numbers.Number):
ink = self.palette.getcolor(ink, self._image)
- ink = self.draw.draw_ink(ink)
+ result_ink = self.draw.draw_ink(ink)
if fill is not None:
if isinstance(fill, str):
fill = ImageColor.getcolor(fill, self.mode)
if self.palette and not isinstance(fill, numbers.Number):
fill = self.palette.getcolor(fill, self._image)
- fill = self.draw.draw_ink(fill)
- return ink, fill
+ result_fill = self.draw.draw_ink(fill)
+ return result_ink, result_fill
- def arc(self, xy: Coords, start, end, fill=None, width=1) -> None:
+ def arc(
+ self,
+ xy: Coords,
+ start: float,
+ end: float,
+ fill: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw an arc."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_arc(xy, start, end, ink, width)
- def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None:
+ def bitmap(
+ self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None
+ ) -> None:
"""Draw a bitmap."""
bitmap.load()
ink, fill = self._getink(fill)
@@ -165,30 +195,55 @@ class ImageDraw:
if ink is not None:
self.draw.draw_bitmap(xy, bitmap.im, ink)
- def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None:
+ def chord(
+ self,
+ xy: Coords,
+ start: float,
+ end: float,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw a chord."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_chord(xy, start, end, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_chord(xy, start, end, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_chord(xy, start, end, ink, 0, width)
- def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None:
+ def ellipse(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw an ellipse."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_ellipse(xy, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_ellipse(xy, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_ellipse(xy, ink, 0, width)
def circle(
- self, xy: Sequence[float], radius: float, fill=None, outline=None, width=1
+ self,
+ xy: Sequence[float],
+ radius: float,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
) -> None:
"""Draw a circle given center coordinates and a radius."""
ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius)
self.ellipse(ellipse_xy, fill, outline, width)
- def line(self, xy: Coords, fill=None, width=0, joint=None) -> None:
+ def line(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ width: int = 0,
+ joint: str | None = None,
+ ) -> None:
"""Draw a line, or a connected sequence of line segments."""
ink = self._getink(fill)[0]
if ink is not None:
@@ -216,7 +271,9 @@ class ImageDraw:
# This is a straight line, so no joint is required
continue
- def coord_at_angle(coord, angle):
+ def coord_at_angle(
+ coord: Sequence[float], angle: float
+ ) -> tuple[float, ...]:
x, y = coord
angle -= 90
distance = width / 2 - 1
@@ -257,37 +314,54 @@ class ImageDraw:
]
self.line(gap_coords, fill, width=3)
- def shape(self, shape, fill=None, outline=None) -> None:
+ def shape(
+ self,
+ shape: Image.core._Outline,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ ) -> None:
"""(Experimental) Draw a shape."""
shape.close()
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_outline(shape, fill, 1)
- if ink is not None and ink != fill:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_outline(shape, fill_ink, 1)
+ if ink is not None and ink != fill_ink:
self.draw.draw_outline(shape, ink, 0)
def pieslice(
- self, xy: Coords, start, end, fill=None, outline=None, width=1
+ self,
+ xy: Coords,
+ start: float,
+ end: float,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
) -> None:
"""Draw a pieslice."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_pieslice(xy, start, end, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_pieslice(xy, start, end, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
- def point(self, xy: Coords, fill=None) -> None:
+ def point(self, xy: Coords, fill: _Ink | None = None) -> None:
"""Draw one or more individual pixels."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_points(xy, ink)
- def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None:
+ def polygon(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw a polygon."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_polygon(xy, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_polygon(xy, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
if width == 1:
self.draw.draw_polygon(xy, ink, 0, width)
elif self.im is not None:
@@ -313,22 +387,41 @@ class ImageDraw:
self.im.paste(im.im, (0, 0) + im.size, mask.im)
def regular_polygon(
- self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1
+ self,
+ bounding_circle: Sequence[Sequence[float] | float],
+ n_sides: int,
+ rotation: float = 0,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
) -> None:
"""Draw a regular polygon."""
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
self.polygon(xy, fill, outline, width)
- def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None:
+ def rectangle(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
"""Draw a rectangle."""
- ink, fill = self._getink(outline, fill)
- if fill is not None:
- self.draw.draw_rectangle(xy, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_rectangle(xy, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_rectangle(xy, ink, 0, width)
def rounded_rectangle(
- self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None
+ self,
+ xy: Coords,
+ radius: float = 0,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ *,
+ corners: tuple[bool, bool, bool, bool] | None = None,
) -> None:
"""Draw a rounded rectangle."""
if isinstance(xy[0], (list, tuple)):
@@ -370,10 +463,10 @@ class ImageDraw:
# that is a rectangle
return self.rectangle(xy, fill, outline, width)
- r = d // 2
- ink, fill = self._getink(outline, fill)
+ r = int(d // 2)
+ ink, fill_ink = self._getink(outline, fill)
- def draw_corners(pieslice) -> None:
+ def draw_corners(pieslice: bool) -> None:
parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
if full_x:
# Draw top and bottom halves
@@ -403,32 +496,32 @@ class ImageDraw:
)
for part in parts:
if pieslice:
- self.draw.draw_pieslice(*(part + (fill, 1)))
+ self.draw.draw_pieslice(*(part + (fill_ink, 1)))
else:
self.draw.draw_arc(*(part + (ink, width)))
- if fill is not None:
+ if fill_ink is not None:
draw_corners(True)
if full_x:
- self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1)
+ self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
else:
- self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1)
+ self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
if not full_x and not full_y:
left = [x0, y0, x0 + r, y1]
if corners[0]:
left[1] += r + 1
if corners[3]:
left[3] -= r + 1
- self.draw.draw_rectangle(left, fill, 1)
+ self.draw.draw_rectangle(left, fill_ink, 1)
right = [x1 - r, y0, x1, y1]
if corners[1]:
right[1] += r + 1
if corners[2]:
right[3] -= r + 1
- self.draw.draw_rectangle(right, fill, 1)
- if ink is not None and ink != fill and width != 0:
+ self.draw.draw_rectangle(right, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
draw_corners(False)
if not full_x:
@@ -460,15 +553,13 @@ class ImageDraw:
right[3] -= r + 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"
return split_character in text
- def _multiline_split(self, text) -> list[str | bytes]:
- split_character = "\n" if isinstance(text, str) else b"\n"
-
- return text.split(split_character)
+ def _multiline_split(self, text: AnyStr) -> list[AnyStr]:
+ return text.split("\n" if isinstance(text, str) else b"\n")
def _multiline_spacing(self, font, spacing, stroke_width):
return (
@@ -479,10 +570,15 @@ class ImageDraw:
def text(
self,
- xy,
- text,
+ xy: tuple[float, float],
+ text: str,
fill=None,
- font=None,
+ font: (
+ ImageFont.ImageFont
+ | ImageFont.FreeTypeFont
+ | ImageFont.TransposedFont
+ | None
+ ) = None,
anchor=None,
spacing=4,
align="left",
@@ -520,10 +616,11 @@ class ImageDraw:
embedded_color,
)
- def getink(fill):
- ink, fill = self._getink(fill)
+ def getink(fill: _Ink | None) -> int:
+ ink, fill_ink = self._getink(fill)
if ink is None:
- return fill
+ assert fill_ink is not None
+ return fill_ink
return ink
def draw_text(ink, stroke_width=0, stroke_offset=None) -> None:
@@ -536,7 +633,7 @@ class ImageDraw:
coord.append(int(xy[i]))
start.append(math.modf(xy[i])[0])
try:
- mask, offset = font.getmask2(
+ mask, offset = font.getmask2( # type: ignore[union-attr,misc]
text,
mode,
direction=direction,
@@ -552,7 +649,7 @@ class ImageDraw:
coord = [coord[0] + offset[0], coord[1] + offset[1]]
except AttributeError:
try:
- mask = font.getmask(
+ mask = font.getmask( # type: ignore[misc]
text,
mode,
direction,
@@ -601,10 +698,15 @@ class ImageDraw:
def multiline_text(
self,
- xy,
- text,
+ xy: tuple[float, float],
+ text: str,
fill=None,
- font=None,
+ font: (
+ ImageFont.ImageFont
+ | ImageFont.FreeTypeFont
+ | ImageFont.TransposedFont
+ | None
+ ) = None,
anchor=None,
spacing=4,
align="left",
@@ -634,7 +736,7 @@ class ImageDraw:
font = self._getfont(font_size)
widths = []
- max_width = 0
+ max_width: float = 0
lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines:
@@ -688,15 +790,20 @@ class ImageDraw:
def textlength(
self,
- text,
- font=None,
+ text: str,
+ font: (
+ ImageFont.ImageFont
+ | ImageFont.FreeTypeFont
+ | ImageFont.TransposedFont
+ | None
+ ) = None,
direction=None,
features=None,
language=None,
embedded_color=False,
*,
font_size=None,
- ):
+ ) -> float:
"""Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text):
msg = "can't measure length of multiline text"
@@ -788,7 +895,7 @@ class ImageDraw:
font = self._getfont(font_size)
widths = []
- max_width = 0
+ max_width: float = 0
lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines:
@@ -860,7 +967,7 @@ class ImageDraw:
return bbox
-def Draw(im, mode: str | None = None) -> ImageDraw:
+def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
"""
A simple 2D drawing interface for PIL images.
@@ -872,45 +979,38 @@ def Draw(im, mode: str | None = None) -> ImageDraw:
defaults to the mode of the image.
"""
try:
- return im.getdraw(mode)
+ return getattr(im, "getdraw")(mode)
except AttributeError:
return ImageDraw(im, mode)
-# experimental access to the outline API
-try:
- Outline = Image.core.outline
-except AttributeError:
- Outline = None
-
-
-def getdraw(im=None, hints=None):
+def getdraw(
+ im: Image.Image | None = None, hints: list[str] | None = None
+) -> tuple[ImageDraw2.Draw | None, ModuleType]:
"""
- (Experimental) A more advanced 2D drawing interface for PIL images,
- based on the WCK interface.
-
: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.
"""
- # FIXME: this needs more work!
- # FIXME: come up with a better 'hints' scheme.
- handler = None
- if not hints or "nicest" in hints:
- try:
- from . import _imagingagg as handler
- except ImportError:
- pass
- if handler is None:
- from . import ImageDraw2 as handler
- if im:
- im = handler.Draw(im)
- return im, handler
+ if hints is not None:
+ deprecate("'hints' parameter", 12)
+ from . import ImageDraw2
+
+ draw = ImageDraw2.Draw(im) if im is not None else None
+ return draw, ImageDraw2
-def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
+def floodfill(
+ image: Image.Image,
+ xy: tuple[int, int],
+ value: float | tuple[int, ...],
+ border: float | tuple[int, ...] | None = None,
+ thresh: float = 0,
+) -> None:
"""
- (experimental) Fills a bounded region with a given color.
+ .. warning:: This method is experimental.
+
+ Fills a bounded region with a given color.
:param image: Target image.
:param xy: Seed position (a 2-item coordinate tuple). See
@@ -928,6 +1028,7 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
# based on an implementation by Eric S. Raymond
# amended by yo1995 @20180806
pixel = image.load()
+ assert pixel is not None
x, y = xy
try:
background = pixel[x, y]
@@ -965,12 +1066,12 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
def _compute_regular_polygon_vertices(
- bounding_circle, n_sides, rotation
+ bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
) -> list[tuple[float, float]]:
"""
Generate a list of vertices for a 2D regular polygon.
- :param bounding_circle: The bounding circle is a tuple defined
+ :param bounding_circle: The bounding circle is a sequence defined
by a point and radius. The polygon is inscribed in this circle.
(e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
:param n_sides: Number of sides
@@ -1008,7 +1109,7 @@ def _compute_regular_polygon_vertices(
# 1. Error Handling
# 1.1 Check `n_sides` has an appropriate value
if not isinstance(n_sides, int):
- msg = "n_sides should be an int"
+ msg = "n_sides should be an int" # type: ignore[unreachable]
raise TypeError(msg)
if n_sides < 3:
msg = "n_sides should be an int > 2"
@@ -1020,9 +1121,24 @@ def _compute_regular_polygon_vertices(
raise TypeError(msg)
if len(bounding_circle) == 3:
- *centroid, polygon_radius = bounding_circle
- elif len(bounding_circle) == 2:
- centroid, polygon_radius = bounding_circle
+ if not all(isinstance(i, (int, float)) for i in bounding_circle):
+ msg = "bounding_circle should only contain numeric data"
+ raise ValueError(msg)
+
+ *centroid, polygon_radius = cast(list[float], list(bounding_circle))
+ elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
+ if not all(
+ isinstance(i, (int, float)) for i in bounding_circle[0]
+ ) or not isinstance(bounding_circle[1], (int, float)):
+ msg = "bounding_circle should only contain numeric data"
+ raise ValueError(msg)
+
+ if len(bounding_circle[0]) != 2:
+ msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
+ raise ValueError(msg)
+
+ centroid = cast(list[float], list(bounding_circle[0]))
+ polygon_radius = cast(float, bounding_circle[1])
else:
msg = (
"bounding_circle should contain 2D coordinates "
@@ -1030,25 +1146,17 @@ def _compute_regular_polygon_vertices(
)
raise ValueError(msg)
- if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)):
- msg = "bounding_circle should only contain numeric data"
- raise ValueError(msg)
-
- if not len(centroid) == 2:
- msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
- raise ValueError(msg)
-
if polygon_radius <= 0:
msg = "bounding_circle radius should be > 0"
raise ValueError(msg)
# 1.3 Check `rotation` has an appropriate value
if not isinstance(rotation, (int, float)):
- msg = "rotation should be an int or float"
+ msg = "rotation should be an int or float" # type: ignore[unreachable]
raise ValueError(msg)
# 2. Define Helper Functions
- def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]:
+ def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
return (
round(
point[0] * math.cos(math.radians(360 - degrees))
@@ -1064,7 +1172,7 @@ def _compute_regular_polygon_vertices(
),
)
- def _compute_polygon_vertex(angle: float) -> tuple[int, int]:
+ def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
start_point = [polygon_radius, 0]
return _apply_rotation(start_point, angle)
@@ -1087,11 +1195,13 @@ def _compute_regular_polygon_vertices(
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.
"""
- if isinstance(color2, tuple):
- return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2)))
- else:
- return abs(color1 - color2)
+ first = color1 if isinstance(color1, tuple) else (color1,)
+ second = color2 if isinstance(color2, tuple) else (color2,)
+
+ return sum(abs(first[i] - second[i]) for i in range(0, len(second)))
diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py
index 35ee5834e..e89a78be4 100644
--- a/src/PIL/ImageDraw2.py
+++ b/src/PIL/ImageDraw2.py
@@ -24,13 +24,16 @@
"""
from __future__ import annotations
+from typing import BinaryIO
+
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
+from ._typing import StrOrBytesPath
class Pen:
"""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.width = width
@@ -38,14 +41,16 @@ class Pen:
class Brush:
"""Stores a fill color"""
- def __init__(self, color, opacity=255):
+ def __init__(self, color: str, opacity: int = 255) -> None:
self.color = ImageColor.getrgb(color)
class Font:
"""Stores a TrueType font and color"""
- def __init__(self, color, file, size=12):
+ def __init__(
+ self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12
+ ) -> None:
# FIXME: add support for bitmap fonts
self.color = ImageColor.getrgb(color)
self.font = ImageFont.truetype(file, size)
@@ -56,14 +61,22 @@ class Draw:
(Experimental) WCK-style drawing interface
"""
- def __init__(self, image, size=None, color=None):
- if not hasattr(image, "im"):
+ def __init__(
+ self,
+ image: Image.Image | str,
+ size: tuple[int, int] | list[int] | None = None,
+ color: float | tuple[float, ...] | str | None = None,
+ ) -> None:
+ if isinstance(image, str):
+ if size is None:
+ msg = "If image argument is mode string, size must be a list or tuple"
+ raise ValueError(msg)
image = Image.new(image, size, color)
self.draw = ImageDraw.Draw(image)
self.image = image
self.transform = None
- def flush(self):
+ def flush(self) -> Image.Image:
return self.image
def render(self, op, xy, pen, brush=None):
diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py
index 93a50d2a2..d7e99a968 100644
--- a/src/PIL/ImageEnhance.py
+++ b/src/PIL/ImageEnhance.py
@@ -23,7 +23,10 @@ from . import Image, ImageFilter, ImageStat
class _Enhance:
- def enhance(self, factor):
+ image: Image.Image
+ degenerate: Image.Image
+
+ def enhance(self, factor: float) -> Image.Image:
"""
Returns an enhanced image.
@@ -46,7 +49,7 @@ class Color(_Enhance):
the original image.
"""
- def __init__(self, image):
+ def __init__(self, image: Image.Image) -> None:
self.image = image
self.intermediate_mode = "L"
if "A" in image.getbands():
@@ -63,7 +66,7 @@ class Contrast(_Enhance):
gives a solid gray image. A factor of 1.0 gives the original image.
"""
- def __init__(self, image):
+ def __init__(self, image: Image.Image) -> None:
self.image = image
mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5)
self.degenerate = Image.new("L", image.size, mean).convert(image.mode)
@@ -80,7 +83,7 @@ class Brightness(_Enhance):
original image.
"""
- def __init__(self, image):
+ def __init__(self, image: Image.Image) -> None:
self.image = image
self.degenerate = Image.new(image.mode, image.size, 0)
@@ -96,7 +99,7 @@ class Sharpness(_Enhance):
original image, and a factor of 2.0 gives a sharpened image.
"""
- def __init__(self, image):
+ def __init__(self, image: Image.Image) -> None:
self.image = image
self.degenerate = image.filter(ImageFilter.SMOOTH)
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index e07a34f17..7e56793aa 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -28,6 +28,7 @@
#
from __future__ import annotations
+import abc
import io
import itertools
import struct
@@ -64,7 +65,7 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`,
# Helpers
-def _get_oserror(error, *, encoder):
+def _get_oserror(error: int, *, encoder: bool) -> OSError:
try:
msg = Image.core.getcodecstatus(error)
except AttributeError:
@@ -75,7 +76,7 @@ def _get_oserror(error, *, encoder):
return OSError(msg)
-def raise_oserror(error):
+def raise_oserror(error: int) -> OSError:
deprecate(
"raise_oserror",
12,
@@ -155,11 +156,12 @@ class ImageFile(Image.Image):
self.fp.close()
raise
- def get_format_mimetype(self):
+ def get_format_mimetype(self) -> str | None:
if self.custom_mimetype:
return self.custom_mimetype
if self.format is not None:
return Image.MIME.get(self.format.upper())
+ return None
def __setstate__(self, state):
self.tile = []
@@ -349,6 +351,15 @@ class ImageFile(Image.Image):
return self.tell() != frame
+class StubHandler:
+ def open(self, im: StubImageFile) -> None:
+ pass
+
+ @abc.abstractmethod
+ def load(self, im: StubImageFile) -> Image.Image:
+ pass
+
+
class StubImageFile(ImageFile):
"""
Base class for stub image loaders.
@@ -357,7 +368,7 @@ class StubImageFile(ImageFile):
certain format, but relies on external code to load the file.
"""
- def _open(self):
+ def _open(self) -> None:
msg = "StubImageFile subclass must implement _open"
raise NotImplementedError(msg)
@@ -373,7 +384,7 @@ class StubImageFile(ImageFile):
self.__dict__ = image.__dict__
return image.load()
- def _load(self):
+ def _load(self) -> StubHandler | None:
"""(Hook) Find actual image loader."""
msg = "StubImageFile subclass must implement _load"
raise NotImplementedError(msg)
@@ -476,10 +487,10 @@ class Parser:
self.image = im
- def __enter__(self):
+ def __enter__(self) -> Parser:
return self
- def __exit__(self, *args):
+ def __exit__(self, *args: object) -> None:
self.close()
def close(self):
@@ -571,7 +582,7 @@ def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
encoder.cleanup()
-def _safe_read(fp, size):
+def _safe_read(fp: IO[bytes], size: int) -> bytes:
"""
Reads large blocks in a safe way. Unlike fp.read(n), this function
doesn't trust the user. If the requested size is larger than
@@ -592,18 +603,18 @@ def _safe_read(fp, size):
msg = "Truncated File Read"
raise OSError(msg)
return data
- data = []
+ blocks: list[bytes] = []
remaining_size = size
while remaining_size > 0:
block = fp.read(min(remaining_size, SAFEBLOCK))
if not block:
break
- data.append(block)
+ blocks.append(block)
remaining_size -= len(block)
- if sum(len(d) for d in data) < size:
+ if sum(len(block) for block in blocks) < size:
msg = "Truncated File Read"
raise OSError(msg)
- return b"".join(data)
+ return b"".join(blocks)
class PyCodecState:
@@ -613,7 +624,7 @@ class PyCodecState:
self.xoff = 0
self.yoff = 0
- def extents(self):
+ def extents(self) -> tuple[int, int, int, int]:
return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize
@@ -627,7 +638,7 @@ class PyCodec:
self.mode = mode
self.init(args)
- def init(self, args):
+ def init(self, args) -> None:
"""
Override to perform codec specific initialization
@@ -644,7 +655,7 @@ class PyCodec:
"""
pass
- def setfd(self, fd):
+ def setfd(self, fd) -> None:
"""
Called from ImageFile to set the Python file-like object
@@ -653,7 +664,7 @@ class PyCodec:
"""
self.fd = fd
- def setimage(self, im, extents=None):
+ def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None:
"""
Called from ImageFile to set the core output image for the codec
@@ -702,10 +713,10 @@ class PyDecoder(PyCodec):
_pulls_fd = False
@property
- def pulls_fd(self):
+ def pulls_fd(self) -> bool:
return self._pulls_fd
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
"""
Override to perform the decoding process.
@@ -730,6 +741,7 @@ class PyDecoder(PyCodec):
if not rawmode:
rawmode = self.mode
d = Image._getdecoder(self.mode, "raw", rawmode)
+ assert self.im is not None
d.setimage(self.im, self.state.extents())
s = d.decode(data)
@@ -752,10 +764,10 @@ class PyEncoder(PyCodec):
_pushes_fd = False
@property
- def pushes_fd(self):
+ def pushes_fd(self) -> bool:
return self._pushes_fd
- def encode(self, bufsize):
+ def encode(self, bufsize: int) -> tuple[int, int, bytes]:
"""
Override to perform the encoding process.
@@ -767,7 +779,7 @@ class PyEncoder(PyCodec):
msg = "unavailable in base encoder"
raise NotImplementedError(msg)
- def encode_to_pyfd(self):
+ def encode_to_pyfd(self) -> tuple[int, int]:
"""
If ``pushes_fd`` is ``True``, then this method will be used,
and ``encode()`` will only be called once.
@@ -779,10 +791,11 @@ class PyEncoder(PyCodec):
return 0, -8 # bad configuration
bytes_consumed, errcode, data = self.encode(0)
if data:
+ assert self.fd is not None
self.fd.write(data)
return bytes_consumed, errcode
- def encode_to_file(self, fh, bufsize):
+ def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
"""
:param fh: File handle.
:param bufsize: Buffer size.
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 678bd29a2..8b0974b2c 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -18,11 +18,18 @@ from __future__ import annotations
import abc
import functools
+from collections.abc import Sequence
+from types import ModuleType
+from typing import TYPE_CHECKING, Any, Callable, cast
+
+if TYPE_CHECKING:
+ from . import _imaging
+ from ._typing import NumpyArray
class Filter:
@abc.abstractmethod
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
pass
@@ -31,7 +38,9 @@ class MultibandFilter(Filter):
class BuiltinFilter(MultibandFilter):
- def filter(self, image):
+ filterargs: tuple[Any, ...]
+
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
if image.mode == "P":
msg = "cannot filter palette images"
raise ValueError(msg)
@@ -56,7 +65,13 @@ class Kernel(BuiltinFilter):
name = "Kernel"
- def __init__(self, size, kernel, scale=None, offset=0):
+ def __init__(
+ self,
+ size: tuple[int, int],
+ kernel: Sequence[float],
+ scale: float | None = None,
+ offset: float = 0,
+ ) -> None:
if scale is None:
# default scale is sum of kernel
scale = functools.reduce(lambda a, b: a + b, kernel)
@@ -79,11 +94,11 @@ class RankFilter(Filter):
name = "Rank"
- def __init__(self, size, rank):
+ def __init__(self, size: int, rank: int) -> None:
self.size = size
self.rank = rank
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
if image.mode == "P":
msg = "cannot filter palette images"
raise ValueError(msg)
@@ -101,7 +116,7 @@ class MedianFilter(RankFilter):
name = "Median"
- def __init__(self, size=3):
+ def __init__(self, size: int = 3) -> None:
self.size = size
self.rank = size * size // 2
@@ -116,7 +131,7 @@ class MinFilter(RankFilter):
name = "Min"
- def __init__(self, size=3):
+ def __init__(self, size: int = 3) -> None:
self.size = size
self.rank = 0
@@ -131,7 +146,7 @@ class MaxFilter(RankFilter):
name = "Max"
- def __init__(self, size=3):
+ def __init__(self, size: int = 3) -> None:
self.size = size
self.rank = size * size - 1
@@ -147,10 +162,10 @@ class ModeFilter(Filter):
name = "Mode"
- def __init__(self, size=3):
+ def __init__(self, size: int = 3) -> None:
self.size = size
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
return image.modefilter(self.size)
@@ -165,12 +180,12 @@ class GaussianBlur(MultibandFilter):
name = "GaussianBlur"
- def __init__(self, radius=2):
+ def __init__(self, radius: float | Sequence[float] = 2) -> None:
self.radius = radius
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
xy = self.radius
- if not isinstance(xy, (tuple, list)):
+ if isinstance(xy, (int, float)):
xy = (xy, xy)
if xy == (0, 0):
return image.copy()
@@ -193,18 +208,16 @@ class BoxBlur(MultibandFilter):
name = "BoxBlur"
- def __init__(self, radius):
- xy = radius
- if not isinstance(xy, (tuple, list)):
- xy = (xy, xy)
+ def __init__(self, radius: float | Sequence[float]) -> None:
+ xy = radius if isinstance(radius, (tuple, list)) else (radius, radius)
if xy[0] < 0 or xy[1] < 0:
msg = "radius must be >= 0"
raise ValueError(msg)
self.radius = radius
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
xy = self.radius
- if not isinstance(xy, (tuple, list)):
+ if isinstance(xy, (int, float)):
xy = (xy, xy)
if xy == (0, 0):
return image.copy()
@@ -228,12 +241,14 @@ class UnsharpMask(MultibandFilter):
name = "UnsharpMask"
- def __init__(self, radius=2, percent=150, threshold=3):
+ def __init__(
+ self, radius: float = 2, percent: int = 150, threshold: int = 3
+ ) -> None:
self.radius = radius
self.percent = percent
self.threshold = threshold
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
return image.unsharp_mask(self.radius, self.percent, self.threshold)
@@ -378,7 +393,14 @@ class Color3DLUT(MultibandFilter):
name = "Color 3D LUT"
- def __init__(self, size, table, channels=3, target_mode=None, **kwargs):
+ def __init__(
+ self,
+ size: int | tuple[int, int, int],
+ table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray,
+ channels: int = 3,
+ target_mode: str | None = None,
+ **kwargs: bool,
+ ) -> None:
if channels not in (3, 4):
msg = "Only 3 or 4 output channels are supported"
raise ValueError(msg)
@@ -392,7 +414,7 @@ class Color3DLUT(MultibandFilter):
items = size[0] * size[1] * size[2]
wrong_size = False
- numpy = None
+ numpy: ModuleType | None = None
if hasattr(table, "shape"):
try:
import numpy
@@ -400,15 +422,16 @@ class Color3DLUT(MultibandFilter):
pass
if numpy and isinstance(table, numpy.ndarray):
+ numpy_table: NumpyArray = table
if copy_table:
- table = table.copy()
+ numpy_table = numpy_table.copy()
- if table.shape in [
+ if numpy_table.shape in [
(items * channels,),
(items, channels),
(size[2], size[1], size[0], channels),
]:
- table = table.reshape(items * channels)
+ table = numpy_table.reshape(items * channels)
else:
wrong_size = True
@@ -418,7 +441,8 @@ class Color3DLUT(MultibandFilter):
# Convert to a flat list
if table and isinstance(table[0], (list, tuple)):
- table, raw_table = [], table
+ raw_table = cast(Sequence[Sequence[int]], table)
+ flat_table: list[int] = []
for pixel in raw_table:
if len(pixel) != channels:
msg = (
@@ -426,7 +450,8 @@ class Color3DLUT(MultibandFilter):
f"have a length of {channels}."
)
raise ValueError(msg)
- table.extend(pixel)
+ flat_table.extend(pixel)
+ table = flat_table
if wrong_size or len(table) != items * channels:
msg = (
@@ -439,7 +464,7 @@ class Color3DLUT(MultibandFilter):
self.table = table
@staticmethod
- def _check_size(size):
+ def _check_size(size: Any) -> tuple[int, int, int]:
try:
_, _, _ = size
except ValueError as e:
@@ -447,7 +472,7 @@ class Color3DLUT(MultibandFilter):
raise ValueError(msg) from e
except TypeError:
size = (size, size, size)
- size = [int(x) for x in size]
+ size = tuple(int(x) for x in size)
for size_1d in size:
if not 2 <= size_1d <= 65:
msg = "Size should be in [2, 65] range."
@@ -455,7 +480,13 @@ class Color3DLUT(MultibandFilter):
return size
@classmethod
- def generate(cls, size, callback, channels=3, target_mode=None):
+ def generate(
+ cls,
+ size: int | tuple[int, int, int],
+ callback: Callable[[float, float, float], tuple[float, ...]],
+ channels: int = 3,
+ target_mode: str | None = None,
+ ) -> Color3DLUT:
"""Generates new LUT using provided callback.
:param size: Size of the table. Passed to the constructor.
@@ -472,7 +503,7 @@ class Color3DLUT(MultibandFilter):
msg = "Only 3 or 4 output channels are supported"
raise ValueError(msg)
- table = [0] * (size_1d * size_2d * size_3d * channels)
+ table: list[float] = [0] * (size_1d * size_2d * size_3d * channels)
idx_out = 0
for b in range(size_3d):
for g in range(size_2d):
@@ -490,7 +521,13 @@ class Color3DLUT(MultibandFilter):
_copy_table=False,
)
- def transform(self, callback, with_normals=False, channels=None, target_mode=None):
+ def transform(
+ self,
+ callback: Callable[..., tuple[float, ...]],
+ with_normals: bool = False,
+ channels: int | None = None,
+ target_mode: str | None = None,
+ ) -> Color3DLUT:
"""Transforms the table values using provided callback and returns
a new LUT with altered values.
@@ -554,7 +591,7 @@ class Color3DLUT(MultibandFilter):
r.append(f"target_mode={self.mode}")
return "<{}>".format(" ".join(r))
- def filter(self, image):
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
from . import Image
return image.color_lut_3d(
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index ad5f75459..d260eef69 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -33,11 +33,17 @@ import sys
import warnings
from enum import IntEnum
from io import BytesIO
-from typing import BinaryIO
+from types import ModuleType
+from typing import IO, TYPE_CHECKING, Any, BinaryIO
from . import Image
from ._typing import StrOrBytesPath
-from ._util import is_directory, is_path
+from ._util import DeferredError, is_path
+
+if TYPE_CHECKING:
+ from . import ImageFile
+ from ._imaging import ImagingFont
+ from ._imagingft import Font
class Layout(IntEnum):
@@ -48,15 +54,14 @@ class Layout(IntEnum):
MAX_STRING_LENGTH = 1_000_000
+core: ModuleType | DeferredError
try:
from . import _imagingft as core
except ImportError as ex:
- from ._util import DeferredError
-
core = DeferredError.new(ex)
-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:
msg = "too many characters in string"
raise ValueError(msg)
@@ -81,9 +86,11 @@ def _string_length_check(text):
class ImageFont:
"""PIL font wrapper"""
- def _load_pilfont(self, filename):
+ font: ImagingFont
+
+ def _load_pilfont(self, filename: str) -> None:
with open(filename, "rb") as fp:
- image = None
+ image: ImageFile.ImageFile | None = None
for ext in (".png", ".gif", ".pbm"):
if image:
image.close()
@@ -106,7 +113,7 @@ class ImageFont:
self._load_pilfont_data(fp, image)
image.close()
- def _load_pilfont_data(self, file, image):
+ def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
# read PILfont header
if file.readline() != b"PILfont\n":
msg = "Not a PILfont file"
@@ -153,7 +160,9 @@ class ImageFont:
Image._decompression_bomb_check(self.font.getsize(text))
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.
@@ -167,7 +176,9 @@ class ImageFont:
width, height = self.font.getsize(text)
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.
This is the amount by which following text should be offset.
@@ -187,6 +198,9 @@ class ImageFont:
class FreeTypeFont:
"""FreeType font wrapper (requires _imagingft service)"""
+ font: Font
+ font_bytes: bytes
+
def __init__(
self,
font: StrOrBytesPath | BinaryIO | None = None,
@@ -197,6 +211,9 @@ class FreeTypeFont:
) -> None:
# FIXME: use service provider instead
+ if isinstance(core, DeferredError):
+ raise core.ex
+
if size <= 0:
msg = "font size must be greater than 0"
raise ValueError(msg)
@@ -250,14 +267,14 @@ class FreeTypeFont:
path, size, index, encoding, layout_engine = state
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
(e.g. Bold)
"""
return self.font.family, self.font.style
- def getmetrics(self):
+ def getmetrics(self) -> tuple[int, int]:
"""
:return: A tuple of the font ascent (the distance from the baseline to
the highest outline point) and descent (the distance from the
@@ -265,7 +282,9 @@ class FreeTypeFont:
"""
return self.font.ascent, self.font.descent
- def getlength(self, text, mode="", direction=None, features=None, language=None):
+ def getlength(
+ self, text: str | bytes, mode="", direction=None, features=None, language=None
+ ) -> float:
"""
Returns length (in pixels with 1/64 precision) of given text when rendered
in font with provided direction, features, and language.
@@ -339,14 +358,14 @@ class FreeTypeFont:
def getbbox(
self,
- text,
- mode="",
- direction=None,
- features=None,
- language=None,
- stroke_width=0,
- anchor=None,
- ):
+ text: str | bytes,
+ mode: str = "",
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ stroke_width: float = 0,
+ anchor: str | None = None,
+ ) -> tuple[float, float, float, float]:
"""
Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language.
@@ -496,7 +515,7 @@ class FreeTypeFont:
def getmask2(
self,
- text,
+ text: str | bytes,
mode="",
direction=None,
features=None,
@@ -624,7 +643,7 @@ class FreeTypeFont:
layout_engine=layout_engine or self.layout_engine,
)
- def get_variation_names(self):
+ def get_variation_names(self) -> list[bytes]:
"""
:returns: A list of the named styles in a variation font.
:exception OSError: If the font is not a variation font.
@@ -666,10 +685,11 @@ class FreeTypeFont:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
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
- 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.
:exception OSError: If the font is not a variation font.
@@ -714,14 +734,14 @@ class TransposedFont:
return 0, 0, height, width
return 0, 0, width, height
- def getlength(self, text, *args, **kwargs):
+ def getlength(self, text: str | bytes, *args, **kwargs) -> float:
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
msg = "text length is undefined for text rotated by 90 or 270 degrees"
raise ValueError(msg)
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
bitmap font file, and returns the corresponding font object.
@@ -735,7 +755,13 @@ def load(filename):
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,
and create a font object.
@@ -753,10 +779,15 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
:param font: A filename or file-like object containing a TrueType font.
If the file is not found in this filename, the loader may also
- search in other directories, such as the :file:`fonts/`
- directory on Windows or :file:`/Library/Fonts/`,
- :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on
- macOS.
+ search in other directories, such as:
+
+ * The :file:`fonts/` directory on Windows,
+ * :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/`
+ and :file:`~/Library/Fonts/` on macOS.
+ * :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`,
+ and :file:`/usr/share/fonts` on Linux; or those specified by
+ the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables
+ for user-installed and system-wide fonts, respectively.
:param size: The requested size, in pixels.
:param index: Which font face to load (default is first available face).
@@ -796,7 +827,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
: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)
try:
@@ -815,12 +846,21 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
if windir:
dirs.append(os.path.join(windir, "fonts"))
elif sys.platform in ("linux", "linux2"):
- lindirs = os.environ.get("XDG_DATA_DIRS")
- if not lindirs:
- # According to the freedesktop spec, XDG_DATA_DIRS should
- # default to /usr/share
- lindirs = "/usr/share"
- dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")]
+ data_home = os.environ.get("XDG_DATA_HOME")
+ if not data_home:
+ # The freedesktop spec defines the following default directory for
+ # when XDG_DATA_HOME is unset or empty. This user-level directory
+ # takes precedence over system-level directories.
+ data_home = os.path.expanduser("~/.local/share")
+ xdg_dirs = [data_home]
+
+ data_dirs = os.environ.get("XDG_DATA_DIRS")
+ if not data_dirs:
+ # Similarly, defaults are defined for the system-level directories
+ data_dirs = "/usr/local/share:/usr/share"
+ xdg_dirs += data_dirs.split(":")
+
+ dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs]
elif sys.platform == "darwin":
dirs += [
"/Library/Fonts",
@@ -846,7 +886,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
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
bitmap font along the Python path.
@@ -855,18 +895,153 @@ def load_path(filename):
:return: A font object.
:exception OSError: If the file could not be read.
"""
+ if not isinstance(filename, str):
+ filename = filename.decode("utf-8")
for directory in sys.path:
- if is_directory(directory):
- if not isinstance(filename, str):
- filename = filename.decode("utf-8")
- try:
- return load(os.path.join(directory, filename))
- except OSError:
- pass
+ try:
+ return load(os.path.join(directory, filename))
+ except OSError:
+ pass
msg = "cannot find font file"
raise OSError(msg)
+def load_default_imagefont() -> ImageFont:
+ f = ImageFont()
+ f._load_pilfont_data(
+ # courB08
+ BytesIO(
+ base64.b64decode(
+ b"""
+UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA
+BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL
+AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA
+AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB
+ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A
+BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB
+//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA
+AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH
+AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA
+ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv
+AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/
+/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5
+AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA
+AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG
+AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA
+BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA
+AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA
+2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF
+AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA////
++gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA
+////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA
+BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv
+AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA
+AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA
+AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA
+BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP//
+//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA
+AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF
+AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB
+mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn
+AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA
+AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7
+AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA
+Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB
+//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA
+AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ
+AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC
+DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ
+AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/
++wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5
+AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/
+///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG
+AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA
+BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA
+Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC
+eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG
+AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA////
++gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA
+////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA
+BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT
+AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A
+AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA
+Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA
+Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP//
+//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA
+AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ
+AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA
+LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5
+AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA
+AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5
+AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA
+AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG
+AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA
+EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK
+AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
+pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
+AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
++QAGAAIAzgAKANUAEw==
+"""
+ )
+ ),
+ Image.open(
+ BytesIO(
+ base64.b64decode(
+ b"""
+iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
+Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
+M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
+LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F
+IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA
+Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791
+NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx
+in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9
+SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY
+AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt
+y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG
+ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY
+lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H
+/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3
+AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47
+c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/
+/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw
+pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv
+oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR
+evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
+AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
+Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
+w7IkEbzhVQAAAABJRU5ErkJggg==
+"""
+ )
+ )
+ ),
+ )
+ return f
+
+
def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""If FreeType support is available, load a version of Aileron Regular,
https://dotcolon.net/font/aileron, with a more limited character set.
@@ -881,8 +1056,8 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
:return: A font object.
"""
- if core.__class__.__name__ == "module" or size is not None:
- f = truetype(
+ if isinstance(core, ModuleType) or size is not None:
+ return truetype(
BytesIO(
base64.b64decode(
b"""
@@ -1112,137 +1287,4 @@ AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
10 if size is None else size,
layout_engine=Layout.BASIC,
)
- else:
- f = ImageFont()
- f._load_pilfont_data(
- # courB08
- BytesIO(
- base64.b64decode(
- b"""
-UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA
-BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL
-AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA
-AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB
-ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A
-BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB
-//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA
-AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH
-AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA
-ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv
-AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/
-/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5
-AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA
-AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG
-AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA
-BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA
-AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA
-2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF
-AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA////
-+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA
-////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA
-BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv
-AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA
-AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA
-AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA
-BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP//
-//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA
-AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF
-AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB
-mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn
-AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA
-AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7
-AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA
-Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB
-//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA
-AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ
-AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC
-DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ
-AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/
-+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5
-AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/
-///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG
-AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA
-BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA
-Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC
-eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG
-AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA////
-+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA
-////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA
-BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT
-AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A
-AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA
-Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA
-Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP//
-//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA
-AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ
-AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA
-LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5
-AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA
-AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5
-AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA
-AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG
-AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA
-EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK
-AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
-pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
-AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
-+QAGAAIAzgAKANUAEw==
-"""
- )
- ),
- Image.open(
- BytesIO(
- base64.b64decode(
- b"""
-iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
-Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
-M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
-LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F
-IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA
-Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791
-NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx
-in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9
-SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY
-AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt
-y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG
-ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY
-lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H
-/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3
-AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47
-c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/
-/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw
-pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv
-oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR
-evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
-AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
-Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
-w7IkEbzhVQAAAABJRU5ErkJggg==
-"""
- )
- )
- ),
- )
- return f
+ return load_default_imagefont()
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index 3f3be706d..e27ca7e50 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -26,7 +26,13 @@ import tempfile
from . import Image
-def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None):
+def grab(
+ bbox: tuple[int, int, int, int] | None = None,
+ include_layered_windows: bool = False,
+ all_screens: bool = False,
+ xdisplay: str | None = None,
+) -> Image.Image:
+ im: Image.Image
if xdisplay is None:
if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png")
@@ -63,14 +69,16 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
left, top, right, bottom = bbox
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im
+ # Cast to Optional[str] needed for Windows and macOS.
+ display_name: str | None = xdisplay
try:
if not Image.core.HAVE_XCB:
msg = "Pillow was built without XCB support"
raise OSError(msg)
- size, data = Image.core.grabscreen_x11(xdisplay)
+ size, data = Image.core.grabscreen_x11(display_name)
except OSError:
if (
- xdisplay is None
+ display_name is None
and sys.platform not in ("darwin", "win32")
and shutil.which("gnome-screenshot")
):
@@ -94,7 +102,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
return im
-def grabclipboard():
+def grabclipboard() -> Image.Image | list[str] | None:
if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py
index 6ee8c4f25..6a43983d3 100644
--- a/src/PIL/ImageMorph.py
+++ b/src/PIL/ImageMorph.py
@@ -200,7 +200,7 @@ class MorphOp:
elif patterns is not None:
self.lut = LutBuilder(patterns=patterns).build_lut()
- def apply(self, image: Image.Image):
+ def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
"""Run a single morphological operation on an image
Returns a tuple of the number of changed pixels and the
@@ -216,7 +216,7 @@ class MorphOp:
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage
- def match(self, image: Image.Image):
+ def match(self, image: Image.Image) -> list[tuple[int, int]]:
"""Get a list of coordinates matching the morphological operation on
an image.
@@ -231,7 +231,7 @@ class MorphOp:
raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id)
- def get_on_pixels(self, image: Image.Image):
+ def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
"""Get a list of all turned on pixels in a binary image
Returns a list of tuples of (x,y) coordinates
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 33db8fa50..44aad0c3c 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -21,7 +21,8 @@ from __future__ import annotations
import functools
import operator
import re
-from typing import Protocol, Sequence, cast
+from collections.abc import Sequence
+from typing import Protocol, cast
from . import ExifTags, Image, ImagePalette
@@ -361,7 +362,9 @@ def pad(
else:
out = Image.new(image.mode, size, color)
if resized.palette:
- out.putpalette(resized.getpalette())
+ palette = resized.getpalette()
+ if palette is not None:
+ out.putpalette(palette)
if resized.width != size[0]:
x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
out.paste(resized, (x, 0))
@@ -497,7 +500,7 @@ def expand(
color = _color(fill, image.mode)
if image.palette:
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)
else:
palette = None
@@ -698,7 +701,6 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
transposed_image = image.transpose(method)
if in_place:
image.im = transposed_image.im
- image.pyaccess = None
image._size = transposed_image._size
exif_image = image if in_place else transposed_image
@@ -709,14 +711,18 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
exif_image.info["exif"] = exif.tobytes()
elif "Raw profile type exif" in exif_image.info:
exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
- elif "XML:com.adobe.xmp" in exif_image.info:
- for pattern in (
- r'tiff:Orientation="([0-9])"',
- r"([0-9])",
- ):
- exif_image.info["XML:com.adobe.xmp"] = re.sub(
- pattern, "", exif_image.info["XML:com.adobe.xmp"]
- )
+ for key in ("XML:com.adobe.xmp", "xmp"):
+ if key in exif_image.info:
+ for pattern in (
+ r'tiff:Orientation="([0-9])"',
+ r"([0-9])",
+ ):
+ value = exif_image.info[key]
+ exif_image.info[key] = (
+ re.sub(pattern, "", value)
+ if isinstance(value, str)
+ else re.sub(pattern.encode(), b"", value)
+ )
if not in_place:
return transposed_image
elif not in_place:
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index ae5c5dec0..8ccecbd07 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -18,10 +18,14 @@
from __future__ import annotations
import array
-from typing import Sequence
+from collections.abc import Sequence
+from typing import IO, TYPE_CHECKING
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
+if TYPE_CHECKING:
+ from . import Image
+
class ImagePalette:
"""
@@ -35,23 +39,27 @@ class ImagePalette:
Defaults to an empty palette.
"""
- def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None:
+ def __init__(
+ self,
+ mode: str = "RGB",
+ palette: Sequence[int] | bytes | bytearray | None = None,
+ ) -> None:
self.mode = mode
- self.rawmode = None # if set, palette contains raw data
+ self.rawmode: str | None = None # if set, palette contains raw data
self.palette = palette or bytearray()
self.dirty: int | None = None
@property
- def palette(self):
+ def palette(self) -> Sequence[int] | bytes | bytearray:
return self._palette
@palette.setter
- def palette(self, palette):
- self._colors = None
+ def palette(self, palette: Sequence[int] | bytes | bytearray) -> None:
+ self._colors: dict[tuple[int, ...], int] | None = None
self._palette = palette
@property
- def colors(self):
+ def colors(self) -> dict[tuple[int, ...], int]:
if self._colors is None:
mode_len = len(self.mode)
self._colors = {}
@@ -63,7 +71,7 @@ class ImagePalette:
return self._colors
@colors.setter
- def colors(self, colors):
+ def colors(self, colors: dict[tuple[int, ...], int]) -> None:
self._colors = colors
def copy(self) -> ImagePalette:
@@ -77,7 +85,7 @@ class ImagePalette:
return new
- def getdata(self) -> tuple[str, bytes]:
+ def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]:
"""
Get palette contents in format suitable for the low-level
``im.putpalette`` primitive.
@@ -104,11 +112,13 @@ class ImagePalette:
# Declare tostring as an alias for tobytes
tostring = tobytes
- def _new_color_index(self, image=None, e=None):
+ def _new_color_index(
+ self, image: Image.Image | None = None, e: Exception | None = None
+ ) -> int:
if not isinstance(self.palette, bytearray):
self._palette = bytearray(self.palette)
index = len(self.palette) // 3
- special_colors = ()
+ special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
if image:
special_colors = (
image.info.get("background"),
@@ -128,7 +138,11 @@ class ImagePalette:
raise ValueError(msg) from e
return index
- def getcolor(self, color, image=None) -> int:
+ def getcolor(
+ self,
+ color: tuple[int, ...],
+ image: Image.Image | None = None,
+ ) -> int:
"""Given an rgb tuple, allocate palette entry.
.. warning:: This method is experimental.
@@ -151,22 +165,23 @@ class ImagePalette:
except KeyError as e:
# allocate new color slot
index = self._new_color_index(image, e)
+ assert isinstance(self._palette, bytearray)
self.colors[color] = index
if index * 3 < len(self.palette):
self._palette = (
- self.palette[: index * 3]
+ self._palette[: index * 3]
+ bytes(color)
- + self.palette[index * 3 + 3 :]
+ + self._palette[index * 3 + 3 :]
)
else:
self._palette += bytes(color)
self.dirty = 1
return index
else:
- msg = f"unknown color specifier: {repr(color)}"
+ msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable]
raise ValueError(msg)
- def save(self, fp):
+ def save(self, fp: str | IO[str]) -> None:
"""Save palette to text file.
.. warning:: This method is experimental.
@@ -193,7 +208,7 @@ class ImagePalette:
# Internal
-def raw(rawmode, data) -> ImagePalette:
+def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
palette = ImagePalette()
palette.rawmode = rawmode
palette.palette = data
@@ -205,50 +220,57 @@ def raw(rawmode, data) -> ImagePalette:
# Factories
-def make_linear_lut(black, white):
+def make_linear_lut(black: int, white: float) -> list[int]:
if black == 0:
- return [white * i // 255 for i in range(256)]
+ return [int(white * i // 255) for i in range(256)]
msg = "unavailable when black is non-zero"
raise NotImplementedError(msg) # FIXME
-def make_gamma_lut(exp):
+def make_gamma_lut(exp: float) -> list[int]:
return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
-def negative(mode="RGB"):
+def negative(mode: str = "RGB") -> ImagePalette:
palette = list(range(256 * len(mode)))
palette.reverse()
return ImagePalette(mode, [i // len(mode) for i in palette])
-def random(mode="RGB"):
+def random(mode: str = "RGB") -> ImagePalette:
from random import randint
palette = [randint(0, 255) for _ in range(256 * len(mode))]
return ImagePalette(mode, palette)
-def sepia(white="#fff0c0"):
+def sepia(white: str = "#fff0c0") -> ImagePalette:
bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
-def wedge(mode="RGB"):
+def wedge(mode: str = "RGB") -> ImagePalette:
palette = list(range(256 * len(mode)))
return ImagePalette(mode, [i // len(mode) for i in palette])
-def load(filename):
+def load(filename: str) -> tuple[bytes, str]:
# FIXME: supports GIMP gradients only
with open(filename, "rb") as fp:
- for paletteHandler in [
+ paletteHandlers: list[
+ type[
+ GimpPaletteFile.GimpPaletteFile
+ | GimpGradientFile.GimpGradientFile
+ | PaletteFile.PaletteFile
+ ]
+ ] = [
GimpPaletteFile.GimpPaletteFile,
GimpGradientFile.GimpGradientFile,
PaletteFile.PaletteFile,
- ]:
+ ]
+ for paletteHandler in paletteHandlers:
try:
fp.seek(0)
lut = paletteHandler(fp).getpalette()
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index 293ba4941..35a37760c 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -152,7 +152,7 @@ def _toqclass_helper(im):
elif im.mode == "RGBA":
data = im.tobytes("raw", "BGRA")
format = qt_format.Format_ARGB32
- elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
+ elif im.mode == "I;16":
im = im.point(lambda i: i * 256)
format = qt_format.Format_Grayscale16
@@ -196,7 +196,7 @@ if qt_is_installed:
self.setColorTable(im_data["colortable"])
-def toqimage(im):
+def toqimage(im) -> ImageQt:
return ImageQt(im)
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index f60b1e11e..d62893d9c 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -26,7 +26,7 @@ from . import Image
_viewers = []
-def register(viewer, order: int = 1) -> None:
+def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None:
"""
The :py:func:`register` function is used to register additional viewers::
@@ -40,11 +40,8 @@ def register(viewer, order: int = 1) -> None:
Zero or a negative integer to prepend this viewer to the list,
a positive integer to append it.
"""
- try:
- if issubclass(viewer, Viewer):
- viewer = viewer()
- except TypeError:
- pass # raised if viewer wasn't a class
+ if isinstance(viewer, type) and issubclass(viewer, Viewer):
+ viewer = viewer()
if order > 0:
_viewers.append(viewer)
else:
@@ -118,6 +115,8 @@ class Viewer:
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
os.system(self.get_command(path, **options)) # nosec
return 1
@@ -142,6 +141,8 @@ class WindowsViewer(Viewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.Popen(
self.get_command(path, **options),
shell=True,
@@ -171,6 +172,8 @@ class MacViewer(Viewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.call(["open", "-a", "Preview.app", path])
executable = sys.executable or shutil.which("python3")
if executable:
@@ -215,6 +218,8 @@ class XDGViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.Popen(["xdg-open", path])
return 1
@@ -237,6 +242,8 @@ class DisplayViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
args = ["display"]
title = options.get("title")
if title:
@@ -259,6 +266,8 @@ class GmDisplayViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.Popen(["gm", "display", path])
return 1
@@ -275,6 +284,8 @@ class EogViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
subprocess.Popen(["eog", "-n", path])
return 1
@@ -299,6 +310,8 @@ class XVViewer(UnixViewer):
"""
Display given file.
"""
+ if not os.path.exists(path):
+ raise FileNotFoundError
args = ["xv"]
title = options.get("title")
if title:
diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py
index 6e2e7db1e..6b13e57a0 100644
--- a/src/PIL/ImageTk.py
+++ b/src/PIL/ImageTk.py
@@ -28,8 +28,9 @@ from __future__ import annotations
import tkinter
from io import BytesIO
+from typing import TYPE_CHECKING, Any, cast
-from . import Image
+from . import Image, ImageFile
# --------------------------------------------------------------------
# Check for Tkinter interface hooks
@@ -37,7 +38,7 @@ from . import Image
_pilbitmap_ok = None
-def _pilbitmap_check():
+def _pilbitmap_check() -> int:
global _pilbitmap_ok
if _pilbitmap_ok is None:
try:
@@ -49,17 +50,20 @@ def _pilbitmap_check():
return _pilbitmap_ok
-def _get_image_from_kw(kw):
+def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
source = None
if "file" in kw:
source = kw.pop("file")
elif "data" in kw:
source = BytesIO(kw.pop("data"))
- if source:
- return Image.open(source)
+ if not source:
+ return None
+ return Image.open(source)
-def _pyimagingtkcall(command, photo, id):
+def _pyimagingtkcall(
+ command: str, photo: PhotoImage | tkinter.PhotoImage, id: int
+) -> None:
tk = photo.tk
try:
tk.call(command, photo, id)
@@ -96,12 +100,27 @@ class PhotoImage:
image file).
"""
- def __init__(self, image=None, size=None, **kw):
+ def __init__(
+ self,
+ image: Image.Image | str | None = None,
+ size: tuple[int, int] | None = None,
+ **kw: Any,
+ ) -> None:
# Tk compatibility: file or data
if image is None:
image = _get_image_from_kw(kw)
- if hasattr(image, "mode") and hasattr(image, "size"):
+ if image is None:
+ msg = "Image is required"
+ raise ValueError(msg)
+ elif isinstance(image, str):
+ mode = image
+ image = None
+
+ if size is None:
+ msg = "If first argument is mode, size is required"
+ raise ValueError(msg)
+ else:
# got an image instead of a mode
mode = image.mode
if mode == "P":
@@ -114,9 +133,6 @@ class PhotoImage:
mode = "RGB" # default
size = image.size
kw["width"], kw["height"] = size
- else:
- mode = image
- image = None
if mode not in ["1", "L", "RGB", "RGBA"]:
mode = Image.getmodebase(mode)
@@ -162,7 +178,7 @@ class PhotoImage:
"""
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
be very slow if the photo image is displayed.
@@ -201,11 +217,14 @@ class BitmapImage:
:param image: A PIL image.
"""
- def __init__(self, image=None, **kw):
+ def __init__(self, image: Image.Image | None = None, **kw: Any) -> None:
# Tk compatibility: file or data
if image is None:
image = _get_image_from_kw(kw)
+ if image is None:
+ msg = "Image is required"
+ raise ValueError(msg)
self.__mode = image.mode
self.__size = image.size
@@ -254,7 +273,7 @@ class BitmapImage:
return str(self.__photo)
-def getimage(photo):
+def getimage(photo: PhotoImage) -> Image.Image:
"""Copies the contents of a PhotoImage to a PIL image memory."""
im = Image.new("RGBA", (photo.width(), photo.height()))
block = im.im
@@ -264,18 +283,23 @@ def getimage(photo):
return im
-def _show(image, title):
+def _show(image: Image.Image, title: str | None) -> None:
"""Helper for the Image.show method."""
class UI(tkinter.Label):
- def __init__(self, master, im):
+ def __init__(self, master: tkinter.Toplevel, im: Image.Image) -> None:
+ self.image: BitmapImage | PhotoImage
if im.mode == "1":
self.image = BitmapImage(im, foreground="white", master=master)
else:
self.image = PhotoImage(im, master=master)
- super().__init__(master, image=self.image, bg="black", bd=0)
+ if TYPE_CHECKING:
+ image = cast(tkinter._Image, self.image)
+ else:
+ image = self.image
+ super().__init__(master, image=image, bg="black", bd=0)
- if not tkinter._default_root:
+ if not getattr(tkinter, "_default_root"):
msg = "tkinter not initialized"
raise OSError(msg)
top = tkinter.Toplevel()
diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py
index 6aa82dadd..a3d8f441a 100644
--- a/src/PIL/ImageTransform.py
+++ b/src/PIL/ImageTransform.py
@@ -14,7 +14,8 @@
#
from __future__ import annotations
-from typing import Sequence
+from collections.abc import Sequence
+from typing import Any
from . import Image
@@ -24,7 +25,7 @@ class Transform(Image.ImageTransformHandler):
method: Image.Transform
- def __init__(self, data: Sequence[int]) -> None:
+ def __init__(self, data: Sequence[Any]) -> None:
self.data = data
def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
@@ -34,7 +35,7 @@ class Transform(Image.ImageTransformHandler):
self,
size: tuple[int, int],
image: Image.Image,
- **options: dict[str, str | int | tuple[int, ...] | list[int]],
+ **options: Any,
) -> Image.Image:
"""Perform the transform. Called from :py:meth:`.Image.transform`."""
# can be overridden
diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py
index 77e57a415..978c5a9d1 100644
--- a/src/PIL/ImageWin.py
+++ b/src/PIL/ImageWin.py
@@ -28,10 +28,10 @@ class HDC:
methods.
"""
- def __init__(self, dc):
+ def __init__(self, dc: int) -> None:
self.dc = dc
- def __int__(self):
+ def __int__(self) -> int:
return self.dc
@@ -42,10 +42,10 @@ class HWND:
methods, instead of a DC.
"""
- def __init__(self, wnd):
+ def __init__(self, wnd: int) -> None:
self.wnd = wnd
- def __int__(self):
+ def __int__(self) -> int:
return self.wnd
@@ -69,19 +69,22 @@ class Dib:
defines the size of the image.
"""
- def __init__(self, image, size=None):
- if hasattr(image, "mode") and hasattr(image, "size"):
+ def __init__(
+ self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None
+ ) -> None:
+ if isinstance(image, str):
+ mode = image
+ image = ""
+ else:
mode = image.mode
size = image.size
- else:
- mode = image
- image = None
if mode not in ["1", "L", "P", "RGB"]:
mode = Image.getmodebase(mode)
self.image = Image.core.display(mode, size)
self.mode = mode
self.size = size
if image:
+ assert not isinstance(image, str)
self.paste(image)
def expose(self, handle):
@@ -149,7 +152,9 @@ class Dib:
result = self.image.query_palette(handle)
return result
- def paste(self, im, box=None):
+ def paste(
+ self, im: Image.Image, box: tuple[int, int, int, int] | None = None
+ ) -> None:
"""
Paste a PIL image into the bitmap image.
@@ -169,16 +174,16 @@ class Dib:
else:
self.image.paste(im.im)
- def frombytes(self, buffer):
+ def frombytes(self, buffer: bytes) -> None:
"""
Load display memory contents from byte data.
:param buffer: A buffer containing display data (usually
data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`)
"""
- return self.image.frombytes(buffer)
+ self.image.frombytes(buffer)
- def tobytes(self):
+ def tobytes(self) -> bytes:
"""
Copy display memory contents to bytes object.
@@ -190,7 +195,9 @@ class Dib:
class Window:
"""Create a Window with the given title size."""
- def __init__(self, title="PIL", width=None, height=None):
+ def __init__(
+ self, title: str = "PIL", width: int | None = None, height: int | None = None
+ ) -> None:
self.hwnd = Image.core.createwindow(
title, self.__dispatcher, width or 0, height or 0
)
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index 73df83bfb..a04616fbd 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -16,8 +16,8 @@
#
from __future__ import annotations
+from collections.abc import Sequence
from io import BytesIO
-from typing import Sequence
from . import Image, ImageFile
from ._binary import i16be as i16
@@ -148,7 +148,7 @@ class IptcImageFile(ImageFile.ImageFile):
if tag == (8, 10):
self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
return ImageFile.ImageFile.load(self)
@@ -176,6 +176,7 @@ class IptcImageFile(ImageFile.ImageFile):
with Image.open(o) as _im:
_im.load()
self.im = _im.im
+ return None
Image.register_open(IptcImageFile.format, IptcImageFile)
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index ce6342bdb..eeec41686 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -18,6 +18,7 @@ from __future__ import annotations
import io
import os
import struct
+from typing import IO, cast
from . import Image, ImageFile, ImagePalette, _binary
@@ -28,13 +29,13 @@ class BoxReader:
and to easily step into and read sub-boxes.
"""
- def __init__(self, fp, length=-1):
+ def __init__(self, fp: IO[bytes], length: int = -1) -> None:
self.fp = fp
self.has_length = length >= 0
self.length = length
self.remaining_in_box = -1
- def _can_read(self, num_bytes):
+ def _can_read(self, num_bytes: int) -> bool:
if self.has_length and self.fp.tell() + num_bytes > self.length:
# Outside box: ensure we don't read past the known file length
return False
@@ -44,7 +45,7 @@ class BoxReader:
else:
return True # No length known, just read
- def _read_bytes(self, num_bytes):
+ def _read_bytes(self, num_bytes: int) -> bytes:
if not self._can_read(num_bytes):
msg = "Not enough data in header"
raise SyntaxError(msg)
@@ -58,7 +59,7 @@ class BoxReader:
self.remaining_in_box -= num_bytes
return data
- def read_fields(self, field_format):
+ def read_fields(self, field_format: str) -> tuple[int | bytes, ...]:
size = struct.calcsize(field_format)
data = self._read_bytes(size)
return struct.unpack(field_format, data)
@@ -74,16 +75,16 @@ class BoxReader:
else:
return True
- def next_box_type(self):
+ def next_box_type(self) -> bytes:
# Skip the rest of the box if it has not been read
if self.remaining_in_box > 0:
self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
self.remaining_in_box = -1
# 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:
- lbox = self.read_fields(">Q")[0]
+ lbox = cast(int, self.read_fields(">Q")[0])
hlen = 16
else:
hlen = 8
@@ -96,7 +97,7 @@ class BoxReader:
return tbox
-def _parse_codestream(fp):
+def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]:
"""Parse the JPEG 2000 codestream to extract the size and component
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
@@ -121,20 +122,30 @@ def _parse_codestream(fp):
elif csiz == 4:
mode = "RGBA"
else:
- mode = None
+ msg = "unable to determine J2K image mode"
+ raise SyntaxError(msg)
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,
calculated as (num / denom) * 10^exp and stored in dots per meter,
to floating-point dots per inch."""
- if denom != 0:
- return (254 * num * (10**exp)) / (10000 * denom)
+ if denom == 0:
+ return None
+ return (254 * num * (10**exp)) / (10000 * denom)
-def _parse_jp2_header(fp):
+def _parse_jp2_header(
+ fp: IO[bytes],
+) -> tuple[
+ tuple[int, int],
+ str,
+ str | None,
+ tuple[float, float] | None,
+ ImagePalette.ImagePalette | None,
+]:
"""Parse the JP2 header box to extract size, component count,
color space information, and optionally DPI information,
returning a (size, mode, mimetype, dpi) tuple."""
@@ -152,6 +163,7 @@ def _parse_jp2_header(fp):
elif tbox == b"ftyp":
if reader.read_fields(">4s")[0] == b"jpx ":
mimetype = "image/jpx"
+ assert header is not None
size = None
mode = None
@@ -165,6 +177,9 @@ def _parse_jp2_header(fp):
if tbox == b"ihdr":
height, width, nc, bpc = header.read_fields(">IIHB")
+ assert isinstance(height, int)
+ assert isinstance(width, int)
+ assert isinstance(bpc, int)
size = (width, height)
if nc == 1 and (bpc & 0x7F) > 8:
mode = "I;16"
@@ -182,11 +197,21 @@ def _parse_jp2_header(fp):
mode = "CMYK"
elif tbox == b"pclr" and mode in ("L", "LA"):
ne, npc = header.read_fields(">HB")
- bitdepths = header.read_fields(">" + ("B" * npc))
- if max(bitdepths) <= 8:
+ assert isinstance(ne, int)
+ assert isinstance(npc, int)
+ max_bitdepth = 0
+ for bitdepth in header.read_fields(">" + ("B" * npc)):
+ assert isinstance(bitdepth, int)
+ if bitdepth > max_bitdepth:
+ max_bitdepth = bitdepth
+ if max_bitdepth <= 8:
palette = ImagePalette.ImagePalette()
for i in range(ne):
- palette.getcolor(header.read_fields(">" + ("B" * npc)))
+ color: list[int] = []
+ for value in header.read_fields(">" + ("B" * npc)):
+ assert isinstance(value, int)
+ color.append(value)
+ palette.getcolor(tuple(color))
mode = "P" if mode == "L" else "PA"
elif tbox == b"res ":
res = header.read_boxes()
@@ -194,6 +219,12 @@ def _parse_jp2_header(fp):
tres = res.next_box_type()
if tres == b"resc":
vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
+ assert isinstance(vrcn, int)
+ assert isinstance(vrcd, int)
+ assert isinstance(hrcn, int)
+ assert isinstance(hrcd, int)
+ assert isinstance(vrce, int)
+ assert isinstance(hrce, int)
hres = _res_to_dpi(hrcn, hrcd, hrce)
vres = _res_to_dpi(vrcn, vrcd, vrce)
if hres is not None and vres is not None:
@@ -235,10 +266,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
msg = "not a JPEG 2000 file"
raise SyntaxError(msg)
- if self.size is None or self.mode is None:
- msg = "unable to determine size/mode"
- raise SyntaxError(msg)
-
self._reduce = 0
self.layers = 0
@@ -300,7 +327,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
def reduce(self, value):
self._reduce = value
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if self.tile and self._reduce:
power = 1 << self._reduce
adjust = power >> 1
@@ -328,11 +355,13 @@ def _accept(prefix: bytes) -> bool:
# Save support
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# Get the keyword arguments
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"
else:
kind = "jp2"
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 909911dfe..4916727be 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -42,6 +42,7 @@ import subprocess
import sys
import tempfile
import warnings
+from typing import IO, Any
from . import Image, ImageFile
from ._binary import i16be as i16
@@ -54,12 +55,12 @@ from .JpegPresets import presets
# Parser
-def Skip(self, marker):
+def Skip(self: JpegImageFile, marker: int) -> None:
n = i16(self.fp.read(2)) - 2
ImageFile._safe_read(self.fp, n)
-def APP(self, marker):
+def APP(self: JpegImageFile, marker: int) -> None:
#
# Application marker. Store these in the APP dictionary.
# Also look for well-known application markers.
@@ -94,6 +95,8 @@ def APP(self, marker):
else:
self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6
+ elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00":
+ self.info["xmp"] = s.split(b"\x00", 1)[1]
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
@@ -130,13 +133,14 @@ def APP(self, marker):
offset += 4
data = s[offset : offset + size]
if code == 0x03ED: # ResolutionInfo
- data = {
+ photoshop[code] = {
"XResolution": i32(data, 0) / 65536,
"DisplayedUnitsX": i16(data, 4),
"YResolution": i32(data, 8) / 65536,
"DisplayedUnitsY": i16(data, 12),
}
- photoshop[code] = data
+ else:
+ photoshop[code] = data
offset += size
offset += offset & 1 # align
except struct.error:
@@ -158,40 +162,8 @@ def APP(self, marker):
# plus constant header size
self.info["mpoffset"] = self.fp.tell() - n + 4
- # If DPI isn't in JPEG header, fetch from EXIF
- if "dpi" not in self.info and "exif" in self.info:
- try:
- exif = self.getexif()
- resolution_unit = exif[0x0128]
- x_resolution = exif[0x011A]
- try:
- dpi = float(x_resolution[0]) / x_resolution[1]
- except TypeError:
- dpi = x_resolution
- if math.isnan(dpi):
- msg = "DPI is not a number"
- raise ValueError(msg)
- if resolution_unit == 3: # cm
- # 1 dpcm = 2.54 dpi
- dpi *= 2.54
- self.info["dpi"] = dpi, dpi
- except (
- struct.error,
- KeyError,
- SyntaxError,
- TypeError,
- ValueError,
- ZeroDivisionError,
- ):
- # struct.error for truncated EXIF
- # KeyError for dpi not included
- # SyntaxError for invalid/unreadable EXIF
- # ValueError or TypeError for dpi being an invalid float
- # ZeroDivisionError for invalid dpi rational value
- self.info["dpi"] = 72, 72
-
-def COM(self, marker):
+def COM(self: JpegImageFile, marker: int) -> None:
#
# Comment marker. Store these in the APP dictionary.
n = i16(self.fp.read(2)) - 2
@@ -202,7 +174,7 @@ def COM(self, marker):
self.applist.append(("COM", s))
-def SOF(self, marker):
+def SOF(self: JpegImageFile, marker: int) -> None:
#
# Start of frame marker. Defines the size and mode of the
# image. JPEG is colour blind, so we use some simple
@@ -250,7 +222,7 @@ def SOF(self, marker):
self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2]))
-def DQT(self, marker):
+def DQT(self: JpegImageFile, marker: int) -> None:
#
# Define quantization table. Note that there might be more
# than one table in each marker.
@@ -367,6 +339,7 @@ class JpegImageFile(ImageFile.ImageFile):
# Create attributes
self.bits = self.layers = 0
+ self._exif_offset = 0
# JPEG specifics (internal)
self.layer = []
@@ -408,6 +381,8 @@ class JpegImageFile(ImageFile.ImageFile):
msg = "no marker found"
raise SyntaxError(msg)
+ self._read_dpi_from_exif()
+
def load_read(self, read_bytes: int) -> bytes:
"""
internal: read more image data
@@ -425,7 +400,7 @@ class JpegImageFile(ImageFile.ImageFile):
return s
def draft(
- self, mode: str, size: tuple[int, int]
+ self, mode: str | None, size: tuple[int, int] | None
) -> tuple[str, tuple[int, int, float, float]] | None:
if len(self.tile) != 1:
return None
@@ -493,35 +468,49 @@ class JpegImageFile(ImageFile.ImageFile):
self.tile = []
- def _getexif(self):
+ def _getexif(self) -> dict[str, Any] | None:
return _getexif(self)
- def _getmp(self):
+ def _read_dpi_from_exif(self) -> None:
+ # If DPI isn't in JPEG header, fetch from EXIF
+ if "dpi" in self.info or "exif" not in self.info:
+ return
+ try:
+ exif = self.getexif()
+ resolution_unit = exif[0x0128]
+ x_resolution = exif[0x011A]
+ try:
+ dpi = float(x_resolution[0]) / x_resolution[1]
+ except TypeError:
+ dpi = x_resolution
+ if math.isnan(dpi):
+ msg = "DPI is not a number"
+ raise ValueError(msg)
+ if resolution_unit == 3: # cm
+ # 1 dpcm = 2.54 dpi
+ dpi *= 2.54
+ self.info["dpi"] = dpi, dpi
+ except (
+ struct.error, # truncated EXIF
+ KeyError, # dpi not included
+ SyntaxError, # invalid/unreadable EXIF
+ TypeError, # dpi is an invalid float
+ ValueError, # dpi is an invalid float
+ ZeroDivisionError, # invalid dpi rational value
+ ):
+ self.info["dpi"] = 72, 72
+
+ def _getmp(self) -> dict[int, Any] | None:
return _getmp(self)
- def getxmp(self):
- """
- Returns a dictionary containing the XMP tags.
- Requires defusedxml to be installed.
- :returns: XMP tags in a dictionary.
- """
-
- for segment, content in self.applist:
- if segment == "APP1":
- marker, xmp_tags = content.split(b"\x00")[:2]
- if marker == b"http://ns.adobe.com/xap/1.0/":
- return self._getxmp(xmp_tags)
- return {}
-
-
-def _getexif(self):
+def _getexif(self: JpegImageFile) -> dict[str, Any] | None:
if "exif" not in self.info:
return None
return self.getexif()._get_merged_dict()
-def _getmp(self):
+def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
# Extract MP information. This method was inspired by the "highly
# experimental" _getexif version that's been in use for years now,
# itself based on the ImageFileDirectory class in the TIFF plugin.
@@ -629,7 +618,7 @@ samplings = {
# fmt: on
-def get_sampling(im):
+def get_sampling(im: Image.Image) -> int:
# There's no subsampling when images have only 1 layer
# (grayscale images) or when they are CMYK (4 layers),
# so set subsampling to the default value.
@@ -637,13 +626,13 @@ def get_sampling(im):
# NOTE: currently Pillow can't encode JPEG to YCCK format.
# If YCCK support is added in the future, subsampling code will have
# to be updated (here and in JpegEncode.c) to deal with 4 layers.
- if not hasattr(im, "layers") or im.layers in (1, 4):
+ if not isinstance(im, JpegImageFile) or im.layers in (1, 4):
return -1
sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
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:
msg = "cannot write empty image as JPEG"
raise ValueError(msg)
@@ -826,7 +815,7 @@ def _save(im, fp, filename):
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.
tempfile = im._dump()
subprocess.check_call(["cjpeg", "-outfile", filename, tempfile])
@@ -843,6 +832,10 @@ def jpeg_factory(fp=None, filename=None):
try:
mpheader = im._getmp()
if mpheader[45057] > 1:
+ for segment, content in im.applist:
+ if segment == "APP1" and b' hdrgm:Version="' in content:
+ # Ultra HDR images are not yet supported
+ return im
# It's actually an MPO
from .MpoImagePlugin import MpoImageFile
diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py
index 5aef94dfb..5f23a34b9 100644
--- a/src/PIL/MicImagePlugin.py
+++ b/src/PIL/MicImagePlugin.py
@@ -63,14 +63,14 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
msg = "not an MIC file; no image entries"
raise SyntaxError(msg)
- self.frame = None
+ self.frame = -1
self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1
self.__fp = self.fp
self.seek(0)
- def seek(self, frame):
+ def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
try:
@@ -85,7 +85,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.frame = frame
- def tell(self):
+ def tell(self) -> int:
return self.frame
def close(self) -> None:
@@ -93,7 +93,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.ole.close()
super().close()
- def __exit__(self, *args):
+ def __exit__(self, *args: object) -> None:
self.__fp.close()
self.ole.close()
super().__exit__()
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index 766e1290c..5ed9f56a1 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -22,6 +22,7 @@ from __future__ import annotations
import itertools
import os
import struct
+from typing import IO, Any, cast
from . import (
Image,
@@ -32,23 +33,18 @@ from . import (
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)
-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", [])
- if not append_images:
- try:
- animated = im.is_animated
- except AttributeError:
- animated = False
- if not animated:
- _save(im, fp, filename)
- return
+ if not append_images and not getattr(im, "is_animated", False):
+ _save(im, fp, filename)
+ return
mpf_offset = 28
- offsets = []
+ offsets: list[int] = []
for imSequence in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(imSequence):
if not offsets:
@@ -105,8 +101,11 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
JpegImagePlugin.JpegImageFile._open(self)
self._after_jpeg_open()
- def _after_jpeg_open(self, mpheader=None):
+ def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
self.mpinfo = mpheader if mpheader is not None else self._getmp()
+ if self.mpinfo is None:
+ msg = "Image appears to be a malformed MPO file"
+ raise ValueError(msg)
self.n_frames = self.mpinfo[0xB001]
self.__mpoffsets = [
mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
@@ -153,7 +152,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
return self.__frame
@staticmethod
- def adopt(jpeg_instance, mpheader=None):
+ def adopt(
+ jpeg_instance: JpegImagePlugin.JpegImageFile,
+ mpheader: dict[int, Any] | None = None,
+ ) -> MpoImageFile:
"""
Transform the instance of JpegImageFile into
an instance of MpoImageFile.
@@ -165,8 +167,9 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
double call to _open.
"""
jpeg_instance.__class__ = MpoImageFile
- jpeg_instance._after_jpeg_open(mpheader)
- return jpeg_instance
+ mpo_instance = cast(MpoImageFile, jpeg_instance)
+ mpo_instance._after_jpeg_open(mpheader)
+ return mpo_instance
# ---------------------------------------------------------------------
diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py
index 65cc70624..0a75c868b 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -164,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder)
# 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":
msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg)
diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py
index 49c06ce13..673eae1d1 100644
--- a/src/PIL/PSDraw.py
+++ b/src/PIL/PSDraw.py
@@ -17,6 +17,7 @@
from __future__ import annotations
import sys
+from typing import TYPE_CHECKING
from . import EpsImagePlugin
@@ -38,7 +39,7 @@ class PSDraw:
fp = sys.stdout
self.fp = fp
- def begin_document(self, id=None):
+ def begin_document(self, id: str | None = None) -> None:
"""Set up printing of a document. (Write PostScript DSC header.)"""
# FIXME: incomplete
self.fp.write(
@@ -52,7 +53,7 @@ class PSDraw:
self.fp.write(EDROFF_PS)
self.fp.write(VDI_PS)
self.fp.write(b"%%EndProlog\n")
- self.isofont = {}
+ self.isofont: dict[bytes, int] = {}
def end_document(self) -> None:
"""Ends printing. (Write PostScript DSC footer.)"""
@@ -60,22 +61,24 @@ class PSDraw:
if hasattr(self.fp, "flush"):
self.fp.flush()
- def setfont(self, font, size):
+ def setfont(self, font: str, size: int) -> None:
"""
Selects which font to use.
:param font: A PostScript font name
:param size: Size in points.
"""
- font = bytes(font, "UTF-8")
- if font not in self.isofont:
+ font_bytes = bytes(font, "UTF-8")
+ if font_bytes not in self.isofont:
# reencode font
- self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font))
- self.isofont[font] = 1
+ self.fp.write(
+ b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes)
+ )
+ self.isofont[font_bytes] = 1
# rough
- self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font))
+ self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes))
- def line(self, xy0, xy1):
+ def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None:
"""
Draws a line between the two points. Coordinates are given in
PostScript point coordinates (72 points per inch, (0, 0) is the lower
@@ -83,7 +86,7 @@ class PSDraw:
"""
self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1))
- def rectangle(self, box):
+ def rectangle(self, box: tuple[int, int, int, int]) -> None:
"""
Draws a rectangle.
@@ -92,18 +95,22 @@ class PSDraw:
"""
self.fp.write(b"%d %d M 0 %d %d Vr\n" % box)
- def text(self, xy, text):
+ def text(self, xy: tuple[int, int], text: str) -> None:
"""
Draws text at the given position. You must use
:py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
"""
- text = bytes(text, "UTF-8")
- text = b"\\(".join(text.split(b"("))
- text = b"\\)".join(text.split(b")"))
- xy += (text,)
- self.fp.write(b"%d %d M (%s) S\n" % xy)
+ text_bytes = bytes(text, "UTF-8")
+ text_bytes = b"\\(".join(text_bytes.split(b"("))
+ text_bytes = b"\\)".join(text_bytes.split(b")"))
+ self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,)))
- def image(self, box, im, dpi=None):
+ if TYPE_CHECKING:
+ from . import Image
+
+ def image(
+ self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None
+ ) -> None:
"""Draw a PIL image, centered in the given box."""
# default resolution depends on mode
if not dpi:
@@ -131,7 +138,7 @@ class PSDraw:
sx = x / im.size[0]
sy = y / im.size[1]
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")
diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py
index dc3175402..81652e5ee 100644
--- a/src/PIL/PaletteFile.py
+++ b/src/PIL/PaletteFile.py
@@ -14,6 +14,8 @@
#
from __future__ import annotations
+from typing import IO
+
from ._binary import o8
@@ -22,8 +24,8 @@ class PaletteFile:
rawmode = "RGB"
- def __init__(self, fp):
- self.palette = [(i, i, i) for i in range(256)]
+ def __init__(self, fp: IO[bytes]) -> None:
+ palette = [o8(i) * 3 for i in range(256)]
while True:
s = fp.readline()
@@ -44,9 +46,9 @@ class PaletteFile:
g = b = r
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):
+ def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode
diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py
index 85f9fe1bf..1735070f8 100644
--- a/src/PIL/PalmImagePlugin.py
+++ b/src/PIL/PalmImagePlugin.py
@@ -8,6 +8,8 @@
##
from __future__ import annotations
+from typing import IO
+
from . import Image, ImageFile
from ._binary import o8
from ._binary import o16be as o16b
@@ -82,10 +84,10 @@ _Palm8BitColormapValues = (
# 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.putdata(list(range(len(_Palm8BitColormapValues))))
- palettedata = ()
+ palettedata: tuple[int, ...] = ()
for colormapValue in _Palm8BitColormapValues:
palettedata += colormapValue
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.
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "P":
# we assume this is a color Palm image with the standard colormap,
# unless the "info" dict has a "custom-colormap" field
@@ -127,21 +129,22 @@ def _save(im, fp, filename):
# and invert it because
# Palm does grayscale from white (0) to black (1)
bpp = im.encoderinfo["bpp"]
- im = im.point(
- lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift)
- )
+ maxval = (1 << bpp) - 1
+ shift = 8 - bpp
+ im = im.point(lambda x: maxval - (x >> shift))
elif im.info.get("bpp") in (1, 2, 4):
# here we assume that even though the inherent mode is 8-bit grayscale,
# only the lower bpp bits are significant.
# We invert them to match the Palm.
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:
msg = f"cannot write mode {im.mode} as Palm"
raise OSError(msg)
# we ignore the palette here
- im.mode = "P"
+ im._mode = "P"
rawmode = f"P;{bpp}"
version = 1
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 026bfd9a0..dd42003b5 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -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:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
index 1777f1f20..f0da1e047 100644
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -25,6 +25,7 @@ import io
import math
import os
import time
+from typing import IO
from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
@@ -39,7 +40,7 @@ from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
# 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)
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index 68501d625..7cb2d241b 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -8,12 +8,12 @@ import os
import re
import time
import zlib
-from typing import TYPE_CHECKING, Any, List, NamedTuple, Union
+from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
# on page 656
-def encode_text(s):
+def encode_text(s: str) -> bytes:
return codecs.BOM_UTF16_BE + s.encode("utf_16_be")
@@ -62,7 +62,7 @@ PDFDocEncoding = {
}
-def decode_text(b):
+def decode_text(b: bytes) -> str:
if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE:
return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be")
else:
@@ -76,7 +76,7 @@ class PdfFormatError(RuntimeError):
pass
-def check_format_condition(condition, error_message):
+def check_format_condition(condition: bool, error_message: str) -> None:
if not condition:
raise PdfFormatError(error_message)
@@ -93,17 +93,16 @@ class IndirectReference(IndirectReferenceTuple):
def __bytes__(self) -> bytes:
return self.__str__().encode("us-ascii")
- def __eq__(self, other):
- return (
- other.__class__ is self.__class__
- and other.object_id == self.object_id
- and other.generation == self.generation
- )
+ def __eq__(self, other: object) -> bool:
+ if self.__class__ is not other.__class__:
+ return False
+ assert isinstance(other, IndirectReference)
+ return other.object_id == self.object_id and other.generation == self.generation
- def __ne__(self, other):
+ def __ne__(self, other: object) -> bool:
return not (self == other)
- def __hash__(self):
+ def __hash__(self) -> int:
return hash((self.object_id, self.generation))
@@ -113,13 +112,17 @@ class IndirectObjectDef(IndirectReference):
class XrefTable:
- def __init__(self):
- self.existing_entries = {} # object ID => (offset, generation)
- self.new_entries = {} # object ID => (offset, generation)
+ def __init__(self) -> None:
+ self.existing_entries: dict[int, tuple[int, int]] = (
+ {}
+ ) # object ID => (offset, generation)
+ self.new_entries: dict[int, tuple[int, int]] = (
+ {}
+ ) # object ID => (offset, generation)
self.deleted_entries = {0: 65536} # object ID => generation
self.reading_finished = False
- def __setitem__(self, key, value):
+ def __setitem__(self, key: int, value: tuple[int, int]) -> None:
if self.reading_finished:
self.new_entries[key] = value
else:
@@ -127,13 +130,13 @@ class XrefTable:
if key in self.deleted_entries:
del self.deleted_entries[key]
- def __getitem__(self, key):
+ def __getitem__(self, key: int) -> tuple[int, int]:
try:
return self.new_entries[key]
except KeyError:
return self.existing_entries[key]
- def __delitem__(self, key):
+ def __delitem__(self, key: int) -> None:
if key in self.new_entries:
generation = self.new_entries[key][1] + 1
del self.new_entries[key]
@@ -147,7 +150,7 @@ class XrefTable:
msg = f"object ID {key} cannot be deleted because it doesn't exist"
raise IndexError(msg)
- def __contains__(self, key):
+ def __contains__(self, key: int) -> bool:
return key in self.existing_entries or key in self.new_entries
def __len__(self) -> int:
@@ -157,19 +160,19 @@ class XrefTable:
| set(self.deleted_entries.keys())
)
- def keys(self):
+ def keys(self) -> set[int]:
return (
set(self.existing_entries.keys()) - set(self.deleted_entries.keys())
) | set(self.new_entries.keys())
- def write(self, f):
+ def write(self, f: IO[bytes]) -> int:
keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys()))
deleted_keys = sorted(set(self.deleted_entries.keys()))
startxref = f.tell()
f.write(b"xref\n")
while keys:
# find a contiguous sequence of object IDs
- prev = None
+ prev: int | None = None
for index, key in enumerate(keys):
if prev is None or prev + 1 == key:
prev = key
@@ -179,7 +182,7 @@ class XrefTable:
break
else:
contiguous_keys = keys
- keys = None
+ keys = []
f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys)))
for object_id in contiguous_keys:
if object_id in self.new_entries:
@@ -203,7 +206,9 @@ class XrefTable:
class PdfName:
- def __init__(self, name):
+ name: bytes
+
+ def __init__(self, name: PdfName | bytes | str) -> None:
if isinstance(name, PdfName):
self.name = name.name
elif isinstance(name, bytes):
@@ -214,19 +219,19 @@ class PdfName:
def name_as_str(self) -> str:
return self.name.decode("us-ascii")
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
return (
isinstance(other, PdfName) and other.name == self.name
) or other == self.name
- def __hash__(self):
+ def __hash__(self) -> int:
return hash(self.name)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({repr(self.name)})"
@classmethod
- def from_pdf_stream(cls, data):
+ def from_pdf_stream(cls, data: bytes) -> PdfName:
return cls(PdfParser.interpret_name(data))
allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
@@ -241,7 +246,7 @@ class PdfName:
return bytes(result)
-class PdfArray(List[Any]):
+class PdfArray(list[Any]):
def __bytes__(self) -> bytes:
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
@@ -253,13 +258,13 @@ else:
class PdfDict(_DictBase):
- def __setattr__(self, key, value):
+ def __setattr__(self, key: str, value: Any) -> None:
if key == "data":
collections.UserDict.__setattr__(self, key, value)
else:
self[key.encode("us-ascii")] = value
- def __getattr__(self, key):
+ def __getattr__(self, key: str) -> str | time.struct_time:
try:
value = self[key.encode("us-ascii")]
except KeyError as e:
@@ -301,7 +306,7 @@ class PdfDict(_DictBase):
class PdfBinary:
- def __init__(self, data):
+ def __init__(self, data: list[int] | bytes) -> None:
self.data = data
def __bytes__(self) -> bytes:
@@ -309,27 +314,27 @@ class PdfBinary:
class PdfStream:
- def __init__(self, dictionary, buf):
+ def __init__(self, dictionary: PdfDict, buf: bytes) -> None:
self.dictionary = dictionary
self.buf = buf
- def decode(self):
+ def decode(self) -> bytes:
try:
- filter = self.dictionary.Filter
- except AttributeError:
+ filter = self.dictionary[b"Filter"]
+ except KeyError:
return self.buf
if filter == b"FlateDecode":
try:
- expected_length = self.dictionary.DL
- except AttributeError:
- expected_length = self.dictionary.Length
+ expected_length = self.dictionary[b"DL"]
+ except KeyError:
+ expected_length = self.dictionary[b"Length"]
return zlib.decompress(self.buf, bufsize=int(expected_length))
else:
- msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
+ msg = f"stream filter {repr(filter)} unknown/unsupported"
raise NotImplementedError(msg)
-def pdf_repr(x):
+def pdf_repr(x: Any) -> bytes:
if x is True:
return b"true"
elif x is False:
@@ -364,12 +369,19 @@ class PdfParser:
Supports PDF up to 1.4
"""
- def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"):
+ def __init__(
+ self,
+ filename: str | None = None,
+ f: IO[bytes] | None = None,
+ buf: bytes | bytearray | None = None,
+ start_offset: int = 0,
+ mode: str = "rb",
+ ) -> None:
if buf and f:
msg = "specify buf or f or filename, but not both buf and f"
raise RuntimeError(msg)
self.filename = filename
- self.buf = buf
+ self.buf: bytes | bytearray | mmap.mmap | None = buf
self.f = f
self.start_offset = start_offset
self.should_close_buf = False
@@ -378,12 +390,16 @@ class PdfParser:
self.f = f = open(filename, mode)
self.should_close_file = True
if f is not None:
- self.buf = buf = self.get_buf_from_file(f)
+ self.buf = self.get_buf_from_file(f)
self.should_close_buf = True
if not filename and hasattr(f, "name"):
self.filename = f.name
- self.cached_objects = {}
- if buf:
+ self.cached_objects: dict[IndirectReference, Any] = {}
+ self.root_ref: IndirectReference | None
+ self.info_ref: IndirectReference | None
+ self.pages_ref: IndirectReference | None
+ self.last_xref_section_offset: int | None
+ if self.buf:
self.read_pdf_info()
else:
self.file_size_total = self.file_size_this = 0
@@ -391,33 +407,30 @@ class PdfParser:
self.root_ref = None
self.info = PdfDict()
self.info_ref = None
- self.page_tree_root = {}
- self.pages = []
- self.orig_pages = []
+ self.page_tree_root = PdfDict()
+ self.pages: list[IndirectReference] = []
+ self.orig_pages: list[IndirectReference] = []
self.pages_ref = None
self.last_xref_section_offset = None
- self.trailer_dict = {}
+ self.trailer_dict: dict[bytes, Any] = {}
self.xref_table = XrefTable()
self.xref_table.reading_finished = True
if f:
self.seek_end()
- def __enter__(self):
+ def __enter__(self) -> PdfParser:
return self
- def __exit__(self, exc_type, exc_value, traceback):
+ def __exit__(self, *args: object) -> None:
self.close()
- return False # do not suppress exceptions
def start_writing(self) -> None:
self.close_buf()
self.seek_end()
def close_buf(self) -> None:
- try:
+ if isinstance(self.buf, mmap.mmap):
self.buf.close()
- except AttributeError:
- pass
self.buf = None
def close(self) -> None:
@@ -428,15 +441,19 @@ class PdfParser:
self.f = None
def seek_end(self) -> None:
+ assert self.f is not None
self.f.seek(0, os.SEEK_END)
def write_header(self) -> None:
+ assert self.f is not None
self.f.write(b"%PDF-1.4\n")
- def write_comment(self, s):
+ def write_comment(self, s: str) -> None:
+ assert self.f is not None
self.f.write(f"% {s}\n".encode())
- def write_catalog(self):
+ def write_catalog(self) -> IndirectReference:
+ assert self.f is not None
self.del_root()
self.root_ref = self.next_object_id(self.f.tell())
self.pages_ref = self.next_object_id(0)
@@ -479,7 +496,10 @@ class PdfParser:
pages_tree_node_ref = pages_tree_node.get(b"Parent", None)
self.orig_pages = []
- def write_xref_and_trailer(self, new_root_ref=None):
+ def write_xref_and_trailer(
+ self, new_root_ref: IndirectReference | None = None
+ ) -> None:
+ assert self.f is not None
if new_root_ref:
self.del_root()
self.root_ref = new_root_ref
@@ -487,7 +507,10 @@ class PdfParser:
self.info_ref = self.write_obj(None, self.info)
start_xref = self.xref_table.write(self.f)
num_entries = len(self.xref_table)
- trailer_dict = {b"Root": self.root_ref, b"Size": num_entries}
+ trailer_dict: dict[str | bytes, Any] = {
+ b"Root": self.root_ref,
+ b"Size": num_entries,
+ }
if self.last_xref_section_offset is not None:
trailer_dict[b"Prev"] = self.last_xref_section_offset
if self.info:
@@ -499,16 +522,20 @@ class PdfParser:
+ b"\nstartxref\n%d\n%%%%EOF" % start_xref
)
- def write_page(self, ref, *objs, **dict_obj):
- if isinstance(ref, int):
- ref = self.pages[ref]
+ def write_page(
+ self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any
+ ) -> IndirectReference:
+ obj_ref = self.pages[ref] if isinstance(ref, int) else ref
if "Type" not in dict_obj:
dict_obj["Type"] = PdfName(b"Page")
if "Parent" not in dict_obj:
dict_obj["Parent"] = self.pages_ref
- return self.write_obj(ref, *objs, **dict_obj)
+ return self.write_obj(obj_ref, *objs, **dict_obj)
- def write_obj(self, ref, *objs, **dict_obj):
+ def write_obj(
+ self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any
+ ) -> IndirectReference:
+ assert self.f is not None
f = self.f
if ref is None:
ref = self.next_object_id(f.tell())
@@ -536,7 +563,7 @@ class PdfParser:
del self.xref_table[self.root[b"Pages"].object_id]
@staticmethod
- def get_buf_from_file(f):
+ def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap:
if hasattr(f, "getbuffer"):
return f.getbuffer()
elif hasattr(f, "getvalue"):
@@ -548,10 +575,15 @@ class PdfParser:
return b""
def read_pdf_info(self) -> None:
+ assert self.buf is not None
self.file_size_total = len(self.buf)
self.file_size_this = self.file_size_total - self.start_offset
self.read_trailer()
+ check_format_condition(
+ self.trailer_dict.get(b"Root") is not None, "Root is missing"
+ )
self.root_ref = self.trailer_dict[b"Root"]
+ assert self.root_ref is not None
self.info_ref = self.trailer_dict.get(b"Info", None)
self.root = PdfDict(self.read_indirect(self.root_ref))
if self.info_ref is None:
@@ -562,12 +594,15 @@ class PdfParser:
check_format_condition(
self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog"
)
- check_format_condition(b"Pages" in self.root, "/Pages missing in Root")
+ check_format_condition(
+ self.root.get(b"Pages") is not None, "/Pages missing in Root"
+ )
check_format_condition(
isinstance(self.root[b"Pages"], IndirectReference),
"/Pages in Root is not an indirect reference",
)
self.pages_ref = self.root[b"Pages"]
+ assert self.pages_ref is not None
self.page_tree_root = self.read_indirect(self.pages_ref)
self.pages = self.linearize_page_tree(self.page_tree_root)
# save the original list of page references
@@ -575,7 +610,7 @@ class PdfParser:
# and we need to rewrite the pages and their list
self.orig_pages = self.pages[:]
- def next_object_id(self, offset=None):
+ def next_object_id(self, offset: int | None = None) -> IndirectReference:
try:
# TODO: support reuse of deleted objects
reference = IndirectReference(max(self.xref_table.keys()) + 1, 0)
@@ -625,12 +660,13 @@ class PdfParser:
re.DOTALL,
)
- def read_trailer(self):
+ def read_trailer(self) -> None:
+ assert self.buf is not None
search_start_offset = len(self.buf) - 16384
if search_start_offset < self.start_offset:
search_start_offset = self.start_offset
m = self.re_trailer_end.search(self.buf, search_start_offset)
- check_format_condition(m, "trailer end not found")
+ check_format_condition(m is not None, "trailer end not found")
# make sure we found the LAST trailer
last_match = m
while m:
@@ -638,6 +674,7 @@ class PdfParser:
m = self.re_trailer_end.search(self.buf, m.start() + 16)
if not m:
m = last_match
+ assert m is not None
trailer_data = m.group(1)
self.last_xref_section_offset = int(m.group(2))
self.trailer_dict = self.interpret_trailer(trailer_data)
@@ -646,12 +683,14 @@ class PdfParser:
if b"Prev" in self.trailer_dict:
self.read_prev_trailer(self.trailer_dict[b"Prev"])
- def read_prev_trailer(self, xref_section_offset):
+ def read_prev_trailer(self, xref_section_offset: int) -> None:
+ assert self.buf is not None
trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset)
m = self.re_trailer_prev.search(
self.buf[trailer_offset : trailer_offset + 16384]
)
- check_format_condition(m, "previous trailer not found")
+ check_format_condition(m is not None, "previous trailer not found")
+ assert m is not None
trailer_data = m.group(1)
check_format_condition(
int(m.group(2)) == xref_section_offset,
@@ -672,7 +711,7 @@ class PdfParser:
re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional)
@classmethod
- def interpret_trailer(cls, trailer_data):
+ def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]:
trailer = {}
offset = 0
while True:
@@ -680,14 +719,18 @@ class PdfParser:
if not m:
m = cls.re_dict_end.match(trailer_data, offset)
check_format_condition(
- m and m.end() == len(trailer_data),
+ m is not None and m.end() == len(trailer_data),
"name not found in trailer, remaining data: "
+ repr(trailer_data[offset:]),
)
break
key = cls.interpret_name(m.group(1))
- value, offset = cls.get_value(trailer_data, m.end())
+ assert isinstance(key, bytes)
+ value, value_offset = cls.get_value(trailer_data, m.end())
trailer[key] = value
+ if value_offset is None:
+ break
+ offset = value_offset
check_format_condition(
b"Size" in trailer and isinstance(trailer[b"Size"], int),
"/Size not in trailer or not an integer",
@@ -701,7 +744,7 @@ class PdfParser:
re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?")
@classmethod
- def interpret_name(cls, raw, as_text=False):
+ def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes:
name = b""
for m in cls.re_hashes_in_name.finditer(raw):
if m.group(3):
@@ -763,7 +806,13 @@ class PdfParser:
)
@classmethod
- def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1):
+ def get_value(
+ cls,
+ data: bytes | bytearray | mmap.mmap,
+ offset: int,
+ expect_indirect: IndirectReference | None = None,
+ max_nesting: int = -1,
+ ) -> tuple[Any, int | None]:
if max_nesting == 0:
return None, None
m = cls.re_comment.match(data, offset)
@@ -785,11 +834,16 @@ class PdfParser:
== IndirectReference(int(m.group(1)), int(m.group(2))),
"indirect object definition different than expected",
)
- object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1)
- if offset is None:
+ object, object_offset = cls.get_value(
+ data, m.end(), max_nesting=max_nesting - 1
+ )
+ if object_offset is None:
return object, None
- m = cls.re_indirect_def_end.match(data, offset)
- check_format_condition(m, "indirect object definition end not found")
+ m = cls.re_indirect_def_end.match(data, object_offset)
+ check_format_condition(
+ m is not None, "indirect object definition end not found"
+ )
+ assert m is not None
return object, m.end()
check_format_condition(
not expect_indirect, "indirect object definition not found"
@@ -808,46 +862,53 @@ class PdfParser:
m = cls.re_dict_start.match(data, offset)
if m:
offset = m.end()
- result = {}
+ result: dict[Any, Any] = {}
m = cls.re_dict_end.match(data, offset)
+ current_offset: int | None = offset
while not m:
- key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
- if offset is None:
+ assert current_offset is not None
+ key, current_offset = cls.get_value(
+ data, current_offset, max_nesting=max_nesting - 1
+ )
+ if current_offset is None:
return result, None
- value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
+ value, current_offset = cls.get_value(
+ data, current_offset, max_nesting=max_nesting - 1
+ )
result[key] = value
- if offset is None:
+ if current_offset is None:
return result, None
- m = cls.re_dict_end.match(data, offset)
- offset = m.end()
- m = cls.re_stream_start.match(data, offset)
+ m = cls.re_dict_end.match(data, current_offset)
+ current_offset = m.end()
+ m = cls.re_stream_start.match(data, current_offset)
if m:
- try:
- stream_len_str = result.get(b"Length")
- stream_len = int(stream_len_str)
- except (TypeError, ValueError) as e:
- msg = f"bad or missing Length in stream dict ({stream_len_str})"
- raise PdfFormatError(msg) from e
+ stream_len = result.get(b"Length")
+ if stream_len is None or not isinstance(stream_len, int):
+ msg = f"bad or missing Length in stream dict ({stream_len})"
+ raise PdfFormatError(msg)
stream_data = data[m.end() : m.end() + stream_len]
m = cls.re_stream_end.match(data, m.end() + stream_len)
- check_format_condition(m, "stream end not found")
- offset = m.end()
- result = PdfStream(PdfDict(result), stream_data)
- else:
- result = PdfDict(result)
- return result, offset
+ check_format_condition(m is not None, "stream end not found")
+ assert m is not None
+ current_offset = m.end()
+ return PdfStream(PdfDict(result), stream_data), current_offset
+ return PdfDict(result), current_offset
m = cls.re_array_start.match(data, offset)
if m:
offset = m.end()
- result = []
+ results = []
m = cls.re_array_end.match(data, offset)
+ current_offset = offset
while not m:
- value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
- result.append(value)
- if offset is None:
- return result, None
- m = cls.re_array_end.match(data, offset)
- return result, m.end()
+ assert current_offset is not None
+ value, current_offset = cls.get_value(
+ data, current_offset, max_nesting=max_nesting - 1
+ )
+ results.append(value)
+ if current_offset is None:
+ return results, None
+ m = cls.re_array_end.match(data, current_offset)
+ return results, m.end()
m = cls.re_null.match(data, offset)
if m:
return None, m.end()
@@ -907,7 +968,9 @@ class PdfParser:
}
@classmethod
- def get_literal_string(cls, data, offset):
+ def get_literal_string(
+ cls, data: bytes | bytearray | mmap.mmap, offset: int
+ ) -> tuple[bytes, int]:
nesting_depth = 0
result = bytearray()
for m in cls.re_lit_str_token.finditer(data, offset):
@@ -943,12 +1006,14 @@ class PdfParser:
)
re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
- def read_xref_table(self, xref_section_offset):
+ def read_xref_table(self, xref_section_offset: int) -> int:
+ assert self.buf is not None
subsection_found = False
m = self.re_xref_section_start.match(
self.buf, xref_section_offset + self.start_offset
)
- check_format_condition(m, "xref section start not found")
+ check_format_condition(m is not None, "xref section start not found")
+ assert m is not None
offset = m.end()
while True:
m = self.re_xref_subsection_start.match(self.buf, offset)
@@ -963,7 +1028,8 @@ class PdfParser:
num_objects = int(m.group(2))
for i in range(first_object, first_object + num_objects):
m = self.re_xref_entry.match(self.buf, offset)
- check_format_condition(m, "xref entry not found")
+ check_format_condition(m is not None, "xref entry not found")
+ assert m is not None
offset = m.end()
is_free = m.group(3) == b"f"
if not is_free:
@@ -973,13 +1039,14 @@ class PdfParser:
self.xref_table[i] = new_entry
return offset
- def read_indirect(self, ref, max_nesting=-1):
+ def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any:
offset, generation = self.xref_table[ref[0]]
check_format_condition(
generation == ref[1],
f"expected to find generation {ref[1]} for object ID {ref[0]} in xref "
f"table, instead found generation {generation} at offset {offset}",
)
+ assert self.buf is not None
value = self.get_value(
self.buf,
offset + self.start_offset,
@@ -989,14 +1056,15 @@ class PdfParser:
self.cached_objects[ref] = value
return value
- def linearize_page_tree(self, node=None):
- if node is None:
- node = self.page_tree_root
+ def linearize_page_tree(
+ self, node: PdfDict | None = None
+ ) -> list[IndirectReference]:
+ page_node = node if node is not None else self.page_tree_root
check_format_condition(
- node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
+ page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
)
pages = []
- for kid in node[b"Kids"]:
+ for kid in page_node[b"Kids"]:
kid_object = self.read_indirect(kid)
if kid_object[b"Type"] == b"Page":
pages.append(kid)
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 0d5751962..fa117d19a 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -39,7 +39,7 @@ import struct
import warnings
import zlib
from enum import IntEnum
-from typing import IO
+from typing import IO, TYPE_CHECKING, Any, NoReturn
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16
@@ -48,6 +48,9 @@ from ._binary import o8
from ._binary import o16be as o16
from ._binary import o32be as o32
+if TYPE_CHECKING:
+ from . import _imaging
+
logger = logging.getLogger(__name__)
is_cid = re.compile(rb"\w\w\w\w").match
@@ -178,7 +181,7 @@ class ChunkStream:
def __enter__(self) -> ChunkStream:
return self
- def __exit__(self, *args):
+ def __exit__(self, *args: object) -> None:
self.close()
def close(self) -> None:
@@ -227,6 +230,7 @@ class ChunkStream:
cids = []
+ assert self.fp is not None
while True:
try:
cid, pos, length = self.read()
@@ -249,6 +253,9 @@ class iTXt(str):
"""
+ lang: str | bytes | None
+ tkey: str | bytes | None
+
@staticmethod
def __new__(cls, text, lang=None, tkey=None):
"""
@@ -270,10 +277,10 @@ class PngInfo:
"""
- def __init__(self):
- self.chunks = []
+ def __init__(self) -> None:
+ 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.
:param cid: a byte string, 4 bytes long.
@@ -283,12 +290,16 @@ class PngInfo:
"""
- chunk = [cid, data]
- if after_idat:
- chunk.append(True)
- self.chunks.append(tuple(chunk))
+ self.chunks.append((cid, data, after_idat))
- 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.
:param key: latin-1 encodable text key name
@@ -316,7 +327,9 @@ class PngInfo:
else:
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.
:param key: latin-1 encodable text key name
@@ -326,7 +339,13 @@ class PngInfo:
"""
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
if not isinstance(value, bytes):
@@ -389,6 +408,7 @@ class PngStream(ChunkStream):
def chunk_iCCP(self, pos: int, length: int) -> bytes:
# ICC profile
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
# according to PNG spec, the iCCP chunk contains:
# Profile name 1-79 bytes (character string)
@@ -416,6 +436,7 @@ class PngStream(ChunkStream):
def chunk_IHDR(self, pos: int, length: int) -> bytes:
# image header
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 13:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@@ -434,7 +455,7 @@ class PngStream(ChunkStream):
raise SyntaxError(msg)
return s
- def chunk_IDAT(self, pos, length):
+ def chunk_IDAT(self, pos: int, length: int) -> NoReturn:
# image data
if "bbox" in self.im_info:
tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)]
@@ -447,12 +468,13 @@ class PngStream(ChunkStream):
msg = "image data found"
raise EOFError(msg)
- def chunk_IEND(self, pos, length):
+ def chunk_IEND(self, pos: int, length: int) -> NoReturn:
msg = "end of PNG image"
raise EOFError(msg)
def chunk_PLTE(self, pos: int, length: int) -> bytes:
# palette
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
self.im_palette = "RGB", s
@@ -460,6 +482,7 @@ class PngStream(ChunkStream):
def chunk_tRNS(self, pos: int, length: int) -> bytes:
# transparency
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
if _simple_palette.match(s):
@@ -480,6 +503,7 @@ class PngStream(ChunkStream):
def chunk_gAMA(self, pos: int, length: int) -> bytes:
# gamma setting
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
self.im_info["gamma"] = i32(s) / 100000.0
return s
@@ -488,6 +512,7 @@ class PngStream(ChunkStream):
# chromaticity, 8 unsigned ints, actual value is scaled by 100,000
# WP x,y, Red x,y, Green x,y Blue x,y
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
raw_vals = struct.unpack(">%dI" % (len(s) // 4), s)
self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
@@ -500,6 +525,7 @@ class PngStream(ChunkStream):
# 2 saturation
# 3 absolute colorimetric
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 1:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@@ -511,6 +537,7 @@ class PngStream(ChunkStream):
def chunk_pHYs(self, pos: int, length: int) -> bytes:
# pixels per unit
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 9:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@@ -528,6 +555,7 @@ class PngStream(ChunkStream):
def chunk_tEXt(self, pos: int, length: int) -> bytes:
# text
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
try:
k, v = s.split(b"\0", 1)
@@ -536,17 +564,18 @@ class PngStream(ChunkStream):
k = s
v = b""
if k:
- k = k.decode("latin-1", "strict")
+ k_str = k.decode("latin-1", "strict")
v_str = v.decode("latin-1", "replace")
- self.im_info[k] = v if k == "exif" else v_str
- self.im_text[k] = v_str
+ self.im_info[k_str] = v if k == b"exif" else v_str
+ self.im_text[k_str] = v_str
self.check_text_memory(len(v_str))
return s
def chunk_zTXt(self, pos: int, length: int) -> bytes:
# compressed text
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
try:
k, v = s.split(b"\0", 1)
@@ -571,16 +600,17 @@ class PngStream(ChunkStream):
v = b""
if k:
- k = k.decode("latin-1", "strict")
- v = v.decode("latin-1", "replace")
+ k_str = k.decode("latin-1", "strict")
+ v_str = v.decode("latin-1", "replace")
- self.im_info[k] = self.im_text[k] = v
- self.check_text_memory(len(v))
+ self.im_info[k_str] = self.im_text[k_str] = v_str
+ self.check_text_memory(len(v_str))
return s
def chunk_iTXt(self, pos: int, length: int) -> bytes:
# international text
+ assert self.fp is not None
r = s = ImageFile._safe_read(self.fp, length)
try:
k, r = r.split(b"\0", 1)
@@ -606,26 +636,30 @@ class PngStream(ChunkStream):
return s
else:
return s
+ if k == b"XML:com.adobe.xmp":
+ self.im_info["xmp"] = v
try:
- k = k.decode("latin-1", "strict")
- lang = lang.decode("utf-8", "strict")
- tk = tk.decode("utf-8", "strict")
- v = v.decode("utf-8", "strict")
+ k_str = k.decode("latin-1", "strict")
+ lang_str = lang.decode("utf-8", "strict")
+ tk_str = tk.decode("utf-8", "strict")
+ v_str = v.decode("utf-8", "strict")
except UnicodeError:
return s
- self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)
- self.check_text_memory(len(v))
+ self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str)
+ self.check_text_memory(len(v_str))
return s
def chunk_eXIf(self, pos: int, length: int) -> bytes:
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
self.im_info["exif"] = b"Exif\x00\x00" + s
return s
# APNG chunks
def chunk_acTL(self, pos: int, length: int) -> bytes:
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 8:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@@ -646,6 +680,7 @@ class PngStream(ChunkStream):
return s
def chunk_fcTL(self, pos: int, length: int) -> bytes:
+ assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 26:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@@ -675,6 +710,7 @@ class PngStream(ChunkStream):
return s
def chunk_fdAT(self, pos: int, length: int) -> bytes:
+ assert self.fp is not None
if length < 4:
if ImageFile.LOAD_TRUNCATED_IMAGES:
s = ImageFile._safe_read(self.fp, length)
@@ -821,15 +857,16 @@ class PngImageFile(ImageFile.ImageFile):
msg = "no more images in APNG file"
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 rewind:
self._fp.seek(self.__rewind)
self.png.rewind()
self.__prepare_idat = self.__rewind_idat
self.im = None
- if self.pyaccess:
- self.pyaccess = None
self.info = self.png.im_info
self.tile = self.png.im_tile
self.fp = self._fp
@@ -906,14 +943,14 @@ class PngImageFile(ImageFile.ImageFile):
if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
self.dispose_op = Disposal.OP_BACKGROUND
+ self.dispose = None
if self.dispose_op == Disposal.OP_PREVIOUS:
- self.dispose = self._prev_im.copy()
- self.dispose = self._crop(self.dispose, self.dispose_extent)
+ if self._prev_im:
+ self.dispose = self._prev_im.copy()
+ self.dispose = self._crop(self.dispose, self.dispose_extent)
elif self.dispose_op == Disposal.OP_BACKGROUND:
self.dispose = Image.core.fill(self.mode, self.size)
self.dispose = self._crop(self.dispose, self.dispose_extent)
- else:
- self.dispose = None
def tell(self) -> int:
return self.__frame
@@ -1016,35 +1053,20 @@ class PngImageFile(ImageFile.ImageFile):
mask = updated.convert("RGBA")
self._prev_im.paste(updated, self.dispose_extent, mask)
self.im = self._prev_im
- if self.pyaccess:
- self.pyaccess = None
- def _getexif(self):
+ def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info:
self.load()
if "exif" not in self.info and "Raw profile type exif" not in self.info:
return None
return self.getexif()._get_merged_dict()
- def getexif(self):
+ def getexif(self) -> Image.Exif:
if "exif" not in self.info:
self.load()
return super().getexif()
- def getxmp(self):
- """
- Returns a dictionary containing the XMP tags.
- Requires defusedxml to be installed.
-
- :returns: XMP tags in a dictionary.
- """
- return (
- self._getxmp(self.info["XML:com.adobe.xmp"])
- if "XML:com.adobe.xmp" in self.info
- else {}
- )
-
# --------------------------------------------------------------------
# PNG writer
@@ -1104,8 +1126,8 @@ class _fdat:
self.seq_num += 1
-def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):
- duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
+def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_images):
+ duration = im.encoderinfo.get("duration")
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
@@ -1119,13 +1141,15 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
frame_count = 0
for im_seq in chain:
for im_frame in ImageSequence.Iterator(im_seq):
- if im_frame.mode == rawmode:
+ if im_frame.mode == mode:
im_frame = im_frame.copy()
else:
- im_frame = im_frame.convert(rawmode)
+ im_frame = im_frame.convert(mode)
encoderinfo = im.encoderinfo.copy()
if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count]
+ elif duration is None and "duration" in im_frame.info:
+ encoderinfo["duration"] = im_frame.info["duration"]
if isinstance(disposal, (list, tuple)):
encoderinfo["disposal"] = disposal[frame_count]
if isinstance(blend, (list, tuple)):
@@ -1160,15 +1184,12 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
not bbox
and prev_disposal == encoderinfo.get("disposal")
and prev_blend == encoderinfo.get("blend")
+ and "duration" in encoderinfo
):
- previous["encoderinfo"]["duration"] += encoderinfo.get(
- "duration", duration
- )
+ previous["encoderinfo"]["duration"] += encoderinfo["duration"]
continue
else:
bbox = None
- if "duration" not in encoderinfo:
- encoderinfo["duration"] = duration
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
if len(im_frames) == 1 and not default_image:
@@ -1184,8 +1205,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
# default image IDAT (if it exists)
if default_image:
- if im.mode != rawmode:
- im = im.convert(rawmode)
+ if im.mode != mode:
+ im = im.convert(mode)
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
seq_num = 0
@@ -1198,7 +1219,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
im_frame = im_frame.crop(bbox)
size = im_frame.size
encoderinfo = frame_data["encoderinfo"]
- frame_duration = int(round(encoderinfo["duration"]))
+ frame_duration = int(round(encoderinfo.get("duration", 0)))
frame_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend)
# frame control
@@ -1234,7 +1255,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
seq_num = fdat_chunks.seq_num
-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)
@@ -1262,6 +1283,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
size = im.size
mode = im.mode
+ outmode = mode
if mode == "P":
#
# attempt to minimize storage requirements for palette images
@@ -1282,7 +1304,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
bits = 2
else:
bits = 4
- mode = f"{mode};{bits}"
+ outmode += f";{bits}"
# encoder options
im.encoderconfig = (
@@ -1294,7 +1316,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
# get the corresponding PNG mode
try:
- rawmode, bit_depth, color_type = _OUTMODES[mode]
+ rawmode, bit_depth, color_type = _OUTMODES[outmode]
except KeyError as e:
msg = f"cannot write mode {mode} as PNG"
raise OSError(msg) from e
@@ -1346,7 +1368,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, cid, data)
elif cid[1:2].islower():
# Private chunk
- after_idat = info_chunk[2:3]
+ after_idat = len(info_chunk) == 3 and info_chunk[2]
if not after_idat:
chunk(fp, cid, data)
@@ -1415,7 +1437,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
if save_all:
im = _write_multiple_frames(
- im, fp, chunk, rawmode, default_image, append_images
+ im, fp, chunk, mode, rawmode, default_image, append_images
)
if im:
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
@@ -1425,7 +1447,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
cid, data = info_chunk[:2]
if cid[1:2].islower():
# Private chunk
- after_idat = info_chunk[2:3]
+ after_idat = len(info_chunk) == 3 and info_chunk[2]
if after_idat:
chunk(fp, cid, data)
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 94bf430b8..16c9ccbba 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -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":
rawmode, head = "1;I", b"P4"
elif im.mode == "L":
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index 86c1a6763..31dfd4d12 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import io
+from functools import cached_property
from . import Image, ImageFile, ImagePalette
from ._binary import i8
@@ -118,18 +119,17 @@ class PsdImageFile(ImageFile.ImageFile):
#
# layer and mask information
- self.layers = []
+ self._layers_position = None
size = i32(read(4))
if size:
end = self.fp.tell() + size
size = i32(read(4))
if size:
- _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size))
- self.layers = _layerinfo(_layer_data, size)
+ self._layers_position = self.fp.tell()
+ self._layers_size = size
self.fp.seek(end)
- self.n_frames = len(self.layers)
- self.is_animated = self.n_frames > 1
+ self._n_frames: int | None = None
#
# image descriptor
@@ -141,6 +141,26 @@ class PsdImageFile(ImageFile.ImageFile):
self.frame = 1
self._min_frame = 1
+ @cached_property
+ def layers(self):
+ layers = []
+ if self._layers_position is not None:
+ self._fp.seek(self._layers_position)
+ _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size))
+ layers = _layerinfo(_layer_data, self._layers_size)
+ self._n_frames = len(layers)
+ return layers
+
+ @property
+ def n_frames(self) -> int:
+ if self._n_frames is None:
+ self._n_frames = len(self.layers)
+ return self._n_frames
+
+ @property
+ def is_animated(self) -> bool:
+ return len(self.layers) > 1
+
def seek(self, layer: int) -> None:
if not self._seek_check(layer):
return
@@ -165,7 +185,7 @@ def _layerinfo(fp, ct_bytes):
# read layerinfo block
layers = []
- def read(size):
+ def read(size: int) -> bytes:
return ImageFile._safe_read(fp, size)
ct = si16(read(2))
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
deleted file mode 100644
index a9da90613..000000000
--- a/src/PIL/PyAccess.py
+++ /dev/null
@@ -1,365 +0,0 @@
-#
-# The Python Imaging Library
-# Pillow fork
-#
-# Python implementation of the PixelAccess Object
-#
-# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved.
-# Copyright (c) 1995-2009 by Fredrik Lundh.
-# Copyright (c) 2013 Eric Soroos
-#
-# See the README file for information on usage and redistribution
-#
-
-# Notes:
-#
-# * Implements the pixel access object following Access.c
-# * Taking only the tuple form, which is used from python.
-# * Fill.c uses the integer form, but it's still going to use the old
-# Access.c implementation.
-#
-from __future__ import annotations
-
-import logging
-import sys
-
-from ._deprecate import deprecate
-
-FFI: type
-try:
- from cffi import FFI
-
- defs = """
- struct Pixel_RGBA {
- unsigned char r,g,b,a;
- };
- struct Pixel_I16 {
- unsigned char l,r;
- };
- """
- ffi = FFI()
- ffi.cdef(defs)
-except ImportError as ex:
- # Allow error import for doc purposes, but error out when accessing
- # anything in core.
- from ._util import DeferredError
-
- FFI = ffi = DeferredError.new(ex)
-
-logger = logging.getLogger(__name__)
-
-
-class PyAccess:
- def __init__(self, img, readonly=False):
- deprecate("PyAccess", 11)
- vals = dict(img.im.unsafe_ptrs)
- self.readonly = readonly
- self.image8 = ffi.cast("unsigned char **", vals["image8"])
- self.image32 = ffi.cast("int **", vals["image32"])
- self.image = ffi.cast("unsigned char **", vals["image"])
- self.xsize, self.ysize = img.im.size
- self._img = img
-
- # Keep pointer to im object to prevent dereferencing.
- self._im = img.im
- if self._im.mode in ("P", "PA"):
- self._palette = img.palette
-
- # Debugging is polluting test traces, only useful here
- # when hacking on PyAccess
- # logger.debug("%s", vals)
- self._post_init()
-
- def _post_init(self) -> None:
- pass
-
- def __setitem__(self, xy, color):
- """
- Modifies the pixel at x,y. The color is given as a single
- numerical value for single band images, and a tuple for
- multi-band images
-
- :param xy: The pixel coordinate, given as (x, y). See
- :ref:`coordinate-system`.
- :param color: The pixel value.
- """
- if self.readonly:
- msg = "Attempt to putpixel a read only image"
- raise ValueError(msg)
- (x, y) = xy
- if x < 0:
- x = self.xsize + x
- if y < 0:
- y = self.ysize + y
- (x, y) = self.check_xy((x, y))
-
- if (
- self._im.mode in ("P", "PA")
- and isinstance(color, (list, tuple))
- and len(color) in [3, 4]
- ):
- # RGB or RGBA value for a P or PA image
- if self._im.mode == "PA":
- alpha = color[3] if len(color) == 4 else 255
- color = color[:3]
- color = self._palette.getcolor(color, self._img)
- if self._im.mode == "PA":
- color = (color, alpha)
-
- return self.set_pixel(x, y, color)
-
- def __getitem__(self, xy):
- """
- Returns the pixel at x,y. The pixel is returned as a single
- value for single band images or a tuple for multiple band
- images
-
- :param xy: The pixel coordinate, given as (x, y). See
- :ref:`coordinate-system`.
- :returns: a pixel value for single band images, a tuple of
- pixel values for multiband images.
- """
- (x, y) = xy
- if x < 0:
- x = self.xsize + x
- if y < 0:
- y = self.ysize + y
- (x, y) = self.check_xy((x, y))
- return self.get_pixel(x, y)
-
- putpixel = __setitem__
- getpixel = __getitem__
-
- def check_xy(self, xy):
- (x, y) = xy
- if not (0 <= x < self.xsize and 0 <= y < self.ysize):
- msg = "pixel location out of range"
- raise ValueError(msg)
- return xy
-
-
-class _PyAccess32_2(PyAccess):
- """PA, LA, stored in first and last bytes of a 32 bit word"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
-
- def get_pixel(self, x, y):
- pixel = self.pixels[y][x]
- return pixel.r, pixel.a
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- # tuple
- pixel.r = min(color[0], 255)
- pixel.a = min(color[1], 255)
-
-
-class _PyAccess32_3(PyAccess):
- """RGB and friends, stored in the first three bytes of a 32 bit word"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
-
- def get_pixel(self, x, y):
- pixel = self.pixels[y][x]
- return pixel.r, pixel.g, pixel.b
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- # tuple
- pixel.r = min(color[0], 255)
- pixel.g = min(color[1], 255)
- pixel.b = min(color[2], 255)
- pixel.a = 255
-
-
-class _PyAccess32_4(PyAccess):
- """RGBA etc, all 4 bytes of a 32 bit word"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
-
- def get_pixel(self, x, y):
- pixel = self.pixels[y][x]
- return pixel.r, pixel.g, pixel.b, pixel.a
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- # tuple
- pixel.r = min(color[0], 255)
- pixel.g = min(color[1], 255)
- pixel.b = min(color[2], 255)
- pixel.a = min(color[3], 255)
-
-
-class _PyAccess8(PyAccess):
- """1, L, P, 8 bit images stored as uint8"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = self.image8
-
- def get_pixel(self, x, y):
- return self.pixels[y][x]
-
- def set_pixel(self, x, y, color):
- try:
- # integer
- self.pixels[y][x] = min(color, 255)
- except TypeError:
- # tuple
- self.pixels[y][x] = min(color[0], 255)
-
-
-class _PyAccessI16_N(PyAccess):
- """I;16 access, native bitendian without conversion"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("unsigned short **", self.image)
-
- def get_pixel(self, x, y):
- return self.pixels[y][x]
-
- def set_pixel(self, x, y, color):
- try:
- # integer
- self.pixels[y][x] = min(color, 65535)
- except TypeError:
- # tuple
- self.pixels[y][x] = min(color[0], 65535)
-
-
-class _PyAccessI16_L(PyAccess):
- """I;16L access, with conversion"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
-
- def get_pixel(self, x, y):
- pixel = self.pixels[y][x]
- return pixel.l + pixel.r * 256
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- try:
- color = min(color, 65535)
- except TypeError:
- color = min(color[0], 65535)
-
- pixel.l = color & 0xFF
- pixel.r = color >> 8
-
-
-class _PyAccessI16_B(PyAccess):
- """I;16B access, with conversion"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
-
- def get_pixel(self, x, y):
- pixel = self.pixels[y][x]
- return pixel.l * 256 + pixel.r
-
- def set_pixel(self, x, y, color):
- pixel = self.pixels[y][x]
- try:
- color = min(color, 65535)
- except Exception:
- color = min(color[0], 65535)
-
- pixel.l = color >> 8
- pixel.r = color & 0xFF
-
-
-class _PyAccessI32_N(PyAccess):
- """Signed Int32 access, native endian"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = self.image32
-
- def get_pixel(self, x, y):
- return self.pixels[y][x]
-
- def set_pixel(self, x, y, color):
- self.pixels[y][x] = color
-
-
-class _PyAccessI32_Swap(PyAccess):
- """I;32L/B access, with byteswapping conversion"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = self.image32
-
- def reverse(self, i):
- orig = ffi.new("int *", i)
- chars = ffi.cast("unsigned char *", orig)
- chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0]
- return ffi.cast("int *", chars)[0]
-
- def get_pixel(self, x, y):
- return self.reverse(self.pixels[y][x])
-
- def set_pixel(self, x, y, color):
- self.pixels[y][x] = self.reverse(color)
-
-
-class _PyAccessF(PyAccess):
- """32 bit float access"""
-
- def _post_init(self, *args, **kwargs):
- self.pixels = ffi.cast("float **", self.image32)
-
- def get_pixel(self, x, y):
- return self.pixels[y][x]
-
- def set_pixel(self, x, y, color):
- try:
- # not a tuple
- self.pixels[y][x] = color
- except TypeError:
- # tuple
- self.pixels[y][x] = color[0]
-
-
-mode_map = {
- "1": _PyAccess8,
- "L": _PyAccess8,
- "P": _PyAccess8,
- "I;16N": _PyAccessI16_N,
- "LA": _PyAccess32_2,
- "La": _PyAccess32_2,
- "PA": _PyAccess32_2,
- "RGB": _PyAccess32_3,
- "LAB": _PyAccess32_3,
- "HSV": _PyAccess32_3,
- "YCbCr": _PyAccess32_3,
- "RGBA": _PyAccess32_4,
- "RGBa": _PyAccess32_4,
- "RGBX": _PyAccess32_4,
- "CMYK": _PyAccess32_4,
- "F": _PyAccessF,
- "I": _PyAccessI32_N,
-}
-
-if sys.byteorder == "little":
- mode_map["I;16"] = _PyAccessI16_N
- mode_map["I;16L"] = _PyAccessI16_N
- mode_map["I;16B"] = _PyAccessI16_B
-
- mode_map["I;32L"] = _PyAccessI32_N
- mode_map["I;32B"] = _PyAccessI32_Swap
-else:
- mode_map["I;16"] = _PyAccessI16_L
- mode_map["I;16L"] = _PyAccessI16_L
- mode_map["I;16B"] = _PyAccessI16_N
-
- mode_map["I;32L"] = _PyAccessI32_Swap
- mode_map["I;32B"] = _PyAccessI32_N
-
-
-def new(img, readonly=False):
- access_type = mode_map.get(img.mode, None)
- if not access_type:
- logger.debug("PyAccess Not Implemented: %s", img.mode)
- return None
- return access_type(img, readonly)
diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py
index cea8b60da..202ef52d0 100644
--- a/src/PIL/QoiImagePlugin.py
+++ b/src/PIL/QoiImagePlugin.py
@@ -37,17 +37,20 @@ class QoiImageFile(ImageFile.ImageFile):
class QoiDecoder(ImageFile.PyDecoder):
_pulls_fd = True
+ _previous_pixel: bytes | bytearray | None = None
+ _previously_seen_pixels: dict[int, bytes | bytearray] = {}
- def _add_to_previous_pixels(self, value):
+ def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
self._previous_pixel = value
r, g, b, a = value
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
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._previous_pixel = None
self._add_to_previous_pixels(bytearray((0, 0, 0, 255)))
data = bytearray()
@@ -55,7 +58,8 @@ class QoiDecoder(ImageFile.PyDecoder):
dest_length = self.state.xsize * self.state.ysize * bands
while len(data) < dest_length:
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:]
elif byte == 0b11111111: # QOI_OP_RGBA
value = self.fd.read(4)
@@ -66,7 +70,7 @@ class QoiDecoder(ImageFile.PyDecoder):
value = self._previously_seen_pixels.get(
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(
(
(self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
@@ -77,7 +81,7 @@ class QoiDecoder(ImageFile.PyDecoder):
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]
diff_green = (byte & 0b00111111) - 32
diff_red = ((second_byte & 0b11110000) >> 4) - 8
@@ -90,7 +94,7 @@ class QoiDecoder(ImageFile.PyDecoder):
)
)
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
value = self._previous_pixel
if bands == 3:
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index 7bd84ebd4..50d979109 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -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"}:
msg = "Unsupported SGI image mode"
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)
pinmax = 255
# Image name (79 characters max, truncated below in write)
- filename = os.path.basename(filename)
- img_name = os.path.splitext(filename)[0].encode("ascii", "ignore")
+ img_name = os.path.splitext(os.path.basename(filename))[0]
+ if isinstance(img_name, str):
+ img_name = img_name.encode("ascii", "ignore")
# Standard representation of pixel in the file
colormap = 0
fp.write(struct.pack(">h", magic_number))
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 5b8ad47f0..a07101e54 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -37,12 +37,12 @@ from __future__ import annotations
import os
import struct
import sys
-from typing import TYPE_CHECKING
+from typing import IO, TYPE_CHECKING, Any, cast
from . import Image, ImageFile
-def isInt(f):
+def isInt(f: Any) -> int:
try:
i = int(f)
if f - i == 0:
@@ -62,7 +62,7 @@ iforms = [1, 3, -11, -12, -21, -22]
# otherwise returns 0
-def isSpiderHeader(t):
+def isSpiderHeader(t: tuple[float, ...]) -> int:
h = (99,) + t # add 1 value so can use spider header index start=1
# header values 1,2,5,12,13,22,23 should be integers
for i in [1, 2, 5, 12, 13, 22, 23]:
@@ -82,7 +82,7 @@ def isSpiderHeader(t):
return labbyt
-def isSpiderImage(filename):
+def isSpiderImage(filename: str) -> int:
with open(filename, "rb") as fp:
f = fp.read(92) # read 23 * 4 bytes
t = struct.unpack(">23f", f) # try big-endian first
@@ -184,13 +184,15 @@ class SpiderImageFile(ImageFile.ImageFile):
self._open()
# returns a byte image after rescaling to 0..255
- def convert2byte(self, depth=255):
- (minimum, maximum) = self.getextrema()
- m = 1
+ def convert2byte(self, depth: int = 255) -> Image.Image:
+ extrema = self.getextrema()
+ assert isinstance(extrema[0], float)
+ minimum, maximum = cast(tuple[float, float], extrema)
+ m: float = 1
if maximum != minimum:
m = depth / (maximum - minimum)
b = -m * minimum
- return self.point(lambda i, m=m, b=b: i * m + b).convert("L")
+ return self.point(lambda i: i * m + b).convert("L")
if TYPE_CHECKING:
from . import ImageTk
@@ -207,10 +209,10 @@ class SpiderImageFile(ImageFile.ImageFile):
# given a list of filenames, return a list of images
-def loadImageSeries(filelist=None):
+def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None:
"""create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
if filelist is None or len(filelist) < 1:
- return
+ return None
imglist = []
for img in filelist:
@@ -233,7 +235,7 @@ def loadImageSeries(filelist=None):
# For saving images in Spider format
-def makeSpiderHeader(im):
+def makeSpiderHeader(im: Image.Image) -> list[bytes]:
nsam, nrow = im.size
lenbyt = nsam * 4 # There are labrec records in the header
labrec = int(1024 / lenbyt)
@@ -263,7 +265,7 @@ def makeSpiderHeader(im):
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":
im = im.convert("F")
@@ -279,9 +281,10 @@ def _save(im, fp, filename):
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
- 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)
_save(im, fp, filename)
diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py
index 7470663b4..cba26d4b0 100644
--- a/src/PIL/TarIO.py
+++ b/src/PIL/TarIO.py
@@ -16,7 +16,6 @@
from __future__ import annotations
import io
-from types import TracebackType
from . import ContainerIO
@@ -61,12 +60,7 @@ class TarIO(ContainerIO.ContainerIO[bytes]):
def __enter__(self) -> TarIO:
return self
- def __exit__(
- self,
- exc_type: type[BaseException] | None,
- exc_val: BaseException | None,
- exc_tb: TracebackType | None,
- ) -> None:
+ def __exit__(self, *args: object) -> None:
self.close()
def close(self) -> None:
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 401a83f9f..39104aece 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -36,7 +36,7 @@ MODES = {
(3, 1): "1",
(3, 8): "L",
(3, 16): "LA",
- (2, 16): "BGR;5",
+ (2, 16): "BGRA;15Z",
(2, 24): "BGR",
(2, 32): "BGRA",
}
@@ -87,9 +87,7 @@ class TgaImageFile(ImageFile.ImageFile):
elif imagetype in (1, 9):
self._mode = "P" if colormaptype else "L"
elif imagetype in (2, 10):
- self._mode = "RGB"
- if depth == 32:
- self._mode = "RGBA"
+ self._mode = "RGB" if depth == 24 else "RGBA"
else:
msg = "unknown TGA mode"
raise SyntaxError(msg)
@@ -118,15 +116,16 @@ class TgaImageFile(ImageFile.ImageFile):
start, size, mapdepth = i16(s, 3), i16(s, 5), s[7]
if mapdepth == 16:
self.palette = ImagePalette.raw(
- "BGR;15", b"\0" * 2 * start + self.fp.read(2 * size)
+ "BGRA;15Z", bytes(2 * start) + self.fp.read(2 * size)
)
+ self.palette.mode = "RGBA"
elif mapdepth == 24:
self.palette = ImagePalette.raw(
- "BGR", b"\0" * 3 * start + self.fp.read(3 * size)
+ "BGR", bytes(3 * start) + self.fp.read(3 * size)
)
elif mapdepth == 32:
self.palette = ImagePalette.raw(
- "BGRA", b"\0" * 4 * start + self.fp.read(4 * size)
+ "BGRA", bytes(4 * start) + self.fp.read(4 * size)
)
else:
msg = "unknown TGA map depth"
@@ -178,7 +177,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:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 35cf4ef7e..8f7898cbc 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -50,7 +50,7 @@ import warnings
from collections.abc import MutableMapping
from fractions import Fraction
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 ._binary import i16be as i16
@@ -240,12 +240,12 @@ OPEN_INFO = {
(MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
(II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
(MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
- (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
- (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
- (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
(II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
(MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
(MM, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
@@ -267,8 +267,8 @@ OPEN_INFO = {
(MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
- (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"),
- (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"),
+ (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"),
+ (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
(II, 2, (1, 1, 1, 1), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
@@ -430,10 +430,10 @@ class IFDRational(Rational):
def __repr__(self) -> str:
return str(float(self._val))
- def __hash__(self):
+ def __hash__(self) -> int:
return self._val.__hash__()
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
val = self._val
if isinstance(other, IFDRational):
other = other._val
@@ -597,7 +597,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
_load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], 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.
To construct an ImageFileDirectory from a real file, pass the 8-byte
@@ -621,7 +626,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise SyntaxError(msg)
self._bigtiff = ifh[2] == 43
self.group = group
- self.tagtype = {}
+ self.tagtype: dict[int, int] = {}
""" Dictionary of tag types """
self.reset()
(self.next,) = (
@@ -633,18 +638,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
offset = property(lambda self: self._offset)
@property
- def legacy_api(self):
+ def legacy_api(self) -> bool:
return self._legacy_api
@legacy_api.setter
- def legacy_api(self, value):
+ def legacy_api(self, value: bool) -> NoReturn:
msg = "Not allowing setting of legacy api"
raise Exception(msg)
- def reset(self):
- self._tags_v1 = {} # will remain empty if legacy_api is false
- self._tags_v2 = {} # main tag storage
- self._tagdata = {}
+ def reset(self) -> None:
+ self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false
+ self._tags_v2: dict[int, Any] = {} # main tag storage
+ self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None
self._offset = None
@@ -763,7 +768,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
# Unspec'd, and length > 1
dest[tag] = values
- def __delitem__(self, tag):
+ def __delitem__(self, tag: int) -> None:
self._tags_v2.pop(tag, None)
self._tags_v1.pop(tag, None)
self._tagdata.pop(tag, None)
@@ -1152,7 +1157,7 @@ class TiffImageFile(ImageFile.ImageFile):
super().__init__(fp, filename)
- def _open(self):
+ def _open(self) -> None:
"""Open the first image in a TIFF file"""
# Header
@@ -1169,8 +1174,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.__first = self.__next = self.tag_v2.next
self.__frame = -1
self._fp = self.fp
- self._frame_pos = []
- self._n_frames = None
+ self._frame_pos: list[int] = []
+ self._n_frames: int | None = None
logger.debug("*** TiffImageFile._open ***")
logger.debug("- __first: %s", self.__first)
@@ -1238,6 +1243,10 @@ class TiffImageFile(ImageFile.ImageFile):
self.__frame += 1
self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp)
+ if XMP in self.tag_v2:
+ self.info["xmp"] = self.tag_v2[XMP]
+ elif "xmp" in self.info:
+ del self.info["xmp"]
self._reload_exif()
# fill the legacy tag/ifd entries
self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
@@ -1248,15 +1257,6 @@ class TiffImageFile(ImageFile.ImageFile):
"""Return the current frame number"""
return self.__frame
- def getxmp(self):
- """
- Returns a dictionary containing the XMP tags.
- Requires defusedxml to be installed.
-
- :returns: XMP tags in a dictionary.
- """
- return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}
-
def get_photoshop_blocks(self):
"""
Returns a dictionary of Photoshop "Image Resource Blocks".
@@ -1278,7 +1278,7 @@ class TiffImageFile(ImageFile.ImageFile):
val = val[math.ceil((10 + n + size) / 2) * 2 :]
return blocks
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if self.tile and self.use_load_libtiff:
return self._load_libtiff()
return super().load()
@@ -1699,6 +1699,20 @@ def _save(im, fp, filename):
except Exception:
pass # might not be an IFD. Might not have populated type
+ legacy_ifd = {}
+ if hasattr(im, "tag"):
+ legacy_ifd = im.tag.to_v2()
+
+ supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})}
+ for tag in (
+ # IFD offset that may not be correct in the saved image
+ EXIFIFD,
+ # Determined by the image format and should not be copied from legacy_ifd.
+ SAMPLEFORMAT,
+ ):
+ if tag in supplied_tags:
+ del supplied_tags[tag]
+
# additions written by Greg Couch, gregc@cgl.ucsf.edu
# inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
if hasattr(im, "tag_v2"):
@@ -1712,8 +1726,14 @@ def _save(im, fp, filename):
XMP,
):
if key in im.tag_v2:
- ifd[key] = im.tag_v2[key]
- ifd.tagtype[key] = im.tag_v2.tagtype[key]
+ if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in (
+ TiffTags.BYTE,
+ TiffTags.UNDEFINED,
+ ):
+ del supplied_tags[key]
+ else:
+ ifd[key] = im.tag_v2[key]
+ ifd.tagtype[key] = im.tag_v2.tagtype[key]
# preserve ICC profile (should also work when saving other formats
# which support profiles as TIFF) -- 2008-06-06 Florian Hoech
@@ -1853,16 +1873,6 @@ def _save(im, fp, filename):
# Merge the ones that we have with (optional) more bits from
# the original file, e.g x,y resolution so that we can
# save(load('')) == original file.
- legacy_ifd = {}
- if hasattr(im, "tag"):
- legacy_ifd = im.tag.to_v2()
-
- # SAMPLEFORMAT is determined by the image format and should not be copied
- # from legacy_ifd.
- supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd}
- if SAMPLEFORMAT in supplied_tags:
- del supplied_tags[SAMPLEFORMAT]
-
for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
# Libtiff can only process certain core items without adding
# them to the custom dictionary.
@@ -2036,13 +2046,12 @@ class AppendingTiffWriter:
self.finalize()
self.setup()
- def __enter__(self):
+ def __enter__(self) -> AppendingTiffWriter:
return self
- def __exit__(self, exc_type, exc_value, traceback):
+ def __exit__(self, *args: object) -> None:
if self.close_fp:
self.close()
- return False
def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage
@@ -2064,7 +2073,7 @@ class AppendingTiffWriter:
self.f.write(bytes(pad_bytes))
self.offsetOfNewPage = self.f.tell()
- def setEndian(self, endian):
+ def setEndian(self, endian: str) -> None:
self.endian = endian
self.longFmt = f"{self.endian}L"
self.shortFmt = f"{self.endian}H"
@@ -2081,45 +2090,45 @@ class AppendingTiffWriter:
num_tags = self.readShort()
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)
- def readShort(self):
+ def readShort(self) -> int:
(value,) = struct.unpack(self.shortFmt, self.f.read(2))
return value
- def readLong(self):
+ def readLong(self) -> int:
(value,) = struct.unpack(self.longFmt, self.f.read(4))
return value
- def rewriteLastShortToLong(self, value):
+ def rewriteLastShortToLong(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
- def rewriteLastShort(self, value):
+ def rewriteLastShort(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg)
- def rewriteLastLong(self, value):
+ def rewriteLastLong(self, value: int) -> None:
self.f.seek(-4, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
- def writeShort(self, value):
+ def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg)
- def writeLong(self, value):
+ def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4"
@@ -2138,9 +2147,9 @@ class AppendingTiffWriter:
field_size = self.fieldSizes[field_type]
total_size = field_size * count
is_local = total_size <= 4
+ offset: int | None
if not is_local:
- offset = self.readLong()
- offset += self.offsetOfNewPage
+ offset = self.readLong() + self.offsetOfNewPage
self.rewriteLastLong(offset)
if tag in self.Tags:
@@ -2164,7 +2173,9 @@ class AppendingTiffWriter:
# skip the locally stored value that is not an offset
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:
msg = "offset is neither short nor long"
raise RuntimeError(msg)
@@ -2190,7 +2201,7 @@ class AppendingTiffWriter:
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()
encoderconfig = im.encoderconfig
append_images = list(encoderinfo.get("append_images", []))
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 89fad7033..e318c8739 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -89,7 +89,7 @@ DOUBLE = 12
IFD = 13
LONG8 = 16
-TAGS_V2 = {
+_tags_v2 = {
254: ("NewSubfileType", LONG, 1),
255: ("SubfileType", SHORT, 1),
256: ("ImageWidth", LONG, 1),
@@ -425,9 +425,11 @@ TAGS = {
50784: "Alias Layer Metadata",
}
+TAGS_V2: dict[int, TagInfo] = {}
+
def _populate():
- for k, v in TAGS_V2.items():
+ for k, v in _tags_v2.items():
# Populate legacy structure.
TAGS[k] = v[0]
if len(v) == 4:
diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py
index fbd7be6ed..895d5616a 100644
--- a/src/PIL/WalImageFile.py
+++ b/src/PIL/WalImageFile.py
@@ -50,7 +50,7 @@ class WalImageFile(ImageFile.ImageFile):
if next_name:
self.info["next_name"] = next_name
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if not self.im:
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self.size[0] * self.size[1]))
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index cae124e9f..011de9c6a 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -1,6 +1,7 @@
from __future__ import annotations
from io import BytesIO
+from typing import IO, Any
from . import Image, ImageFile
@@ -95,20 +96,11 @@ class WebPImageFile(ImageFile.ImageFile):
# Initialize seek state
self._reset(reset=False)
- def _getexif(self):
+ def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info:
return None
return self.getexif()._get_merged_dict()
- def getxmp(self):
- """
- Returns a dictionary containing the XMP tags.
- Requires defusedxml to be installed.
-
- :returns: XMP tags in a dictionary.
- """
- return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {}
-
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
@@ -116,14 +108,14 @@ class WebPImageFile(ImageFile.ImageFile):
# Set logical frame to requested position
self.__logical_frame = frame
- def _reset(self, reset=True):
+ def _reset(self, reset: bool = True) -> None:
if reset:
self._decoder.reset()
self.__physical_frame = 0
self.__loaded = -1
self.__timestamp = 0
- def _get_next(self):
+ def _get_next(self) -> tuple[bytes, int, int]:
# Get next frame
ret = self._decoder.get_next()
self.__physical_frame += 1
@@ -152,7 +144,7 @@ class WebPImageFile(ImageFile.ImageFile):
while self.__physical_frame < frame:
self._get_next() # Advance to the requested frame
- def load(self):
+ def load(self) -> Image.core.PixelAccess | None:
if _webp.HAVE_WEBPANIM:
if self.__loaded != self.__logical_frame:
self._seek(self.__logical_frame)
@@ -181,7 +173,7 @@ class WebPImageFile(ImageFile.ImageFile):
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()
append_images = list(encoderinfo.get("append_images", []))
@@ -194,7 +186,7 @@ def _save_all(im, fp, filename):
_save(im, fp, filename)
return
- background = (0, 0, 0, 0)
+ background: int | tuple[int, ...] = (0, 0, 0, 0)
if "background" in encoderinfo:
background = encoderinfo["background"]
elif "background" in im.info:
@@ -324,7 +316,7 @@ def _save_all(im, fp, filename):
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)
quality = im.encoderinfo.get("quality", 80)
alpha_quality = im.encoderinfo.get("alpha_quality", 100)
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index b0328657b..68f8a74f5 100644
--- a/src/PIL/WmfImagePlugin.py
+++ b/src/PIL/WmfImagePlugin.py
@@ -20,6 +20,8 @@
# http://wvware.sourceforge.net/caolan/ora-wmf.html
from __future__ import annotations
+from typing import IO
+
from . import Image, ImageFile
from ._binary import i16le as word
from ._binary import si16le as short
@@ -28,7 +30,7 @@ from ._binary import si32le as _long
_handler = None
-def register_handler(handler):
+def register_handler(handler: ImageFile.StubHandler | None) -> None:
"""
Install application-specific WMF image handler.
@@ -41,12 +43,12 @@ def register_handler(handler):
if hasattr(Image.core, "drawwmf"):
# install default handler (windows only)
- class WmfHandler:
- def open(self, im):
+ class WmfHandler(ImageFile.StubHandler):
+ def open(self, im: ImageFile.StubImageFile) -> None:
im._mode = "RGB"
self.bbox = im.info["wmf_bbox"]
- def load(self, im):
+ def load(self, im: ImageFile.StubImageFile) -> Image.Image:
im.fp.seek(0) # rewind
return Image.frombytes(
"RGB",
@@ -147,10 +149,10 @@ class WmfStubImageFile(ImageFile.StubImageFile):
if loader:
loader.open(self)
- def _load(self):
+ def _load(self) -> ImageFile.StubHandler | None:
return _handler
- def load(self, dpi=None):
+ def load(self, dpi: int | None = None) -> Image.core.PixelAccess | None:
if dpi is not None and self._inch is not None:
self.info["dpi"] = dpi
x0, y0, x1, y1 = self.info["wmf_bbox"]
@@ -161,7 +163,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
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"):
msg = "WMF save handler not installed"
raise OSError(msg)
diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py
index eee727436..6d11bbfcf 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -70,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile):
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":
msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg)
diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py
index 33a0e07b3..83952b397 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -45,8 +45,6 @@ def deprecate(
elif when <= int(__version__.split(".")[0]):
msg = f"{deprecated} {is_} deprecated and should be removed."
raise RuntimeError(msg)
- elif when == 11:
- removed = "Pillow 11 (2024-10-15)"
elif when == 12:
removed = "Pillow 12 (2025-10-15)"
else:
diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi
index e27843e53..8cccd3ac7 100644
--- a/src/PIL/_imaging.pyi
+++ b/src/PIL/_imaging.pyi
@@ -1,3 +1,30 @@
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 __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: ...
+ def __setitem__(
+ self, xy: tuple[int, int], color: float | tuple[int, ...]
+ ) -> None: ...
+
+class ImagingDecoder:
+ def __getattr__(self, name: str) -> Any: ...
+
+class ImagingEncoder:
+ def __getattr__(self, name: str) -> Any: ...
+
+class _Outline:
+ def close(self) -> None: ...
+ def __getattr__(self, name: str) -> Any: ...
+
+def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ...
+def outline() -> _Outline: ...
def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi
index f704047be..2abd6d0f7 100644
--- a/src/PIL/_imagingcms.pyi
+++ b/src/PIL/_imagingcms.pyi
@@ -2,7 +2,7 @@ import datetime
import sys
from typing import Literal, SupportsFloat, TypedDict
-littlecms_version: str
+littlecms_version: str | None
_Tuple3f = tuple[float, float, float]
_Tuple2x3f = tuple[_Tuple3f, _Tuple3f]
diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi
index e27843e53..5e97b40b2 100644
--- a/src/PIL/_imagingft.pyi
+++ b/src/PIL/_imagingft.pyi
@@ -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 | bytes,
+ 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 | bytes, 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: ...
diff --git a/src/PIL/_imagingtk.pyi b/src/PIL/_imagingtk.pyi
new file mode 100644
index 000000000..e27843e53
--- /dev/null
+++ b/src/PIL/_imagingtk.pyi
@@ -0,0 +1,3 @@
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py
index 7075e8672..b6bb8d89a 100644
--- a/src/PIL/_typing.py
+++ b/src/PIL/_typing.py
@@ -2,7 +2,16 @@ from __future__ import annotations
import os
import sys
-from typing import Protocol, Sequence, TypeVar, Union
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union
+
+if TYPE_CHECKING:
+ try:
+ import numpy.typing as npt
+
+ NumpyArray = npt.NDArray[Any] # requires numpy>=1.21
+ except (ImportError, AttributeError):
+ pass
if sys.version_info >= (3, 10):
from typing import TypeGuard
@@ -10,7 +19,6 @@ else:
try:
from typing_extensions import TypeGuard
except ImportError:
- from typing import Any
class TypeGuard: # type: ignore[no-redef]
def __class_getitem__(cls, item: Any) -> type[bool]:
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 12d7412ea..c4a72ad7e 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
-__version__ = "10.4.0.dev0"
+__version__ = "11.0.0.dev0"
diff --git a/src/PIL/features.py b/src/PIL/features.py
index 16c749f14..13908c4eb 100644
--- a/src/PIL/features.py
+++ b/src/PIL/features.py
@@ -4,6 +4,7 @@ import collections
import os
import sys
import warnings
+from typing import IO
import PIL
@@ -223,7 +224,7 @@ def get_supported() -> list[str]:
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.
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(f"Pillow {PIL.__version__}", file=out)
- py_version = sys.version.splitlines()
- print(f"Python {py_version[0].strip()}", file=out)
- for py_version in py_version[1:]:
+ py_version_lines = sys.version.splitlines()
+ print(f"Python {py_version_lines[0].strip()}", file=out)
+ for py_version in py_version_lines[1:]:
print(f" {py_version.strip()}", file=out)
print("-" * 68, 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)"),
]:
if check(name):
- if name == "jpg" and check_feature("libjpeg_turbo"):
- v = "libjpeg-turbo " + version_feature("libjpeg_turbo")
- else:
+ v: str | None = None
+ if name == "jpg":
+ 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)
if v is not None:
version_static = name in ("pil", "jpg")
diff --git a/src/_imaging.c b/src/_imaging.c
index efcbf55c2..7313cb4cf 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -1762,10 +1762,11 @@ _putpalette(ImagingObject *self, PyObject *args) {
ImagingShuffler unpack;
int bits;
- char *rawmode, *palette_mode;
+ char *palette_mode, *rawmode;
UINT8 *palette;
Py_ssize_t palettesize;
- if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) {
+ if (!PyArg_ParseTuple(
+ args, "ssy#", &palette_mode, &rawmode, &palette, &palettesize)) {
return NULL;
}
@@ -1775,7 +1776,6 @@ _putpalette(ImagingObject *self, PyObject *args) {
return NULL;
}
- palette_mode = strncmp("RGBA", rawmode, 4) == 0 ? "RGBA" : "RGB";
unpack = ImagingFindUnpacker(palette_mode, rawmode, &bits);
if (!unpack) {
PyErr_SetString(PyExc_ValueError, wrong_raw_mode);
@@ -2065,7 +2065,7 @@ im_setmode(ImagingObject *self, PyObject *args) {
}
static PyObject *
-_transform2(ImagingObject *self, PyObject *args) {
+_transform(ImagingObject *self, PyObject *args) {
static const char *wrong_number = "wrong number of matrix entries";
Imaging imOut;
@@ -3689,7 +3689,7 @@ static struct PyMethodDef methods[] = {
{"resize", (PyCFunction)_resize, METH_VARARGS},
{"reduce", (PyCFunction)_reduce, METH_VARARGS},
{"transpose", (PyCFunction)_transpose, METH_VARARGS},
- {"transform2", (PyCFunction)_transform2, METH_VARARGS},
+ {"transform", (PyCFunction)_transform, METH_VARARGS},
{"isblock", (PyCFunction)_isblock, METH_NOARGS},
diff --git a/src/_imagingcms.c b/src/_imagingcms.c
index 2b9612db7..590e1b983 100644
--- a/src/_imagingcms.c
+++ b/src/_imagingcms.c
@@ -223,20 +223,22 @@ findLCMStype(char *PILmode) {
if (strcmp(PILmode, "CMYK") == 0) {
return TYPE_CMYK_8;
}
- if (strcmp(PILmode, "L;16") == 0) {
+ if (strcmp(PILmode, "I;16") == 0 || strcmp(PILmode, "I;16L") == 0 ||
+ strcmp(PILmode, "L;16") == 0) {
return TYPE_GRAY_16;
}
- if (strcmp(PILmode, "L;16B") == 0) {
+ if (strcmp(PILmode, "I;16B") == 0 || strcmp(PILmode, "L;16B") == 0) {
return TYPE_GRAY_16_SE;
}
- if (strcmp(PILmode, "YCCA") == 0 || strcmp(PILmode, "YCC") == 0) {
+ if (strcmp(PILmode, "YCbCr") == 0 || strcmp(PILmode, "YCCA") == 0 ||
+ strcmp(PILmode, "YCC") == 0) {
return TYPE_YCbCr_8;
}
if (strcmp(PILmode, "LAB") == 0) {
// LabX equivalent like ALab, but not reversed -- no #define in lcms2
return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1));
}
- /* presume "L" by default */
+ /* presume "1" or "L" by default */
return TYPE_GRAY_8;
}
diff --git a/src/_imagingft.c b/src/_imagingft.c
index e83ddfec1..ba36cc72c 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -233,18 +233,6 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
return (PyObject *)self;
}
-static int
-font_getchar(PyObject *string, int index, FT_ULong *char_out) {
- if (PyUnicode_Check(string)) {
- if (index >= PyUnicode_GET_LENGTH(string)) {
- return 0;
- }
- *char_out = PyUnicode_READ_CHAR(string, index);
- return 1;
- }
- return 0;
-}
-
#ifdef HAVE_RAQM
static size_t
@@ -266,28 +254,34 @@ text_layout_raqm(
goto failed;
}
+ Py_ssize_t size;
+ int set_text;
if (PyUnicode_Check(string)) {
Py_UCS4 *text = PyUnicode_AsUCS4Copy(string);
- Py_ssize_t size = PyUnicode_GET_LENGTH(string);
+ size = PyUnicode_GET_LENGTH(string);
if (!text || !size) {
/* return 0 and clean up, no glyphs==no size,
and raqm fails with empty strings */
goto failed;
}
- int set_text = raqm_set_text(rq, text, size);
+ set_text = raqm_set_text(rq, text, size);
PyMem_Free(text);
- if (!set_text) {
- PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed");
+ } else {
+ char *buffer;
+ PyBytes_AsStringAndSize(string, &buffer, &size);
+ if (!buffer || !size) {
+ /* return 0 and clean up, no glyphs==no size,
+ and raqm fails with empty strings */
goto failed;
}
- if (lang) {
- if (!raqm_set_language(rq, lang, start, size)) {
- PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed");
- goto failed;
- }
- }
- } else {
- PyErr_SetString(PyExc_TypeError, "expected string");
+ set_text = raqm_set_text_utf8(rq, buffer, size);
+ }
+ if (!set_text) {
+ PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed");
+ goto failed;
+ }
+ if (lang && !raqm_set_language(rq, lang, start, size)) {
+ PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed");
goto failed;
}
@@ -405,13 +399,13 @@ text_layout_fallback(
GlyphInfo **glyph_info,
int mask,
int color) {
- int error, load_flags;
+ int error, load_flags, i;
+ char *buffer = NULL;
FT_ULong ch;
Py_ssize_t count;
FT_GlyphSlot glyph;
FT_Bool kerning = FT_HAS_KERNING(self->face);
FT_UInt last_index = 0;
- int i;
if (features != Py_None || dir != NULL || lang != NULL) {
PyErr_SetString(
@@ -419,14 +413,11 @@ text_layout_fallback(
"setting text direction, language or font features is not supported "
"without libraqm");
}
- if (!PyUnicode_Check(string)) {
- PyErr_SetString(PyExc_TypeError, "expected string");
- return 0;
- }
- count = 0;
- while (font_getchar(string, count, &ch)) {
- count++;
+ if (PyUnicode_Check(string)) {
+ count = PyUnicode_GET_LENGTH(string);
+ } else {
+ PyBytes_AsStringAndSize(string, &buffer, &count);
}
if (count == 0) {
return 0;
@@ -445,7 +436,12 @@ text_layout_fallback(
if (color) {
load_flags |= FT_LOAD_COLOR;
}
- for (i = 0; font_getchar(string, i, &ch); i++) {
+ for (i = 0; i < count; i++) {
+ if (buffer) {
+ ch = buffer[i];
+ } else {
+ ch = PyUnicode_READ_CHAR(string, i);
+ }
(*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch);
error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags);
if (error) {
diff --git a/src/display.c b/src/display.c
index abf94f1e1..990f4b0a5 100644
--- a/src/display.c
+++ b/src/display.c
@@ -618,7 +618,7 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (callback) {
/* restore thread state */
PyEval_SaveThread();
- PyThreadState_Swap(threadstate);
+ PyThreadState_Swap(current_threadstate);
}
return status;
diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c
index a018225b2..dc67cb41d 100644
--- a/src/libImaging/Paste.c
+++ b/src/libImaging/Paste.c
@@ -65,15 +65,32 @@ paste_mask_1(
int x, y;
if (imOut->image8) {
+ int in_i16 = strncmp(imIn->mode, "I;16", 4) == 0;
+ int out_i16 = strncmp(imOut->mode, "I;16", 4) == 0;
for (y = 0; y < ysize; y++) {
UINT8 *out = imOut->image8[y + dy] + dx;
+ if (out_i16) {
+ out += dx;
+ }
UINT8 *in = imIn->image8[y + sy] + sx;
+ if (in_i16) {
+ in += sx;
+ }
UINT8 *mask = imMask->image8[y + sy] + sx;
for (x = 0; x < xsize; x++) {
- if (*mask++) {
+ if (*mask) {
*out = *in;
}
- out++, in++;
+ if (in_i16) {
+ in++;
+ }
+ if (out_i16) {
+ out++;
+ if (*mask) {
+ *out = *in;
+ }
+ }
+ out++, in++, mask++;
}
}
@@ -415,15 +432,16 @@ fill_mask_L(
unsigned int tmp1;
if (imOut->image8) {
+ int i16 = strncmp(imOut->mode, "I;16", 4) == 0;
for (y = 0; y < ysize; y++) {
UINT8 *out = imOut->image8[y + dy] + dx;
- if (strncmp(imOut->mode, "I;16", 4) == 0) {
+ if (i16) {
out += dx;
}
UINT8 *mask = imMask->image8[y + sy] + sx;
for (x = 0; x < xsize; x++) {
*out = BLEND(*mask, *out, ink[0], tmp1);
- if (strncmp(imOut->mode, "I;16", 4) == 0) {
+ if (i16) {
out++;
*out = BLEND(*mask, *out, ink[1], tmp1);
}
diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c
index 1b84cd68f..eaa4374e3 100644
--- a/src/libImaging/Unpack.c
+++ b/src/libImaging/Unpack.c
@@ -718,6 +718,21 @@ ImagingUnpackBGRA15(UINT8 *out, const UINT8 *in, int pixels) {
}
}
+void
+ImagingUnpackBGRA15Z(UINT8 *out, const UINT8 *in, int pixels) {
+ int i, pixel;
+ /* RGB, rearranged channels, 5/5/5/1 bits per pixel, inverted alpha */
+ for (i = 0; i < pixels; i++) {
+ pixel = in[0] + (in[1] << 8);
+ out[B] = (pixel & 31) * 255 / 31;
+ out[G] = ((pixel >> 5) & 31) * 255 / 31;
+ out[R] = ((pixel >> 10) & 31) * 255 / 31;
+ out[A] = ~((pixel >> 15) * 255);
+ out += 4;
+ in += 2;
+ }
+}
+
void
ImagingUnpackRGB16(UINT8 *out, const UINT8 *in, int pixels) {
int i, pixel;
@@ -1538,7 +1553,7 @@ static struct {
/* flags: "I" inverted data; "R" reversed bit order; "B" big
endian byte order (default is little endian); "L" line
- interleave, "S" signed, "F" floating point */
+ interleave, "S" signed, "F" floating point, "Z" inverted alpha */
/* exception: rawmodes "I" and "F" are always native endian byte order */
@@ -1600,10 +1615,14 @@ static struct {
{"RGB", "BGR;15", 16, ImagingUnpackBGR15},
{"RGB", "RGB;16", 16, ImagingUnpackRGB16},
{"RGB", "BGR;16", 16, ImagingUnpackBGR16},
+ {"RGB", "RGBX;16L", 64, unpackRGBA16L},
+ {"RGB", "RGBX;16B", 64, unpackRGBA16B},
{"RGB", "RGB;4B", 16, ImagingUnpackRGB4B},
{"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */
{"RGB", "RGBX", 32, copy4},
{"RGB", "RGBX;L", 32, unpackRGBAL},
+ {"RGB", "RGBXX", 40, copy4skip1},
+ {"RGB", "RGBXXX", 48, copy4skip2},
{"RGB", "RGBA;L", 32, unpackRGBAL},
{"RGB", "RGBA;15", 16, ImagingUnpackRGBA15},
{"RGB", "BGRX", 32, ImagingUnpackBGRX},
@@ -1642,6 +1661,7 @@ static struct {
{"RGBA", "RGBA;L", 32, unpackRGBAL},
{"RGBA", "RGBA;15", 16, ImagingUnpackRGBA15},
{"RGBA", "BGRA;15", 16, ImagingUnpackBGRA15},
+ {"RGBA", "BGRA;15Z", 16, ImagingUnpackBGRA15Z},
{"RGBA", "RGBA;4B", 16, ImagingUnpackRGBA4B},
{"RGBA", "RGBA;16L", 64, unpackRGBA16L},
{"RGBA", "RGBA;16B", 64, unpackRGBA16B},
diff --git a/tox.ini b/tox.ini
index 85a2020d6..c1bc3b17d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,11 +3,10 @@ requires =
tox>=4.2
env_list =
lint
- py{py3, 312, 311, 310, 39, 38}
+ py{py3, 313, 312, 311, 310, 39}
[testenv]
deps =
- cffi
numpy
extras =
tests
@@ -39,10 +38,11 @@ deps =
ipython
numpy
packaging
- types-cffi
+ pytest
types-defusedxml
types-olefile
+ types-setuptools
extras =
typing
commands =
- mypy src {posargs}
+ mypy src Tests {posargs}
diff --git a/winbuild/README.md b/winbuild/README.md
index 7e81abcb0..c8048bcc9 100644
--- a/winbuild/README.md
+++ b/winbuild/README.md
@@ -16,7 +16,7 @@ For more extensive info, see the [Windows build instructions](build.rst).
The following is a simplified version of the script used on AppVeyor:
```
-set PYTHON=C:\Python38\bin
+set PYTHON=C:\Python39\bin
cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends
build\build_dep_all.cmd
diff --git a/winbuild/build.rst b/winbuild/build.rst
index d0be2943e..96b8803b4 100644
--- a/winbuild/build.rst
+++ b/winbuild/build.rst
@@ -114,7 +114,7 @@ Example
The following is a simplified version of the script used on AppVeyor::
- set PYTHON=C:\Python38\bin
+ set PYTHON=C:\Python39\bin
cd /D C:\Pillow\winbuild
%PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends
build\build_dep_all.cmd
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 0d6da7754..9837589b2 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -112,12 +112,12 @@ ARCHITECTURES = {
V = {
"BROTLI": "1.1.0",
"FREETYPE": "2.13.2",
- "FRIBIDI": "1.0.13",
- "HARFBUZZ": "8.4.0",
- "JPEGTURBO": "3.0.2",
+ "FRIBIDI": "1.0.15",
+ "HARFBUZZ": "8.5.0",
+ "JPEGTURBO": "3.0.3",
"LCMS2": "2.16",
"LIBPNG": "1.6.43",
- "LIBWEBP": "1.3.2",
+ "LIBWEBP": "1.4.0",
"OPENJPEG": "2.5.2",
"TIFF": "4.6.0",
"XZ": "5.4.5",