Reparatur der venv.

This commit is contained in:
2024-07-15 11:11:46 +02:00
parent a7955bc775
commit bfe8d4c2a7
203 changed files with 2375 additions and 1588 deletions
+1
View File
@@ -0,0 +1 @@
*
+1 -1
View File
@@ -103,7 +103,7 @@ def bdf_char(
class BdfFontFile(FontFile.FontFile): class BdfFontFile(FontFile.FontFile):
"""Font file plugin for the X11 BDF format.""" """Font file plugin for the X11 BDF format."""
def __init__(self, fp: BinaryIO): def __init__(self, fp: BinaryIO) -> None:
super().__init__() super().__init__()
s = fp.readline() s = fp.readline()
+40 -28
View File
@@ -31,10 +31,12 @@ BLP files come in many different flavours:
from __future__ import annotations from __future__ import annotations
import abc
import os import os
import struct import struct
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
@@ -55,11 +57,13 @@ class AlphaEncoding(IntEnum):
DXT5 = 7 DXT5 = 7
def unpack_565(i): def unpack_565(i: int) -> tuple[int, int, int]:
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
def decode_dxt1(data, alpha=False): def decode_dxt1(
data: bytes, alpha: bool = False
) -> tuple[bytearray, bytearray, bytearray, bytearray]:
""" """
input: one "row" of data (i.e. will produce 4*width pixels) input: one "row" of data (i.e. will produce 4*width pixels)
""" """
@@ -67,9 +71,9 @@ def decode_dxt1(data, alpha=False):
blocks = len(data) // 8 # number of blocks in row blocks = len(data) // 8 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray()) ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks): for block_index in range(blocks):
# Decode next 8-byte block. # Decode next 8-byte block.
idx = block * 8 idx = block_index * 8
color0, color1, bits = struct.unpack_from("<HHI", data, idx) color0, color1, bits = struct.unpack_from("<HHI", data, idx)
r0, g0, b0 = unpack_565(color0) r0, g0, b0 = unpack_565(color0)
@@ -114,7 +118,7 @@ def decode_dxt1(data, alpha=False):
return ret return ret
def decode_dxt3(data): def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
""" """
input: one "row" of data (i.e. will produce 4*width pixels) input: one "row" of data (i.e. will produce 4*width pixels)
""" """
@@ -122,8 +126,8 @@ def decode_dxt3(data):
blocks = len(data) // 16 # number of blocks in row blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray()) ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks): for block_index in range(blocks):
idx = block * 16 idx = block_index * 16
block = data[idx : idx + 16] block = data[idx : idx + 16]
# Decode next 16-byte block. # Decode next 16-byte block.
bits = struct.unpack_from("<8B", block) bits = struct.unpack_from("<8B", block)
@@ -167,7 +171,7 @@ def decode_dxt3(data):
return ret return ret
def decode_dxt5(data): def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
""" """
input: one "row" of data (i.e. will produce 4 * width pixels) input: one "row" of data (i.e. will produce 4 * width pixels)
""" """
@@ -175,8 +179,8 @@ def decode_dxt5(data):
blocks = len(data) // 16 # number of blocks in row blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray()) ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks): for block_index in range(blocks):
idx = block * 16 idx = block_index * 16
block = data[idx : idx + 16] block = data[idx : idx + 16]
# Decode next 16-byte block. # Decode next 16-byte block.
a0, a1 = struct.unpack_from("<BB", block) a0, a1 = struct.unpack_from("<BB", block)
@@ -241,7 +245,7 @@ class BLPFormatError(NotImplementedError):
pass pass
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] in (b"BLP1", b"BLP2") return prefix[:4] in (b"BLP1", b"BLP2")
@@ -253,7 +257,7 @@ class BlpImageFile(ImageFile.ImageFile):
format = "BLP" format = "BLP"
format_description = "Blizzard Mipmap Format" format_description = "Blizzard Mipmap Format"
def _open(self): def _open(self) -> None:
self.magic = self.fp.read(4) self.magic = self.fp.read(4)
self.fp.seek(5, os.SEEK_CUR) self.fp.seek(5, os.SEEK_CUR)
@@ -275,7 +279,7 @@ class BlpImageFile(ImageFile.ImageFile):
class _BLPBaseDecoder(ImageFile.PyDecoder): class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
try: try:
self._read_blp_header() self._read_blp_header()
self._load() self._load()
@@ -284,7 +288,12 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
raise OSError(msg) from e raise OSError(msg) from e
return -1, 0 return -1, 0
def _read_blp_header(self): @abc.abstractmethod
def _load(self) -> None:
pass
def _read_blp_header(self) -> None:
assert self.fd is not None
self.fd.seek(4) self.fd.seek(4)
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4)) (self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
@@ -303,10 +312,10 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4)) self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4)) self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length): def _safe_read(self, length: int) -> bytes:
return ImageFile._safe_read(self.fd, length) return ImageFile._safe_read(self.fd, length)
def _read_palette(self): def _read_palette(self) -> list[tuple[int, int, int, int]]:
ret = [] ret = []
for i in range(256): for i in range(256):
try: try:
@@ -316,7 +325,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a)) ret.append((b, g, r, a))
return ret return ret
def _read_bgra(self, palette): def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
data = bytearray() data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0])) _data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True: while True:
@@ -325,7 +334,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
except struct.error: except struct.error:
break break
b, g, r, a = palette[offset] b, g, r, a = palette[offset]
d = (r, g, b) d: tuple[int, ...] = (r, g, b)
if self._blp_alpha_depth: if self._blp_alpha_depth:
d += (a,) d += (a,)
data.extend(d) data.extend(d)
@@ -333,7 +342,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
class BLP1Decoder(_BLPBaseDecoder): class BLP1Decoder(_BLPBaseDecoder):
def _load(self): def _load(self) -> None:
if self._blp_compression == Format.JPEG: if self._blp_compression == Format.JPEG:
self._decode_jpeg_stream() self._decode_jpeg_stream()
@@ -349,29 +358,30 @@ class BLP1Decoder(_BLPBaseDecoder):
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
def _decode_jpeg_stream(self): def _decode_jpeg_stream(self) -> None:
from .JpegImagePlugin import JpegImageFile from .JpegImagePlugin import JpegImageFile
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4)) (jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size) jpeg_header = self._safe_read(jpeg_header_size)
assert self.fd is not None
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this? self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._blp_lengths[0]) data = self._safe_read(self._blp_lengths[0])
data = jpeg_header + data data = jpeg_header + data
data = BytesIO(data) image = JpegImageFile(BytesIO(data))
image = JpegImageFile(data)
Image._decompression_bomb_check(image.size) Image._decompression_bomb_check(image.size)
if image.mode == "CMYK": if image.mode == "CMYK":
decoder_name, extents, offset, args = image.tile[0] decoder_name, extents, offset, args = image.tile[0]
image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))] image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
r, g, b = image.convert("RGB").split() r, g, b = image.convert("RGB").split()
image = Image.merge("RGB", (b, g, r)) reversed_image = Image.merge("RGB", (b, g, r))
self.set_as_raw(image.tobytes()) self.set_as_raw(reversed_image.tobytes())
class BLP2Decoder(_BLPBaseDecoder): class BLP2Decoder(_BLPBaseDecoder):
def _load(self): def _load(self) -> None:
palette = self._read_palette() palette = self._read_palette()
assert self.fd is not None
self.fd.seek(self._blp_offsets[0]) self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1: if self._blp_compression == 1:
@@ -418,8 +428,9 @@ class BLP2Decoder(_BLPBaseDecoder):
class BLPEncoder(ImageFile.PyEncoder): class BLPEncoder(ImageFile.PyEncoder):
_pushes_fd = True _pushes_fd = True
def _write_palette(self): def _write_palette(self) -> bytes:
data = b"" data = b""
assert self.im is not None
palette = self.im.getpalette("RGBA", "RGBA") palette = self.im.getpalette("RGBA", "RGBA")
for i in range(len(palette) // 4): for i in range(len(palette) // 4):
r, g, b, a = palette[i * 4 : (i + 1) * 4] r, g, b, a = palette[i * 4 : (i + 1) * 4]
@@ -428,12 +439,13 @@ class BLPEncoder(ImageFile.PyEncoder):
data += b"\x00" * 4 data += b"\x00" * 4
return data return data
def encode(self, bufsize): def encode(self, bufsize: int) -> tuple[int, int, bytes]:
palette_data = self._write_palette() palette_data = self._write_palette()
offset = 20 + 16 * 4 * 2 + len(palette_data) offset = 20 + 16 * 4 * 2 + len(palette_data)
data = struct.pack("<16I", offset, *((0,) * 15)) data = struct.pack("<16I", offset, *((0,) * 15))
assert self.im is not None
w, h = self.im.size w, h = self.im.size
data += struct.pack("<16I", w * h, *((0,) * 15)) data += struct.pack("<16I", w * h, *((0,) * 15))
@@ -446,7 +458,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data return len(data), 0, data
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode != "P": if im.mode != "P":
msg = "Unsupported BLP image mode" msg = "Unsupported BLP image mode"
raise ValueError(msg) raise ValueError(msg)
+33 -16
View File
@@ -25,6 +25,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
from typing import IO
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16 from ._binary import i16le as i16
@@ -48,12 +49,12 @@ BIT2MODE = {
} }
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:2] == b"BM" return prefix[:2] == b"BM"
def _dib_accept(prefix): def _dib_accept(prefix: bytes) -> bool:
return i32(prefix) in [12, 40, 64, 108, 124] return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
# ============================================================================= # =============================================================================
@@ -83,8 +84,9 @@ class BmpImageFile(ImageFile.ImageFile):
# read the rest of the bmp header, without its size # read the rest of the bmp header, without its size
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
# -------------------------------------------------- IBM OS/2 Bitmap v1 # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
# ----- This format has different offsets because of width/height types # ----- This format has different offsets because of width/height types
# 12: BITMAPCOREHEADER/OS21XBITMAPHEADER
if file_info["header_size"] == 12: if file_info["header_size"] == 12:
file_info["width"] = i16(header_data, 0) file_info["width"] = i16(header_data, 0)
file_info["height"] = i16(header_data, 2) file_info["height"] = i16(header_data, 2)
@@ -93,9 +95,14 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["compression"] = self.RAW file_info["compression"] = self.RAW
file_info["palette_padding"] = 3 file_info["palette_padding"] = 3
# --------------------------------------------- Windows Bitmap v2 to v5 # --------------------------------------------- Windows Bitmap v3 to v5
# v3, OS/2 v2, v4, v5 # 40: BITMAPINFOHEADER
elif file_info["header_size"] in (40, 64, 108, 124): # 52: BITMAPV2HEADER
# 56: BITMAPV3HEADER
# 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER
# 108: BITMAPV4HEADER
# 124: BITMAPV5HEADER
elif file_info["header_size"] in (40, 52, 56, 64, 108, 124):
file_info["y_flip"] = header_data[7] == 0xFF file_info["y_flip"] = header_data[7] == 0xFF
file_info["direction"] = 1 if file_info["y_flip"] else -1 file_info["direction"] = 1 if file_info["y_flip"] else -1
file_info["width"] = i32(header_data, 0) file_info["width"] = i32(header_data, 0)
@@ -117,10 +124,13 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["palette_padding"] = 4 file_info["palette_padding"] = 4
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) 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.BITFIELDS:
masks = ["r_mask", "g_mask", "b_mask"]
if len(header_data) >= 48:
if len(header_data) >= 52: if len(header_data) >= 52:
for idx, mask in enumerate( masks.append("a_mask")
["r_mask", "g_mask", "b_mask", "a_mask"] else:
): file_info["a_mask"] = 0x0
for idx, mask in enumerate(masks):
file_info[mask] = i32(header_data, 36 + idx * 4) file_info[mask] = i32(header_data, 36 + idx * 4)
else: else:
# 40 byte headers only have the three components in the # 40 byte headers only have the three components in the
@@ -132,7 +142,7 @@ class BmpImageFile(ImageFile.ImageFile):
# location, but it is listed as a reserved component, # location, but it is listed as a reserved component,
# and it is not generally an alpha channel # and it is not generally an alpha channel
file_info["a_mask"] = 0x0 file_info["a_mask"] = 0x0
for mask in ["r_mask", "g_mask", "b_mask"]: for mask in masks:
file_info[mask] = i32(read(4)) file_info[mask] = i32(read(4))
file_info["rgb_mask"] = ( file_info["rgb_mask"] = (
file_info["r_mask"], file_info["r_mask"],
@@ -175,9 +185,11 @@ class BmpImageFile(ImageFile.ImageFile):
32: [ 32: [
(0xFF0000, 0xFF00, 0xFF, 0x0), (0xFF0000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0x0), (0xFF000000, 0xFF0000, 0xFF00, 0x0),
(0xFF000000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0xFF), (0xFF000000, 0xFF0000, 0xFF00, 0xFF),
(0xFF, 0xFF00, 0xFF0000, 0xFF000000), (0xFF, 0xFF00, 0xFF0000, 0xFF000000),
(0xFF0000, 0xFF00, 0xFF, 0xFF000000), (0xFF0000, 0xFF00, 0xFF, 0xFF000000),
(0xFF000000, 0xFF00, 0xFF, 0xFF0000),
(0x0, 0x0, 0x0, 0x0), (0x0, 0x0, 0x0, 0x0),
], ],
24: [(0xFF0000, 0xFF00, 0xFF)], 24: [(0xFF0000, 0xFF00, 0xFF)],
@@ -186,9 +198,11 @@ class BmpImageFile(ImageFile.ImageFile):
MASK_MODES = { MASK_MODES = {
(32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
(32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
(32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR",
(32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
(32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
(32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
(24, (0xFF0000, 0xFF00, 0xFF)): "BGR", (24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
(16, (0xF800, 0x7E0, 0x1F)): "BGR;16", (16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
@@ -270,7 +284,7 @@ class BmpImageFile(ImageFile.ImageFile):
) )
] ]
def _open(self): def _open(self) -> None:
"""Open file, check magic number and read header""" """Open file, check magic number and read header"""
# read 14 bytes: magic number, filesize, reserved, header final offset # read 14 bytes: magic number, filesize, reserved, header final offset
head_data = self.fp.read(14) head_data = self.fp.read(14)
@@ -287,7 +301,8 @@ class BmpImageFile(ImageFile.ImageFile):
class BmpRleDecoder(ImageFile.PyDecoder): class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
rle4 = self.args[1] rle4 = self.args[1]
data = bytearray() data = bytearray()
x = 0 x = 0
@@ -363,7 +378,7 @@ class DibImageFile(BmpImageFile):
format = "DIB" format = "DIB"
format_description = "Windows Bitmap" format_description = "Windows Bitmap"
def _open(self): def _open(self) -> None:
self._bitmap() self._bitmap()
@@ -381,11 +396,13 @@ SAVE = {
} }
def _dib_save(im, fp, filename): def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, False) _save(im, fp, filename, False)
def _save(im, fp, filename, bitmap_header=True): def _save(
im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
) -> None:
try: try:
rawmode, bits, colors = SAVE[im.mode] rawmode, bits, colors = SAVE[im.mode]
except KeyError as e: except KeyError as e:
@@ -10,12 +10,14 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
_handler = None _handler = None
def register_handler(handler): def register_handler(handler: ImageFile.StubHandler | None) -> None:
""" """
Install application-specific BUFR image handler. Install application-specific BUFR image handler.
@@ -29,7 +31,7 @@ def register_handler(handler):
# Image adapter # Image adapter
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
@@ -37,7 +39,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
format = "BUFR" format = "BUFR"
format_description = "BUFR" format_description = "BUFR"
def _open(self): def _open(self) -> None:
offset = self.fp.tell() offset = self.fp.tell()
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
@@ -54,11 +56,11 @@ class BufrStubImageFile(ImageFile.StubImageFile):
if loader: if loader:
loader.open(self) loader.open(self)
def _load(self): def _load(self) -> ImageFile.StubHandler | None:
return _handler return _handler
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"): if _handler is None or not hasattr(_handler, "save"):
msg = "BUFR save handler not installed" msg = "BUFR save handler not installed"
raise OSError(msg) raise OSError(msg)
@@ -25,7 +25,7 @@ from ._binary import i32le as i32
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\0\0\2\0" return prefix[:4] == b"\0\0\2\0"
@@ -37,7 +37,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
format = "CUR" format = "CUR"
format_description = "Windows Cursor" format_description = "Windows Cursor"
def _open(self): def _open(self) -> None:
offset = self.fp.tell() offset = self.fp.tell()
# check magic # check magic
@@ -29,7 +29,7 @@ from .PcxImagePlugin import PcxImageFile
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return len(prefix) >= 4 and i32(prefix) == MAGIC return len(prefix) >= 4 and i32(prefix) == MAGIC
@@ -42,7 +42,7 @@ class DcxImageFile(PcxImageFile):
format_description = "Intel DCX" format_description = "Intel DCX"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
# Header # Header
s = self.fp.read(4) s = self.fp.read(4)
if not _accept(s): if not _accept(s):
@@ -58,12 +58,12 @@ class DcxImageFile(PcxImageFile):
self._offset.append(offset) self._offset.append(offset)
self._fp = self.fp self._fp = self.fp
self.frame = None self.frame = -1
self.n_frames = len(self._offset) self.n_frames = len(self._offset)
self.is_animated = self.n_frames > 1 self.is_animated = self.n_frames > 1
self.seek(0) self.seek(0)
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self.frame = frame self.frame = frame
@@ -71,7 +71,7 @@ class DcxImageFile(PcxImageFile):
self.fp.seek(self._offset[frame]) self.fp.seek(self._offset[frame])
PcxImageFile._open(self) PcxImageFile._open(self)
def tell(self): def tell(self) -> int:
return self.frame return self.frame
+12 -9
View File
@@ -16,6 +16,7 @@ import io
import struct import struct
import sys import sys
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
from typing import IO
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i32le as i32 from ._binary import i32le as i32
@@ -271,16 +272,16 @@ class D3DFMT(IntEnum):
module = sys.modules[__name__] module = sys.modules[__name__]
for item in DDSD: for item in DDSD:
assert item.name is not None assert item.name is not None
setattr(module, "DDSD_" + item.name, item.value) setattr(module, f"DDSD_{item.name}", item.value)
for item1 in DDSCAPS: for item1 in DDSCAPS:
assert item1.name is not None assert item1.name is not None
setattr(module, "DDSCAPS_" + item1.name, item1.value) setattr(module, f"DDSCAPS_{item1.name}", item1.value)
for item2 in DDSCAPS2: for item2 in DDSCAPS2:
assert item2.name is not None assert item2.name is not None
setattr(module, "DDSCAPS2_" + item2.name, item2.value) setattr(module, f"DDSCAPS2_{item2.name}", item2.value)
for item3 in DDPF: for item3 in DDPF:
assert item3.name is not None assert item3.name is not None
setattr(module, "DDPF_" + item3.name, item3.value) setattr(module, f"DDPF_{item3.name}", item3.value)
DDS_FOURCC = DDPF.FOURCC DDS_FOURCC = DDPF.FOURCC
DDS_RGB = DDPF.RGB DDS_RGB = DDPF.RGB
@@ -331,7 +332,7 @@ class DdsImageFile(ImageFile.ImageFile):
format = "DDS" format = "DDS"
format_description = "DirectDraw Surface" format_description = "DirectDraw Surface"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not a DDS file" msg = "not a DDS file"
raise SyntaxError(msg) raise SyntaxError(msg)
@@ -379,6 +380,7 @@ class DdsImageFile(ImageFile.ImageFile):
elif pfflags & DDPF.PALETTEINDEXED8: elif pfflags & DDPF.PALETTEINDEXED8:
self._mode = "P" self._mode = "P"
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024)) self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
self.palette.mode = "RGBA"
elif pfflags & DDPF.FOURCC: elif pfflags & DDPF.FOURCC:
offset = header_size + 4 offset = header_size + 4
if fourcc == D3DFMT.DXT1: if fourcc == D3DFMT.DXT1:
@@ -472,14 +474,15 @@ class DdsImageFile(ImageFile.ImageFile):
else: else:
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)] self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
pass pass
class DdsRgbDecoder(ImageFile.PyDecoder): class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
bitcount, masks = self.args bitcount, masks = self.args
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
@@ -510,7 +513,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
return -1, 0 return -1, 0
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode not in ("RGB", "RGBA", "L", "LA"): if im.mode not in ("RGB", "RGBA", "L", "LA"):
msg = f"cannot write mode {im.mode} as DDS" msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg) raise OSError(msg)
@@ -562,7 +565,7 @@ def _save(im, fp, filename):
) )
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS " return prefix[:4] == b"DDS "
+21 -17
View File
@@ -27,6 +27,7 @@ import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i32le as i32 from ._binary import i32le as i32
@@ -42,7 +43,7 @@ gs_binary: str | bool | None = None
gs_windows_binary = None gs_windows_binary = None
def has_ghostscript(): def has_ghostscript() -> bool:
global gs_binary, gs_windows_binary global gs_binary, gs_windows_binary
if gs_binary is None: if gs_binary is None:
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
@@ -178,7 +179,7 @@ class PSFile:
self.char = None self.char = None
self.fp.seek(offset, whence) self.fp.seek(offset, whence)
def readline(self): def readline(self) -> str:
s = [self.char or b""] s = [self.char or b""]
self.char = None self.char = None
@@ -195,7 +196,7 @@ class PSFile:
return b"".join(s).decode("latin-1") return b"".join(s).decode("latin-1")
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
@@ -212,7 +213,7 @@ class EpsImageFile(ImageFile.ImageFile):
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
def _open(self): def _open(self) -> None:
(length, offset) = self._find_offset(self.fp) (length, offset) = self._find_offset(self.fp)
# go to offset - start of "%!PS" # go to offset - start of "%!PS"
@@ -228,7 +229,12 @@ class EpsImageFile(ImageFile.ImageFile):
reading_trailer_comments = False reading_trailer_comments = False
trailer_reached = 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: if "PS-Adobe" not in self.info:
msg = 'EPS header missing "%!PS-Adobe" comment' msg = 'EPS header missing "%!PS-Adobe" comment'
raise SyntaxError(msg) raise SyntaxError(msg)
@@ -236,7 +242,7 @@ class EpsImageFile(ImageFile.ImageFile):
msg = 'EPS header missing "%%BoundingBox" comment' msg = 'EPS header missing "%%BoundingBox" comment'
raise SyntaxError(msg) raise SyntaxError(msg)
def _read_comment(s): def _read_comment(s: str) -> bool:
nonlocal reading_trailer_comments nonlocal reading_trailer_comments
try: try:
m = split.match(s) m = split.match(s)
@@ -244,24 +250,22 @@ class EpsImageFile(ImageFile.ImageFile):
msg = "not an EPS file" msg = "not an EPS file"
raise SyntaxError(msg) from e raise SyntaxError(msg) from e
if m: if not m:
return False
k, v = m.group(1, 2) k, v = m.group(1, 2)
self.info[k] = v self.info[k] = v
if k == "BoundingBox": if k == "BoundingBox":
if v == "(atend)": if v == "(atend)":
reading_trailer_comments = True reading_trailer_comments = True
elif not self._size or ( elif not self._size or (trailer_reached and reading_trailer_comments):
trailer_reached and reading_trailer_comments
):
try: try:
# Note: The DSC spec says that BoundingBox # Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers # fields should be integers, but some drivers
# put floating point values there anyway. # put floating point values there anyway.
box = [int(float(i)) for i in v.split()] box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1] self._size = box[2] - box[0], box[3] - box[1]
self.tile = [ self.tile = [("eps", (0, 0) + self.size, offset, (length, box))]
("eps", (0, 0) + self.size, offset, (length, box))
]
except Exception: except Exception:
pass pass
return True return True
@@ -271,6 +275,8 @@ class EpsImageFile(ImageFile.ImageFile):
if byte == b"": if byte == b"":
# if we didn't read a byte we must be at the end of the file # if we didn't read a byte we must be at the end of the file
if bytes_read == 0: if bytes_read == 0:
if reading_header_comments:
check_required_header_comments()
break break
elif byte in b"\r\n": elif byte in b"\r\n":
# if we read a line ending character, ignore it and parse what # if we read a line ending character, ignore it and parse what
@@ -366,8 +372,6 @@ class EpsImageFile(ImageFile.ImageFile):
trailer_reached = True trailer_reached = True
bytes_read = 0 bytes_read = 0
check_required_header_comments()
if not self._size: if not self._size:
msg = "cannot determine EPS bounding box" msg = "cannot determine EPS bounding box"
raise OSError(msg) raise OSError(msg)
@@ -404,7 +408,7 @@ class EpsImageFile(ImageFile.ImageFile):
self.tile = [] self.tile = []
return Image.Image.load(self) return Image.Image.load(self)
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
# we can't incrementally load, so force ImageFile.parser to # we can't incrementally load, so force ImageFile.parser to
# use our custom load method by defining this method. # use our custom load method by defining this method.
pass pass
@@ -413,7 +417,7 @@ class EpsImageFile(ImageFile.ImageFile):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _save(im, fp, filename, eps=1): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
"""EPS Writer for the Python Imaging Library.""" """EPS Writer for the Python Imaging Library."""
# make sure image data is available # make sure image data is available
+1 -1
View File
@@ -346,7 +346,7 @@ class Interop(IntEnum):
InteropVersion = 2 InteropVersion = 2
RelatedImageFileFormat = 4096 RelatedImageFileFormat = 4096
RelatedImageWidth = 4097 RelatedImageWidth = 4097
RleatedImageHeight = 4098 RelatedImageHeight = 4098
class IFD(IntEnum): class IFD(IntEnum):
@@ -115,14 +115,18 @@ class FitsImageFile(ImageFile.ImageFile):
elif number_of_bits in (-32, -64): elif number_of_bits in (-32, -64):
self._mode = "F" self._mode = "F"
args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,) args: tuple[str | int, ...]
if decoder_name == "raw":
args = (self.mode, 0, -1)
else:
args = (number_of_bits,)
return decoder_name, offset, args return decoder_name, offset, args
class FitsGzipDecoder(ImageFile.PyDecoder): class FitsGzipDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
value = gzip.decompress(self.fd.read()) value = gzip.decompress(self.fd.read())
@@ -27,7 +27,7 @@ from ._binary import o8
# decoder # decoder
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return ( return (
len(prefix) >= 6 len(prefix) >= 6
and i16(prefix, 4) in [0xAF11, 0xAF12] and i16(prefix, 4) in [0xAF11, 0xAF12]
@@ -123,7 +123,7 @@ class FliImageFile(ImageFile.ImageFile):
palette[i] = (r, g, b) palette[i] = (r, g, b)
i += 1 i += 1
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if frame < self.__frame: if frame < self.__frame:
@@ -132,7 +132,7 @@ class FliImageFile(ImageFile.ImageFile):
for f in range(self.__frame + 1, frame + 1): for f in range(self.__frame + 1, frame + 1):
self._seek(f) self._seek(f)
def _seek(self, frame): def _seek(self, frame: int) -> None:
if frame == 0: if frame == 0:
self.__frame = -1 self.__frame = -1
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)
@@ -162,7 +162,7 @@ class FliImageFile(ImageFile.ImageFile):
self.__offset += framesize self.__offset += framesize
def tell(self): def tell(self) -> int:
return self.__frame return self.__frame
@@ -41,7 +41,7 @@ MODES = {
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:8] == olefile.MAGIC return prefix[:8] == olefile.MAGIC
@@ -70,7 +70,7 @@ class FpxImageFile(ImageFile.ImageFile):
self._open_index(1) self._open_index(1)
def _open_index(self, index=1): def _open_index(self, index: int = 1) -> None:
# #
# get the Image Contents Property Set # get the Image Contents Property Set
@@ -85,7 +85,7 @@ class FpxImageFile(ImageFile.ImageFile):
size = max(self.size) size = max(self.size)
i = 1 i = 1
while size > 64: while size > 64:
size = size / 2 size = size // 2
i += 1 i += 1
self.maxid = i - 1 self.maxid = i - 1
@@ -118,7 +118,7 @@ class FpxImageFile(ImageFile.ImageFile):
self._open_subimage(1, self.maxid) 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 # setup tile descriptors for a given subimage
@@ -237,11 +237,11 @@ class FpxImageFile(ImageFile.ImageFile):
return ImageFile.ImageFile.load(self) return ImageFile.ImageFile.load(self)
def close(self): def close(self) -> None:
self.ole.close() self.ole.close()
super().close() super().close()
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.ole.close() self.ole.close()
super().__exit__() super().__exit__()
@@ -71,7 +71,7 @@ class FtexImageFile(ImageFile.ImageFile):
format = "FTEX" format = "FTEX"
format_description = "Texture File Format (IW2:EOC)" format_description = "Texture File Format (IW2:EOC)"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not an FTEX file" msg = "not an FTEX file"
raise SyntaxError(msg) raise SyntaxError(msg)
@@ -103,11 +103,11 @@ class FtexImageFile(ImageFile.ImageFile):
self.fp.close() self.fp.close()
self.fp = BytesIO(data) self.fp = BytesIO(data)
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
pass pass
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == MAGIC return prefix[:4] == MAGIC
@@ -29,7 +29,7 @@ from . import Image, ImageFile
from ._binary import i32be as i32 from ._binary import i32be as i32
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2) return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2)
@@ -41,7 +41,7 @@ class GbrImageFile(ImageFile.ImageFile):
format = "GBR" format = "GBR"
format_description = "GIMP brush file" format_description = "GIMP brush file"
def _open(self): def _open(self) -> None:
header_size = i32(self.fp.read(4)) header_size = i32(self.fp.read(4))
if header_size < 20: if header_size < 20:
msg = "not a GIMP brush" msg = "not a GIMP brush"
+134 -82
View File
@@ -29,7 +29,10 @@ import itertools
import math import math
import os import os
import subprocess import subprocess
import sys
from enum import IntEnum from enum import IntEnum
from functools import cached_property
from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union
from . import ( from . import (
Image, Image,
@@ -44,6 +47,9 @@ from ._binary import i16le as i16
from ._binary import o8 from ._binary import o8
from ._binary import o16le as o16 from ._binary import o16le as o16
if TYPE_CHECKING:
from . import _imaging
class LoadingStrategy(IntEnum): class LoadingStrategy(IntEnum):
""".. versionadded:: 9.1.0""" """.. versionadded:: 9.1.0"""
@@ -60,7 +66,7 @@ LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
# Identify/read GIF files # Identify/read GIF files
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:6] in [b"GIF87a", b"GIF89a"] return prefix[:6] in [b"GIF87a", b"GIF89a"]
@@ -76,19 +82,19 @@ class GifImageFile(ImageFile.ImageFile):
global_palette = None global_palette = None
def data(self): def data(self) -> bytes | None:
s = self.fp.read(1) s = self.fp.read(1)
if s and s[0]: if s and s[0]:
return self.fp.read(s[0]) return self.fp.read(s[0])
return None return None
def _is_palette_needed(self, p): def _is_palette_needed(self, p: bytes) -> bool:
for i in range(0, len(p), 3): for i in range(0, len(p), 3):
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
return True return True
return False return False
def _open(self): def _open(self) -> None:
# Screen # Screen
s = self.fp.read(13) s = self.fp.read(13)
if not _accept(s): if not _accept(s):
@@ -112,12 +118,11 @@ class GifImageFile(ImageFile.ImageFile):
self._fp = self.fp # FIXME: hack self._fp = self.fp # FIXME: hack
self.__rewind = self.fp.tell() self.__rewind = self.fp.tell()
self._n_frames = None self._n_frames: int | None = None
self._is_animated = None
self._seek(0) # get ready to read first frame self._seek(0) # get ready to read first frame
@property @property
def n_frames(self): def n_frames(self) -> int:
if self._n_frames is None: if self._n_frames is None:
current = self.tell() current = self.tell()
try: try:
@@ -128,26 +133,25 @@ class GifImageFile(ImageFile.ImageFile):
self.seek(current) self.seek(current)
return self._n_frames return self._n_frames
@property @cached_property
def is_animated(self): def is_animated(self) -> bool:
if self._is_animated is None:
if self._n_frames is not None: if self._n_frames is not None:
self._is_animated = self._n_frames != 1 return self._n_frames != 1
else:
current = self.tell() current = self.tell()
if current: if current:
self._is_animated = True return True
else:
try: try:
self._seek(1, False) self._seek(1, False)
self._is_animated = True is_animated = True
except EOFError: except EOFError:
self._is_animated = False is_animated = False
self.seek(current) self.seek(current)
return self._is_animated return is_animated
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if frame < self.__frame: if frame < self.__frame:
@@ -163,11 +167,11 @@ class GifImageFile(ImageFile.ImageFile):
msg = "no more images in GIF file" msg = "no more images in GIF file"
raise EOFError(msg) from e raise EOFError(msg) from e
def _seek(self, frame, update_image=True): def _seek(self, frame: int, update_image: bool = True) -> None:
if frame == 0: if frame == 0:
# rewind # rewind
self.__offset = 0 self.__offset = 0
self.dispose = None self.dispose: _imaging.ImagingCore | None = None
self.__frame = -1 self.__frame = -1
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)
self.disposal_method = 0 self.disposal_method = 0
@@ -195,9 +199,9 @@ class GifImageFile(ImageFile.ImageFile):
msg = "no more images in GIF file" msg = "no more images in GIF file"
raise EOFError(msg) raise EOFError(msg)
palette = None palette: ImagePalette.ImagePalette | Literal[False] | None = None
info = {} info: dict[str, Any] = {}
frame_transparency = None frame_transparency = None
interlace = None interlace = None
frame_dispose_extent = None frame_dispose_extent = None
@@ -213,7 +217,7 @@ class GifImageFile(ImageFile.ImageFile):
# #
s = self.fp.read(1) s = self.fp.read(1)
block = self.data() block = self.data()
if s[0] == 249: if s[0] == 249 and block is not None:
# #
# graphic control extension # graphic control extension
# #
@@ -249,14 +253,14 @@ class GifImageFile(ImageFile.ImageFile):
info["comment"] = comment info["comment"] = comment
s = None s = None
continue continue
elif s[0] == 255 and frame == 0: elif s[0] == 255 and frame == 0 and block is not None:
# #
# application extension # application extension
# #
info["extension"] = block, self.fp.tell() info["extension"] = block, self.fp.tell()
if block[:11] == b"NETSCAPE2.0": if block[:11] == b"NETSCAPE2.0":
block = self.data() block = self.data()
if len(block) >= 3 and block[0] == 1: if block and len(block) >= 3 and block[0] == 1:
self.info["loop"] = i16(block, 1) self.info["loop"] = i16(block, 1)
while self.data(): while self.data():
pass pass
@@ -337,21 +341,19 @@ class GifImageFile(ImageFile.ImageFile):
self._mode = "RGB" self._mode = "RGB"
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
def _rgb(color): def _rgb(color: int) -> tuple[int, int, int]:
if self._frame_palette: if self._frame_palette:
if color * 3 + 3 > len(self._frame_palette.palette): if color * 3 + 3 > len(self._frame_palette.palette):
color = 0 color = 0
color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
else: else:
color = (color, color, color) return (color, color, color)
return color
self.dispose_extent = frame_dispose_extent
try:
if self.disposal_method < 2:
# do not dispose or none specified
self.dispose = None self.dispose = None
elif self.disposal_method == 2: self.dispose_extent = frame_dispose_extent
if self.dispose_extent and self.disposal_method >= 2:
try:
if self.disposal_method == 2:
# replace with background colour # replace with background colour
# only dispose the extent in this frame # only dispose the extent in this frame
@@ -388,7 +390,9 @@ class GifImageFile(ImageFile.ImageFile):
if self.mode in ("RGB", "RGBA"): if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGBA" dispose_mode = "RGBA"
color = _rgb(frame_transparency) + (0,) color = _rgb(frame_transparency) + (0,)
self.dispose = Image.core.fill(dispose_mode, dispose_size, color) self.dispose = Image.core.fill(
dispose_mode, dispose_size, color
)
except AttributeError: except AttributeError:
pass pass
@@ -417,7 +421,7 @@ class GifImageFile(ImageFile.ImageFile):
elif k in self.info: elif k in self.info:
del self.info[k] del self.info[k]
def load_prepare(self): def load_prepare(self) -> None:
temp_mode = "P" if self._frame_palette else "L" temp_mode = "P" if self._frame_palette else "L"
self._prev_im = None self._prev_im = None
if self.__frame == 0: if self.__frame == 0:
@@ -429,7 +433,7 @@ class GifImageFile(ImageFile.ImageFile):
self._prev_im = self.im self._prev_im = self.im
if self._frame_palette: if self._frame_palette:
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) 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: else:
self.im = None self.im = None
self._mode = temp_mode self._mode = temp_mode
@@ -437,7 +441,7 @@ class GifImageFile(ImageFile.ImageFile):
super().load_prepare() super().load_prepare()
def load_end(self): def load_end(self) -> None:
if self.__frame == 0: if self.__frame == 0:
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
if self._frame_transparency is not None: if self._frame_transparency is not None:
@@ -454,6 +458,8 @@ class GifImageFile(ImageFile.ImageFile):
frame_im = self.im.convert("RGBA") frame_im = self.im.convert("RGBA")
else: else:
frame_im = self.im.convert("RGB") frame_im = self.im.convert("RGB")
assert self.dispose_extent is not None
frame_im = self._crop(frame_im, self.dispose_extent) frame_im = self._crop(frame_im, self.dispose_extent)
self.im = self._prev_im self.im = self._prev_im
@@ -463,7 +469,7 @@ class GifImageFile(ImageFile.ImageFile):
else: else:
self.im.paste(frame_im, self.dispose_extent) self.im.paste(frame_im, self.dispose_extent)
def tell(self): def tell(self) -> int:
return self.__frame return self.__frame
@@ -474,7 +480,7 @@ class GifImageFile(ImageFile.ImageFile):
RAWMODE = {"1": "L", "L": "L", "P": "P"} RAWMODE = {"1": "L", "L": "L", "P": "P"}
def _normalize_mode(im): def _normalize_mode(im: Image.Image) -> Image.Image:
""" """
Takes an image (or frame), returns an image in a mode that is appropriate Takes an image (or frame), returns an image in a mode that is appropriate
for saving in a Gif. for saving in a Gif.
@@ -499,7 +505,12 @@ def _normalize_mode(im):
return im.convert("L") return im.convert("L")
def _normalize_palette(im, palette, info): _Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette]
def _normalize_palette(
im: Image.Image, palette: _Palette | None, info: dict[str, Any]
) -> Image.Image:
""" """
Normalizes the palette for image. Normalizes the palette for image.
- Sets the palette to the incoming palette, if provided. - Sets the palette to the incoming palette, if provided.
@@ -527,8 +538,10 @@ def _normalize_palette(im, palette, info):
source_palette = bytearray(i // 3 for i in range(768)) source_palette = bytearray(i // 3 for i in range(768))
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
used_palette_colors: list[int] | None
if palette: if palette:
used_palette_colors = [] used_palette_colors = []
assert source_palette is not None
for i in range(0, len(source_palette), 3): for i in range(0, len(source_palette), 3):
source_color = tuple(source_palette[i : i + 3]) source_color = tuple(source_palette[i : i + 3])
index = im.palette.colors.get(source_color) index = im.palette.colors.get(source_color)
@@ -559,7 +572,11 @@ def _normalize_palette(im, palette, info):
return im return im
def _write_single_frame(im, fp, palette): def _write_single_frame(
im: Image.Image,
fp: IO[bytes],
palette: _Palette | None,
) -> None:
im_out = _normalize_mode(im) im_out = _normalize_mode(im)
for k, v in im_out.info.items(): for k, v in im_out.info.items():
im.encoderinfo.setdefault(k, v) im.encoderinfo.setdefault(k, v)
@@ -580,7 +597,9 @@ def _write_single_frame(im, fp, palette):
fp.write(b"\0") # end of image data fp.write(b"\0") # end of image data
def _getbbox(base_im, im_frame): def _getbbox(
base_im: Image.Image, im_frame: Image.Image
) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im):
im_frame = im_frame.convert("RGBA") im_frame = im_frame.convert("RGBA")
base_im = base_im.convert("RGBA") base_im = base_im.convert("RGBA")
@@ -588,12 +607,20 @@ def _getbbox(base_im, im_frame):
return delta, delta.getbbox(alpha_only=False) return delta, delta.getbbox(alpha_only=False)
def _write_multiple_frames(im, fp, palette): class _Frame(NamedTuple):
im: Image.Image
bbox: tuple[int, int, int, int] | None
encoderinfo: dict[str, Any]
def _write_multiple_frames(
im: Image.Image, fp: IO[bytes], palette: _Palette | None
) -> bool:
duration = im.encoderinfo.get("duration") duration = im.encoderinfo.get("duration")
disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
im_frames = [] im_frames: list[_Frame] = []
previous_im = None previous_im: Image.Image | None = None
frame_count = 0 frame_count = 0
background_im = None background_im = None
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
@@ -619,24 +646,22 @@ def _write_multiple_frames(im, fp, palette):
frame_count += 1 frame_count += 1
diff_frame = None diff_frame = None
if im_frames: if im_frames and previous_im:
# delta frame # delta frame
delta, bbox = _getbbox(previous_im, im_frame) delta, bbox = _getbbox(previous_im, im_frame)
if not bbox: if not bbox:
# This frame is identical to the previous frame # This frame is identical to the previous frame
if encoderinfo.get("duration"): if encoderinfo.get("duration"):
im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[ im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
"duration"
]
continue continue
if im_frames[-1]["encoderinfo"].get("disposal") == 2: if im_frames[-1].encoderinfo.get("disposal") == 2:
if background_im is None: if background_im is None:
color = im.encoderinfo.get( color = im.encoderinfo.get(
"transparency", im.info.get("transparency", (0, 0, 0)) "transparency", im.info.get("transparency", (0, 0, 0))
) )
background = _get_background(im_frame, color) background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background) background_im = Image.new("P", im_frame.size, background)
background_im.putpalette(im_frames[0]["im"].palette) background_im.putpalette(im_frames[0].im.palette)
bbox = _getbbox(background_im, im_frame)[1] bbox = _getbbox(background_im, im_frame)[1]
elif encoderinfo.get("optimize") and im_frame.mode != "1": elif encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo: if "transparency" not in encoderinfo:
@@ -682,39 +707,39 @@ def _write_multiple_frames(im, fp, palette):
else: else:
bbox = None bbox = None
previous_im = im_frame previous_im = im_frame
im_frames.append( im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))
{"im": diff_frame or im_frame, "bbox": bbox, "encoderinfo": encoderinfo}
)
if len(im_frames) == 1: if len(im_frames) == 1:
if "duration" in im.encoderinfo: if "duration" in im.encoderinfo:
# Since multiple frames will not be written, use the combined duration # Since multiple frames will not be written, use the combined duration
im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"] im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
return return False
for frame_data in im_frames: for frame_data in im_frames:
im_frame = frame_data["im"] im_frame = frame_data.im
if not frame_data["bbox"]: if not frame_data.bbox:
# global header # global header
for s in _get_global_header(im_frame, frame_data["encoderinfo"]): for s in _get_global_header(im_frame, frame_data.encoderinfo):
fp.write(s) fp.write(s)
offset = (0, 0) offset = (0, 0)
else: else:
# compress difference # compress difference
if not palette: if not palette:
frame_data["encoderinfo"]["include_color_table"] = True frame_data.encoderinfo["include_color_table"] = True
im_frame = im_frame.crop(frame_data["bbox"]) im_frame = im_frame.crop(frame_data.bbox)
offset = frame_data["bbox"][:2] offset = frame_data.bbox[:2]
_write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"]) _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
return True return True
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True) _save(im, fp, filename, save_all=True)
def _save(im, fp, filename, save_all=False): def _save(
im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
) -> None:
# header # header
if "palette" in im.encoderinfo or "palette" in im.info: if "palette" in im.encoderinfo or "palette" in im.info:
palette = im.encoderinfo.get("palette", im.info.get("palette")) palette = im.encoderinfo.get("palette", im.info.get("palette"))
@@ -731,7 +756,7 @@ def _save(im, fp, filename, save_all=False):
fp.flush() fp.flush()
def get_interlace(im): def get_interlace(im: Image.Image) -> int:
interlace = im.encoderinfo.get("interlace", 1) interlace = im.encoderinfo.get("interlace", 1)
# workaround for @PIL153 # workaround for @PIL153
@@ -741,7 +766,9 @@ def get_interlace(im):
return interlace return interlace
def _write_local_header(fp, im, offset, flags): def _write_local_header(
fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int
) -> None:
try: try:
transparency = im.encoderinfo["transparency"] transparency = im.encoderinfo["transparency"]
except KeyError: except KeyError:
@@ -789,7 +816,7 @@ def _write_local_header(fp, im, offset, flags):
fp.write(o8(8)) # bits fp.write(o8(8)) # bits
def _save_netpbm(im, fp, filename): def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# Unused by default. # Unused by default.
# To use, uncomment the register_save call at the end of the file. # To use, uncomment the register_save call at the end of the file.
# #
@@ -820,6 +847,7 @@ def _save_netpbm(im, fp, filename):
) )
# Allow ppmquant to receive SIGPIPE if ppmtogif exits # Allow ppmquant to receive SIGPIPE if ppmtogif exits
assert quant_proc.stdout is not None
quant_proc.stdout.close() quant_proc.stdout.close()
retcode = quant_proc.wait() retcode = quant_proc.wait()
@@ -841,7 +869,7 @@ def _save_netpbm(im, fp, filename):
_FORCE_OPTIMIZE = False _FORCE_OPTIMIZE = False
def _get_optimize(im, info): def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
""" """
Palette optimization is a potentially expensive operation. Palette optimization is a potentially expensive operation.
@@ -885,9 +913,10 @@ def _get_optimize(im, info):
and current_palette_size > 2 and current_palette_size > 2
): ):
return used_palette_colors return used_palette_colors
return None
def _get_color_table_size(palette_bytes): def _get_color_table_size(palette_bytes: bytes) -> int:
# calculate the palette size for the header # calculate the palette size for the header
if not palette_bytes: if not palette_bytes:
return 0 return 0
@@ -897,7 +926,7 @@ def _get_color_table_size(palette_bytes):
return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1
def _get_header_palette(palette_bytes): def _get_header_palette(palette_bytes: bytes) -> bytes:
""" """
Returns the palette, null padded to the next power of 2 (*3) bytes Returns the palette, null padded to the next power of 2 (*3) bytes
suitable for direct inclusion in the GIF header suitable for direct inclusion in the GIF header
@@ -915,7 +944,7 @@ def _get_header_palette(palette_bytes):
return palette_bytes return palette_bytes
def _get_palette_bytes(im): def _get_palette_bytes(im: Image.Image) -> bytes:
""" """
Gets the palette for inclusion in the gif header Gets the palette for inclusion in the gif header
@@ -925,7 +954,10 @@ def _get_palette_bytes(im):
return im.palette.palette if im.palette else b"" return im.palette.palette if im.palette else b""
def _get_background(im, info_background): def _get_background(
im: Image.Image,
info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,
) -> int:
background = 0 background = 0
if info_background: if info_background:
if isinstance(info_background, tuple): if isinstance(info_background, tuple):
@@ -948,7 +980,7 @@ def _get_background(im, info_background):
return background return background
def _get_global_header(im, info): def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
"""Return a list of strings representing a GIF header""" """Return a list of strings representing a GIF header"""
# Header Block # Header Block
@@ -1010,7 +1042,12 @@ def _get_global_header(im, info):
return header return header
def _write_frame_data(fp, im_frame, offset, params): def _write_frame_data(
fp: IO[bytes],
im_frame: Image.Image,
offset: tuple[int, int],
params: dict[str, Any],
) -> None:
try: try:
im_frame.encoderinfo = params im_frame.encoderinfo = params
@@ -1030,7 +1067,9 @@ def _write_frame_data(fp, im_frame, offset, params):
# Legacy GIF utilities # Legacy GIF utilities
def getheader(im, palette=None, info=None): def getheader(
im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None
) -> tuple[list[bytes], list[int] | None]:
""" """
Legacy Method to get Gif data from image. Legacy Method to get Gif data from image.
@@ -1042,11 +1081,11 @@ def getheader(im, palette=None, info=None):
:returns: tuple of(list of header items, optimized palette) :returns: tuple of(list of header items, optimized palette)
""" """
used_palette_colors = _get_optimize(im, info)
if info is None: if info is None:
info = {} info = {}
used_palette_colors = _get_optimize(im, info)
if "background" not in info and "background" in im.info: if "background" not in info and "background" in im.info:
info["background"] = im.info["background"] info["background"] = im.info["background"]
@@ -1058,7 +1097,9 @@ def getheader(im, palette=None, info=None):
return header, used_palette_colors return header, used_palette_colors
def getdata(im, offset=(0, 0), **params): def getdata(
im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
) -> list[bytes]:
""" """
Legacy Method Legacy Method
@@ -1075,12 +1116,23 @@ def getdata(im, offset=(0, 0), **params):
:returns: List of bytes containing GIF encoded frame data :returns: List of bytes containing GIF encoded frame data
""" """
from io import BytesIO
class Collector: class Collector(BytesIO):
data = [] data = []
def write(self, data): if sys.version_info >= (3, 12):
from collections.abc import Buffer
def write(self, data: Buffer) -> int:
self.data.append(data) self.data.append(data)
return len(data)
else:
def write(self, data: Any) -> int:
self.data.append(data)
return len(data)
im.load() # make sure raster data is available im.load() # make sure raster data is available
+24 -12
View File
@@ -21,6 +21,7 @@ See the GIMP distribution for more information.)
from __future__ import annotations from __future__ import annotations
from math import log, pi, sin, sqrt from math import log, pi, sin, sqrt
from typing import IO, Callable
from ._binary import o8 from ._binary import o8
@@ -28,7 +29,7 @@ EPSILON = 1e-10
"""""" # Enable auto-doc for data member """""" # Enable auto-doc for data member
def linear(middle, pos): def linear(middle: float, pos: float) -> float:
if pos <= middle: if pos <= middle:
if middle < EPSILON: if middle < EPSILON:
return 0.0 return 0.0
@@ -43,19 +44,19 @@ def linear(middle, pos):
return 0.5 + 0.5 * pos / middle return 0.5 + 0.5 * pos / middle
def curved(middle, pos): def curved(middle: float, pos: float) -> float:
return pos ** (log(0.5) / log(max(middle, EPSILON))) return pos ** (log(0.5) / log(max(middle, EPSILON)))
def sine(middle, pos): def sine(middle: float, pos: float) -> float:
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
def sphere_increasing(middle, pos): def sphere_increasing(middle: float, pos: float) -> float:
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
def sphere_decreasing(middle, pos): def sphere_decreasing(middle: float, pos: float) -> float:
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
@@ -64,9 +65,22 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
class GradientFile: class GradientFile:
gradient = None gradient: (
list[
tuple[
float,
float,
float,
list[float],
list[float],
Callable[[float, float], float],
]
]
| None
) = None
def getpalette(self, entries=256): def getpalette(self, entries: int = 256) -> tuple[bytes, str]:
assert self.gradient is not None
palette = [] palette = []
ix = 0 ix = 0
@@ -101,7 +115,7 @@ class GradientFile:
class GimpGradientFile(GradientFile): class GimpGradientFile(GradientFile):
"""File handler for GIMP's gradient format.""" """File handler for GIMP's gradient format."""
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
if fp.readline()[:13] != b"GIMP Gradient": if fp.readline()[:13] != b"GIMP Gradient":
msg = "not a GIMP gradient file" msg = "not a GIMP gradient file"
raise SyntaxError(msg) raise SyntaxError(msg)
@@ -114,7 +128,7 @@ class GimpGradientFile(GradientFile):
count = int(line) count = int(line)
gradient = [] self.gradient = []
for i in range(count): for i in range(count):
s = fp.readline().split() s = fp.readline().split()
@@ -132,6 +146,4 @@ class GimpGradientFile(GradientFile):
msg = "cannot handle HSV colour space" msg = "cannot handle HSV colour space"
raise OSError(msg) raise OSError(msg)
gradient.append((x0, x1, xm, rgb0, rgb1, segment)) self.gradient.append((x0, x1, xm, rgb0, rgb1, segment))
self.gradient = gradient
@@ -16,6 +16,7 @@
from __future__ import annotations from __future__ import annotations
import re import re
from typing import IO
from ._binary import o8 from ._binary import o8
@@ -25,8 +26,8 @@ class GimpPaletteFile:
rawmode = "RGB" rawmode = "RGB"
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
self.palette = [o8(i) * 3 for i in range(256)] palette = [o8(i) * 3 for i in range(256)]
if fp.readline()[:12] != b"GIMP Palette": if fp.readline()[:12] != b"GIMP Palette":
msg = "not a GIMP palette file" msg = "not a GIMP palette file"
@@ -49,9 +50,9 @@ class GimpPaletteFile:
msg = "bad palette entry" msg = "bad palette entry"
raise ValueError(msg) 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): def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode return self.palette, self.rawmode
@@ -10,12 +10,14 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
_handler = None _handler = None
def register_handler(handler): def register_handler(handler: ImageFile.StubHandler | None) -> None:
""" """
Install application-specific GRIB image handler. Install application-specific GRIB image handler.
@@ -29,7 +31,7 @@ def register_handler(handler):
# Image adapter # Image adapter
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"GRIB" and prefix[7] == 1 return prefix[:4] == b"GRIB" and prefix[7] == 1
@@ -37,7 +39,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
format = "GRIB" format = "GRIB"
format_description = "GRIB" format_description = "GRIB"
def _open(self): def _open(self) -> None:
offset = self.fp.tell() offset = self.fp.tell()
if not _accept(self.fp.read(8)): if not _accept(self.fp.read(8)):
@@ -54,11 +56,11 @@ class GribStubImageFile(ImageFile.StubImageFile):
if loader: if loader:
loader.open(self) loader.open(self)
def _load(self): def _load(self) -> ImageFile.StubHandler | None:
return _handler return _handler
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"): if _handler is None or not hasattr(_handler, "save"):
msg = "GRIB save handler not installed" msg = "GRIB save handler not installed"
raise OSError(msg) raise OSError(msg)
@@ -10,12 +10,14 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
_handler = None _handler = None
def register_handler(handler): def register_handler(handler: ImageFile.StubHandler | None) -> None:
""" """
Install application-specific HDF5 image handler. Install application-specific HDF5 image handler.
@@ -29,7 +31,7 @@ def register_handler(handler):
# Image adapter # Image adapter
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:8] == b"\x89HDF\r\n\x1a\n" return prefix[:8] == b"\x89HDF\r\n\x1a\n"
@@ -37,7 +39,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
format = "HDF5" format = "HDF5"
format_description = "HDF5" format_description = "HDF5"
def _open(self): def _open(self) -> None:
offset = self.fp.tell() offset = self.fp.tell()
if not _accept(self.fp.read(8)): if not _accept(self.fp.read(8)):
@@ -54,11 +56,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
if loader: if loader:
loader.open(self) loader.open(self)
def _load(self): def _load(self) -> ImageFile.StubHandler | None:
return _handler return _handler
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"): if _handler is None or not hasattr(_handler, "save"):
msg = "HDF5 save handler not installed" msg = "HDF5 save handler not installed"
raise OSError(msg) raise OSError(msg)
+11 -12
View File
@@ -22,6 +22,7 @@ import io
import os import os
import struct import struct
import sys import sys
from typing import IO
from . import Image, ImageFile, PngImagePlugin, features from . import Image, ImageFile, PngImagePlugin, features
@@ -252,7 +253,7 @@ class IcnsImageFile(ImageFile.ImageFile):
format = "ICNS" format = "ICNS"
format_description = "Mac OS icns resource" format_description = "Mac OS icns resource"
def _open(self): def _open(self) -> None:
self.icns = IcnsFile(self.fp) self.icns = IcnsFile(self.fp)
self._mode = "RGBA" self._mode = "RGBA"
self.info["sizes"] = self.icns.itersizes() self.info["sizes"] = self.icns.itersizes()
@@ -312,7 +313,7 @@ class IcnsImageFile(ImageFile.ImageFile):
return px return px
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
""" """
Saves the image as a series of PNG files, Saves the image as a series of PNG files,
that are then combined into a .icns file. that are then combined into a .icns file.
@@ -346,35 +347,33 @@ def _save(im, fp, filename):
entries = [] entries = []
for type, size in sizes.items(): for type, size in sizes.items():
stream = size_streams[size] stream = size_streams[size]
entries.append( entries.append((type, HEADERSIZE + len(stream), stream))
{"type": type, "size": HEADERSIZE + len(stream), "stream": stream}
)
# Header # Header
fp.write(MAGIC) fp.write(MAGIC)
file_length = HEADERSIZE # Header file_length = HEADERSIZE # Header
file_length += HEADERSIZE + 8 * len(entries) # TOC file_length += HEADERSIZE + 8 * len(entries) # TOC
file_length += sum(entry["size"] for entry in entries) file_length += sum(entry[1] for entry in entries)
fp.write(struct.pack(">i", file_length)) fp.write(struct.pack(">i", file_length))
# TOC # TOC
fp.write(b"TOC ") fp.write(b"TOC ")
fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
for entry in entries: for entry in entries:
fp.write(entry["type"]) fp.write(entry[0])
fp.write(struct.pack(">i", entry["size"])) fp.write(struct.pack(">i", entry[1]))
# Data # Data
for entry in entries: for entry in entries:
fp.write(entry["type"]) fp.write(entry[0])
fp.write(struct.pack(">i", entry["size"])) fp.write(struct.pack(">i", entry[1]))
fp.write(entry["stream"]) fp.write(entry[2])
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == MAGIC return prefix[:4] == MAGIC
@@ -25,6 +25,7 @@ from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO
from math import ceil, log from math import ceil, log
from typing import IO
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16 from ._binary import i16le as i16
@@ -39,7 +40,7 @@ from ._binary import o32le as o32
_MAGIC = b"\0\0\1\0" _MAGIC = b"\0\0\1\0"
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(_MAGIC) # (2+2) fp.write(_MAGIC) # (2+2)
bmp = im.encoderinfo.get("bitmap_format") == "bmp" bmp = im.encoderinfo.get("bitmap_format") == "bmp"
sizes = im.encoderinfo.get( sizes = im.encoderinfo.get(
@@ -114,7 +115,7 @@ def _save(im, fp, filename):
fp.seek(current) fp.seek(current)
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == _MAGIC return prefix[:4] == _MAGIC
@@ -194,7 +195,7 @@ class IcoFile:
""" """
return self.frame(self.getentryindex(size, bpp)) return self.frame(self.getentryindex(size, bpp))
def frame(self, idx): def frame(self, idx: int) -> Image.Image:
""" """
Get an image from frame idx Get an image from frame idx
""" """
@@ -205,6 +206,7 @@ class IcoFile:
data = self.buf.read(8) data = self.buf.read(8)
self.buf.seek(header["offset"]) self.buf.seek(header["offset"])
im: Image.Image
if data[:8] == PngImagePlugin._MAGIC: if data[:8] == PngImagePlugin._MAGIC:
# png frame # png frame
im = PngImagePlugin.PngImageFile(self.buf) im = PngImagePlugin.PngImageFile(self.buf)
@@ -302,7 +304,7 @@ class IcoImageFile(ImageFile.ImageFile):
format = "ICO" format = "ICO"
format_description = "Windows Icon" format_description = "Windows Icon"
def _open(self): def _open(self) -> None:
self.ico = IcoFile(self.fp) self.ico = IcoFile(self.fp)
self.info["sizes"] = self.ico.sizes() self.info["sizes"] = self.ico.sizes()
self.size = self.ico.entry[0]["dim"] self.size = self.ico.entry[0]["dim"]
@@ -341,7 +343,7 @@ class IcoImageFile(ImageFile.ImageFile):
self.size = im.size self.size = im.size
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
# Flag the ImageFile.Parser so that it # Flag the ImageFile.Parser so that it
# just does all the decode at the end. # just does all the decode at the end.
pass pass
+12 -9
View File
@@ -28,6 +28,7 @@ from __future__ import annotations
import os import os
import re import re
from typing import IO, Any
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
@@ -78,7 +79,7 @@ OPEN = {
"LA image": ("LA", "LA;L"), "LA image": ("LA", "LA;L"),
"PA image": ("LA", "PA;L"), "PA image": ("LA", "PA;L"),
"RGBA image": ("RGBA", "RGBA;L"), "RGBA image": ("RGBA", "RGBA;L"),
"RGBX image": ("RGBX", "RGBX;L"), "RGBX image": ("RGB", "RGBX;L"),
"CMYK image": ("CMYK", "CMYK;L"), "CMYK image": ("CMYK", "CMYK;L"),
"YCC image": ("YCbCr", "YCbCr;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]*$") split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
def number(s): def number(s: Any) -> float:
try: try:
return int(s) return int(s)
except ValueError: except ValueError:
@@ -119,7 +120,7 @@ class ImImageFile(ImageFile.ImageFile):
format_description = "IFUNC Image Memory" format_description = "IFUNC Image Memory"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
# Quick rejection: if there's not an LF among the first # Quick rejection: if there's not an LF among the first
# 100 bytes, this is (probably) not a text header. # 100 bytes, this is (probably) not a text header.
@@ -196,7 +197,7 @@ class ImImageFile(ImageFile.ImageFile):
n += 1 n += 1
else: else:
msg = "Syntax error in IM header: " + s.decode("ascii", "replace") msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}"
raise SyntaxError(msg) raise SyntaxError(msg)
if not n: if not n:
@@ -271,14 +272,14 @@ class ImImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
@property @property
def n_frames(self): def n_frames(self) -> int:
return self.info[FRAMES] return self.info[FRAMES]
@property @property
def is_animated(self): def is_animated(self) -> bool:
return self.info[FRAMES] > 1 return self.info[FRAMES] > 1
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
@@ -296,7 +297,7 @@ class ImImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
def tell(self): def tell(self) -> int:
return self.frame return self.frame
@@ -325,7 +326,7 @@ SAVE = {
} }
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
image_type, rawmode = SAVE[im.mode] image_type, rawmode = SAVE[im.mode]
except KeyError as e: except KeyError as e:
@@ -340,6 +341,8 @@ def _save(im, fp, filename):
# or: SyntaxError("not an IM file") # or: SyntaxError("not an IM file")
# 8 characters are used for "Name: " and "\r\n" # 8 characters are used for "Name: " and "\r\n"
# Keep just the filename, ditch the potentially overlong path # Keep just the filename, ditch the potentially overlong path
if isinstance(filename, bytes):
filename = filename.decode("ascii")
name, ext = os.path.splitext(os.path.basename(filename)) name, ext = os.path.splitext(os.path.basename(filename))
name = "".join([name[: 92 - len(ext)], ext]) name = "".join([name[: 92 - len(ext)], ext])
File diff suppressed because it is too large Load Diff
+38 -13
View File
@@ -299,6 +299,31 @@ class ImageCmsTransform(Image.ImagePointHandler):
proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
flags: Flags = Flags.NONE, 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: if proof is None:
self.transform = core.buildTransform( self.transform = core.buildTransform(
input.profile, output.profile, input_mode, output_mode, intent, flags input.profile, output.profile, input_mode, output_mode, intent, flags
@@ -704,12 +729,12 @@ def applyTransform(
""" """
(pyCMS) Applies a transform to a given image. (pyCMS) Applies a transform to a given image.
If ``im.mode != transform.inMode``, a :exc:`PyCMSError` is raised. If ``im.mode != transform.input_mode``, a :exc:`PyCMSError` is raised.
If ``inPlace`` is ``True`` and ``transform.inMode != transform.outMode``, a If ``inPlace`` is ``True`` and ``transform.input_mode != transform.output_mode``, a
:exc:`PyCMSError` is raised. :exc:`PyCMSError` is raised.
If ``im.mode``, ``transform.inMode`` or ``transform.outMode`` is not If ``im.mode``, ``transform.input_mode`` or ``transform.output_mode`` is not
supported by pyCMSdll or the profiles you used for the transform, a supported by pyCMSdll or the profiles you used for the transform, a
:exc:`PyCMSError` is raised. :exc:`PyCMSError` is raised.
@@ -723,13 +748,13 @@ def applyTransform(
If you want to modify im in-place instead of receiving a new image as If you want to modify im in-place instead of receiving a new image as
the return value, set ``inPlace`` to ``True``. This can only be done if the return value, set ``inPlace`` to ``True``. This can only be done if
``transform.inMode`` and ``transform.outMode`` are the same, because we can't ``transform.input_mode`` and ``transform.output_mode`` are the same, because we
change the mode in-place (the buffer sizes for some modes are can't change the mode in-place (the buffer sizes for some modes are
different). The default behavior is to return a new :py:class:`~PIL.Image.Image` different). The default behavior is to return a new :py:class:`~PIL.Image.Image`
object of the same dimensions in mode ``transform.outMode``. object of the same dimensions in mode ``transform.output_mode``.
:param im: An :py:class:`~PIL.Image.Image` object, and im.mode must be the same :param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same
as the ``inMode`` supported by the transform. as the ``input_mode`` supported by the transform.
:param transform: A valid CmsTransform class object :param transform: A valid CmsTransform class object
:param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is
returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the
@@ -754,7 +779,7 @@ def applyTransform(
def createProfile( def createProfile(
colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = -1 colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0
) -> core.CmsProfile: ) -> core.CmsProfile:
""" """
(pyCMS) Creates a profile. (pyCMS) Creates a profile.
@@ -777,7 +802,7 @@ def createProfile(
:param colorSpace: String, the color space of the profile you wish to :param colorSpace: String, the color space of the profile you wish to
create. create.
Currently only "LAB", "XYZ", and "sRGB" are supported. 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 degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50
illuminant if omitted (5000k). colorTemp is ONLY applied to LAB illuminant if omitted (5000k). colorTemp is ONLY applied to LAB
profiles, and is ignored for XYZ and sRGB. profiles, and is ignored for XYZ and sRGB.
@@ -838,8 +863,8 @@ def getProfileName(profile: _CmsProfileCompatible) -> str:
if not (model or manufacturer): if not (model or manufacturer):
return (profile.profile.profile_description or "") + "\n" return (profile.profile.profile_description or "") + "\n"
if not manufacturer or len(model) > 30: # type: ignore[arg-type] if not manufacturer or (model and len(model) > 30):
return model + "\n" # type: ignore[operator] return f"{model}\n"
return f"{model} - {manufacturer}\n" return f"{model} - {manufacturer}\n"
except (AttributeError, OSError, TypeError, ValueError) as v: except (AttributeError, OSError, TypeError, ValueError) as v:
@@ -1089,7 +1114,7 @@ def isIntentSupported(
raise PyCMSError(v) from v raise PyCMSError(v) from v
def versions() -> tuple[str, str, str, str]: def versions() -> tuple[str, str | None, str, str]:
""" """
(pyCMS) Fetches versions. (pyCMS) Fetches versions.
""" """
+28 -25
View File
@@ -25,7 +25,7 @@ from . import Image
@lru_cache @lru_cache
def getrgb(color): def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]:
""" """
Convert a color string to an RGB or RGBA tuple. If the string cannot be Convert a color string to an RGB or RGBA tuple. If the string cannot be
parsed, this function raises a :py:exc:`ValueError` exception. parsed, this function raises a :py:exc:`ValueError` exception.
@@ -44,8 +44,10 @@ def getrgb(color):
if rgb: if rgb:
if isinstance(rgb, tuple): if isinstance(rgb, tuple):
return rgb return rgb
colormap[color] = rgb = getrgb(rgb) rgb_tuple = getrgb(rgb)
return rgb assert len(rgb_tuple) == 3
colormap[color] = rgb_tuple
return rgb_tuple
# check for known string formats # check for known string formats
if re.match("#[a-f0-9]{3}$", color): if re.match("#[a-f0-9]{3}$", color):
@@ -88,15 +90,15 @@ def getrgb(color):
if m: if m:
from colorsys import hls_to_rgb from colorsys import hls_to_rgb
rgb = hls_to_rgb( rgb_floats = hls_to_rgb(
float(m.group(1)) / 360.0, float(m.group(1)) / 360.0,
float(m.group(3)) / 100.0, float(m.group(3)) / 100.0,
float(m.group(2)) / 100.0, float(m.group(2)) / 100.0,
) )
return ( return (
int(rgb[0] * 255 + 0.5), int(rgb_floats[0] * 255 + 0.5),
int(rgb[1] * 255 + 0.5), int(rgb_floats[1] * 255 + 0.5),
int(rgb[2] * 255 + 0.5), int(rgb_floats[2] * 255 + 0.5),
) )
m = re.match( m = re.match(
@@ -105,15 +107,15 @@ def getrgb(color):
if m: if m:
from colorsys import hsv_to_rgb from colorsys import hsv_to_rgb
rgb = hsv_to_rgb( rgb_floats = hsv_to_rgb(
float(m.group(1)) / 360.0, float(m.group(1)) / 360.0,
float(m.group(2)) / 100.0, float(m.group(2)) / 100.0,
float(m.group(3)) / 100.0, float(m.group(3)) / 100.0,
) )
return ( return (
int(rgb[0] * 255 + 0.5), int(rgb_floats[0] * 255 + 0.5),
int(rgb[1] * 255 + 0.5), int(rgb_floats[1] * 255 + 0.5),
int(rgb[2] * 255 + 0.5), int(rgb_floats[2] * 255 + 0.5),
) )
m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
@@ -124,7 +126,7 @@ def getrgb(color):
@lru_cache @lru_cache
def getcolor(color, mode: str) -> tuple[int, ...]: def getcolor(color: str, mode: str) -> int | tuple[int, ...]:
""" """
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
@@ -136,33 +138,34 @@ def getcolor(color, mode: str) -> tuple[int, ...]:
:param color: A color string :param color: A color string
:param mode: Convert result to this mode :param mode: Convert result to this mode
:return: ``(graylevel[, alpha]) or (red, green, blue[, alpha])`` :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])``
""" """
# same as getrgb, but converts the result to the given mode # same as getrgb, but converts the result to the given mode
color, alpha = getrgb(color), 255 rgb, alpha = getrgb(color), 255
if len(color) == 4: if len(rgb) == 4:
color, alpha = color[:3], color[3] alpha = rgb[3]
rgb = rgb[:3]
if mode == "HSV": if mode == "HSV":
from colorsys import rgb_to_hsv from colorsys import rgb_to_hsv
r, g, b = color r, g, b = rgb
h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255)
return int(h * 255), int(s * 255), int(v * 255) return int(h * 255), int(s * 255), int(v * 255)
elif Image.getmodebase(mode) == "L": elif Image.getmodebase(mode) == "L":
r, g, b = color r, g, b = rgb
# ITU-R Recommendation 601-2 for nonlinear RGB # ITU-R Recommendation 601-2 for nonlinear RGB
# scaled to 24 bits to match the convert's implementation. # scaled to 24 bits to match the convert's implementation.
color = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16
if mode[-1] == "A": if mode[-1] == "A":
return color, alpha return graylevel, alpha
else: return graylevel
if mode[-1] == "A": elif mode[-1] == "A":
return color + (alpha,) return rgb + (alpha,)
return color return rgb
colormap = { colormap: dict[str, str | tuple[int, int, int]] = {
# X11 colour table from https://drafts.csswg.org/css-color-4/, with # X11 colour table from https://drafts.csswg.org/css-color-4/, with
# gray/grey spelling issues fixed. This is a superset of HTML 4.0 # gray/grey spelling issues fixed. This is a superset of HTML 4.0
# colour names used in CSS 1. # colour names used in CSS 1.
+251 -132
View File
@@ -34,11 +34,25 @@ from __future__ import annotations
import math import math
import numbers import numbers
import struct import struct
from typing import Sequence, cast from types import ModuleType
from typing import TYPE_CHECKING, AnyStr, Callable, List, Sequence, Tuple, Union, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._deprecate import deprecate
from ._typing import Coords 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. A simple 2D drawing interface for PIL images.
<p> <p>
@@ -48,7 +62,9 @@ directly.
class ImageDraw: class ImageDraw:
font = None font: (
ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
) = None
def __init__(self, im: Image.Image, mode: str | None = None) -> None: def __init__(self, im: Image.Image, mode: str | None = None) -> None:
""" """
@@ -92,7 +108,9 @@ class ImageDraw:
self.fontmode = "L" # aliasing is okay for other modes self.fontmode = "L" # aliasing is okay for other modes
self.fill = False self.fill = False
def getfont(self): def getfont(
self,
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
""" """
Get the current default font. Get the current default font.
@@ -117,43 +135,57 @@ class ImageDraw:
self.font = ImageFont.load_default() self.font = ImageFont.load_default()
return self.font return self.font
def _getfont(self, font_size: float | None): def _getfont(
self, font_size: float | None
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
if font_size is not None: if font_size is not None:
from . import ImageFont from . import ImageFont
font = ImageFont.load_default(font_size) return ImageFont.load_default(font_size)
else: else:
font = self.getfont() return self.getfont()
return font
def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: def _getink(
self, ink: _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 ink is None and fill is None:
if self.fill: if self.fill:
fill = self.ink result_fill = self.ink
else: else:
ink = self.ink result_ink = self.ink
else: else:
if ink is not None: if ink is not None:
if isinstance(ink, str): if isinstance(ink, str):
ink = ImageColor.getcolor(ink, self.mode) ink = ImageColor.getcolor(ink, self.mode)
if self.palette and not isinstance(ink, numbers.Number): if self.palette and not isinstance(ink, numbers.Number):
ink = self.palette.getcolor(ink, self._image) 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 fill is not None:
if isinstance(fill, str): if isinstance(fill, str):
fill = ImageColor.getcolor(fill, self.mode) fill = ImageColor.getcolor(fill, self.mode)
if self.palette and not isinstance(fill, numbers.Number): if self.palette and not isinstance(fill, numbers.Number):
fill = self.palette.getcolor(fill, self._image) fill = self.palette.getcolor(fill, self._image)
fill = self.draw.draw_ink(fill) result_fill = self.draw.draw_ink(fill)
return 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.""" """Draw an arc."""
ink, fill = self._getink(fill) ink, fill = self._getink(fill)
if ink is not None: if ink is not None:
self.draw.draw_arc(xy, start, end, ink, width) 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.""" """Draw a bitmap."""
bitmap.load() bitmap.load()
ink, fill = self._getink(fill) ink, fill = self._getink(fill)
@@ -162,23 +194,55 @@ class ImageDraw:
if ink is not None: if ink is not None:
self.draw.draw_bitmap(xy, bitmap.im, ink) 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.""" """Draw a chord."""
ink, fill = self._getink(outline, fill) ink, fill_ink = self._getink(outline, fill)
if fill is not None: if fill_ink is not None:
self.draw.draw_chord(xy, start, end, fill, 1) self.draw.draw_chord(xy, start, end, fill_ink, 1)
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_chord(xy, start, end, ink, 0, width) 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.""" """Draw an ellipse."""
ink, fill = self._getink(outline, fill) ink, fill_ink = self._getink(outline, fill)
if fill is not None: if fill_ink is not None:
self.draw.draw_ellipse(xy, fill, 1) self.draw.draw_ellipse(xy, fill_ink, 1)
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_ellipse(xy, ink, 0, width) self.draw.draw_ellipse(xy, ink, 0, width)
def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: def circle(
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: _Ink | None = None,
width: int = 0,
joint: str | None = None,
) -> None:
"""Draw a line, or a connected sequence of line segments.""" """Draw a line, or a connected sequence of line segments."""
ink = self._getink(fill)[0] ink = self._getink(fill)[0]
if ink is not None: if ink is not None:
@@ -206,7 +270,9 @@ class ImageDraw:
# This is a straight line, so no joint is required # This is a straight line, so no joint is required
continue continue
def coord_at_angle(coord, angle): def coord_at_angle(
coord: Sequence[float], angle: float
) -> tuple[float, ...]:
x, y = coord x, y = coord
angle -= 90 angle -= 90
distance = width / 2 - 1 distance = width / 2 - 1
@@ -247,37 +313,54 @@ class ImageDraw:
] ]
self.line(gap_coords, fill, width=3) 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.""" """(Experimental) Draw a shape."""
shape.close() shape.close()
ink, fill = self._getink(outline, fill) ink, fill_ink = self._getink(outline, fill)
if fill is not None: if fill_ink is not None:
self.draw.draw_outline(shape, fill, 1) self.draw.draw_outline(shape, fill_ink, 1)
if ink is not None and ink != fill: if ink is not None and ink != fill_ink:
self.draw.draw_outline(shape, ink, 0) self.draw.draw_outline(shape, ink, 0)
def pieslice( 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: ) -> None:
"""Draw a pieslice.""" """Draw a pieslice."""
ink, fill = self._getink(outline, fill) ink, fill_ink = self._getink(outline, fill)
if fill is not None: if fill_ink is not None:
self.draw.draw_pieslice(xy, start, end, fill, 1) self.draw.draw_pieslice(xy, start, end, fill_ink, 1)
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_pieslice(xy, start, end, ink, 0, width) 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.""" """Draw one or more individual pixels."""
ink, fill = self._getink(fill) ink, fill = self._getink(fill)
if ink is not None: if ink is not None:
self.draw.draw_points(xy, ink) 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.""" """Draw a polygon."""
ink, fill = self._getink(outline, fill) ink, fill_ink = self._getink(outline, fill)
if fill is not None: if fill_ink is not None:
self.draw.draw_polygon(xy, fill, 1) self.draw.draw_polygon(xy, fill_ink, 1)
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill_ink and width != 0:
if width == 1: if width == 1:
self.draw.draw_polygon(xy, ink, 0, width) self.draw.draw_polygon(xy, ink, 0, width)
elif self.im is not None: elif self.im is not None:
@@ -303,22 +386,41 @@ class ImageDraw:
self.im.paste(im.im, (0, 0) + im.size, mask.im) self.im.paste(im.im, (0, 0) + im.size, mask.im)
def regular_polygon( 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: ) -> None:
"""Draw a regular polygon.""" """Draw a regular polygon."""
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
self.polygon(xy, fill, outline, width) 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.""" """Draw a rectangle."""
ink, fill = self._getink(outline, fill) ink, fill_ink = self._getink(outline, fill)
if fill is not None: if fill_ink is not None:
self.draw.draw_rectangle(xy, fill, 1) self.draw.draw_rectangle(xy, fill_ink, 1)
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill_ink and width != 0:
self.draw.draw_rectangle(xy, ink, 0, width) self.draw.draw_rectangle(xy, ink, 0, width)
def rounded_rectangle( 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: ) -> None:
"""Draw a rounded rectangle.""" """Draw a rounded rectangle."""
if isinstance(xy[0], (list, tuple)): if isinstance(xy[0], (list, tuple)):
@@ -360,10 +462,10 @@ class ImageDraw:
# that is a rectangle # that is a rectangle
return self.rectangle(xy, fill, outline, width) return self.rectangle(xy, fill, outline, width)
r = d // 2 r = int(d // 2)
ink, fill = self._getink(outline, fill) 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], ...] parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
if full_x: if full_x:
# Draw top and bottom halves # Draw top and bottom halves
@@ -393,32 +495,32 @@ class ImageDraw:
) )
for part in parts: for part in parts:
if pieslice: if pieslice:
self.draw.draw_pieslice(*(part + (fill, 1))) self.draw.draw_pieslice(*(part + (fill_ink, 1)))
else: else:
self.draw.draw_arc(*(part + (ink, width))) self.draw.draw_arc(*(part + (ink, width)))
if fill is not None: if fill_ink is not None:
draw_corners(True) draw_corners(True)
if full_x: 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: 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: if not full_x and not full_y:
left = [x0, y0, x0 + r, y1] left = [x0, y0, x0 + r, y1]
if corners[0]: if corners[0]:
left[1] += r + 1 left[1] += r + 1
if corners[3]: if corners[3]:
left[3] -= r + 1 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] right = [x1 - r, y0, x1, y1]
if corners[1]: if corners[1]:
right[1] += r + 1 right[1] += r + 1
if corners[2]: if corners[2]:
right[3] -= r + 1 right[3] -= r + 1
self.draw.draw_rectangle(right, fill, 1) self.draw.draw_rectangle(right, fill_ink, 1)
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill_ink and width != 0:
draw_corners(False) draw_corners(False)
if not full_x: if not full_x:
@@ -450,15 +552,13 @@ class ImageDraw:
right[3] -= r + 1 right[3] -= r + 1
self.draw.draw_rectangle(right, ink, 1) self.draw.draw_rectangle(right, ink, 1)
def _multiline_check(self, text) -> bool: def _multiline_check(self, text: AnyStr) -> bool:
split_character = "\n" if isinstance(text, str) else b"\n" split_character = "\n" if isinstance(text, str) else b"\n"
return split_character in text return split_character in text
def _multiline_split(self, text) -> list[str | bytes]: def _multiline_split(self, text: AnyStr) -> list[AnyStr]:
split_character = "\n" if isinstance(text, str) else b"\n" return text.split("\n" if isinstance(text, str) else b"\n")
return text.split(split_character)
def _multiline_spacing(self, font, spacing, stroke_width): def _multiline_spacing(self, font, spacing, stroke_width):
return ( return (
@@ -469,10 +569,15 @@ class ImageDraw:
def text( def text(
self, self,
xy, xy: tuple[float, float],
text, text: str,
fill=None, fill=None,
font=None, font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
anchor=None, anchor=None,
spacing=4, spacing=4,
align="left", align="left",
@@ -510,10 +615,11 @@ class ImageDraw:
embedded_color, embedded_color,
) )
def getink(fill): def getink(fill: _Ink | None) -> int:
ink, fill = self._getink(fill) ink, fill_ink = self._getink(fill)
if ink is None: if ink is None:
return fill assert fill_ink is not None
return fill_ink
return ink return ink
def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: def draw_text(ink, stroke_width=0, stroke_offset=None) -> None:
@@ -526,7 +632,7 @@ class ImageDraw:
coord.append(int(xy[i])) coord.append(int(xy[i]))
start.append(math.modf(xy[i])[0]) start.append(math.modf(xy[i])[0])
try: try:
mask, offset = font.getmask2( mask, offset = font.getmask2( # type: ignore[union-attr,misc]
text, text,
mode, mode,
direction=direction, direction=direction,
@@ -542,7 +648,7 @@ class ImageDraw:
coord = [coord[0] + offset[0], coord[1] + offset[1]] coord = [coord[0] + offset[0], coord[1] + offset[1]]
except AttributeError: except AttributeError:
try: try:
mask = font.getmask( mask = font.getmask( # type: ignore[misc]
text, text,
mode, mode,
direction, direction,
@@ -591,10 +697,15 @@ class ImageDraw:
def multiline_text( def multiline_text(
self, self,
xy, xy: tuple[float, float],
text, text: str,
fill=None, fill=None,
font=None, font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
anchor=None, anchor=None,
spacing=4, spacing=4,
align="left", align="left",
@@ -624,7 +735,7 @@ class ImageDraw:
font = self._getfont(font_size) font = self._getfont(font_size)
widths = [] widths = []
max_width = 0 max_width: float = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width) line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines: for line in lines:
@@ -678,15 +789,20 @@ class ImageDraw:
def textlength( def textlength(
self, self,
text, text: str,
font=None, font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
direction=None, direction=None,
features=None, features=None,
language=None, language=None,
embedded_color=False, embedded_color=False,
*, *,
font_size=None, font_size=None,
): ) -> float:
"""Get the length of a given string, in pixels with 1/64 precision.""" """Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text): if self._multiline_check(text):
msg = "can't measure length of multiline text" msg = "can't measure length of multiline text"
@@ -778,7 +894,7 @@ class ImageDraw:
font = self._getfont(font_size) font = self._getfont(font_size)
widths = [] widths = []
max_width = 0 max_width: float = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width) line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines: for line in lines:
@@ -850,7 +966,7 @@ class ImageDraw:
return bbox return bbox
def Draw(im, mode: str | None = None) -> ImageDraw: def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
""" """
A simple 2D drawing interface for PIL images. A simple 2D drawing interface for PIL images.
@@ -862,45 +978,38 @@ def Draw(im, mode: str | None = None) -> ImageDraw:
defaults to the mode of the image. defaults to the mode of the image.
""" """
try: try:
return im.getdraw(mode) return getattr(im, "getdraw")(mode)
except AttributeError: except AttributeError:
return ImageDraw(im, mode) return ImageDraw(im, mode)
# experimental access to the outline API def getdraw(
try: im: Image.Image | None = None, hints: list[str] | None = None
Outline = Image.core.outline ) -> tuple[ImageDraw2.Draw | None, ModuleType]:
except AttributeError:
Outline = None
def getdraw(im=None, hints=None):
""" """
(Experimental) A more advanced 2D drawing interface for PIL images,
based on the WCK interface.
:param im: The image to draw in. :param im: The image to draw in.
:param hints: An optional list of hints. :param hints: An optional list of hints. Deprecated.
:returns: A (drawing context, drawing resource factory) tuple. :returns: A (drawing context, drawing resource factory) tuple.
""" """
# FIXME: this needs more work! if hints is not None:
# FIXME: come up with a better 'hints' scheme. deprecate("'hints' parameter", 12)
handler = None from . import ImageDraw2
if not hints or "nicest" in hints:
try: draw = ImageDraw2.Draw(im) if im is not None else None
from . import _imagingagg as handler return draw, ImageDraw2
except ImportError:
pass
if handler is None:
from . import ImageDraw2 as handler
if im:
im = handler.Draw(im)
return im, handler
def floodfill(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 image: Target image.
:param xy: Seed position (a 2-item coordinate tuple). See :param xy: Seed position (a 2-item coordinate tuple). See
@@ -918,6 +1027,7 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
# based on an implementation by Eric S. Raymond # based on an implementation by Eric S. Raymond
# amended by yo1995 @20180806 # amended by yo1995 @20180806
pixel = image.load() pixel = image.load()
assert pixel is not None
x, y = xy x, y = xy
try: try:
background = pixel[x, y] background = pixel[x, y]
@@ -955,12 +1065,12 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
def _compute_regular_polygon_vertices( 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]]: ) -> list[tuple[float, float]]:
""" """
Generate a list of vertices for a 2D regular polygon. 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. by a point and radius. The polygon is inscribed in this circle.
(e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
:param n_sides: Number of sides :param n_sides: Number of sides
@@ -998,7 +1108,7 @@ def _compute_regular_polygon_vertices(
# 1. Error Handling # 1. Error Handling
# 1.1 Check `n_sides` has an appropriate value # 1.1 Check `n_sides` has an appropriate value
if not isinstance(n_sides, int): 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) raise TypeError(msg)
if n_sides < 3: if n_sides < 3:
msg = "n_sides should be an int > 2" msg = "n_sides should be an int > 2"
@@ -1010,9 +1120,24 @@ def _compute_regular_polygon_vertices(
raise TypeError(msg) raise TypeError(msg)
if len(bounding_circle) == 3: if len(bounding_circle) == 3:
*centroid, polygon_radius = bounding_circle if not all(isinstance(i, (int, float)) for i in bounding_circle):
elif len(bounding_circle) == 2: msg = "bounding_circle should only contain numeric data"
centroid, polygon_radius = bounding_circle 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: else:
msg = ( msg = (
"bounding_circle should contain 2D coordinates " "bounding_circle should contain 2D coordinates "
@@ -1020,25 +1145,17 @@ def _compute_regular_polygon_vertices(
) )
raise ValueError(msg) 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: if polygon_radius <= 0:
msg = "bounding_circle radius should be > 0" msg = "bounding_circle radius should be > 0"
raise ValueError(msg) raise ValueError(msg)
# 1.3 Check `rotation` has an appropriate value # 1.3 Check `rotation` has an appropriate value
if not isinstance(rotation, (int, float)): 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) raise ValueError(msg)
# 2. Define Helper Functions # 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 ( return (
round( round(
point[0] * math.cos(math.radians(360 - degrees)) point[0] * math.cos(math.radians(360 - degrees))
@@ -1054,7 +1171,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] start_point = [polygon_radius, 0]
return _apply_rotation(start_point, angle) return _apply_rotation(start_point, angle)
@@ -1077,11 +1194,13 @@ def _compute_regular_polygon_vertices(
return [_compute_polygon_vertex(angle) for angle in angles] return [_compute_polygon_vertex(angle) for angle in angles]
def _color_diff(color1, color2: float | tuple[int, ...]) -> float: def _color_diff(
color1: float | tuple[int, ...], color2: float | tuple[int, ...]
) -> float:
""" """
Uses 1-norm distance to calculate difference between two values. Uses 1-norm distance to calculate difference between two values.
""" """
if isinstance(color2, tuple): first = color1 if isinstance(color1, tuple) else (color1,)
return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2))) second = color2 if isinstance(color2, tuple) else (color2,)
else:
return abs(color1 - color2) return sum(abs(first[i] - second[i]) for i in range(0, len(second)))
+19 -6
View File
@@ -24,13 +24,16 @@
""" """
from __future__ import annotations from __future__ import annotations
from typing import BinaryIO
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
from ._typing import StrOrBytesPath
class Pen: class Pen:
"""Stores an outline color and width.""" """Stores an outline color and width."""
def __init__(self, color, width=1, opacity=255): def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None:
self.color = ImageColor.getrgb(color) self.color = ImageColor.getrgb(color)
self.width = width self.width = width
@@ -38,14 +41,16 @@ class Pen:
class Brush: class Brush:
"""Stores a fill color""" """Stores a fill color"""
def __init__(self, color, opacity=255): def __init__(self, color: str, opacity: int = 255) -> None:
self.color = ImageColor.getrgb(color) self.color = ImageColor.getrgb(color)
class Font: class Font:
"""Stores a TrueType font and color""" """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 # FIXME: add support for bitmap fonts
self.color = ImageColor.getrgb(color) self.color = ImageColor.getrgb(color)
self.font = ImageFont.truetype(file, size) self.font = ImageFont.truetype(file, size)
@@ -56,14 +61,22 @@ class Draw:
(Experimental) WCK-style drawing interface (Experimental) WCK-style drawing interface
""" """
def __init__(self, image, size=None, color=None): def __init__(
if not hasattr(image, "im"): 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) image = Image.new(image, size, color)
self.draw = ImageDraw.Draw(image) self.draw = ImageDraw.Draw(image)
self.image = image self.image = image
self.transform = None self.transform = None
def flush(self): def flush(self) -> Image.Image:
return self.image return self.image
def render(self, op, xy, pen, brush=None): def render(self, op, xy, pen, brush=None):
+8 -5
View File
@@ -23,7 +23,10 @@ from . import Image, ImageFilter, ImageStat
class _Enhance: class _Enhance:
def enhance(self, factor): image: Image.Image
degenerate: Image.Image
def enhance(self, factor: float) -> Image.Image:
""" """
Returns an enhanced image. Returns an enhanced image.
@@ -46,7 +49,7 @@ class Color(_Enhance):
the original image. the original image.
""" """
def __init__(self, image): def __init__(self, image: Image.Image) -> None:
self.image = image self.image = image
self.intermediate_mode = "L" self.intermediate_mode = "L"
if "A" in image.getbands(): 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. 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 self.image = image
mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5)
self.degenerate = Image.new("L", image.size, mean).convert(image.mode) self.degenerate = Image.new("L", image.size, mean).convert(image.mode)
@@ -80,7 +83,7 @@ class Brightness(_Enhance):
original image. original image.
""" """
def __init__(self, image): def __init__(self, image: Image.Image) -> None:
self.image = image self.image = image
self.degenerate = Image.new(image.mode, image.size, 0) 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. 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.image = image
self.degenerate = image.filter(ImageFilter.SMOOTH) self.degenerate = image.filter(ImageFilter.SMOOTH)
+34 -21
View File
@@ -28,6 +28,7 @@
# #
from __future__ import annotations from __future__ import annotations
import abc
import io import io
import itertools import itertools
import struct import struct
@@ -64,7 +65,7 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`,
# Helpers # Helpers
def _get_oserror(error, *, encoder): def _get_oserror(error: int, *, encoder: bool) -> OSError:
try: try:
msg = Image.core.getcodecstatus(error) msg = Image.core.getcodecstatus(error)
except AttributeError: except AttributeError:
@@ -75,7 +76,7 @@ def _get_oserror(error, *, encoder):
return OSError(msg) return OSError(msg)
def raise_oserror(error): def raise_oserror(error: int) -> OSError:
deprecate( deprecate(
"raise_oserror", "raise_oserror",
12, 12,
@@ -153,17 +154,18 @@ class ImageFile(Image.Image):
self.fp.close() self.fp.close()
raise raise
def get_format_mimetype(self): def get_format_mimetype(self) -> str | None:
if self.custom_mimetype: if self.custom_mimetype:
return self.custom_mimetype return self.custom_mimetype
if self.format is not None: if self.format is not None:
return Image.MIME.get(self.format.upper()) return Image.MIME.get(self.format.upper())
return None
def __setstate__(self, state): def __setstate__(self, state):
self.tile = [] self.tile = []
super().__setstate__(state) super().__setstate__(state)
def verify(self): def verify(self) -> None:
"""Check file integrity""" """Check file integrity"""
# raise exception if something's wrong. must be called # raise exception if something's wrong. must be called
@@ -311,7 +313,7 @@ class ImageFile(Image.Image):
return Image.Image.load(self) return Image.Image.load(self)
def load_prepare(self): def load_prepare(self) -> None:
# create image memory if necessary # create image memory if necessary
if not self.im or self.im.mode != self.mode or self.im.size != self.size: if not self.im or self.im.mode != self.mode or self.im.size != self.size:
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self.size)
@@ -319,16 +321,16 @@ class ImageFile(Image.Image):
if self.mode == "P": if self.mode == "P":
Image.Image.load(self) Image.Image.load(self)
def load_end(self): def load_end(self) -> None:
# may be overridden # may be overridden
pass pass
# may be defined for contained formats # may be defined for contained formats
# def load_seek(self, pos): # def load_seek(self, pos: int) -> None:
# pass # pass
# may be defined for blocked formats (e.g. PNG) # may be defined for blocked formats (e.g. PNG)
# def load_read(self, read_bytes): # def load_read(self, read_bytes: int) -> bytes:
# pass # pass
def _seek_check(self, frame): def _seek_check(self, frame):
@@ -347,6 +349,15 @@ class ImageFile(Image.Image):
return self.tell() != frame 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): class StubImageFile(ImageFile):
""" """
Base class for stub image loaders. Base class for stub image loaders.
@@ -355,7 +366,7 @@ class StubImageFile(ImageFile):
certain format, but relies on external code to load the file. certain format, but relies on external code to load the file.
""" """
def _open(self): def _open(self) -> None:
msg = "StubImageFile subclass must implement _open" msg = "StubImageFile subclass must implement _open"
raise NotImplementedError(msg) raise NotImplementedError(msg)
@@ -371,7 +382,7 @@ class StubImageFile(ImageFile):
self.__dict__ = image.__dict__ self.__dict__ = image.__dict__
return image.load() return image.load()
def _load(self): def _load(self) -> StubHandler | None:
"""(Hook) Find actual image loader.""" """(Hook) Find actual image loader."""
msg = "StubImageFile subclass must implement _load" msg = "StubImageFile subclass must implement _load"
raise NotImplementedError(msg) raise NotImplementedError(msg)
@@ -390,7 +401,7 @@ class Parser:
offset = 0 offset = 0
finished = 0 finished = 0
def reset(self): def reset(self) -> None:
""" """
(Consumer) Reset the parser. Note that you can only call this (Consumer) Reset the parser. Note that you can only call this
method immediately after you've created a parser; parser method immediately after you've created a parser; parser
@@ -477,7 +488,7 @@ class Parser:
def __enter__(self): def __enter__(self):
return self return self
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.close() self.close()
def close(self): def close(self):
@@ -605,13 +616,13 @@ def _safe_read(fp, size):
class PyCodecState: class PyCodecState:
def __init__(self): def __init__(self) -> None:
self.xsize = 0 self.xsize = 0
self.ysize = 0 self.ysize = 0
self.xoff = 0 self.xoff = 0
self.yoff = 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 return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize
@@ -634,7 +645,7 @@ class PyCodec:
""" """
self.args = args self.args = args
def cleanup(self): def cleanup(self) -> None:
""" """
Override to perform codec specific cleanup Override to perform codec specific cleanup
@@ -651,7 +662,7 @@ class PyCodec:
""" """
self.fd = fd 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 Called from ImageFile to set the core output image for the codec
@@ -700,10 +711,10 @@ class PyDecoder(PyCodec):
_pulls_fd = False _pulls_fd = False
@property @property
def pulls_fd(self): def pulls_fd(self) -> bool:
return self._pulls_fd return self._pulls_fd
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
""" """
Override to perform the decoding process. Override to perform the decoding process.
@@ -728,6 +739,7 @@ class PyDecoder(PyCodec):
if not rawmode: if not rawmode:
rawmode = self.mode rawmode = self.mode
d = Image._getdecoder(self.mode, "raw", rawmode) d = Image._getdecoder(self.mode, "raw", rawmode)
assert self.im is not None
d.setimage(self.im, self.state.extents()) d.setimage(self.im, self.state.extents())
s = d.decode(data) s = d.decode(data)
@@ -750,10 +762,10 @@ class PyEncoder(PyCodec):
_pushes_fd = False _pushes_fd = False
@property @property
def pushes_fd(self): def pushes_fd(self) -> bool:
return self._pushes_fd return self._pushes_fd
def encode(self, bufsize): def encode(self, bufsize: int) -> tuple[int, int, bytes]:
""" """
Override to perform the encoding process. Override to perform the encoding process.
@@ -765,7 +777,7 @@ class PyEncoder(PyCodec):
msg = "unavailable in base encoder" msg = "unavailable in base encoder"
raise NotImplementedError(msg) 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, If ``pushes_fd`` is ``True``, then this method will be used,
and ``encode()`` will only be called once. and ``encode()`` will only be called once.
@@ -777,6 +789,7 @@ class PyEncoder(PyCodec):
return 0, -8 # bad configuration return 0, -8 # bad configuration
bytes_consumed, errcode, data = self.encode(0) bytes_consumed, errcode, data = self.encode(0)
if data: if data:
assert self.fd is not None
self.fd.write(data) self.fd.write(data)
return bytes_consumed, errcode return bytes_consumed, errcode
+73 -34
View File
@@ -16,10 +16,19 @@
# #
from __future__ import annotations from __future__ import annotations
import abc
import functools import functools
from types import ModuleType
from typing import TYPE_CHECKING, Any, Callable, Sequence, cast
if TYPE_CHECKING:
from . import _imaging
from ._typing import NumpyArray
class Filter: class Filter:
@abc.abstractmethod
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
pass pass
@@ -28,7 +37,9 @@ class MultibandFilter(Filter):
class BuiltinFilter(MultibandFilter): class BuiltinFilter(MultibandFilter):
def filter(self, image): filterargs: tuple[Any, ...]
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
if image.mode == "P": if image.mode == "P":
msg = "cannot filter palette images" msg = "cannot filter palette images"
raise ValueError(msg) raise ValueError(msg)
@@ -53,7 +64,13 @@ class Kernel(BuiltinFilter):
name = "Kernel" 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: if scale is None:
# default scale is sum of kernel # default scale is sum of kernel
scale = functools.reduce(lambda a, b: a + b, kernel) scale = functools.reduce(lambda a, b: a + b, kernel)
@@ -76,11 +93,11 @@ class RankFilter(Filter):
name = "Rank" name = "Rank"
def __init__(self, size, rank): def __init__(self, size: int, rank: int) -> None:
self.size = size self.size = size
self.rank = rank self.rank = rank
def filter(self, image): def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
if image.mode == "P": if image.mode == "P":
msg = "cannot filter palette images" msg = "cannot filter palette images"
raise ValueError(msg) raise ValueError(msg)
@@ -98,7 +115,7 @@ class MedianFilter(RankFilter):
name = "Median" name = "Median"
def __init__(self, size=3): def __init__(self, size: int = 3) -> None:
self.size = size self.size = size
self.rank = size * size // 2 self.rank = size * size // 2
@@ -113,7 +130,7 @@ class MinFilter(RankFilter):
name = "Min" name = "Min"
def __init__(self, size=3): def __init__(self, size: int = 3) -> None:
self.size = size self.size = size
self.rank = 0 self.rank = 0
@@ -128,7 +145,7 @@ class MaxFilter(RankFilter):
name = "Max" name = "Max"
def __init__(self, size=3): def __init__(self, size: int = 3) -> None:
self.size = size self.size = size
self.rank = size * size - 1 self.rank = size * size - 1
@@ -144,10 +161,10 @@ class ModeFilter(Filter):
name = "Mode" name = "Mode"
def __init__(self, size=3): def __init__(self, size: int = 3) -> None:
self.size = size self.size = size
def filter(self, image): def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
return image.modefilter(self.size) return image.modefilter(self.size)
@@ -162,12 +179,12 @@ class GaussianBlur(MultibandFilter):
name = "GaussianBlur" name = "GaussianBlur"
def __init__(self, radius=2): def __init__(self, radius: float | Sequence[float] = 2) -> None:
self.radius = radius self.radius = radius
def filter(self, image): def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
xy = self.radius xy = self.radius
if not isinstance(xy, (tuple, list)): if isinstance(xy, (int, float)):
xy = (xy, xy) xy = (xy, xy)
if xy == (0, 0): if xy == (0, 0):
return image.copy() return image.copy()
@@ -190,18 +207,16 @@ class BoxBlur(MultibandFilter):
name = "BoxBlur" name = "BoxBlur"
def __init__(self, radius): def __init__(self, radius: float | Sequence[float]) -> None:
xy = radius xy = radius if isinstance(radius, (tuple, list)) else (radius, radius)
if not isinstance(xy, (tuple, list)):
xy = (xy, xy)
if xy[0] < 0 or xy[1] < 0: if xy[0] < 0 or xy[1] < 0:
msg = "radius must be >= 0" msg = "radius must be >= 0"
raise ValueError(msg) raise ValueError(msg)
self.radius = radius self.radius = radius
def filter(self, image): def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
xy = self.radius xy = self.radius
if not isinstance(xy, (tuple, list)): if isinstance(xy, (int, float)):
xy = (xy, xy) xy = (xy, xy)
if xy == (0, 0): if xy == (0, 0):
return image.copy() return image.copy()
@@ -225,12 +240,14 @@ class UnsharpMask(MultibandFilter):
name = "UnsharpMask" 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.radius = radius
self.percent = percent self.percent = percent
self.threshold = threshold 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) return image.unsharp_mask(self.radius, self.percent, self.threshold)
@@ -375,7 +392,14 @@ class Color3DLUT(MultibandFilter):
name = "Color 3D LUT" 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): if channels not in (3, 4):
msg = "Only 3 or 4 output channels are supported" msg = "Only 3 or 4 output channels are supported"
raise ValueError(msg) raise ValueError(msg)
@@ -389,7 +413,7 @@ class Color3DLUT(MultibandFilter):
items = size[0] * size[1] * size[2] items = size[0] * size[1] * size[2]
wrong_size = False wrong_size = False
numpy = None numpy: ModuleType | None = None
if hasattr(table, "shape"): if hasattr(table, "shape"):
try: try:
import numpy import numpy
@@ -397,15 +421,16 @@ class Color3DLUT(MultibandFilter):
pass pass
if numpy and isinstance(table, numpy.ndarray): if numpy and isinstance(table, numpy.ndarray):
numpy_table: NumpyArray = table
if copy_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,),
(items, channels), (items, channels),
(size[2], size[1], size[0], channels), (size[2], size[1], size[0], channels),
]: ]:
table = table.reshape(items * channels) table = numpy_table.reshape(items * channels)
else: else:
wrong_size = True wrong_size = True
@@ -415,7 +440,8 @@ class Color3DLUT(MultibandFilter):
# Convert to a flat list # Convert to a flat list
if table and isinstance(table[0], (list, tuple)): 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: for pixel in raw_table:
if len(pixel) != channels: if len(pixel) != channels:
msg = ( msg = (
@@ -423,7 +449,8 @@ class Color3DLUT(MultibandFilter):
f"have a length of {channels}." f"have a length of {channels}."
) )
raise ValueError(msg) raise ValueError(msg)
table.extend(pixel) flat_table.extend(pixel)
table = flat_table
if wrong_size or len(table) != items * channels: if wrong_size or len(table) != items * channels:
msg = ( msg = (
@@ -436,7 +463,7 @@ class Color3DLUT(MultibandFilter):
self.table = table self.table = table
@staticmethod @staticmethod
def _check_size(size): def _check_size(size: Any) -> tuple[int, int, int]:
try: try:
_, _, _ = size _, _, _ = size
except ValueError as e: except ValueError as e:
@@ -444,7 +471,7 @@ class Color3DLUT(MultibandFilter):
raise ValueError(msg) from e raise ValueError(msg) from e
except TypeError: except TypeError:
size = (size, size, size) size = (size, size, size)
size = [int(x) for x in size] size = tuple(int(x) for x in size)
for size_1d in size: for size_1d in size:
if not 2 <= size_1d <= 65: if not 2 <= size_1d <= 65:
msg = "Size should be in [2, 65] range." msg = "Size should be in [2, 65] range."
@@ -452,7 +479,13 @@ class Color3DLUT(MultibandFilter):
return size return size
@classmethod @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. """Generates new LUT using provided callback.
:param size: Size of the table. Passed to the constructor. :param size: Size of the table. Passed to the constructor.
@@ -469,7 +502,7 @@ class Color3DLUT(MultibandFilter):
msg = "Only 3 or 4 output channels are supported" msg = "Only 3 or 4 output channels are supported"
raise ValueError(msg) 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 idx_out = 0
for b in range(size_3d): for b in range(size_3d):
for g in range(size_2d): for g in range(size_2d):
@@ -487,7 +520,13 @@ class Color3DLUT(MultibandFilter):
_copy_table=False, _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 """Transforms the table values using provided callback and returns
a new LUT with altered values. a new LUT with altered values.
@@ -541,7 +580,7 @@ class Color3DLUT(MultibandFilter):
_copy_table=False, _copy_table=False,
) )
def __repr__(self): def __repr__(self) -> str:
r = [ r = [
f"{self.__class__.__name__} from {self.table.__class__.__name__}", f"{self.__class__.__name__} from {self.table.__class__.__name__}",
"size={:d}x{:d}x{:d}".format(*self.size), "size={:d}x{:d}x{:d}".format(*self.size),
@@ -551,7 +590,7 @@ class Color3DLUT(MultibandFilter):
r.append(f"target_mode={self.mode}") r.append(f"target_mode={self.mode}")
return "<{}>".format(" ".join(r)) return "<{}>".format(" ".join(r))
def filter(self, image): def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
from . import Image from . import Image
return image.color_lut_3d( return image.color_lut_3d(
+219 -181
View File
@@ -33,11 +33,17 @@ import sys
import warnings import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from typing import BinaryIO from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO
from . import Image from . import Image
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
from ._util import is_directory, is_path from ._util import DeferredError, is_path
if TYPE_CHECKING:
from . import ImageFile
from ._imaging import ImagingFont
from ._imagingft import Font
class Layout(IntEnum): class Layout(IntEnum):
@@ -48,15 +54,14 @@ class Layout(IntEnum):
MAX_STRING_LENGTH = 1_000_000 MAX_STRING_LENGTH = 1_000_000
core: ModuleType | DeferredError
try: try:
from . import _imagingft as core from . import _imagingft as core
except ImportError as ex: except ImportError as ex:
from ._util import DeferredError
core = DeferredError.new(ex) core = DeferredError.new(ex)
def _string_length_check(text): def _string_length_check(text: str | bytes | bytearray) -> None:
if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
msg = "too many characters in string" msg = "too many characters in string"
raise ValueError(msg) raise ValueError(msg)
@@ -81,9 +86,11 @@ def _string_length_check(text):
class ImageFont: class ImageFont:
"""PIL font wrapper""" """PIL font wrapper"""
def _load_pilfont(self, filename): font: ImagingFont
def _load_pilfont(self, filename: str) -> None:
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
image = None image: ImageFile.ImageFile | None = None
for ext in (".png", ".gif", ".pbm"): for ext in (".png", ".gif", ".pbm"):
if image: if image:
image.close() image.close()
@@ -106,7 +113,7 @@ class ImageFont:
self._load_pilfont_data(fp, image) self._load_pilfont_data(fp, image)
image.close() image.close()
def _load_pilfont_data(self, file, image): def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
# read PILfont header # read PILfont header
if file.readline() != b"PILfont\n": if file.readline() != b"PILfont\n":
msg = "Not a PILfont file" msg = "Not a PILfont file"
@@ -153,17 +160,15 @@ class ImageFont:
Image._decompression_bomb_check(self.font.getsize(text)) Image._decompression_bomb_check(self.font.getsize(text))
return self.font.getmask(text, mode) return self.font.getmask(text, mode)
def getbbox(self, text, *args, **kwargs): def getbbox(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
) -> tuple[int, int, int, int]:
""" """
Returns bounding box (in pixels) of given text. Returns bounding box (in pixels) of given text.
.. versionadded:: 9.2.0 .. versionadded:: 9.2.0
:param text: Text to render. :param text: Text to render.
:param mode: Used by some graphics drivers to indicate what mode the
driver prefers; if empty, the renderer may return either
mode. Note that the mode is always a string, to simplify
C-level implementations.
:return: ``(left, top, right, bottom)`` bounding box :return: ``(left, top, right, bottom)`` bounding box
""" """
@@ -171,7 +176,9 @@ class ImageFont:
width, height = self.font.getsize(text) width, height = self.font.getsize(text)
return 0, 0, width, height return 0, 0, width, height
def getlength(self, text, *args, **kwargs): def getlength(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
) -> int:
""" """
Returns length (in pixels) of given text. Returns length (in pixels) of given text.
This is the amount by which following text should be offset. This is the amount by which following text should be offset.
@@ -191,6 +198,9 @@ class ImageFont:
class FreeTypeFont: class FreeTypeFont:
"""FreeType font wrapper (requires _imagingft service)""" """FreeType font wrapper (requires _imagingft service)"""
font: Font
font_bytes: bytes
def __init__( def __init__(
self, self,
font: StrOrBytesPath | BinaryIO | None = None, font: StrOrBytesPath | BinaryIO | None = None,
@@ -201,6 +211,9 @@ class FreeTypeFont:
) -> None: ) -> None:
# FIXME: use service provider instead # FIXME: use service provider instead
if isinstance(core, DeferredError):
raise core.ex
if size <= 0: if size <= 0:
msg = "font size must be greater than 0" msg = "font size must be greater than 0"
raise ValueError(msg) raise ValueError(msg)
@@ -254,14 +267,14 @@ class FreeTypeFont:
path, size, index, encoding, layout_engine = state path, size, index, encoding, layout_engine = state
self.__init__(path, size, index, encoding, layout_engine) self.__init__(path, size, index, encoding, layout_engine)
def getname(self): def getname(self) -> tuple[str | None, str | None]:
""" """
:return: A tuple of the font family (e.g. Helvetica) and the font style :return: A tuple of the font family (e.g. Helvetica) and the font style
(e.g. Bold) (e.g. Bold)
""" """
return self.font.family, self.font.style 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 :return: A tuple of the font ascent (the distance from the baseline to
the highest outline point) and descent (the distance from the the highest outline point) and descent (the distance from the
@@ -269,7 +282,9 @@ class FreeTypeFont:
""" """
return self.font.ascent, self.font.descent return self.font.ascent, self.font.descent
def getlength(self, text, mode="", direction=None, features=None, language=None): def getlength(
self, text: str | bytes, mode="", direction=None, features=None, language=None
) -> float:
""" """
Returns length (in pixels with 1/64 precision) of given text when rendered Returns length (in pixels with 1/64 precision) of given text when rendered
in font with provided direction, features, and language. in font with provided direction, features, and language.
@@ -343,14 +358,14 @@ class FreeTypeFont:
def getbbox( def getbbox(
self, self,
text, text: str | bytes,
mode="", mode: str = "",
direction=None, direction: str | None = None,
features=None, features: list[str] | None = None,
language=None, language: str | None = None,
stroke_width=0, stroke_width: float = 0,
anchor=None, anchor: str | None = None,
): ) -> tuple[float, float, float, float]:
""" """
Returns bounding box (in pixels) of given text relative to given anchor Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language. when rendered in font with provided direction, features, and language.
@@ -500,7 +515,7 @@ class FreeTypeFont:
def getmask2( def getmask2(
self, self,
text, text: str | bytes,
mode="", mode="",
direction=None, direction=None,
features=None, features=None,
@@ -628,7 +643,7 @@ class FreeTypeFont:
layout_engine=layout_engine or self.layout_engine, 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. :returns: A list of the named styles in a variation font.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
@@ -670,10 +685,11 @@ class FreeTypeFont:
msg = "FreeType 2.9.1 or greater is required" msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e raise NotImplementedError(msg) from e
for axis in axes: for axis in axes:
if axis["name"]:
axis["name"] = axis["name"].replace(b"\x00", b"") axis["name"] = axis["name"].replace(b"\x00", b"")
return axes return axes
def set_variation_by_axes(self, axes): def set_variation_by_axes(self, axes: list[float]) -> None:
""" """
:param axes: A list of values for each axis. :param axes: A list of values for each axis.
:exception OSError: If the font is not a variation font. :exception OSError: If the font is not a variation font.
@@ -718,14 +734,14 @@ class TransposedFont:
return 0, 0, height, width return 0, 0, height, width
return 0, 0, width, height return 0, 0, width, height
def getlength(self, text, *args, **kwargs): def getlength(self, text: str | bytes, *args, **kwargs) -> float:
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
msg = "text length is undefined for text rotated by 90 or 270 degrees" msg = "text length is undefined for text rotated by 90 or 270 degrees"
raise ValueError(msg) raise ValueError(msg)
return self.font.getlength(text, *args, **kwargs) return self.font.getlength(text, *args, **kwargs)
def load(filename): def load(filename: str) -> ImageFont:
""" """
Load a font file. This function loads a font object from the given Load a font file. This function loads a font object from the given
bitmap font file, and returns the corresponding font object. bitmap font file, and returns the corresponding font object.
@@ -739,7 +755,13 @@ def load(filename):
return f return f
def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): def truetype(
font: StrOrBytesPath | BinaryIO | None = None,
size: float = 10,
index: int = 0,
encoding: str = "",
layout_engine: Layout | None = None,
) -> FreeTypeFont:
""" """
Load a TrueType or OpenType font from a file or file-like object, Load a TrueType or OpenType font from a file or file-like object,
and create a font object. and create a font object.
@@ -757,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. :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 If the file is not found in this filename, the loader may also
search in other directories, such as the :file:`fonts/` search in other directories, such as:
directory on Windows or :file:`/Library/Fonts/`,
:file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on * The :file:`fonts/` directory on Windows,
macOS. * :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 size: The requested size, in pixels.
:param index: Which font face to load (default is first available face). :param index: Which font face to load (default is first available face).
@@ -800,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. :exception ValueError: If the font size is not greater than zero.
""" """
def freetype(font): def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont:
return FreeTypeFont(font, size, index, encoding, layout_engine) return FreeTypeFont(font, size, index, encoding, layout_engine)
try: try:
@@ -819,12 +846,21 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
if windir: if windir:
dirs.append(os.path.join(windir, "fonts")) dirs.append(os.path.join(windir, "fonts"))
elif sys.platform in ("linux", "linux2"): elif sys.platform in ("linux", "linux2"):
lindirs = os.environ.get("XDG_DATA_DIRS") data_home = os.environ.get("XDG_DATA_HOME")
if not lindirs: if not data_home:
# According to the freedesktop spec, XDG_DATA_DIRS should # The freedesktop spec defines the following default directory for
# default to /usr/share # when XDG_DATA_HOME is unset or empty. This user-level directory
lindirs = "/usr/share" # takes precedence over system-level directories.
dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")] 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": elif sys.platform == "darwin":
dirs += [ dirs += [
"/Library/Fonts", "/Library/Fonts",
@@ -850,7 +886,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
raise raise
def load_path(filename): def load_path(filename: str | bytes) -> ImageFont:
""" """
Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a
bitmap font along the Python path. bitmap font along the Python path.
@@ -859,10 +895,9 @@ def load_path(filename):
:return: A font object. :return: A font object.
:exception OSError: If the file could not be read. :exception OSError: If the file could not be read.
""" """
for directory in sys.path:
if is_directory(directory):
if not isinstance(filename, str): if not isinstance(filename, str):
filename = filename.decode("utf-8") filename = filename.decode("utf-8")
for directory in sys.path:
try: try:
return load(os.path.join(directory, filename)) return load(os.path.join(directory, filename))
except OSError: except OSError:
@@ -871,6 +906,142 @@ def load_path(filename):
raise OSError(msg) 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: def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""If FreeType support is available, load a version of Aileron Regular, """If FreeType support is available, load a version of Aileron Regular,
https://dotcolon.net/font/aileron, with a more limited character set. https://dotcolon.net/font/aileron, with a more limited character set.
@@ -885,8 +1056,8 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
:return: A font object. :return: A font object.
""" """
if core.__class__.__name__ == "module" or size is not None: if isinstance(core, ModuleType) or size is not None:
f = truetype( return truetype(
BytesIO( BytesIO(
base64.b64decode( base64.b64decode(
b""" b"""
@@ -1116,137 +1287,4 @@ AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
10 if size is None else size, 10 if size is None else size,
layout_engine=Layout.BASIC, layout_engine=Layout.BASIC,
) )
else: return load_default_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
+12 -4
View File
@@ -26,7 +26,13 @@ import tempfile
from . import Image 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 xdisplay is None:
if sys.platform == "darwin": if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png") 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 left, top, right, bottom = bbox
im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im return im
# Cast to Optional[str] needed for Windows and macOS.
display_name: str | None = xdisplay
try: try:
if not Image.core.HAVE_XCB: if not Image.core.HAVE_XCB:
msg = "Pillow was built without XCB support" msg = "Pillow was built without XCB support"
raise OSError(msg) raise OSError(msg)
size, data = Image.core.grabscreen_x11(xdisplay) size, data = Image.core.grabscreen_x11(display_name)
except OSError: except OSError:
if ( if (
xdisplay is None display_name is None
and sys.platform not in ("darwin", "win32") and sys.platform not in ("darwin", "win32")
and shutil.which("gnome-screenshot") and shutil.which("gnome-screenshot")
): ):
@@ -94,7 +102,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
return im return im
def grabclipboard(): def grabclipboard() -> Image.Image | list[str] | None:
if sys.platform == "darwin": if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png") fh, filepath = tempfile.mkstemp(".png")
os.close(fh) os.close(fh)
+2 -2
View File
@@ -61,7 +61,7 @@ class _Operand:
out = Image.new(mode or im_1.mode, im_1.size, None) out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load() im_1.load()
try: try:
op = getattr(_imagingmath, op + "_" + im_1.mode) op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e raise TypeError(msg) from e
@@ -89,7 +89,7 @@ class _Operand:
im_1.load() im_1.load()
im_2.load() im_2.load()
try: try:
op = getattr(_imagingmath, op + "_" + im_1.mode) op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e raise TypeError(msg) from e
+15 -19
View File
@@ -16,24 +16,19 @@ from __future__ import annotations
import sys import sys
from functools import lru_cache from functools import lru_cache
from typing import NamedTuple
from ._deprecate import deprecate
class ModeDescriptor: class ModeDescriptor(NamedTuple):
"""Wrapper for mode strings.""" """Wrapper for mode strings."""
def __init__( mode: str
self, bands: tuple[str, ...]
mode: str, basemode: str
bands: tuple[str, ...], basetype: str
basemode: str, typestr: str
basetype: str,
typestr: str,
) -> None:
self.mode = mode
self.bands = bands
self.basemode = basemode
self.basetype = basetype
self.typestr = typestr
def __str__(self) -> str: def __str__(self) -> str:
return self.mode return self.mode
@@ -42,7 +37,6 @@ class ModeDescriptor:
@lru_cache @lru_cache
def getmode(mode: str) -> ModeDescriptor: def getmode(mode: str) -> ModeDescriptor:
"""Gets a mode descriptor for the given mode.""" """Gets a mode descriptor for the given mode."""
# initialize mode cache
endian = "<" if sys.byteorder == "little" else ">" endian = "<" if sys.byteorder == "little" else ">"
modes = { modes = {
@@ -50,8 +44,8 @@ def getmode(mode: str) -> ModeDescriptor:
# Bits need to be extended to bytes # Bits need to be extended to bytes
"1": ("L", "L", ("1",), "|b1"), "1": ("L", "L", ("1",), "|b1"),
"L": ("L", "L", ("L",), "|u1"), "L": ("L", "L", ("L",), "|u1"),
"I": ("L", "I", ("I",), endian + "i4"), "I": ("L", "I", ("I",), f"{endian}i4"),
"F": ("L", "F", ("F",), endian + "f4"), "F": ("L", "F", ("F",), f"{endian}f4"),
"P": ("P", "L", ("P",), "|u1"), "P": ("P", "L", ("P",), "|u1"),
"RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"),
"RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"),
@@ -71,6 +65,8 @@ def getmode(mode: str) -> ModeDescriptor:
"PA": ("RGB", "L", ("P", "A"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"),
} }
if mode in modes: if mode in modes:
if mode in ("BGR;15", "BGR;16", "BGR;24"):
deprecate(mode, 12)
base_mode, base_type, bands, type_str = modes[mode] base_mode, base_type, bands, type_str = modes[mode]
return ModeDescriptor(mode, bands, base_mode, base_type, type_str) return ModeDescriptor(mode, bands, base_mode, base_type, type_str)
@@ -82,8 +78,8 @@ def getmode(mode: str) -> ModeDescriptor:
"I;16LS": "<i2", "I;16LS": "<i2",
"I;16B": ">u2", "I;16B": ">u2",
"I;16BS": ">i2", "I;16BS": ">i2",
"I;16N": endian + "u2", "I;16N": f"{endian}u2",
"I;16NS": endian + "i2", "I;16NS": f"{endian}i2",
"I;32": "<u4", "I;32": "<u4",
"I;32B": ">u4", "I;32B": ">u4",
"I;32L": "<u4", "I;32L": "<u4",
+4 -4
View File
@@ -84,7 +84,7 @@ class LutBuilder:
], ],
} }
if op_name not in known_patterns: if op_name not in known_patterns:
msg = "Unknown pattern " + op_name + "!" msg = f"Unknown pattern {op_name}!"
raise Exception(msg) raise Exception(msg)
self.patterns = known_patterns[op_name] self.patterns = known_patterns[op_name]
@@ -200,7 +200,7 @@ class MorphOp:
elif patterns is not None: elif patterns is not None:
self.lut = LutBuilder(patterns=patterns).build_lut() 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 """Run a single morphological operation on an image
Returns a tuple of the number of changed pixels and the 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) count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage 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 """Get a list of coordinates matching the morphological operation on
an image. an image.
@@ -231,7 +231,7 @@ class MorphOp:
raise ValueError(msg) raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id) 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 """Get a list of all turned on pixels in a binary image
Returns a list of tuples of (x,y) coordinates Returns a list of tuples of (x,y) coordinates
+8 -4
View File
@@ -497,7 +497,7 @@ def expand(
color = _color(fill, image.mode) color = _color(fill, image.mode)
if image.palette: if image.palette:
palette = ImagePalette.ImagePalette(palette=image.getpalette()) palette = ImagePalette.ImagePalette(palette=image.getpalette())
if isinstance(color, tuple): if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
color = palette.getcolor(color) color = palette.getcolor(color)
else: else:
palette = None palette = None
@@ -709,13 +709,17 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
exif_image.info["exif"] = exif.tobytes() exif_image.info["exif"] = exif.tobytes()
elif "Raw profile type exif" in exif_image.info: elif "Raw profile type exif" in exif_image.info:
exif_image.info["Raw profile type exif"] = exif.tobytes().hex() exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
elif "XML:com.adobe.xmp" in exif_image.info: for key in ("XML:com.adobe.xmp", "xmp"):
if key in exif_image.info:
for pattern in ( for pattern in (
r'tiff:Orientation="([0-9])"', r'tiff:Orientation="([0-9])"',
r"<tiff:Orientation>([0-9])</tiff:Orientation>", r"<tiff:Orientation>([0-9])</tiff:Orientation>",
): ):
exif_image.info["XML:com.adobe.xmp"] = re.sub( value = exif_image.info[key]
pattern, "", exif_image.info["XML:com.adobe.xmp"] exif_image.info[key] = (
re.sub(pattern, "", value)
if isinstance(value, str)
else re.sub(pattern.encode(), b"", value)
) )
if not in_place: if not in_place:
return transposed_image return transposed_image
+50 -29
View File
@@ -18,10 +18,13 @@
from __future__ import annotations from __future__ import annotations
import array import array
from typing import Sequence from typing import IO, TYPE_CHECKING, Sequence
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
if TYPE_CHECKING:
from . import Image
class ImagePalette: class ImagePalette:
""" """
@@ -35,23 +38,27 @@ class ImagePalette:
Defaults to an empty palette. 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.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.palette = palette or bytearray()
self.dirty: int | None = None self.dirty: int | None = None
@property @property
def palette(self): def palette(self) -> Sequence[int] | bytes | bytearray:
return self._palette return self._palette
@palette.setter @palette.setter
def palette(self, palette): def palette(self, palette: Sequence[int] | bytes | bytearray) -> None:
self._colors = None self._colors: dict[tuple[int, ...], int] | None = None
self._palette = palette self._palette = palette
@property @property
def colors(self): def colors(self) -> dict[tuple[int, ...], int]:
if self._colors is None: if self._colors is None:
mode_len = len(self.mode) mode_len = len(self.mode)
self._colors = {} self._colors = {}
@@ -63,10 +70,10 @@ class ImagePalette:
return self._colors return self._colors
@colors.setter @colors.setter
def colors(self, colors): def colors(self, colors: dict[tuple[int, ...], int]) -> None:
self._colors = colors self._colors = colors
def copy(self): def copy(self) -> ImagePalette:
new = ImagePalette() new = ImagePalette()
new.mode = self.mode new.mode = self.mode
@@ -77,7 +84,7 @@ class ImagePalette:
return new return new
def getdata(self): def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]:
""" """
Get palette contents in format suitable for the low-level Get palette contents in format suitable for the low-level
``im.putpalette`` primitive. ``im.putpalette`` primitive.
@@ -88,7 +95,7 @@ class ImagePalette:
return self.rawmode, self.palette return self.rawmode, self.palette
return self.mode, self.tobytes() return self.mode, self.tobytes()
def tobytes(self): def tobytes(self) -> bytes:
"""Convert palette to bytes. """Convert palette to bytes.
.. warning:: This method is experimental. .. warning:: This method is experimental.
@@ -104,11 +111,13 @@ class ImagePalette:
# Declare tostring as an alias for tobytes # Declare tostring as an alias for tobytes
tostring = tobytes tostring = tobytes
def _new_color_index(self, image=None, e=None): def _new_color_index(
self, image: Image.Image | None = None, e: Exception | None = None
) -> int:
if not isinstance(self.palette, bytearray): if not isinstance(self.palette, bytearray):
self._palette = bytearray(self.palette) self._palette = bytearray(self.palette)
index = len(self.palette) // 3 index = len(self.palette) // 3
special_colors = () special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
if image: if image:
special_colors = ( special_colors = (
image.info.get("background"), image.info.get("background"),
@@ -128,7 +137,11 @@ class ImagePalette:
raise ValueError(msg) from e raise ValueError(msg) from e
return index return index
def getcolor(self, color, image=None) -> int: def getcolor(
self,
color: tuple[int, ...],
image: Image.Image | None = None,
) -> int:
"""Given an rgb tuple, allocate palette entry. """Given an rgb tuple, allocate palette entry.
.. warning:: This method is experimental. .. warning:: This method is experimental.
@@ -151,22 +164,23 @@ class ImagePalette:
except KeyError as e: except KeyError as e:
# allocate new color slot # allocate new color slot
index = self._new_color_index(image, e) index = self._new_color_index(image, e)
assert isinstance(self._palette, bytearray)
self.colors[color] = index self.colors[color] = index
if index * 3 < len(self.palette): if index * 3 < len(self.palette):
self._palette = ( self._palette = (
self.palette[: index * 3] self._palette[: index * 3]
+ bytes(color) + bytes(color)
+ self.palette[index * 3 + 3 :] + self._palette[index * 3 + 3 :]
) )
else: else:
self._palette += bytes(color) self._palette += bytes(color)
self.dirty = 1 self.dirty = 1
return index return index
else: else:
msg = f"unknown color specifier: {repr(color)}" msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable]
raise ValueError(msg) raise ValueError(msg)
def save(self, fp): def save(self, fp: str | IO[str]) -> None:
"""Save palette to text file. """Save palette to text file.
.. warning:: This method is experimental. .. warning:: This method is experimental.
@@ -193,7 +207,7 @@ class ImagePalette:
# Internal # Internal
def raw(rawmode, data) -> ImagePalette: def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
palette = ImagePalette() palette = ImagePalette()
palette.rawmode = rawmode palette.rawmode = rawmode
palette.palette = data palette.palette = data
@@ -205,50 +219,57 @@ def raw(rawmode, data) -> ImagePalette:
# Factories # Factories
def make_linear_lut(black, white): def make_linear_lut(black: int, white: float) -> list[int]:
if black == 0: 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" msg = "unavailable when black is non-zero"
raise NotImplementedError(msg) # FIXME 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)] 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 = list(range(256 * len(mode)))
palette.reverse() palette.reverse()
return ImagePalette(mode, [i // len(mode) for i in palette]) return ImagePalette(mode, [i // len(mode) for i in palette])
def random(mode="RGB"): def random(mode: str = "RGB") -> ImagePalette:
from random import randint from random import randint
palette = [randint(0, 255) for _ in range(256 * len(mode))] palette = [randint(0, 255) for _ in range(256 * len(mode))]
return ImagePalette(mode, palette) 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)] 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)]) 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))) palette = list(range(256 * len(mode)))
return ImagePalette(mode, [i // len(mode) for i in palette]) 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 # FIXME: supports GIMP gradients only
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
for paletteHandler in [ paletteHandlers: list[
type[
GimpPaletteFile.GimpPaletteFile
| GimpGradientFile.GimpGradientFile
| PaletteFile.PaletteFile
]
] = [
GimpPaletteFile.GimpPaletteFile, GimpPaletteFile.GimpPaletteFile,
GimpGradientFile.GimpGradientFile, GimpGradientFile.GimpGradientFile,
PaletteFile.PaletteFile, PaletteFile.PaletteFile,
]: ]
for paletteHandler in paletteHandlers:
try: try:
fp.seek(0) fp.seek(0)
lut = paletteHandler(fp).getpalette() lut = paletteHandler(fp).getpalette()
+2 -2
View File
@@ -152,7 +152,7 @@ def _toqclass_helper(im):
elif im.mode == "RGBA": elif im.mode == "RGBA":
data = im.tobytes("raw", "BGRA") data = im.tobytes("raw", "BGRA")
format = qt_format.Format_ARGB32 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) im = im.point(lambda i: i * 256)
format = qt_format.Format_Grayscale16 format = qt_format.Format_Grayscale16
@@ -196,7 +196,7 @@ if qt_is_installed:
self.setColorTable(im_data["colortable"]) self.setColorTable(im_data["colortable"])
def toqimage(im): def toqimage(im) -> ImageQt:
return ImageQt(im) return ImageQt(im)
+17 -1
View File
@@ -118,6 +118,8 @@ class Viewer:
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
os.system(self.get_command(path, **options)) # nosec os.system(self.get_command(path, **options)) # nosec
return 1 return 1
@@ -142,6 +144,8 @@ class WindowsViewer(Viewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen( subprocess.Popen(
self.get_command(path, **options), self.get_command(path, **options),
shell=True, shell=True,
@@ -171,6 +175,8 @@ class MacViewer(Viewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.call(["open", "-a", "Preview.app", path]) subprocess.call(["open", "-a", "Preview.app", path])
executable = sys.executable or shutil.which("python3") executable = sys.executable or shutil.which("python3")
if executable: if executable:
@@ -199,7 +205,7 @@ class UnixViewer(Viewer):
def get_command(self, file: str, **options: Any) -> str: def get_command(self, file: str, **options: Any) -> str:
command = self.get_command_ex(file, **options)[0] command = self.get_command_ex(file, **options)[0]
return f"({command} {quote(file)}" return f"{command} {quote(file)}"
class XDGViewer(UnixViewer): class XDGViewer(UnixViewer):
@@ -215,6 +221,8 @@ class XDGViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(["xdg-open", path]) subprocess.Popen(["xdg-open", path])
return 1 return 1
@@ -237,6 +245,8 @@ class DisplayViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
args = ["display"] args = ["display"]
title = options.get("title") title = options.get("title")
if title: if title:
@@ -259,6 +269,8 @@ class GmDisplayViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(["gm", "display", path]) subprocess.Popen(["gm", "display", path])
return 1 return 1
@@ -275,6 +287,8 @@ class EogViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(["eog", "-n", path]) subprocess.Popen(["eog", "-n", path])
return 1 return 1
@@ -299,6 +313,8 @@ class XVViewer(UnixViewer):
""" """
Display given file. Display given file.
""" """
if not os.path.exists(path):
raise FileNotFoundError
args = ["xv"] args = ["xv"]
title = options.get("title") title = options.get("title")
if title: if title:
+66 -35
View File
@@ -23,35 +23,58 @@
from __future__ import annotations from __future__ import annotations
import math import math
from functools import cached_property
from . import Image
class Stat: class Stat:
def __init__(self, image_or_list, mask=None): def __init__(
try: self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None
if mask: ) -> None:
"""
Calculate statistics for the given image. If a mask is included,
only the regions covered by that mask are included in the
statistics. You can also pass in a previously calculated histogram.
:param image: A PIL image, or a precalculated histogram.
.. note::
For a PIL image, calculations rely on the
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
grouped into 256 bins, even if the image has more than 8 bits per
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
of more than 255.
:param mask: An optional mask.
"""
if isinstance(image_or_list, Image.Image):
self.h = image_or_list.histogram(mask) self.h = image_or_list.histogram(mask)
elif isinstance(image_or_list, list):
self.h = image_or_list
else: else:
self.h = image_or_list.histogram() msg = "first argument must be image or list" # type: ignore[unreachable]
except AttributeError:
self.h = image_or_list # assume it to be a histogram list
if not isinstance(self.h, list):
msg = "first argument must be image or list"
raise TypeError(msg) raise TypeError(msg)
self.bands = list(range(len(self.h) // 256)) self.bands = list(range(len(self.h) // 256))
def __getattr__(self, id): @cached_property
"""Calculate missing attribute""" def extrema(self) -> list[tuple[int, int]]:
if id[:4] == "_get": """
raise AttributeError(id) Min/max values for each band in the image.
# calculate missing attribute
v = getattr(self, "_get" + id)()
setattr(self, id, v)
return v
def _getextrema(self): .. note::
"""Get min/max values for each band in the image""" This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and
simply returns the low and high bins used. This is correct for
images with 8 bits per channel, but fails for other modes such as
``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to
return per-band extrema for the image. This is more correct and
efficient because, for non-8-bit modes, the histogram method uses
:py:meth:`~PIL.Image.Image.getextrema` to determine the bins used.
"""
def minmax(histogram): def minmax(histogram: list[int]) -> tuple[int, int]:
res_min, res_max = 255, 0 res_min, res_max = 255, 0
for i in range(256): for i in range(256):
if histogram[i]: if histogram[i]:
@@ -65,12 +88,14 @@ class Stat:
return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)] return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)]
def _getcount(self): @cached_property
"""Get total number of pixels in each layer""" def count(self) -> list[int]:
"""Total number of pixels for each band in the image."""
return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)] return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)]
def _getsum(self): @cached_property
"""Get sum of all pixels in each layer""" def sum(self) -> list[float]:
"""Sum of all pixels for each band in the image."""
v = [] v = []
for i in range(0, len(self.h), 256): for i in range(0, len(self.h), 256):
@@ -80,8 +105,9 @@ class Stat:
v.append(layer_sum) v.append(layer_sum)
return v return v
def _getsum2(self): @cached_property
"""Get squared sum of all pixels in each layer""" def sum2(self) -> list[float]:
"""Squared sum of all pixels for each band in the image."""
v = [] v = []
for i in range(0, len(self.h), 256): for i in range(0, len(self.h), 256):
@@ -91,12 +117,14 @@ class Stat:
v.append(sum2) v.append(sum2)
return v return v
def _getmean(self): @cached_property
"""Get average pixel level for each layer""" def mean(self) -> list[float]:
"""Average (arithmetic mean) pixel level for each band in the image."""
return [self.sum[i] / self.count[i] for i in self.bands] return [self.sum[i] / self.count[i] for i in self.bands]
def _getmedian(self): @cached_property
"""Get median pixel level for each layer""" def median(self) -> list[int]:
"""Median pixel level for each band in the image."""
v = [] v = []
for i in self.bands: for i in self.bands:
@@ -110,19 +138,22 @@ class Stat:
v.append(j) v.append(j)
return v return v
def _getrms(self): @cached_property
"""Get RMS for each layer""" def rms(self) -> list[float]:
"""RMS (root-mean-square) for each band in the image."""
return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
def _getvar(self): @cached_property
"""Get variance for each layer""" def var(self) -> list[float]:
"""Variance for each band in the image."""
return [ return [
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
for i in self.bands for i in self.bands
] ]
def _getstddev(self): @cached_property
"""Get standard deviation for each layer""" def stddev(self) -> list[float]:
"""Standard deviation for each band in the image."""
return [math.sqrt(self.var[i]) for i in self.bands] return [math.sqrt(self.var[i]) for i in self.bands]
+11 -11
View File
@@ -37,7 +37,7 @@ from . import Image
_pilbitmap_ok = None _pilbitmap_ok = None
def _pilbitmap_check(): def _pilbitmap_check() -> int:
global _pilbitmap_ok global _pilbitmap_ok
if _pilbitmap_ok is None: if _pilbitmap_ok is None:
try: try:
@@ -128,7 +128,7 @@ class PhotoImage:
if image: if image:
self.paste(image) self.paste(image)
def __del__(self): def __del__(self) -> None:
name = self.__photo.name name = self.__photo.name
self.__photo.name = None self.__photo.name = None
try: try:
@@ -136,7 +136,7 @@ class PhotoImage:
except Exception: except Exception:
pass # ignore internal errors pass # ignore internal errors
def __str__(self): def __str__(self) -> str:
""" """
Get the Tkinter photo image identifier. This method is automatically Get the Tkinter photo image identifier. This method is automatically
called by Tkinter whenever a PhotoImage object is passed to a Tkinter called by Tkinter whenever a PhotoImage object is passed to a Tkinter
@@ -146,7 +146,7 @@ class PhotoImage:
""" """
return str(self.__photo) return str(self.__photo)
def width(self): def width(self) -> int:
""" """
Get the width of the image. Get the width of the image.
@@ -154,7 +154,7 @@ class PhotoImage:
""" """
return self.__size[0] return self.__size[0]
def height(self): def height(self) -> int:
""" """
Get the height of the image. Get the height of the image.
@@ -162,7 +162,7 @@ class PhotoImage:
""" """
return self.__size[1] return self.__size[1]
def paste(self, im): def paste(self, im: Image.Image) -> None:
""" """
Paste a PIL image into the photo image. Note that this can Paste a PIL image into the photo image. Note that this can
be very slow if the photo image is displayed. be very slow if the photo image is displayed.
@@ -219,7 +219,7 @@ class BitmapImage:
kw["data"] = image.tobitmap() kw["data"] = image.tobitmap()
self.__photo = tkinter.BitmapImage(**kw) self.__photo = tkinter.BitmapImage(**kw)
def __del__(self): def __del__(self) -> None:
name = self.__photo.name name = self.__photo.name
self.__photo.name = None self.__photo.name = None
try: try:
@@ -227,7 +227,7 @@ class BitmapImage:
except Exception: except Exception:
pass # ignore internal errors pass # ignore internal errors
def width(self): def width(self) -> int:
""" """
Get the width of the image. Get the width of the image.
@@ -235,7 +235,7 @@ class BitmapImage:
""" """
return self.__size[0] return self.__size[0]
def height(self): def height(self) -> int:
""" """
Get the height of the image. Get the height of the image.
@@ -243,7 +243,7 @@ class BitmapImage:
""" """
return self.__size[1] return self.__size[1]
def __str__(self): def __str__(self) -> str:
""" """
Get the Tkinter bitmap image identifier. This method is automatically Get the Tkinter bitmap image identifier. This method is automatically
called by Tkinter whenever a BitmapImage object is passed to a Tkinter called by Tkinter whenever a BitmapImage object is passed to a Tkinter
@@ -254,7 +254,7 @@ class BitmapImage:
return str(self.__photo) return str(self.__photo)
def getimage(photo): def getimage(photo: PhotoImage) -> Image.Image:
"""Copies the contents of a PhotoImage to a PIL image memory.""" """Copies the contents of a PhotoImage to a PIL image memory."""
im = Image.new("RGBA", (photo.width(), photo.height())) im = Image.new("RGBA", (photo.width(), photo.height()))
block = im.im block = im.im
@@ -14,7 +14,7 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import Sequence from typing import Any, Sequence
from . import Image from . import Image
@@ -24,7 +24,7 @@ class Transform(Image.ImageTransformHandler):
method: Image.Transform method: Image.Transform
def __init__(self, data: Sequence[int]) -> None: def __init__(self, data: Sequence[Any]) -> None:
self.data = data self.data = data
def getdata(self) -> tuple[Image.Transform, Sequence[int]]: def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
@@ -34,7 +34,7 @@ class Transform(Image.ImageTransformHandler):
self, self,
size: tuple[int, int], size: tuple[int, int],
image: Image.Image, image: Image.Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]], **options: Any,
) -> Image.Image: ) -> Image.Image:
"""Perform the transform. Called from :py:meth:`.Image.transform`.""" """Perform the transform. Called from :py:meth:`.Image.transform`."""
# can be overridden # can be overridden
+24 -17
View File
@@ -28,10 +28,10 @@ class HDC:
methods. methods.
""" """
def __init__(self, dc): def __init__(self, dc: int) -> None:
self.dc = dc self.dc = dc
def __int__(self): def __int__(self) -> int:
return self.dc return self.dc
@@ -42,10 +42,10 @@ class HWND:
methods, instead of a DC. methods, instead of a DC.
""" """
def __init__(self, wnd): def __init__(self, wnd: int) -> None:
self.wnd = wnd self.wnd = wnd
def __int__(self): def __int__(self) -> int:
return self.wnd return self.wnd
@@ -69,19 +69,22 @@ class Dib:
defines the size of the image. defines the size of the image.
""" """
def __init__(self, image, size=None): def __init__(
if hasattr(image, "mode") and hasattr(image, "size"): 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 mode = image.mode
size = image.size size = image.size
else:
mode = image
image = None
if mode not in ["1", "L", "P", "RGB"]: if mode not in ["1", "L", "P", "RGB"]:
mode = Image.getmodebase(mode) mode = Image.getmodebase(mode)
self.image = Image.core.display(mode, size) self.image = Image.core.display(mode, size)
self.mode = mode self.mode = mode
self.size = size self.size = size
if image: if image:
assert not isinstance(image, str)
self.paste(image) self.paste(image)
def expose(self, handle): def expose(self, handle):
@@ -149,7 +152,9 @@ class Dib:
result = self.image.query_palette(handle) result = self.image.query_palette(handle)
return result 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. Paste a PIL image into the bitmap image.
@@ -169,16 +174,16 @@ class Dib:
else: else:
self.image.paste(im.im) self.image.paste(im.im)
def frombytes(self, buffer): def frombytes(self, buffer: bytes) -> None:
""" """
Load display memory contents from byte data. Load display memory contents from byte data.
:param buffer: A buffer containing display data (usually :param buffer: A buffer containing display data (usually
data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) 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. Copy display memory contents to bytes object.
@@ -190,13 +195,15 @@ class Dib:
class Window: class Window:
"""Create a Window with the given title size.""" """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( self.hwnd = Image.core.createwindow(
title, self.__dispatcher, width or 0, height or 0 title, self.__dispatcher, width or 0, height or 0
) )
def __dispatcher(self, action, *args): def __dispatcher(self, action, *args):
return getattr(self, "ui_handle_" + action)(*args) return getattr(self, f"ui_handle_{action}")(*args)
def ui_handle_clear(self, dc, x0, y0, x1, y1): def ui_handle_clear(self, dc, x0, y0, x1, y1):
pass pass
@@ -204,7 +211,7 @@ class Window:
def ui_handle_damage(self, x0, y0, x1, y1): def ui_handle_damage(self, x0, y0, x1, y1):
pass pass
def ui_handle_destroy(self): def ui_handle_destroy(self) -> None:
pass pass
def ui_handle_repair(self, dc, x0, y0, x1, y1): def ui_handle_repair(self, dc, x0, y0, x1, y1):
@@ -213,7 +220,7 @@ class Window:
def ui_handle_resize(self, width, height): def ui_handle_resize(self, width, height):
pass pass
def mainloop(self): def mainloop(self) -> None:
Image.core.eventloop() Image.core.eventloop()
@@ -57,7 +57,7 @@ def dump(c: Sequence[int | bytes]) -> None:
""".. deprecated:: 10.2.0""" """.. deprecated:: 10.2.0"""
deprecate("IptcImagePlugin.dump", 12) deprecate("IptcImagePlugin.dump", 12)
for i in c: for i in c:
print("%02x" % _i8(i), end=" ") print(f"{_i8(i):02x}", end=" ")
print() print()
@@ -18,6 +18,7 @@ from __future__ import annotations
import io import io
import os import os
import struct import struct
from typing import IO, Tuple, cast
from . import Image, ImageFile, ImagePalette, _binary from . import Image, ImageFile, ImagePalette, _binary
@@ -34,7 +35,7 @@ class BoxReader:
self.length = length self.length = length
self.remaining_in_box = -1 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: if self.has_length and self.fp.tell() + num_bytes > self.length:
# Outside box: ensure we don't read past the known file length # Outside box: ensure we don't read past the known file length
return False return False
@@ -44,7 +45,7 @@ class BoxReader:
else: else:
return True # No length known, just read 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): if not self._can_read(num_bytes):
msg = "Not enough data in header" msg = "Not enough data in header"
raise SyntaxError(msg) raise SyntaxError(msg)
@@ -58,32 +59,32 @@ class BoxReader:
self.remaining_in_box -= num_bytes self.remaining_in_box -= num_bytes
return data return data
def read_fields(self, field_format): def read_fields(self, field_format: str) -> tuple[int | bytes, ...]:
size = struct.calcsize(field_format) size = struct.calcsize(field_format)
data = self._read_bytes(size) data = self._read_bytes(size)
return struct.unpack(field_format, data) return struct.unpack(field_format, data)
def read_boxes(self): def read_boxes(self) -> BoxReader:
size = self.remaining_in_box size = self.remaining_in_box
data = self._read_bytes(size) data = self._read_bytes(size)
return BoxReader(io.BytesIO(data), size) return BoxReader(io.BytesIO(data), size)
def has_next_box(self): def has_next_box(self) -> bool:
if self.has_length: if self.has_length:
return self.fp.tell() + self.remaining_in_box < self.length return self.fp.tell() + self.remaining_in_box < self.length
else: else:
return True 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 # Skip the rest of the box if it has not been read
if self.remaining_in_box > 0: if self.remaining_in_box > 0:
self.fp.seek(self.remaining_in_box, os.SEEK_CUR) self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
self.remaining_in_box = -1 self.remaining_in_box = -1
# Read the length and type of the next box # Read the length and type of the next box
lbox, tbox = self.read_fields(">I4s") lbox, tbox = cast(Tuple[int, bytes], self.read_fields(">I4s"))
if lbox == 1: if lbox == 1:
lbox = self.read_fields(">Q")[0] lbox = cast(int, self.read_fields(">Q")[0])
hlen = 16 hlen = 16
else: else:
hlen = 8 hlen = 8
@@ -96,7 +97,7 @@ class BoxReader:
return tbox return tbox
def _parse_codestream(fp): def _parse_codestream(fp) -> tuple[tuple[int, int], str]:
"""Parse the JPEG 2000 codestream to extract the size and component """Parse the JPEG 2000 codestream to extract the size and component
count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
@@ -121,16 +122,18 @@ def _parse_codestream(fp):
elif csiz == 4: elif csiz == 4:
mode = "RGBA" mode = "RGBA"
else: else:
mode = None msg = "unable to determine J2K image mode"
raise SyntaxError(msg)
return size, mode return size, mode
def _res_to_dpi(num, denom, exp): def _res_to_dpi(num: int, denom: int, exp: int) -> float | None:
"""Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
calculated as (num / denom) * 10^exp and stored in dots per meter, calculated as (num / denom) * 10^exp and stored in dots per meter,
to floating-point dots per inch.""" to floating-point dots per inch."""
if denom != 0: if denom == 0:
return None
return (254 * num * (10**exp)) / (10000 * denom) return (254 * num * (10**exp)) / (10000 * denom)
@@ -176,6 +179,10 @@ def _parse_jp2_header(fp):
mode = "RGB" mode = "RGB"
elif nc == 4: elif nc == 4:
mode = "RGBA" mode = "RGBA"
elif tbox == b"colr" and nc == 4:
meth, _, _, enumcs = header.read_fields(">BBBI")
if meth == 1 and enumcs == 12:
mode = "CMYK"
elif tbox == b"pclr" and mode in ("L", "LA"): elif tbox == b"pclr" and mode in ("L", "LA"):
ne, npc = header.read_fields(">HB") ne, npc = header.read_fields(">HB")
bitdepths = header.read_fields(">" + ("B" * npc)) bitdepths = header.read_fields(">" + ("B" * npc))
@@ -211,7 +218,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
format = "JPEG2000" format = "JPEG2000"
format_description = "JPEG 2000 (ISO 15444)" format_description = "JPEG 2000 (ISO 15444)"
def _open(self): def _open(self) -> None:
sig = self.fp.read(4) sig = self.fp.read(4)
if sig == b"\xff\x4f\xff\x51": if sig == b"\xff\x4f\xff\x51":
self.codec = "j2k" self.codec = "j2k"
@@ -231,10 +238,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
msg = "not a JPEG 2000 file" msg = "not a JPEG 2000 file"
raise SyntaxError(msg) raise SyntaxError(msg)
if self.size is None or self.mode is None:
msg = "unable to determine size/mode"
raise SyntaxError(msg)
self._reduce = 0 self._reduce = 0
self.layers = 0 self.layers = 0
@@ -263,7 +266,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
) )
] ]
def _parse_comment(self): def _parse_comment(self) -> None:
hdr = self.fp.read(2) hdr = self.fp.read(2)
length = _binary.i16be(hdr) length = _binary.i16be(hdr)
self.fp.seek(length - 2, os.SEEK_CUR) self.fp.seek(length - 2, os.SEEK_CUR)
@@ -313,7 +316,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
return ImageFile.ImageFile.load(self) return ImageFile.ImageFile.load(self)
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return ( return (
prefix[:4] == b"\xff\x4f\xff\x51" prefix[:4] == b"\xff\x4f\xff\x51"
or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
@@ -324,11 +327,13 @@ def _accept(prefix):
# Save support # Save support
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# Get the keyword arguments # Get the keyword arguments
info = im.encoderinfo info = im.encoderinfo
if filename.endswith(".j2k") or info.get("no_jp2", False): if isinstance(filename, str):
filename = filename.encode()
if filename.endswith(b".j2k") or info.get("no_jp2", False):
kind = "j2k" kind = "j2k"
else: else:
kind = "jp2" kind = "jp2"
+54 -61
View File
@@ -42,6 +42,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
import warnings import warnings
from typing import IO, Any
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16
@@ -54,7 +55,7 @@ from .JpegPresets import presets
# Parser # Parser
def Skip(self, marker): def Skip(self: JpegImageFile, marker: int) -> None:
n = i16(self.fp.read(2)) - 2 n = i16(self.fp.read(2)) - 2
ImageFile._safe_read(self.fp, n) ImageFile._safe_read(self.fp, n)
@@ -94,6 +95,8 @@ def APP(self, marker):
else: else:
self.info["exif"] = s self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6 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": elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete) # extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change self.info["flashpix"] = s # FIXME: value will change
@@ -158,40 +161,8 @@ def APP(self, marker):
# plus constant header size # plus constant header size
self.info["mpoffset"] = self.fp.tell() - n + 4 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: JpegImageFile, marker: int) -> None:
def COM(self, marker):
# #
# Comment marker. Store these in the APP dictionary. # Comment marker. Store these in the APP dictionary.
n = i16(self.fp.read(2)) - 2 n = i16(self.fp.read(2)) - 2
@@ -202,7 +173,7 @@ def COM(self, marker):
self.applist.append(("COM", s)) 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 # Start of frame marker. Defines the size and mode of the
# image. JPEG is colour blind, so we use some simple # image. JPEG is colour blind, so we use some simple
@@ -250,7 +221,7 @@ def SOF(self, marker):
self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) 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 # Define quantization table. Note that there might be more
# than one table in each marker. # than one table in each marker.
@@ -344,7 +315,7 @@ MARKER = {
} }
def _accept(prefix): def _accept(prefix: bytes) -> bool:
# Magic number was taken from https://en.wikipedia.org/wiki/JPEG # Magic number was taken from https://en.wikipedia.org/wiki/JPEG
return prefix[:3] == b"\xFF\xD8\xFF" return prefix[:3] == b"\xFF\xD8\xFF"
@@ -408,7 +379,9 @@ class JpegImageFile(ImageFile.ImageFile):
msg = "no marker found" msg = "no marker found"
raise SyntaxError(msg) raise SyntaxError(msg)
def load_read(self, read_bytes): self._read_dpi_from_exif()
def load_read(self, read_bytes: int) -> bytes:
""" """
internal: read more image data internal: read more image data
For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
@@ -424,13 +397,15 @@ class JpegImageFile(ImageFile.ImageFile):
return s return s
def draft(self, mode, size): def draft(
self, mode: str | None, size: tuple[int, int] | None
) -> tuple[str, tuple[int, int, float, float]] | None:
if len(self.tile) != 1: if len(self.tile) != 1:
return return None
# Protect from second call # Protect from second call
if self.decoderconfig: if self.decoderconfig:
return return None
d, e, o, a = self.tile[0] d, e, o, a = self.tile[0]
scale = 1 scale = 1
@@ -460,7 +435,7 @@ class JpegImageFile(ImageFile.ImageFile):
box = (0, 0, original_size[0] / scale, original_size[1] / scale) box = (0, 0, original_size[0] / scale, original_size[1] / scale)
return self.mode, box return self.mode, box
def load_djpeg(self): def load_djpeg(self) -> None:
# ALTERNATIVE: handle JPEGs via the IJG command line utilities # ALTERNATIVE: handle JPEGs via the IJG command line utilities
f, path = tempfile.mkstemp() f, path = tempfile.mkstemp()
@@ -491,29 +466,43 @@ class JpegImageFile(ImageFile.ImageFile):
self.tile = [] self.tile = []
def _getexif(self): def _getexif(self) -> dict[str, Any] | None:
return _getexif(self) return _getexif(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): def _getmp(self):
return _getmp(self) return _getmp(self)
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary. def _getexif(self) -> dict[str, Any] | None:
"""
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):
if "exif" not in self.info: if "exif" not in self.info:
return None return None
return self.getexif()._get_merged_dict() return self.getexif()._get_merged_dict()
@@ -641,7 +630,7 @@ def get_sampling(im):
return samplings.get(sampling, -1) return samplings.get(sampling, -1)
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.width == 0 or im.height == 0: if im.width == 0 or im.height == 0:
msg = "cannot write empty image as JPEG" msg = "cannot write empty image as JPEG"
raise ValueError(msg) raise ValueError(msg)
@@ -824,7 +813,7 @@ def _save(im, fp, filename):
ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize)
def _save_cjpeg(im, fp, filename): def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# ALTERNATIVE: handle JPEGs via the IJG command line utilities. # ALTERNATIVE: handle JPEGs via the IJG command line utilities.
tempfile = im._dump() tempfile = im._dump()
subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) subprocess.check_call(["cjpeg", "-outfile", filename, tempfile])
@@ -841,6 +830,10 @@ def jpeg_factory(fp=None, filename=None):
try: try:
mpheader = im._getmp() mpheader = im._getmp()
if mpheader[45057] > 1: 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 # It's actually an MPO
from .MpoImagePlugin import MpoImageFile from .MpoImagePlugin import MpoImageFile
@@ -25,7 +25,7 @@ from . import Image, TiffImagePlugin
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:8] == olefile.MAGIC return prefix[:8] == olefile.MAGIC
@@ -38,7 +38,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
format_description = "Microsoft Image Composer" format_description = "Microsoft Image Composer"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
# read the OLE directory and see if this is a likely # read the OLE directory and see if this is a likely
# to be a Microsoft Image Composer file # to be a Microsoft Image Composer file
@@ -63,7 +63,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
msg = "not an MIC file; no image entries" msg = "not an MIC file; no image entries"
raise SyntaxError(msg) raise SyntaxError(msg)
self.frame = None self.frame = -1
self._n_frames = len(self.images) self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1 self.is_animated = self._n_frames > 1
@@ -85,15 +85,15 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.frame = frame self.frame = frame
def tell(self): def tell(self) -> int:
return self.frame return self.frame
def close(self): def close(self) -> None:
self.__fp.close() self.__fp.close()
self.ole.close() self.ole.close()
super().close() super().close()
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.__fp.close() self.__fp.close()
self.ole.close() self.ole.close()
super().__exit__() super().__exit__()
@@ -53,6 +53,10 @@ class BitStream:
return v return v
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\x00\x00\x01\xb3"
## ##
# Image plugin for MPEG streams. This plugin can identify a stream, # Image plugin for MPEG streams. This plugin can identify a stream,
# but it cannot read it. # but it cannot read it.
@@ -77,7 +81,7 @@ class MpegImageFile(ImageFile.ImageFile):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Registry stuff # Registry stuff
Image.register_open(MpegImageFile.format, MpegImageFile) Image.register_open(MpegImageFile.format, MpegImageFile, _accept)
Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"]) Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"])
+9 -13
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import itertools import itertools
import os import os
import struct import struct
from typing import IO
from . import ( from . import (
Image, Image,
@@ -32,23 +33,18 @@ from . import (
from ._binary import o32le from ._binary import o32le
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
JpegImagePlugin._save(im, fp, filename) JpegImagePlugin._save(im, fp, filename)
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
append_images = im.encoderinfo.get("append_images", []) append_images = im.encoderinfo.get("append_images", [])
if not append_images: if not append_images and not getattr(im, "is_animated", False):
try:
animated = im.is_animated
except AttributeError:
animated = False
if not animated:
_save(im, fp, filename) _save(im, fp, filename)
return return
mpf_offset = 28 mpf_offset = 28
offsets = [] offsets: list[int] = []
for imSequence in itertools.chain([im], append_images): for imSequence in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(imSequence): for im_frame in ImageSequence.Iterator(imSequence):
if not offsets: if not offsets:
@@ -100,7 +96,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
format_description = "MPO (CIPA DC-007)" format_description = "MPO (CIPA DC-007)"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
self.fp.seek(0) # prep the fp in order to pass the JPEG test self.fp.seek(0) # prep the fp in order to pass the JPEG test
JpegImagePlugin.JpegImageFile._open(self) JpegImagePlugin.JpegImageFile._open(self)
self._after_jpeg_open() self._after_jpeg_open()
@@ -124,10 +120,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
# for now we can only handle reading and individual frame extraction # for now we can only handle reading and individual frame extraction
self.readonly = 1 self.readonly = 1
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
self._fp.seek(pos) self._fp.seek(pos)
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self.fp = self._fp self.fp = self._fp
@@ -149,7 +145,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])] self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
self.__frame = frame self.__frame = frame
def tell(self): def tell(self) -> int:
return self.__frame return self.__frame
@staticmethod @staticmethod
@@ -164,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder)
# write MSP files (uncompressed only) # write MSP files (uncompressed only)
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode != "1": if im.mode != "1":
msg = f"cannot write mode {im.mode} as MSP" msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg) raise OSError(msg)
+26 -19
View File
@@ -17,6 +17,7 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from typing import TYPE_CHECKING
from . import EpsImagePlugin from . import EpsImagePlugin
@@ -38,7 +39,7 @@ class PSDraw:
fp = sys.stdout fp = sys.stdout
self.fp = fp 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.)""" """Set up printing of a document. (Write PostScript DSC header.)"""
# FIXME: incomplete # FIXME: incomplete
self.fp.write( self.fp.write(
@@ -52,30 +53,32 @@ class PSDraw:
self.fp.write(EDROFF_PS) self.fp.write(EDROFF_PS)
self.fp.write(VDI_PS) self.fp.write(VDI_PS)
self.fp.write(b"%%EndProlog\n") self.fp.write(b"%%EndProlog\n")
self.isofont = {} self.isofont: dict[bytes, int] = {}
def end_document(self): def end_document(self) -> None:
"""Ends printing. (Write PostScript DSC footer.)""" """Ends printing. (Write PostScript DSC footer.)"""
self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n")
if hasattr(self.fp, "flush"): if hasattr(self.fp, "flush"):
self.fp.flush() self.fp.flush()
def setfont(self, font, size): def setfont(self, font: str, size: int) -> None:
""" """
Selects which font to use. Selects which font to use.
:param font: A PostScript font name :param font: A PostScript font name
:param size: Size in points. :param size: Size in points.
""" """
font = bytes(font, "UTF-8") font_bytes = bytes(font, "UTF-8")
if font not in self.isofont: if font_bytes not in self.isofont:
# reencode font # reencode font
self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font)) self.fp.write(
self.isofont[font] = 1 b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes)
)
self.isofont[font_bytes] = 1
# rough # 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 Draws a line between the two points. Coordinates are given in
PostScript point coordinates (72 points per inch, (0, 0) is the lower 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)) 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. Draws a rectangle.
@@ -92,18 +95,22 @@ class PSDraw:
""" """
self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) 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 Draws text at the given position. You must use
:py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
""" """
text = bytes(text, "UTF-8") text_bytes = bytes(text, "UTF-8")
text = b"\\(".join(text.split(b"(")) text_bytes = b"\\(".join(text_bytes.split(b"("))
text = b"\\)".join(text.split(b")")) text_bytes = b"\\)".join(text_bytes.split(b")"))
xy += (text,) self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,)))
self.fp.write(b"%d %d M (%s) S\n" % xy)
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.""" """Draw a PIL image, centered in the given box."""
# default resolution depends on mode # default resolution depends on mode
if not dpi: if not dpi:
@@ -131,7 +138,7 @@ class PSDraw:
sx = x / im.size[0] sx = x / im.size[0]
sy = y / im.size[1] sy = y / im.size[1]
self.fp.write(b"%f %f scale\n" % (sx, sy)) self.fp.write(b"%f %f scale\n" % (sx, sy))
EpsImagePlugin._save(im, self.fp, None, 0) EpsImagePlugin._save(im, self.fp, "", 0)
self.fp.write(b"\ngrestore\n") self.fp.write(b"\ngrestore\n")
+7 -5
View File
@@ -14,6 +14,8 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import IO
from ._binary import o8 from ._binary import o8
@@ -22,8 +24,8 @@ class PaletteFile:
rawmode = "RGB" rawmode = "RGB"
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
self.palette = [(i, i, i) for i in range(256)] palette = [o8(i) * 3 for i in range(256)]
while True: while True:
s = fp.readline() s = fp.readline()
@@ -44,9 +46,9 @@ class PaletteFile:
g = b = r g = b = r
if 0 <= i <= 255: if 0 <= i <= 255:
self.palette[i] = o8(r) + o8(g) + o8(b) palette[i] = o8(r) + o8(g) + o8(b)
self.palette = b"".join(self.palette) self.palette = b"".join(palette)
def getpalette(self): def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode return self.palette, self.rawmode
+12 -9
View File
@@ -8,6 +8,8 @@
## ##
from __future__ import annotations from __future__ import annotations
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import o8 from ._binary import o8
from ._binary import o16be as o16b from ._binary import o16be as o16b
@@ -82,10 +84,10 @@ _Palm8BitColormapValues = (
# so build a prototype image to be used for palette resampling # so build a prototype image to be used for palette resampling
def build_prototype_image(): def build_prototype_image() -> Image.Image:
image = Image.new("L", (1, len(_Palm8BitColormapValues))) image = Image.new("L", (1, len(_Palm8BitColormapValues)))
image.putdata(list(range(len(_Palm8BitColormapValues)))) image.putdata(list(range(len(_Palm8BitColormapValues))))
palettedata = () palettedata: tuple[int, ...] = ()
for colormapValue in _Palm8BitColormapValues: for colormapValue in _Palm8BitColormapValues:
palettedata += colormapValue palettedata += colormapValue
palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues))
@@ -112,7 +114,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00}
# (Internal) Image save plugin for the Palm format. # (Internal) Image save plugin for the Palm format.
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "P": if im.mode == "P":
# we assume this is a color Palm image with the standard colormap, # we assume this is a color Palm image with the standard colormap,
# unless the "info" dict has a "custom-colormap" field # unless the "info" dict has a "custom-colormap" field
@@ -127,22 +129,23 @@ def _save(im, fp, filename):
# and invert it because # and invert it because
# Palm does grayscale from white (0) to black (1) # Palm does grayscale from white (0) to black (1)
bpp = im.encoderinfo["bpp"] bpp = im.encoderinfo["bpp"]
im = im.point( maxval = (1 << bpp) - 1
lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift) shift = 8 - bpp
) im = im.point(lambda x: maxval - (x >> shift))
elif im.info.get("bpp") in (1, 2, 4): elif im.info.get("bpp") in (1, 2, 4):
# here we assume that even though the inherent mode is 8-bit grayscale, # here we assume that even though the inherent mode is 8-bit grayscale,
# only the lower bpp bits are significant. # only the lower bpp bits are significant.
# We invert them to match the Palm. # We invert them to match the Palm.
bpp = im.info["bpp"] bpp = im.info["bpp"]
im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) maxval = (1 << bpp) - 1
im = im.point(lambda x: maxval - (x & maxval))
else: else:
msg = f"cannot write mode {im.mode} as Palm" msg = f"cannot write mode {im.mode} as Palm"
raise OSError(msg) raise OSError(msg)
# we ignore the palette here # we ignore the palette here
im.mode = "P" im._mode = "P"
rawmode = "P;" + str(bpp) rawmode = f"P;{bpp}"
version = 1 version = 1
elif im.mode == "1": elif im.mode == "1":
@@ -144,7 +144,7 @@ SAVE = {
} }
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
version, bits, planes, rawmode = SAVE[im.mode] version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e: except KeyError as e:
@@ -25,6 +25,7 @@ import io
import math import math
import os import os
import time import time
from typing import IO
from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
@@ -39,7 +40,7 @@ from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
# 5. page contents # 5. page contents
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True) _save(im, fp, filename, save_all=True)
+37 -42
View File
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any, List, NamedTuple, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
# on page 656 # on page 656
def encode_text(s): def encode_text(s: str) -> bytes:
return codecs.BOM_UTF16_BE + s.encode("utf_16_be") return codecs.BOM_UTF16_BE + s.encode("utf_16_be")
@@ -76,7 +76,7 @@ class PdfFormatError(RuntimeError):
pass pass
def check_format_condition(condition, error_message): def check_format_condition(condition: bool, error_message: str) -> None:
if not condition: if not condition:
raise PdfFormatError(error_message) raise PdfFormatError(error_message)
@@ -87,28 +87,27 @@ class IndirectReferenceTuple(NamedTuple):
class IndirectReference(IndirectReferenceTuple): class IndirectReference(IndirectReferenceTuple):
def __str__(self): def __str__(self) -> str:
return f"{self.object_id} {self.generation} R" return f"{self.object_id} {self.generation} R"
def __bytes__(self): def __bytes__(self) -> bytes:
return self.__str__().encode("us-ascii") return self.__str__().encode("us-ascii")
def __eq__(self, other): def __eq__(self, other: object) -> bool:
return ( if self.__class__ is not other.__class__:
other.__class__ is self.__class__ return False
and other.object_id == self.object_id assert isinstance(other, IndirectReference)
and other.generation == self.generation return other.object_id == self.object_id and other.generation == self.generation
)
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return not (self == other)
def __hash__(self): def __hash__(self) -> int:
return hash((self.object_id, self.generation)) return hash((self.object_id, self.generation))
class IndirectObjectDef(IndirectReference): class IndirectObjectDef(IndirectReference):
def __str__(self): def __str__(self) -> str:
return f"{self.object_id} {self.generation} obj" return f"{self.object_id} {self.generation} obj"
@@ -144,15 +143,13 @@ class XrefTable:
elif key in self.deleted_entries: elif key in self.deleted_entries:
generation = self.deleted_entries[key] generation = self.deleted_entries[key]
else: else:
msg = ( msg = f"object ID {key} cannot be deleted because it doesn't exist"
"object ID " + str(key) + " cannot be deleted because it doesn't exist"
)
raise IndexError(msg) raise IndexError(msg)
def __contains__(self, key): def __contains__(self, key):
return key in self.existing_entries or key in self.new_entries return key in self.existing_entries or key in self.new_entries
def __len__(self): def __len__(self) -> int:
return len( return len(
set(self.existing_entries.keys()) set(self.existing_entries.keys())
| set(self.new_entries.keys()) | set(self.new_entries.keys())
@@ -213,7 +210,7 @@ class PdfName:
else: else:
self.name = name.encode("us-ascii") self.name = name.encode("us-ascii")
def name_as_str(self): def name_as_str(self) -> str:
return self.name.decode("us-ascii") return self.name.decode("us-ascii")
def __eq__(self, other): def __eq__(self, other):
@@ -221,11 +218,11 @@ class PdfName:
isinstance(other, PdfName) and other.name == self.name isinstance(other, PdfName) and other.name == self.name
) or other == self.name ) or other == self.name
def __hash__(self): def __hash__(self) -> int:
return hash(self.name) return hash(self.name)
def __repr__(self): def __repr__(self) -> str:
return f"PdfName({repr(self.name)})" return f"{self.__class__.__name__}({repr(self.name)})"
@classmethod @classmethod
def from_pdf_stream(cls, data): def from_pdf_stream(cls, data):
@@ -233,7 +230,7 @@ class PdfName:
allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
def __bytes__(self): def __bytes__(self) -> bytes:
result = bytearray(b"/") result = bytearray(b"/")
for b in self.name: for b in self.name:
if b in self.allowed_chars: if b in self.allowed_chars:
@@ -244,7 +241,7 @@ class PdfName:
class PdfArray(List[Any]): class PdfArray(List[Any]):
def __bytes__(self): def __bytes__(self) -> bytes:
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
@@ -288,7 +285,7 @@ class PdfDict(_DictBase):
value = time.gmtime(calendar.timegm(value) + offset) value = time.gmtime(calendar.timegm(value) + offset)
return value return value
def __bytes__(self): def __bytes__(self) -> bytes:
out = bytearray(b"<<") out = bytearray(b"<<")
for key, value in self.items(): for key, value in self.items():
if value is None: if value is None:
@@ -306,7 +303,7 @@ class PdfBinary:
def __init__(self, data): def __init__(self, data):
self.data = data self.data = data
def __bytes__(self): def __bytes__(self) -> bytes:
return b"<%s>" % b"".join(b"%02X" % b for b in self.data) return b"<%s>" % b"".join(b"%02X" % b for b in self.data)
@@ -404,41 +401,40 @@ class PdfParser:
if f: if f:
self.seek_end() self.seek_end()
def __enter__(self): def __enter__(self) -> PdfParser:
return self return self
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, *args: object) -> None:
self.close() self.close()
return False # do not suppress exceptions
def start_writing(self): def start_writing(self) -> None:
self.close_buf() self.close_buf()
self.seek_end() self.seek_end()
def close_buf(self): def close_buf(self) -> None:
try: try:
self.buf.close() self.buf.close()
except AttributeError: except AttributeError:
pass pass
self.buf = None self.buf = None
def close(self): def close(self) -> None:
if self.should_close_buf: if self.should_close_buf:
self.close_buf() self.close_buf()
if self.f is not None and self.should_close_file: if self.f is not None and self.should_close_file:
self.f.close() self.f.close()
self.f = None self.f = None
def seek_end(self): def seek_end(self) -> None:
self.f.seek(0, os.SEEK_END) self.f.seek(0, os.SEEK_END)
def write_header(self): def write_header(self) -> None:
self.f.write(b"%PDF-1.4\n") self.f.write(b"%PDF-1.4\n")
def write_comment(self, s): def write_comment(self, s):
self.f.write(f"% {s}\n".encode()) self.f.write(f"% {s}\n".encode())
def write_catalog(self): def write_catalog(self) -> IndirectReference:
self.del_root() self.del_root()
self.root_ref = self.next_object_id(self.f.tell()) self.root_ref = self.next_object_id(self.f.tell())
self.pages_ref = self.next_object_id(0) self.pages_ref = self.next_object_id(0)
@@ -452,7 +448,7 @@ class PdfParser:
) )
return self.root_ref return self.root_ref
def rewrite_pages(self): def rewrite_pages(self) -> None:
pages_tree_nodes_to_delete = [] pages_tree_nodes_to_delete = []
for i, page_ref in enumerate(self.orig_pages): for i, page_ref in enumerate(self.orig_pages):
page_info = self.cached_objects[page_ref] page_info = self.cached_objects[page_ref]
@@ -531,7 +527,7 @@ class PdfParser:
f.write(b"endobj\n") f.write(b"endobj\n")
return ref return ref
def del_root(self): def del_root(self) -> None:
if self.root_ref is None: if self.root_ref is None:
return return
del self.xref_table[self.root_ref.object_id] del self.xref_table[self.root_ref.object_id]
@@ -549,7 +545,7 @@ class PdfParser:
except ValueError: # cannot mmap an empty file except ValueError: # cannot mmap an empty file
return b"" return b""
def read_pdf_info(self): def read_pdf_info(self) -> None:
self.file_size_total = len(self.buf) self.file_size_total = len(self.buf)
self.file_size_this = self.file_size_total - self.start_offset self.file_size_this = self.file_size_total - self.start_offset
self.read_trailer() self.read_trailer()
@@ -825,11 +821,10 @@ class PdfParser:
m = cls.re_stream_start.match(data, offset) m = cls.re_stream_start.match(data, offset)
if m: if m:
try: try:
stream_len = int(result[b"Length"]) stream_len_str = result.get(b"Length")
except (TypeError, KeyError, ValueError) as e: stream_len = int(stream_len_str)
msg = "bad or missing Length in stream dict (%r)" % result.get( except (TypeError, ValueError) as e:
b"Length", None msg = f"bad or missing Length in stream dict ({stream_len_str})"
)
raise PdfFormatError(msg) from e raise PdfFormatError(msg) from e
stream_data = data[m.end() : m.end() + stream_len] stream_data = data[m.end() : m.end() + stream_len]
m = cls.re_stream_end.match(data, m.end() + stream_len) m = cls.re_stream_end.match(data, m.end() + stream_len)
@@ -884,7 +879,7 @@ class PdfParser:
if m: if m:
return cls.get_literal_string(data, m.end()) return cls.get_literal_string(data, m.end())
# return None, offset # fallback (only for debugging) # return None, offset # fallback (only for debugging)
msg = "unrecognized object: " + repr(data[offset : offset + 32]) msg = f"unrecognized object: {repr(data[offset : offset + 32])}"
raise PdfFormatError(msg) raise PdfFormatError(msg)
re_lit_str_token = re.compile( re_lit_str_token = re.compile(
+128 -109
View File
@@ -39,6 +39,7 @@ import struct
import warnings import warnings
import zlib import zlib
from enum import IntEnum from enum import IntEnum
from typing import IO, TYPE_CHECKING, Any, NoReturn
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16 from ._binary import i16be as i16
@@ -47,6 +48,9 @@ from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from ._binary import o32be as o32 from ._binary import o32be as o32
if TYPE_CHECKING:
from . import _imaging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
is_cid = re.compile(rb"\w\w\w\w").match is_cid = re.compile(rb"\w\w\w\w").match
@@ -149,14 +153,15 @@ def _crc32(data, seed=0):
class ChunkStream: class ChunkStream:
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
self.fp = fp self.fp: IO[bytes] | None = fp
self.queue = [] self.queue: list[tuple[bytes, int, int]] | None = []
def read(self): def read(self) -> tuple[bytes, int, int]:
"""Fetch a new chunk. Returns header information.""" """Fetch a new chunk. Returns header information."""
cid = None cid = None
assert self.fp is not None
if self.queue: if self.queue:
cid, pos, length = self.queue.pop() cid, pos, length = self.queue.pop()
self.fp.seek(pos) self.fp.seek(pos)
@@ -173,25 +178,26 @@ class ChunkStream:
return cid, pos, length return cid, pos, length
def __enter__(self): def __enter__(self) -> ChunkStream:
return self return self
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.close() self.close()
def close(self): def close(self) -> None:
self.queue = self.fp = None self.queue = self.fp = None
def push(self, cid, pos, length): def push(self, cid: bytes, pos: int, length: int) -> None:
assert self.queue is not None
self.queue.append((cid, pos, length)) self.queue.append((cid, pos, length))
def call(self, cid, pos, length): def call(self, cid, pos, length):
"""Call the appropriate chunk handler""" """Call the appropriate chunk handler"""
logger.debug("STREAM %r %s %s", cid, pos, length) logger.debug("STREAM %r %s %s", cid, pos, length)
return getattr(self, "chunk_" + cid.decode("ascii"))(pos, length) return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length)
def crc(self, cid, data): def crc(self, cid: bytes, data: bytes) -> None:
"""Read and verify checksum""" """Read and verify checksum"""
# Skip CRC checks for ancillary chunks if allowed to load truncated # Skip CRC checks for ancillary chunks if allowed to load truncated
@@ -201,6 +207,7 @@ class ChunkStream:
self.crc_skip(cid, data) self.crc_skip(cid, data)
return return
assert self.fp is not None
try: try:
crc1 = _crc32(data, _crc32(cid)) crc1 = _crc32(data, _crc32(cid))
crc2 = i32(self.fp.read(4)) crc2 = i32(self.fp.read(4))
@@ -211,12 +218,13 @@ class ChunkStream:
msg = f"broken PNG file (incomplete checksum in {repr(cid)})" msg = f"broken PNG file (incomplete checksum in {repr(cid)})"
raise SyntaxError(msg) from e raise SyntaxError(msg) from e
def crc_skip(self, cid, data): def crc_skip(self, cid: bytes, data: bytes) -> None:
"""Read checksum""" """Read checksum"""
assert self.fp is not None
self.fp.read(4) self.fp.read(4)
def verify(self, endchunk=b"IEND"): def verify(self, endchunk: bytes = b"IEND") -> list[bytes]:
# Simple approach; just calculate checksum for all remaining # Simple approach; just calculate checksum for all remaining
# blocks. Must be called directly after open. # blocks. Must be called directly after open.
@@ -244,6 +252,9 @@ class iTXt(str):
""" """
lang: str | bytes | None
tkey: str | bytes | None
@staticmethod @staticmethod
def __new__(cls, text, lang=None, tkey=None): def __new__(cls, text, lang=None, tkey=None):
""" """
@@ -265,10 +276,10 @@ class PngInfo:
""" """
def __init__(self): def __init__(self) -> None:
self.chunks = [] self.chunks: list[tuple[bytes, bytes, bool]] = []
def add(self, cid, data, after_idat=False): def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None:
"""Appends an arbitrary chunk. Use with caution. """Appends an arbitrary chunk. Use with caution.
:param cid: a byte string, 4 bytes long. :param cid: a byte string, 4 bytes long.
@@ -278,12 +289,16 @@ class PngInfo:
""" """
chunk = [cid, data] self.chunks.append((cid, data, after_idat))
if after_idat:
chunk.append(True)
self.chunks.append(tuple(chunk))
def add_itxt(self, key, value, lang="", tkey="", zip=False): def add_itxt(
self,
key: str | bytes,
value: str | bytes,
lang: str | bytes = "",
tkey: str | bytes = "",
zip: bool = False,
) -> None:
"""Appends an iTXt chunk. """Appends an iTXt chunk.
:param key: latin-1 encodable text key name :param key: latin-1 encodable text key name
@@ -311,7 +326,9 @@ class PngInfo:
else: else:
self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value)
def add_text(self, key, value, zip=False): def add_text(
self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False
) -> None:
"""Appends a text chunk. """Appends a text chunk.
:param key: latin-1 encodable text key name :param key: latin-1 encodable text key name
@@ -321,7 +338,13 @@ class PngInfo:
""" """
if isinstance(value, iTXt): if isinstance(value, iTXt):
return self.add_itxt(key, value, value.lang, value.tkey, zip=zip) return self.add_itxt(
key,
value,
value.lang if value.lang is not None else b"",
value.tkey if value.tkey is not None else b"",
zip=zip,
)
# The tEXt chunk stores latin-1 text # The tEXt chunk stores latin-1 text
if not isinstance(value, bytes): if not isinstance(value, bytes):
@@ -361,7 +384,7 @@ class PngStream(ChunkStream):
self.text_memory = 0 self.text_memory = 0
def check_text_memory(self, chunklen): def check_text_memory(self, chunklen: int) -> None:
self.text_memory += chunklen self.text_memory += chunklen
if self.text_memory > MAX_TEXT_MEMORY: if self.text_memory > MAX_TEXT_MEMORY:
msg = ( msg = (
@@ -370,19 +393,19 @@ class PngStream(ChunkStream):
) )
raise ValueError(msg) raise ValueError(msg)
def save_rewind(self): def save_rewind(self) -> None:
self.rewind_state = { self.rewind_state = {
"info": self.im_info.copy(), "info": self.im_info.copy(),
"tile": self.im_tile, "tile": self.im_tile,
"seq_num": self._seq_num, "seq_num": self._seq_num,
} }
def rewind(self): def rewind(self) -> None:
self.im_info = self.rewind_state["info"].copy() self.im_info = self.rewind_state["info"].copy()
self.im_tile = self.rewind_state["tile"] self.im_tile = self.rewind_state["tile"]
self._seq_num = self.rewind_state["seq_num"] self._seq_num = self.rewind_state["seq_num"]
def chunk_iCCP(self, pos, length): def chunk_iCCP(self, pos: int, length: int) -> bytes:
# ICC profile # ICC profile
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
# according to PNG spec, the iCCP chunk contains: # according to PNG spec, the iCCP chunk contains:
@@ -409,7 +432,7 @@ class PngStream(ChunkStream):
self.im_info["icc_profile"] = icc_profile self.im_info["icc_profile"] = icc_profile
return s return s
def chunk_IHDR(self, pos, length): def chunk_IHDR(self, pos: int, length: int) -> bytes:
# image header # image header
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if length < 13: if length < 13:
@@ -429,7 +452,7 @@ class PngStream(ChunkStream):
raise SyntaxError(msg) raise SyntaxError(msg)
return s return s
def chunk_IDAT(self, pos, length): def chunk_IDAT(self, pos: int, length: int) -> NoReturn:
# image data # image data
if "bbox" in self.im_info: if "bbox" in self.im_info:
tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)]
@@ -442,18 +465,18 @@ class PngStream(ChunkStream):
msg = "image data found" msg = "image data found"
raise EOFError(msg) raise EOFError(msg)
def chunk_IEND(self, pos, length): def chunk_IEND(self, pos: int, length: int) -> NoReturn:
msg = "end of PNG image" msg = "end of PNG image"
raise EOFError(msg) raise EOFError(msg)
def chunk_PLTE(self, pos, length): def chunk_PLTE(self, pos: int, length: int) -> bytes:
# palette # palette
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P": if self.im_mode == "P":
self.im_palette = "RGB", s self.im_palette = "RGB", s
return s return s
def chunk_tRNS(self, pos, length): def chunk_tRNS(self, pos: int, length: int) -> bytes:
# transparency # transparency
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P": if self.im_mode == "P":
@@ -473,13 +496,13 @@ class PngStream(ChunkStream):
self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
return s return s
def chunk_gAMA(self, pos, length): def chunk_gAMA(self, pos: int, length: int) -> bytes:
# gamma setting # gamma setting
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
self.im_info["gamma"] = i32(s) / 100000.0 self.im_info["gamma"] = i32(s) / 100000.0
return s return s
def chunk_cHRM(self, pos, length): def chunk_cHRM(self, pos: int, length: int) -> bytes:
# chromaticity, 8 unsigned ints, actual value is scaled by 100,000 # chromaticity, 8 unsigned ints, actual value is scaled by 100,000
# WP x,y, Red x,y, Green x,y Blue x,y # WP x,y, Red x,y, Green x,y Blue x,y
@@ -488,7 +511,7 @@ class PngStream(ChunkStream):
self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
return s return s
def chunk_sRGB(self, pos, length): def chunk_sRGB(self, pos: int, length: int) -> bytes:
# srgb rendering intent, 1 byte # srgb rendering intent, 1 byte
# 0 perceptual # 0 perceptual
# 1 relative colorimetric # 1 relative colorimetric
@@ -504,7 +527,7 @@ class PngStream(ChunkStream):
self.im_info["srgb"] = s[0] self.im_info["srgb"] = s[0]
return s return s
def chunk_pHYs(self, pos, length): def chunk_pHYs(self, pos: int, length: int) -> bytes:
# pixels per unit # pixels per unit
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if length < 9: if length < 9:
@@ -521,7 +544,7 @@ class PngStream(ChunkStream):
self.im_info["aspect"] = px, py self.im_info["aspect"] = px, py
return s return s
def chunk_tEXt(self, pos, length): def chunk_tEXt(self, pos: int, length: int) -> bytes:
# text # text
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
try: try:
@@ -540,7 +563,7 @@ class PngStream(ChunkStream):
return s return s
def chunk_zTXt(self, pos, length): def chunk_zTXt(self, pos: int, length: int) -> bytes:
# compressed text # compressed text
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
try: try:
@@ -574,7 +597,7 @@ class PngStream(ChunkStream):
return s return s
def chunk_iTXt(self, pos, length): def chunk_iTXt(self, pos: int, length: int) -> bytes:
# international text # international text
r = s = ImageFile._safe_read(self.fp, length) r = s = ImageFile._safe_read(self.fp, length)
try: try:
@@ -601,6 +624,8 @@ class PngStream(ChunkStream):
return s return s
else: else:
return s return s
if k == b"XML:com.adobe.xmp":
self.im_info["xmp"] = v
try: try:
k = k.decode("latin-1", "strict") k = k.decode("latin-1", "strict")
lang = lang.decode("utf-8", "strict") lang = lang.decode("utf-8", "strict")
@@ -614,13 +639,13 @@ class PngStream(ChunkStream):
return s return s
def chunk_eXIf(self, pos, length): def chunk_eXIf(self, pos: int, length: int) -> bytes:
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
self.im_info["exif"] = b"Exif\x00\x00" + s self.im_info["exif"] = b"Exif\x00\x00" + s
return s return s
# APNG chunks # APNG chunks
def chunk_acTL(self, pos, length): def chunk_acTL(self, pos: int, length: int) -> bytes:
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if length < 8: if length < 8:
if ImageFile.LOAD_TRUNCATED_IMAGES: if ImageFile.LOAD_TRUNCATED_IMAGES:
@@ -640,7 +665,7 @@ class PngStream(ChunkStream):
self.im_custom_mimetype = "image/apng" self.im_custom_mimetype = "image/apng"
return s return s
def chunk_fcTL(self, pos, length): def chunk_fcTL(self, pos: int, length: int) -> bytes:
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if length < 26: if length < 26:
if ImageFile.LOAD_TRUNCATED_IMAGES: if ImageFile.LOAD_TRUNCATED_IMAGES:
@@ -669,7 +694,7 @@ class PngStream(ChunkStream):
self.im_info["blend"] = s[25] self.im_info["blend"] = s[25]
return s return s
def chunk_fdAT(self, pos, length): def chunk_fdAT(self, pos: int, length: int) -> bytes:
if length < 4: if length < 4:
if ImageFile.LOAD_TRUNCATED_IMAGES: if ImageFile.LOAD_TRUNCATED_IMAGES:
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
@@ -689,7 +714,7 @@ class PngStream(ChunkStream):
# PNG reader # PNG reader
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:8] == _MAGIC return prefix[:8] == _MAGIC
@@ -701,7 +726,7 @@ class PngImageFile(ImageFile.ImageFile):
format = "PNG" format = "PNG"
format_description = "Portable network graphics" format_description = "Portable network graphics"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(8)): if not _accept(self.fp.read(8)):
msg = "not a PNG file" msg = "not a PNG file"
raise SyntaxError(msg) raise SyntaxError(msg)
@@ -711,8 +736,8 @@ class PngImageFile(ImageFile.ImageFile):
# #
# Parse headers up to the first IDAT or fDAT chunk # Parse headers up to the first IDAT or fDAT chunk
self.private_chunks = [] self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = []
self.png = PngStream(self.fp) self.png: PngStream | None = PngStream(self.fp)
while True: while True:
# #
@@ -783,7 +808,7 @@ class PngImageFile(ImageFile.ImageFile):
self.seek(frame) self.seek(frame)
return self._text return self._text
def verify(self): def verify(self) -> None:
"""Verify PNG file""" """Verify PNG file"""
if self.fp is None: if self.fp is None:
@@ -793,6 +818,7 @@ class PngImageFile(ImageFile.ImageFile):
# back up to beginning of IDAT block # back up to beginning of IDAT block
self.fp.seek(self.tile[0][2] - 8) self.fp.seek(self.tile[0][2] - 8)
assert self.png is not None
self.png.verify() self.png.verify()
self.png.close() self.png.close()
@@ -800,7 +826,7 @@ class PngImageFile(ImageFile.ImageFile):
self.fp.close() self.fp.close()
self.fp = None self.fp = None
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if frame < self.__frame: if frame < self.__frame:
@@ -815,7 +841,10 @@ class PngImageFile(ImageFile.ImageFile):
msg = "no more images in APNG file" msg = "no more images in APNG file"
raise EOFError(msg) from e raise EOFError(msg) from e
def _seek(self, frame, rewind=False): def _seek(self, frame: int, rewind: bool = False) -> None:
assert self.png is not None
self.dispose: _imaging.ImagingCore | None
if frame == 0: if frame == 0:
if rewind: if rewind:
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)
@@ -900,19 +929,19 @@ class PngImageFile(ImageFile.ImageFile):
if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
self.dispose_op = Disposal.OP_BACKGROUND self.dispose_op = Disposal.OP_BACKGROUND
self.dispose = None
if self.dispose_op == Disposal.OP_PREVIOUS: if self.dispose_op == Disposal.OP_PREVIOUS:
if self._prev_im:
self.dispose = self._prev_im.copy() self.dispose = self._prev_im.copy()
self.dispose = self._crop(self.dispose, self.dispose_extent) self.dispose = self._crop(self.dispose, self.dispose_extent)
elif self.dispose_op == Disposal.OP_BACKGROUND: elif self.dispose_op == Disposal.OP_BACKGROUND:
self.dispose = Image.core.fill(self.mode, self.size) self.dispose = Image.core.fill(self.mode, self.size)
self.dispose = self._crop(self.dispose, self.dispose_extent) self.dispose = self._crop(self.dispose, self.dispose_extent)
else:
self.dispose = None
def tell(self): def tell(self) -> int:
return self.__frame return self.__frame
def load_prepare(self): def load_prepare(self) -> None:
"""internal: prepare to read PNG file""" """internal: prepare to read PNG file"""
if self.info.get("interlace"): if self.info.get("interlace"):
@@ -921,9 +950,10 @@ class PngImageFile(ImageFile.ImageFile):
self.__idat = self.__prepare_idat # used by load_read() self.__idat = self.__prepare_idat # used by load_read()
ImageFile.ImageFile.load_prepare(self) ImageFile.ImageFile.load_prepare(self)
def load_read(self, read_bytes): def load_read(self, read_bytes: int) -> bytes:
"""internal: read more image data""" """internal: read more image data"""
assert self.png is not None
while self.__idat == 0: while self.__idat == 0:
# end of chunk, skip forward to next one # end of chunk, skip forward to next one
@@ -954,8 +984,9 @@ class PngImageFile(ImageFile.ImageFile):
return self.fp.read(read_bytes) return self.fp.read(read_bytes)
def load_end(self): def load_end(self) -> None:
"""internal: finished reading image data""" """internal: finished reading image data"""
assert self.png is not None
if self.__idat != 0: if self.__idat != 0:
self.fp.read(self.__idat) self.fp.read(self.__idat)
while True: while True:
@@ -1011,53 +1042,40 @@ class PngImageFile(ImageFile.ImageFile):
if self.pyaccess: if self.pyaccess:
self.pyaccess = None self.pyaccess = None
def _getexif(self): def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info: if "exif" not in self.info:
self.load() self.load()
if "exif" not in self.info and "Raw profile type exif" not in self.info: if "exif" not in self.info and "Raw profile type exif" not in self.info:
return None return None
return self.getexif()._get_merged_dict() return self.getexif()._get_merged_dict()
def getexif(self): def getexif(self) -> Image.Exif:
if "exif" not in self.info: if "exif" not in self.info:
self.load() self.load()
return super().getexif() 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 # PNG writer
_OUTMODES = { _OUTMODES = {
# supported PIL modes, and corresponding rawmodes/bits/color combinations # supported PIL modes, and corresponding rawmode, bit depth and color type
"1": ("1", b"\x01\x00"), "1": ("1", b"\x01", b"\x00"),
"L;1": ("L;1", b"\x01\x00"), "L;1": ("L;1", b"\x01", b"\x00"),
"L;2": ("L;2", b"\x02\x00"), "L;2": ("L;2", b"\x02", b"\x00"),
"L;4": ("L;4", b"\x04\x00"), "L;4": ("L;4", b"\x04", b"\x00"),
"L": ("L", b"\x08\x00"), "L": ("L", b"\x08", b"\x00"),
"LA": ("LA", b"\x08\x04"), "LA": ("LA", b"\x08", b"\x04"),
"I": ("I;16B", b"\x10\x00"), "I": ("I;16B", b"\x10", b"\x00"),
"I;16": ("I;16B", b"\x10\x00"), "I;16": ("I;16B", b"\x10", b"\x00"),
"I;16B": ("I;16B", b"\x10\x00"), "I;16B": ("I;16B", b"\x10", b"\x00"),
"P;1": ("P;1", b"\x01\x03"), "P;1": ("P;1", b"\x01", b"\x03"),
"P;2": ("P;2", b"\x02\x03"), "P;2": ("P;2", b"\x02", b"\x03"),
"P;4": ("P;4", b"\x04\x03"), "P;4": ("P;4", b"\x04", b"\x03"),
"P": ("P", b"\x08\x03"), "P": ("P", b"\x08", b"\x03"),
"RGB": ("RGB", b"\x08\x02"), "RGB": ("RGB", b"\x08", b"\x02"),
"RGBA": ("RGBA", b"\x08\x06"), "RGBA": ("RGBA", b"\x08", b"\x06"),
} }
@@ -1079,7 +1097,7 @@ class _idat:
self.fp = fp self.fp = fp
self.chunk = chunk self.chunk = chunk
def write(self, data): def write(self, data: bytes) -> None:
self.chunk(self.fp, b"IDAT", data) self.chunk(self.fp, b"IDAT", data)
@@ -1091,13 +1109,13 @@ class _fdat:
self.chunk = chunk self.chunk = chunk
self.seq_num = seq_num self.seq_num = seq_num
def write(self, data): def write(self, data: bytes) -> None:
self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
self.seq_num += 1 self.seq_num += 1
def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images): def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_images):
duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) duration = im.encoderinfo.get("duration")
loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
@@ -1111,13 +1129,15 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
frame_count = 0 frame_count = 0
for im_seq in chain: for im_seq in chain:
for im_frame in ImageSequence.Iterator(im_seq): for im_frame in ImageSequence.Iterator(im_seq):
if im_frame.mode == rawmode: if im_frame.mode == mode:
im_frame = im_frame.copy() im_frame = im_frame.copy()
else: else:
im_frame = im_frame.convert(rawmode) im_frame = im_frame.convert(mode)
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
if isinstance(duration, (list, tuple)): if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count] 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)): if isinstance(disposal, (list, tuple)):
encoderinfo["disposal"] = disposal[frame_count] encoderinfo["disposal"] = disposal[frame_count]
if isinstance(blend, (list, tuple)): if isinstance(blend, (list, tuple)):
@@ -1152,15 +1172,12 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
not bbox not bbox
and prev_disposal == encoderinfo.get("disposal") and prev_disposal == encoderinfo.get("disposal")
and prev_blend == encoderinfo.get("blend") and prev_blend == encoderinfo.get("blend")
and "duration" in encoderinfo
): ):
previous["encoderinfo"]["duration"] += encoderinfo.get( previous["encoderinfo"]["duration"] += encoderinfo["duration"]
"duration", duration
)
continue continue
else: else:
bbox = None bbox = None
if "duration" not in encoderinfo:
encoderinfo["duration"] = duration
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
if len(im_frames) == 1 and not default_image: if len(im_frames) == 1 and not default_image:
@@ -1176,8 +1193,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
# default image IDAT (if it exists) # default image IDAT (if it exists)
if default_image: if default_image:
if im.mode != rawmode: if im.mode != mode:
im = im.convert(rawmode) im = im.convert(mode)
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
seq_num = 0 seq_num = 0
@@ -1190,7 +1207,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
im_frame = im_frame.crop(bbox) im_frame = im_frame.crop(bbox)
size = im_frame.size size = im_frame.size
encoderinfo = frame_data["encoderinfo"] 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_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend) frame_blend = encoderinfo.get("blend", blend)
# frame control # frame control
@@ -1226,7 +1243,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
seq_num = fdat_chunks.seq_num seq_num = fdat_chunks.seq_num
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True) _save(im, fp, filename, save_all=True)
@@ -1254,6 +1271,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
size = im.size size = im.size
mode = im.mode mode = im.mode
outmode = mode
if mode == "P": if mode == "P":
# #
# attempt to minimize storage requirements for palette images # attempt to minimize storage requirements for palette images
@@ -1274,7 +1292,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
bits = 2 bits = 2
else: else:
bits = 4 bits = 4
mode = f"{mode};{bits}" outmode += f";{bits}"
# encoder options # encoder options
im.encoderconfig = ( im.encoderconfig = (
@@ -1286,7 +1304,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
# get the corresponding PNG mode # get the corresponding PNG mode
try: try:
rawmode, mode = _OUTMODES[mode] rawmode, bit_depth, color_type = _OUTMODES[outmode]
except KeyError as e: except KeyError as e:
msg = f"cannot write mode {mode} as PNG" msg = f"cannot write mode {mode} as PNG"
raise OSError(msg) from e raise OSError(msg) from e
@@ -1301,7 +1319,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
b"IHDR", b"IHDR",
o32(size[0]), # 0: size o32(size[0]), # 0: size
o32(size[1]), o32(size[1]),
mode, # 8: depth/type bit_depth,
color_type,
b"\0", # 10: compression b"\0", # 10: compression
b"\0", # 11: filter category b"\0", # 11: filter category
b"\0", # 12: interlace flag b"\0", # 12: interlace flag
@@ -1337,7 +1356,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, cid, data) chunk(fp, cid, data)
elif cid[1:2].islower(): elif cid[1:2].islower():
# Private chunk # Private chunk
after_idat = info_chunk[2:3] after_idat = len(info_chunk) == 3 and info_chunk[2]
if not after_idat: if not after_idat:
chunk(fp, cid, data) chunk(fp, cid, data)
@@ -1406,7 +1425,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
if save_all: if save_all:
im = _write_multiple_frames( im = _write_multiple_frames(
im, fp, chunk, rawmode, default_image, append_images im, fp, chunk, mode, rawmode, default_image, append_images
) )
if im: if im:
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
@@ -1416,7 +1435,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
cid, data = info_chunk[:2] cid, data = info_chunk[:2]
if cid[1:2].islower(): if cid[1:2].islower():
# Private chunk # Private chunk
after_idat = info_chunk[2:3] after_idat = len(info_chunk) == 3 and info_chunk[2]
if after_idat: if after_idat:
chunk(fp, cid, data) chunk(fp, cid, data)
@@ -1436,10 +1455,10 @@ def getchunks(im, **params):
class collector: class collector:
data = [] data = []
def write(self, data): def write(self, data: bytes) -> None:
pass pass
def append(self, chunk): def append(self, chunk: bytes) -> None:
self.data.append(chunk) self.data.append(chunk)
def append(fp, cid, *data): def append(fp, cid, *data):
@@ -328,7 +328,7 @@ class PpmDecoder(ImageFile.PyDecoder):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "1": if im.mode == "1":
rawmode, head = "1;I", b"P4" rawmode, head = "1;I", b"P4"
elif im.mode == "L": elif im.mode == "L":
+30 -11
View File
@@ -18,6 +18,7 @@
from __future__ import annotations from __future__ import annotations
import io import io
from functools import cached_property
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i8 from ._binary import i8
@@ -44,7 +45,7 @@ MODES = {
# read PSD images # read PSD images
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"8BPS" return prefix[:4] == b"8BPS"
@@ -57,7 +58,7 @@ class PsdImageFile(ImageFile.ImageFile):
format_description = "Adobe Photoshop" format_description = "Adobe Photoshop"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
read = self.fp.read read = self.fp.read
# #
@@ -118,18 +119,17 @@ class PsdImageFile(ImageFile.ImageFile):
# #
# layer and mask information # layer and mask information
self.layers = [] self._layers_position = None
size = i32(read(4)) size = i32(read(4))
if size: if size:
end = self.fp.tell() + size end = self.fp.tell() + size
size = i32(read(4)) size = i32(read(4))
if size: if size:
_layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size)) self._layers_position = self.fp.tell()
self.layers = _layerinfo(_layer_data, size) self._layers_size = size
self.fp.seek(end) self.fp.seek(end)
self.n_frames = len(self.layers) self._n_frames: int | None = None
self.is_animated = self.n_frames > 1
# #
# image descriptor # image descriptor
@@ -141,23 +141,42 @@ class PsdImageFile(ImageFile.ImageFile):
self.frame = 1 self.frame = 1
self._min_frame = 1 self._min_frame = 1
def seek(self, layer): @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): if not self._seek_check(layer):
return return
# seek to given layer (1..max) # seek to given layer (1..max)
try: try:
name, mode, bbox, tile = self.layers[layer - 1] _, mode, _, tile = self.layers[layer - 1]
self._mode = mode self._mode = mode
self.tile = tile self.tile = tile
self.frame = layer self.frame = layer
self.fp = self._fp self.fp = self._fp
return name, bbox
except IndexError as e: except IndexError as e:
msg = "no such layer" msg = "no such layer"
raise EOFError(msg) from e raise EOFError(msg) from e
def tell(self): def tell(self) -> int:
# return layer number (0=image, 1..max=layers) # return layer number (0=image, 1..max=layers)
return self.frame return self.frame
+36 -20
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import logging import logging
import sys import sys
from typing import TYPE_CHECKING
from ._deprecate import deprecate from ._deprecate import deprecate
@@ -48,9 +49,12 @@ except ImportError as ex:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from . import Image
class PyAccess: class PyAccess:
def __init__(self, img, readonly=False): def __init__(self, img: Image.Image, readonly: bool = False) -> None:
deprecate("PyAccess", 11) deprecate("PyAccess", 11)
vals = dict(img.im.unsafe_ptrs) vals = dict(img.im.unsafe_ptrs)
self.readonly = readonly self.readonly = readonly
@@ -70,14 +74,19 @@ class PyAccess:
# logger.debug("%s", vals) # logger.debug("%s", vals)
self._post_init() self._post_init()
def _post_init(self): def _post_init(self) -> None:
pass pass
def __setitem__(self, xy, color): def __setitem__(
self,
xy: tuple[int, int] | list[int],
color: float | tuple[int, ...] | list[int],
) -> None:
""" """
Modifies the pixel at x,y. The color is given as a single Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for numerical value for single band images, and a tuple for
multi-band images multi-band images. In addition to this, RGB and RGBA tuples
are accepted for P and PA images.
:param xy: The pixel coordinate, given as (x, y). See :param xy: The pixel coordinate, given as (x, y). See
:ref:`coordinate-system`. :ref:`coordinate-system`.
@@ -102,13 +111,12 @@ class PyAccess:
if self._im.mode == "PA": if self._im.mode == "PA":
alpha = color[3] if len(color) == 4 else 255 alpha = color[3] if len(color) == 4 else 255
color = color[:3] color = color[:3]
color = self._palette.getcolor(color, self._img) palette_index = self._palette.getcolor(color, self._img)
if self._im.mode == "PA": color = (palette_index, alpha) if self._im.mode == "PA" else palette_index
color = (color, alpha)
return self.set_pixel(x, y, color) return self.set_pixel(x, y, color)
def __getitem__(self, xy): def __getitem__(self, xy: tuple[int, int] | list[int]) -> float | tuple[int, ...]:
""" """
Returns the pixel at x,y. The pixel is returned as a single Returns the pixel at x,y. The pixel is returned as a single
value for single band images or a tuple for multiple band value for single band images or a tuple for multiple band
@@ -130,13 +138,21 @@ class PyAccess:
putpixel = __setitem__ putpixel = __setitem__
getpixel = __getitem__ getpixel = __getitem__
def check_xy(self, xy): def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]:
(x, y) = xy (x, y) = xy
if not (0 <= x < self.xsize and 0 <= y < self.ysize): if not (0 <= x < self.xsize and 0 <= y < self.ysize):
msg = "pixel location out of range" msg = "pixel location out of range"
raise ValueError(msg) raise ValueError(msg)
return xy return xy
def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]:
raise NotImplementedError()
def set_pixel(
self, x: int, y: int, color: float | tuple[int, ...] | list[int]
) -> None:
raise NotImplementedError()
class _PyAccess32_2(PyAccess): class _PyAccess32_2(PyAccess):
"""PA, LA, stored in first and last bytes of a 32 bit word""" """PA, LA, stored in first and last bytes of a 32 bit word"""
@@ -144,7 +160,7 @@ class _PyAccess32_2(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> tuple[int, int]:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.r, pixel.a return pixel.r, pixel.a
@@ -161,7 +177,7 @@ class _PyAccess32_3(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> tuple[int, int, int]:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.r, pixel.g, pixel.b return pixel.r, pixel.g, pixel.b
@@ -180,7 +196,7 @@ class _PyAccess32_4(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.r, pixel.g, pixel.b, pixel.a return pixel.r, pixel.g, pixel.b, pixel.a
@@ -199,7 +215,7 @@ class _PyAccess8(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = self.image8 self.pixels = self.image8
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x] return self.pixels[y][x]
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@@ -217,7 +233,7 @@ class _PyAccessI16_N(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("unsigned short **", self.image) self.pixels = ffi.cast("unsigned short **", self.image)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x] return self.pixels[y][x]
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@@ -235,7 +251,7 @@ class _PyAccessI16_L(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image) self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.l + pixel.r * 256 return pixel.l + pixel.r * 256
@@ -256,7 +272,7 @@ class _PyAccessI16_B(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image) self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
pixel = self.pixels[y][x] pixel = self.pixels[y][x]
return pixel.l * 256 + pixel.r return pixel.l * 256 + pixel.r
@@ -277,7 +293,7 @@ class _PyAccessI32_N(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = self.image32 self.pixels = self.image32
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y][x] return self.pixels[y][x]
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@@ -296,7 +312,7 @@ class _PyAccessI32_Swap(PyAccess):
chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0] chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0]
return ffi.cast("int *", chars)[0] return ffi.cast("int *", chars)[0]
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> int:
return self.reverse(self.pixels[y][x]) return self.reverse(self.pixels[y][x])
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@@ -309,7 +325,7 @@ class _PyAccessF(PyAccess):
def _post_init(self, *args, **kwargs): def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("float **", self.image32) self.pixels = ffi.cast("float **", self.image32)
def get_pixel(self, x, y): def get_pixel(self, x: int, y: int) -> float:
return self.pixels[y][x] return self.pixels[y][x]
def set_pixel(self, x, y, color): def set_pixel(self, x, y, color):
@@ -357,7 +373,7 @@ else:
mode_map["I;32B"] = _PyAccessI32_N mode_map["I;32B"] = _PyAccessI32_N
def new(img, readonly=False): def new(img: Image.Image, readonly: bool = False) -> PyAccess | None:
access_type = mode_map.get(img.mode, None) access_type = mode_map.get(img.mode, None)
if not access_type: if not access_type:
logger.debug("PyAccess Not Implemented: %s", img.mode) logger.debug("PyAccess Not Implemented: %s", img.mode)
+13 -9
View File
@@ -13,7 +13,7 @@ from . import Image, ImageFile
from ._binary import i32be as i32 from ._binary import i32be as i32
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"qoif" return prefix[:4] == b"qoif"
@@ -21,7 +21,7 @@ class QoiImageFile(ImageFile.ImageFile):
format = "QOI" format = "QOI"
format_description = "Quite OK Image" format_description = "Quite OK Image"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not a QOI file" msg = "not a QOI file"
raise SyntaxError(msg) raise SyntaxError(msg)
@@ -37,17 +37,20 @@ class QoiImageFile(ImageFile.ImageFile):
class QoiDecoder(ImageFile.PyDecoder): class QoiDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
_previous_pixel: bytes | bytearray | None = None
_previously_seen_pixels: dict[int, bytes | bytearray] = {}
def _add_to_previous_pixels(self, value): def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
self._previous_pixel = value self._previous_pixel = value
r, g, b, a = value r, g, b, a = value
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
self._previously_seen_pixels[hash_value] = value self._previously_seen_pixels[hash_value] = value
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
self._previously_seen_pixels = {} self._previously_seen_pixels = {}
self._previous_pixel = None
self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) self._add_to_previous_pixels(bytearray((0, 0, 0, 255)))
data = bytearray() data = bytearray()
@@ -55,7 +58,8 @@ class QoiDecoder(ImageFile.PyDecoder):
dest_length = self.state.xsize * self.state.ysize * bands dest_length = self.state.xsize * self.state.ysize * bands
while len(data) < dest_length: while len(data) < dest_length:
byte = self.fd.read(1)[0] byte = self.fd.read(1)[0]
if byte == 0b11111110: # QOI_OP_RGB value: bytes | bytearray
if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB
value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] value = bytearray(self.fd.read(3)) + self._previous_pixel[3:]
elif byte == 0b11111111: # QOI_OP_RGBA elif byte == 0b11111111: # QOI_OP_RGBA
value = self.fd.read(4) value = self.fd.read(4)
@@ -66,7 +70,7 @@ class QoiDecoder(ImageFile.PyDecoder):
value = self._previously_seen_pixels.get( value = self._previously_seen_pixels.get(
op_index, bytearray((0, 0, 0, 0)) op_index, bytearray((0, 0, 0, 0))
) )
elif op == 1: # QOI_OP_DIFF elif op == 1 and self._previous_pixel: # QOI_OP_DIFF
value = bytearray( value = bytearray(
( (
(self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
@@ -77,7 +81,7 @@ class QoiDecoder(ImageFile.PyDecoder):
self._previous_pixel[3], self._previous_pixel[3],
) )
) )
elif op == 2: # QOI_OP_LUMA elif op == 2 and self._previous_pixel: # QOI_OP_LUMA
second_byte = self.fd.read(1)[0] second_byte = self.fd.read(1)[0]
diff_green = (byte & 0b00111111) - 32 diff_green = (byte & 0b00111111) - 32
diff_red = ((second_byte & 0b11110000) >> 4) - 8 diff_red = ((second_byte & 0b11110000) >> 4) - 8
@@ -90,7 +94,7 @@ class QoiDecoder(ImageFile.PyDecoder):
) )
) )
value += self._previous_pixel[3:] value += self._previous_pixel[3:]
elif op == 3: # QOI_OP_RUN elif op == 3 and self._previous_pixel: # QOI_OP_RUN
run_length = (byte & 0b00111111) + 1 run_length = (byte & 0b00111111) + 1
value = self._previous_pixel value = self._previous_pixel
if bands == 3: if bands == 3:
@@ -125,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile):
] ]
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode not in {"RGB", "RGBA", "L"}: if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode" msg = "Unsupported SGI image mode"
raise ValueError(msg) raise ValueError(msg)
@@ -171,8 +171,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
# Maximum Byte value (255 = 8bits per pixel) # Maximum Byte value (255 = 8bits per pixel)
pinmax = 255 pinmax = 255
# Image name (79 characters max, truncated below in write) # Image name (79 characters max, truncated below in write)
filename = os.path.basename(filename) img_name = os.path.splitext(os.path.basename(filename))[0]
img_name = os.path.splitext(filename)[0].encode("ascii", "ignore") if isinstance(img_name, str):
img_name = img_name.encode("ascii", "ignore")
# Standard representation of pixel in the file # Standard representation of pixel in the file
colormap = 0 colormap = 0
fp.write(struct.pack(">h", magic_number)) fp.write(struct.pack(">h", magic_number))
@@ -37,11 +37,12 @@ from __future__ import annotations
import os import os
import struct import struct
import sys import sys
from typing import IO, TYPE_CHECKING, Any, Tuple, cast
from . import Image, ImageFile from . import Image, ImageFile
def isInt(f): def isInt(f: Any) -> int:
try: try:
i = int(f) i = int(f)
if f - i == 0: if f - i == 0:
@@ -61,7 +62,7 @@ iforms = [1, 3, -11, -12, -21, -22]
# otherwise returns 0 # 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 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 # header values 1,2,5,12,13,22,23 should be integers
for i in [1, 2, 5, 12, 13, 22, 23]: for i in [1, 2, 5, 12, 13, 22, 23]:
@@ -81,7 +82,7 @@ def isSpiderHeader(t):
return labbyt return labbyt
def isSpiderImage(filename): def isSpiderImage(filename: str) -> int:
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
f = fp.read(92) # read 23 * 4 bytes f = fp.read(92) # read 23 * 4 bytes
t = struct.unpack(">23f", f) # try big-endian first t = struct.unpack(">23f", f) # try big-endian first
@@ -97,7 +98,7 @@ class SpiderImageFile(ImageFile.ImageFile):
format_description = "Spider 2D image" format_description = "Spider 2D image"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
# check header # check header
n = 27 * 4 # read 27 float values n = 27 * 4 # read 27 float values
f = self.fp.read(n) f = self.fp.read(n)
@@ -157,21 +158,21 @@ class SpiderImageFile(ImageFile.ImageFile):
self._fp = self.fp # FIXME: hack self._fp = self.fp # FIXME: hack
@property @property
def n_frames(self): def n_frames(self) -> int:
return self._nimages return self._nimages
@property @property
def is_animated(self): def is_animated(self) -> bool:
return self._nimages > 1 return self._nimages > 1
# 1st image index is zero (although SPIDER imgnumber starts at 1) # 1st image index is zero (although SPIDER imgnumber starts at 1)
def tell(self): def tell(self) -> int:
if self.imgnumber < 1: if self.imgnumber < 1:
return 0 return 0
else: else:
return self.imgnumber - 1 return self.imgnumber - 1
def seek(self, frame): def seek(self, frame: int) -> None:
if self.istack == 0: if self.istack == 0:
msg = "attempt to seek in a non-stack file" msg = "attempt to seek in a non-stack file"
raise EOFError(msg) raise EOFError(msg)
@@ -183,16 +184,21 @@ class SpiderImageFile(ImageFile.ImageFile):
self._open() self._open()
# returns a byte image after rescaling to 0..255 # returns a byte image after rescaling to 0..255
def convert2byte(self, depth=255): def convert2byte(self, depth: int = 255) -> Image.Image:
(minimum, maximum) = self.getextrema() extrema = self.getextrema()
m = 1 assert isinstance(extrema[0], float)
minimum, maximum = cast(Tuple[float, float], extrema)
m: float = 1
if maximum != minimum: if maximum != minimum:
m = depth / (maximum - minimum) m = depth / (maximum - minimum)
b = -m * 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
# returns a ImageTk.PhotoImage object, after rescaling to 0..255 # returns a ImageTk.PhotoImage object, after rescaling to 0..255
def tkPhotoImage(self): def tkPhotoImage(self) -> ImageTk.PhotoImage:
from . import ImageTk from . import ImageTk
return ImageTk.PhotoImage(self.convert2byte(), palette=256) return ImageTk.PhotoImage(self.convert2byte(), palette=256)
@@ -203,10 +209,10 @@ class SpiderImageFile(ImageFile.ImageFile):
# given a list of filenames, return a list of images # 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""" """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
if filelist is None or len(filelist) < 1: if filelist is None or len(filelist) < 1:
return return None
imglist = [] imglist = []
for img in filelist: for img in filelist:
@@ -218,7 +224,7 @@ def loadImageSeries(filelist=None):
im = im.convert2byte() im = im.convert2byte()
except Exception: except Exception:
if not isSpiderImage(img): if not isSpiderImage(img):
print(img + " is not a Spider image file") print(f"{img} is not a Spider image file")
continue continue
im.info["filename"] = img im.info["filename"] = img
imglist.append(im) imglist.append(im)
@@ -229,7 +235,7 @@ def loadImageSeries(filelist=None):
# For saving images in Spider format # For saving images in Spider format
def makeSpiderHeader(im): def makeSpiderHeader(im: Image.Image) -> list[bytes]:
nsam, nrow = im.size nsam, nrow = im.size
lenbyt = nsam * 4 # There are labrec records in the header lenbyt = nsam * 4 # There are labrec records in the header
labrec = int(1024 / lenbyt) labrec = int(1024 / lenbyt)
@@ -259,7 +265,7 @@ def makeSpiderHeader(im):
return [struct.pack("f", v) for v in hdr] return [struct.pack("f", v) for v in hdr]
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode[0] != "F": if im.mode[0] != "F":
im = im.convert("F") im = im.convert("F")
@@ -275,9 +281,10 @@ def _save(im, fp, filename):
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
def _save_spider(im, fp, filename): def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# get the filename extension and register it with Image # get the filename extension and register it with Image
ext = os.path.splitext(filename)[1] filename_ext = os.path.splitext(filename)[1]
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
Image.register_extension(SpiderImageFile.format, ext) Image.register_extension(SpiderImageFile.format, ext)
_save(im, fp, filename) _save(im, fp, filename)
@@ -299,10 +306,10 @@ if __name__ == "__main__":
sys.exit() sys.exit()
with Image.open(filename) as im: with Image.open(filename) as im:
print("image: " + str(im)) print(f"image: {im}")
print("format: " + str(im.format)) print(f"format: {im.format}")
print("size: " + str(im.size)) print(f"size: {im.size}")
print("mode: " + str(im.mode)) print(f"mode: {im.mode}")
print("max, min: ", end=" ") print("max, min: ", end=" ")
print(im.getextrema()) print(im.getextrema())
+1 -7
View File
@@ -16,7 +16,6 @@
from __future__ import annotations from __future__ import annotations
import io import io
from types import TracebackType
from . import ContainerIO from . import ContainerIO
@@ -61,12 +60,7 @@ class TarIO(ContainerIO.ContainerIO[bytes]):
def __enter__(self) -> TarIO: def __enter__(self) -> TarIO:
return self return self
def __exit__( def __exit__(self, *args: object) -> None:
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.close() self.close()
def close(self) -> None: def close(self) -> None:
@@ -36,7 +36,7 @@ MODES = {
(3, 1): "1", (3, 1): "1",
(3, 8): "L", (3, 8): "L",
(3, 16): "LA", (3, 16): "LA",
(2, 16): "BGR;5", (2, 16): "BGRA;15Z",
(2, 24): "BGR", (2, 24): "BGR",
(2, 32): "BGRA", (2, 32): "BGRA",
} }
@@ -87,9 +87,7 @@ class TgaImageFile(ImageFile.ImageFile):
elif imagetype in (1, 9): elif imagetype in (1, 9):
self._mode = "P" if colormaptype else "L" self._mode = "P" if colormaptype else "L"
elif imagetype in (2, 10): elif imagetype in (2, 10):
self._mode = "RGB" self._mode = "RGB" if depth == 24 else "RGBA"
if depth == 32:
self._mode = "RGBA"
else: else:
msg = "unknown TGA mode" msg = "unknown TGA mode"
raise SyntaxError(msg) raise SyntaxError(msg)
@@ -118,15 +116,16 @@ class TgaImageFile(ImageFile.ImageFile):
start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] start, size, mapdepth = i16(s, 3), i16(s, 5), s[7]
if mapdepth == 16: if mapdepth == 16:
self.palette = ImagePalette.raw( 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: elif mapdepth == 24:
self.palette = ImagePalette.raw( 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: elif mapdepth == 32:
self.palette = ImagePalette.raw( self.palette = ImagePalette.raw(
"BGRA", b"\0" * 4 * start + self.fp.read(4 * size) "BGRA", bytes(4 * start) + self.fp.read(4 * size)
) )
else: else:
msg = "unknown TGA map depth" 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: try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode] rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e: except KeyError as e:
+98 -82
View File
@@ -50,12 +50,13 @@ import warnings
from collections.abc import MutableMapping from collections.abc import MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational from numbers import Number, Rational
from typing import TYPE_CHECKING, Any, Callable from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._deprecate import deprecate
from .TiffTags import TYPES from .TiffTags import TYPES
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -200,12 +201,12 @@ OPEN_INFO = {
(MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
(II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
(MM, 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"), (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
(MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
(II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
(MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "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)): ("RGBX", "RGBXXX"), (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)): ("RGBX", "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"), (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, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
(II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
@@ -224,8 +225,8 @@ OPEN_INFO = {
(MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"), (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"), (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, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
(MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
(II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"),
@@ -244,6 +245,7 @@ OPEN_INFO = {
(MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
(II, 3, (1,), 1, (8,), ()): ("P", "P"), (II, 3, (1,), 1, (8,), ()): ("P", "P"),
(MM, 3, (1,), 1, (8,), ()): ("P", "P"), (MM, 3, (1,), 1, (8,), ()): ("P", "P"),
(II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"),
(II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
(MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
(II, 3, (1,), 2, (8,), ()): ("P", "P;R"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
@@ -276,8 +278,11 @@ PREFIXES = [
b"II\x2B\x00", # BigTIFF with little-endian byte order b"II\x2B\x00", # BigTIFF with little-endian byte order
] ]
if not getattr(Image.core, "libtiff_support_custom_tags", True):
deprecate("Support for LibTIFF earlier than version 4", 12)
def _accept(prefix):
def _accept(prefix: bytes) -> bool:
return prefix[:4] in PREFIXES return prefix[:4] in PREFIXES
@@ -376,13 +381,13 @@ class IFDRational(Rational):
f = self._val.limit_denominator(max_denominator) f = self._val.limit_denominator(max_denominator)
return f.numerator, f.denominator return f.numerator, f.denominator
def __repr__(self): def __repr__(self) -> str:
return str(float(self._val)) return str(float(self._val))
def __hash__(self): def __hash__(self) -> int:
return self._val.__hash__() return self._val.__hash__()
def __eq__(self, other): def __eq__(self, other: object) -> bool:
val = self._val val = self._val
if isinstance(other, IFDRational): if isinstance(other, IFDRational):
other = other._val other = other._val
@@ -464,7 +469,7 @@ def _register_basic(idx_fmt_name):
idx, fmt, name = idx_fmt_name idx, fmt, name = idx_fmt_name
TYPES[idx] = name TYPES[idx] = name
size = struct.calcsize("=" + fmt) size = struct.calcsize(f"={fmt}")
_load_dispatch[idx] = ( # noqa: F821 _load_dispatch[idx] = ( # noqa: F821
size, size,
lambda self, data, legacy_api=True: ( lambda self, data, legacy_api=True: (
@@ -546,7 +551,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
_load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {}
_write_dispatch: dict[int, Callable[..., Any]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {}
def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): def __init__(
self,
ifh: bytes = b"II\052\0\0\0\0\0",
prefix: bytes | None = None,
group: int | None = None,
) -> None:
"""Initialize an ImageFileDirectory. """Initialize an ImageFileDirectory.
To construct an ImageFileDirectory from a real file, pass the 8-byte To construct an ImageFileDirectory from a real file, pass the 8-byte
@@ -570,7 +580,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise SyntaxError(msg) raise SyntaxError(msg)
self._bigtiff = ifh[2] == 43 self._bigtiff = ifh[2] == 43
self.group = group self.group = group
self.tagtype = {} self.tagtype: dict[int, int] = {}
""" Dictionary of tag types """ """ Dictionary of tag types """
self.reset() self.reset()
(self.next,) = ( (self.next,) = (
@@ -582,23 +592,23 @@ class ImageFileDirectory_v2(_IFDv2Base):
offset = property(lambda self: self._offset) offset = property(lambda self: self._offset)
@property @property
def legacy_api(self): def legacy_api(self) -> bool:
return self._legacy_api return self._legacy_api
@legacy_api.setter @legacy_api.setter
def legacy_api(self, value): def legacy_api(self, value: bool) -> NoReturn:
msg = "Not allowing setting of legacy api" msg = "Not allowing setting of legacy api"
raise Exception(msg) raise Exception(msg)
def reset(self): def reset(self) -> None:
self._tags_v1 = {} # will remain empty if legacy_api is false self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false
self._tags_v2 = {} # main tag storage self._tags_v2: dict[int, Any] = {} # main tag storage
self._tagdata = {} self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None self._next = None
self._offset = None self._offset = None
def __str__(self): def __str__(self) -> str:
return str(dict(self)) return str(dict(self))
def named(self): def named(self):
@@ -612,7 +622,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
for code, value in self.items() for code, value in self.items()
} }
def __len__(self): def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v2)) return len(set(self._tagdata) | set(self._tags_v2))
def __getitem__(self, tag): def __getitem__(self, tag):
@@ -712,7 +722,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
# Unspec'd, and length > 1 # Unspec'd, and length > 1
dest[tag] = values dest[tag] = values
def __delitem__(self, tag): def __delitem__(self, tag: int) -> None:
self._tags_v2.pop(tag, None) self._tags_v2.pop(tag, None)
self._tags_v1.pop(tag, None) self._tags_v1.pop(tag, None)
self._tagdata.pop(tag, None) self._tagdata.pop(tag, None)
@@ -982,8 +992,8 @@ ImageFileDirectory_v2._load_dispatch = _load_dispatch
ImageFileDirectory_v2._write_dispatch = _write_dispatch ImageFileDirectory_v2._write_dispatch = _write_dispatch
for idx, name in TYPES.items(): for idx, name in TYPES.items():
name = name.replace(" ", "_") name = name.replace(" ", "_")
setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1]) setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1])
setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx]) setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx])
del _load_dispatch, _write_dispatch, idx, name del _load_dispatch, _write_dispatch, idx, name
@@ -1036,7 +1046,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
ifd.next = original.next # an indicator for multipage tiffs ifd.next = original.next # an indicator for multipage tiffs
return ifd return ifd
def to_v2(self): def to_v2(self) -> ImageFileDirectory_v2:
"""Returns an """Returns an
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
instance with the same data as is contained in the original instance with the same data as is contained in the original
@@ -1056,7 +1066,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
def __contains__(self, tag): def __contains__(self, tag):
return tag in self._tags_v1 or tag in self._tagdata return tag in self._tags_v1 or tag in self._tagdata
def __len__(self): def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v1)) return len(set(self._tagdata) | set(self._tags_v1))
def __iter__(self): def __iter__(self):
@@ -1101,7 +1111,7 @@ class TiffImageFile(ImageFile.ImageFile):
super().__init__(fp, filename) super().__init__(fp, filename)
def _open(self): def _open(self) -> None:
"""Open the first image in a TIFF file""" """Open the first image in a TIFF file"""
# Header # Header
@@ -1118,8 +1128,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.__first = self.__next = self.tag_v2.next self.__first = self.__next = self.tag_v2.next
self.__frame = -1 self.__frame = -1
self._fp = self.fp self._fp = self.fp
self._frame_pos = [] self._frame_pos: list[int] = []
self._n_frames = None self._n_frames: int | None = None
logger.debug("*** TiffImageFile._open ***") logger.debug("*** TiffImageFile._open ***")
logger.debug("- __first: %s", self.__first) logger.debug("- __first: %s", self.__first)
@@ -1138,7 +1148,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.seek(current) self.seek(current)
return self._n_frames return self._n_frames
def seek(self, frame): def seek(self, frame: int) -> None:
"""Select a given frame as current image""" """Select a given frame as current image"""
if not self._seek_check(frame): if not self._seek_check(frame):
return return
@@ -1149,7 +1159,7 @@ class TiffImageFile(ImageFile.ImageFile):
Image._decompression_bomb_check(self.size) Image._decompression_bomb_check(self.size)
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self.size)
def _seek(self, frame): def _seek(self, frame: int) -> None:
self.fp = self._fp self.fp = self._fp
# reset buffered io handle in case fp # reset buffered io handle in case fp
@@ -1187,25 +1197,20 @@ class TiffImageFile(ImageFile.ImageFile):
self.__frame += 1 self.__frame += 1
self.fp.seek(self._frame_pos[frame]) self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp) 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() self._reload_exif()
# fill the legacy tag/ifd entries # fill the legacy tag/ifd entries
self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
self.__frame = frame self.__frame = frame
self._setup() self._setup()
def tell(self): def tell(self) -> int:
"""Return the current frame number""" """Return the current frame number"""
return self.__frame 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): def get_photoshop_blocks(self):
""" """
Returns a dictionary of Photoshop "Image Resource Blocks". Returns a dictionary of Photoshop "Image Resource Blocks".
@@ -1232,7 +1237,7 @@ class TiffImageFile(ImageFile.ImageFile):
return self._load_libtiff() return self._load_libtiff()
return super().load() return super().load()
def load_end(self): def load_end(self) -> None:
# allow closing if we're on the first frame, there's no next # allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below. # This is the ImageFile.load path only, libtiff specific below.
if not self.is_animated: if not self.is_animated:
@@ -1653,6 +1658,20 @@ def _save(im, fp, filename):
except Exception: except Exception:
pass # might not be an IFD. Might not have populated type 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 # additions written by Greg Couch, gregc@cgl.ucsf.edu
# inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
if hasattr(im, "tag_v2"): if hasattr(im, "tag_v2"):
@@ -1666,6 +1685,12 @@ def _save(im, fp, filename):
XMP, XMP,
): ):
if key in im.tag_v2: if key in im.tag_v2:
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[key] = im.tag_v2[key]
ifd.tagtype[key] = im.tag_v2.tagtype[key] ifd.tagtype[key] = im.tag_v2.tagtype[key]
@@ -1807,16 +1832,6 @@ def _save(im, fp, filename):
# Merge the ones that we have with (optional) more bits from # Merge the ones that we have with (optional) more bits from
# the original file, e.g x,y resolution so that we can # the original file, e.g x,y resolution so that we can
# save(load('')) == original file. # 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()): for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
# Libtiff can only process certain core items without adding # Libtiff can only process certain core items without adding
# them to the custom dictionary. # them to the custom dictionary.
@@ -1937,7 +1952,7 @@ class AppendingTiffWriter:
self.beginning = self.f.tell() self.beginning = self.f.tell()
self.setup() self.setup()
def setup(self): def setup(self) -> None:
# Reset everything. # Reset everything.
self.f.seek(self.beginning, os.SEEK_SET) self.f.seek(self.beginning, os.SEEK_SET)
@@ -1962,7 +1977,7 @@ class AppendingTiffWriter:
self.skipIFDs() self.skipIFDs()
self.goToEnd() self.goToEnd()
def finalize(self): def finalize(self) -> None:
if self.isFirst: if self.isFirst:
return return
@@ -1985,20 +2000,19 @@ class AppendingTiffWriter:
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
self.fixIFD() self.fixIFD()
def newFrame(self): def newFrame(self) -> None:
# Call this to finish a frame. # Call this to finish a frame.
self.finalize() self.finalize()
self.setup() self.setup()
def __enter__(self): def __enter__(self) -> AppendingTiffWriter:
return self return self
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, *args: object) -> None:
if self.close_fp: if self.close_fp:
self.close() self.close()
return False
def tell(self): def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage return self.f.tell() - self.offsetOfNewPage
def seek(self, offset, whence=io.SEEK_SET): def seek(self, offset, whence=io.SEEK_SET):
@@ -2008,7 +2022,7 @@ class AppendingTiffWriter:
self.f.seek(offset, whence) self.f.seek(offset, whence)
return self.tell() return self.tell()
def goToEnd(self): def goToEnd(self) -> None:
self.f.seek(0, os.SEEK_END) self.f.seek(0, os.SEEK_END)
pos = self.f.tell() pos = self.f.tell()
@@ -2018,13 +2032,13 @@ class AppendingTiffWriter:
self.f.write(bytes(pad_bytes)) self.f.write(bytes(pad_bytes))
self.offsetOfNewPage = self.f.tell() self.offsetOfNewPage = self.f.tell()
def setEndian(self, endian): def setEndian(self, endian: str) -> None:
self.endian = endian self.endian = endian
self.longFmt = self.endian + "L" self.longFmt = f"{self.endian}L"
self.shortFmt = self.endian + "H" self.shortFmt = f"{self.endian}H"
self.tagFormat = self.endian + "HHL" self.tagFormat = f"{self.endian}HHL"
def skipIFDs(self): def skipIFDs(self) -> None:
while True: while True:
ifd_offset = self.readLong() ifd_offset = self.readLong()
if ifd_offset == 0: if ifd_offset == 0:
@@ -2035,55 +2049,55 @@ class AppendingTiffWriter:
num_tags = self.readShort() num_tags = self.readShort()
self.f.seek(num_tags * 12, os.SEEK_CUR) self.f.seek(num_tags * 12, os.SEEK_CUR)
def write(self, data): def write(self, data: bytes) -> int | None:
return self.f.write(data) return self.f.write(data)
def readShort(self): def readShort(self) -> int:
(value,) = struct.unpack(self.shortFmt, self.f.read(2)) (value,) = struct.unpack(self.shortFmt, self.f.read(2))
return value return value
def readLong(self): def readLong(self) -> int:
(value,) = struct.unpack(self.longFmt, self.f.read(4)) (value,) = struct.unpack(self.longFmt, self.f.read(4))
return value return value
def rewriteLastShortToLong(self, value): def rewriteLastShortToLong(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg) raise RuntimeError(msg)
def rewriteLastShort(self, value): def rewriteLastShort(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2: if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2" msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg) raise RuntimeError(msg)
def rewriteLastLong(self, value): def rewriteLastLong(self, value: int) -> None:
self.f.seek(-4, os.SEEK_CUR) self.f.seek(-4, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg) raise RuntimeError(msg)
def writeShort(self, value): def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2: if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2" msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg) raise RuntimeError(msg)
def writeLong(self, value): def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg) raise RuntimeError(msg)
def close(self): def close(self) -> None:
self.finalize() self.finalize()
self.f.close() self.f.close()
def fixIFD(self): def fixIFD(self) -> None:
num_tags = self.readShort() num_tags = self.readShort()
for i in range(num_tags): for i in range(num_tags):
@@ -2092,9 +2106,9 @@ class AppendingTiffWriter:
field_size = self.fieldSizes[field_type] field_size = self.fieldSizes[field_type]
total_size = field_size * count total_size = field_size * count
is_local = total_size <= 4 is_local = total_size <= 4
offset: int | None
if not is_local: if not is_local:
offset = self.readLong() offset = self.readLong() + self.offsetOfNewPage
offset += self.offsetOfNewPage
self.rewriteLastLong(offset) self.rewriteLastLong(offset)
if tag in self.Tags: if tag in self.Tags:
@@ -2118,7 +2132,9 @@ class AppendingTiffWriter:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(4, os.SEEK_CUR)
def fixOffsets(self, count, isShort=False, isLong=False): def fixOffsets(
self, count: int, isShort: bool = False, isLong: bool = False
) -> None:
if not isShort and not isLong: if not isShort and not isLong:
msg = "offset is neither short nor long" msg = "offset is neither short nor long"
raise RuntimeError(msg) raise RuntimeError(msg)
@@ -2144,7 +2160,7 @@ class AppendingTiffWriter:
self.rewriteLastLong(offset) self.rewriteLastLong(offset)
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
encoderconfig = im.encoderconfig encoderconfig = im.encoderconfig
append_images = list(encoderinfo.get("append_images", [])) append_images = list(encoderinfo.get("append_images", []))
+4 -2
View File
@@ -89,7 +89,7 @@ DOUBLE = 12
IFD = 13 IFD = 13
LONG8 = 16 LONG8 = 16
TAGS_V2 = { _tags_v2 = {
254: ("NewSubfileType", LONG, 1), 254: ("NewSubfileType", LONG, 1),
255: ("SubfileType", SHORT, 1), 255: ("SubfileType", SHORT, 1),
256: ("ImageWidth", LONG, 1), 256: ("ImageWidth", LONG, 1),
@@ -425,9 +425,11 @@ TAGS = {
50784: "Alias Layer Metadata", 50784: "Alias Layer Metadata",
} }
TAGS_V2: dict[int, TagInfo] = {}
def _populate(): def _populate():
for k, v in TAGS_V2.items(): for k, v in _tags_v2.items():
# Populate legacy structure. # Populate legacy structure.
TAGS[k] = v[0] TAGS[k] = v[0]
if len(v) == 4: if len(v) == 4:
+1 -1
View File
@@ -32,7 +32,7 @@ class WalImageFile(ImageFile.ImageFile):
format = "WAL" format = "WAL"
format_description = "Quake2 Texture" format_description = "Quake2 Texture"
def _open(self): def _open(self) -> None:
self._mode = "P" self._mode = "P"
# read header fields # read header fields
+13 -20
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
from typing import IO, Any
from . import Image, ImageFile from . import Image, ImageFile
@@ -23,7 +24,7 @@ _VP8_MODES_BY_IDENTIFIER = {
} }
def _accept(prefix): def _accept(prefix: bytes) -> bool | str:
is_riff_file_format = prefix[:4] == b"RIFF" is_riff_file_format = prefix[:4] == b"RIFF"
is_webp_file = prefix[8:12] == b"WEBP" is_webp_file = prefix[8:12] == b"WEBP"
is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER
@@ -34,6 +35,7 @@ def _accept(prefix):
"image file could not be identified because WEBP support not installed" "image file could not be identified because WEBP support not installed"
) )
return True return True
return False
class WebPImageFile(ImageFile.ImageFile): class WebPImageFile(ImageFile.ImageFile):
@@ -42,7 +44,7 @@ class WebPImageFile(ImageFile.ImageFile):
__loaded = 0 __loaded = 0
__logical_frame = 0 __logical_frame = 0
def _open(self): def _open(self) -> None:
if not _webp.HAVE_WEBPANIM: if not _webp.HAVE_WEBPANIM:
# Legacy mode # Legacy mode
data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode(
@@ -94,28 +96,19 @@ class WebPImageFile(ImageFile.ImageFile):
# Initialize seek state # Initialize seek state
self._reset(reset=False) self._reset(reset=False)
def _getexif(self): def _getexif(self) -> dict[str, Any] | None:
if "exif" not in self.info: if "exif" not in self.info:
return None return None
return self.getexif()._get_merged_dict() return self.getexif()._get_merged_dict()
def getxmp(self): def seek(self, frame: int) -> None:
"""
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):
if not self._seek_check(frame): if not self._seek_check(frame):
return return
# Set logical frame to requested position # Set logical frame to requested position
self.__logical_frame = frame self.__logical_frame = frame
def _reset(self, reset=True): def _reset(self, reset: bool = True) -> None:
if reset: if reset:
self._decoder.reset() self._decoder.reset()
self.__physical_frame = 0 self.__physical_frame = 0
@@ -143,7 +136,7 @@ class WebPImageFile(ImageFile.ImageFile):
timestamp -= duration timestamp -= duration
return data, timestamp, duration return data, timestamp, duration
def _seek(self, frame): def _seek(self, frame: int) -> None:
if self.__physical_frame == frame: if self.__physical_frame == frame:
return # Nothing to do return # Nothing to do
if frame < self.__physical_frame: if frame < self.__physical_frame:
@@ -170,17 +163,17 @@ class WebPImageFile(ImageFile.ImageFile):
return super().load() return super().load()
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
pass pass
def tell(self): def tell(self) -> int:
if not _webp.HAVE_WEBPANIM: if not _webp.HAVE_WEBPANIM:
return super().tell() return super().tell()
return self.__logical_frame return self.__logical_frame
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
append_images = list(encoderinfo.get("append_images", [])) append_images = list(encoderinfo.get("append_images", []))
@@ -193,7 +186,7 @@ def _save_all(im, fp, filename):
_save(im, fp, filename) _save(im, fp, filename)
return return
background = (0, 0, 0, 0) background: int | tuple[int, ...] = (0, 0, 0, 0)
if "background" in encoderinfo: if "background" in encoderinfo:
background = encoderinfo["background"] background = encoderinfo["background"]
elif "background" in im.info: elif "background" in im.info:
@@ -323,7 +316,7 @@ def _save_all(im, fp, filename):
fp.write(data) fp.write(data)
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
lossless = im.encoderinfo.get("lossless", False) lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
alpha_quality = im.encoderinfo.get("alpha_quality", 100) alpha_quality = im.encoderinfo.get("alpha_quality", 100)
+10 -8
View File
@@ -20,6 +20,8 @@
# http://wvware.sourceforge.net/caolan/ora-wmf.html # http://wvware.sourceforge.net/caolan/ora-wmf.html
from __future__ import annotations from __future__ import annotations
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16le as word from ._binary import i16le as word
from ._binary import si16le as short from ._binary import si16le as short
@@ -28,7 +30,7 @@ from ._binary import si32le as _long
_handler = None _handler = None
def register_handler(handler): def register_handler(handler: ImageFile.StubHandler | None) -> None:
""" """
Install application-specific WMF image handler. Install application-specific WMF image handler.
@@ -41,12 +43,12 @@ def register_handler(handler):
if hasattr(Image.core, "drawwmf"): if hasattr(Image.core, "drawwmf"):
# install default handler (windows only) # install default handler (windows only)
class WmfHandler: class WmfHandler(ImageFile.StubHandler):
def open(self, im): def open(self, im: ImageFile.StubImageFile) -> None:
im._mode = "RGB" im._mode = "RGB"
self.bbox = im.info["wmf_bbox"] self.bbox = im.info["wmf_bbox"]
def load(self, im): def load(self, im: ImageFile.StubImageFile) -> Image.Image:
im.fp.seek(0) # rewind im.fp.seek(0) # rewind
return Image.frombytes( return Image.frombytes(
"RGB", "RGB",
@@ -65,7 +67,7 @@ if hasattr(Image.core, "drawwmf"):
# Read WMF file # Read WMF file
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return ( return (
prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00"
) )
@@ -79,7 +81,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
format = "WMF" format = "WMF"
format_description = "Windows Metafile" format_description = "Windows Metafile"
def _open(self): def _open(self) -> None:
self._inch = None self._inch = None
# check placable header # check placable header
@@ -147,7 +149,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
if loader: if loader:
loader.open(self) loader.open(self)
def _load(self): def _load(self) -> ImageFile.StubHandler | None:
return _handler return _handler
def load(self, dpi=None): def load(self, dpi=None):
@@ -161,7 +163,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
return super().load() return super().load()
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"): if _handler is None or not hasattr(_handler, "save"):
msg = "WMF save handler not installed" msg = "WMF save handler not installed"
raise OSError(msg) raise OSError(msg)
@@ -70,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile):
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] self.tile = [("xbm", (0, 0) + self.size, m.end(), None)]
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode != "1": if im.mode != "1":
msg = f"cannot write mode {im.mode} as XBM" msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg) raise OSError(msg)
@@ -24,7 +24,7 @@ from ._binary import o8
xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)')
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:9] == b"/* XPM */" return prefix[:9] == b"/* XPM */"
@@ -36,7 +36,7 @@ class XpmImageFile(ImageFile.ImageFile):
format = "XPM" format = "XPM"
format_description = "X11 Pixel Map" format_description = "X11 Pixel Map"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(9)): if not _accept(self.fp.read(9)):
msg = "not an XPM file" msg = "not an XPM file"
raise SyntaxError(msg) raise SyntaxError(msg)
@@ -103,16 +103,13 @@ class XpmImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))]
def load_read(self, read_bytes): def load_read(self, read_bytes: int) -> bytes:
# #
# load all image data in one chunk # load all image data in one chunk
xsize, ysize = self.size xsize, ysize = self.size
s = [None] * ysize s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)]
for i in range(ysize):
s[i] = self.fp.readline()[1 : xsize + 1].ljust(xsize)
return b"".join(s) return b"".join(s)

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