diff --git a/.venv/.gitignore b/.venv/.gitignore index e69de29..f59ec20 100644 --- a/.venv/.gitignore +++ b/.venv/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/.venv/Lib/site-packages/PIL/BdfFontFile.py b/.venv/Lib/site-packages/PIL/BdfFontFile.py index e3eda4f..bc1416c 100644 --- a/.venv/Lib/site-packages/PIL/BdfFontFile.py +++ b/.venv/Lib/site-packages/PIL/BdfFontFile.py @@ -103,7 +103,7 @@ def bdf_char( class BdfFontFile(FontFile.FontFile): """Font file plugin for the X11 BDF format.""" - def __init__(self, fp: BinaryIO): + def __init__(self, fp: BinaryIO) -> None: super().__init__() s = fp.readline() diff --git a/.venv/Lib/site-packages/PIL/BlpImagePlugin.py b/.venv/Lib/site-packages/PIL/BlpImagePlugin.py index 95e8077..b9cefaf 100644 --- a/.venv/Lib/site-packages/PIL/BlpImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/BlpImagePlugin.py @@ -31,10 +31,12 @@ BLP files come in many different flavours: from __future__ import annotations +import abc import os import struct from enum import IntEnum from io import BytesIO +from typing import IO from . import Image, ImageFile @@ -55,11 +57,13 @@ class AlphaEncoding(IntEnum): 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 -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) """ @@ -67,9 +71,9 @@ def decode_dxt1(data, alpha=False): blocks = len(data) // 8 # number of blocks in row ret = (bytearray(), bytearray(), bytearray(), bytearray()) - for block in range(blocks): + for block_index in range(blocks): # Decode next 8-byte block. - idx = block * 8 + idx = block_index * 8 color0, color1, bits = struct.unpack_from(" tuple[bytearray, bytearray, bytearray, bytearray]: """ 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 ret = (bytearray(), bytearray(), bytearray(), bytearray()) - for block in range(blocks): - idx = block * 16 + for block_index in range(blocks): + idx = block_index * 16 block = data[idx : idx + 16] # Decode next 16-byte block. bits = struct.unpack_from("<8B", block) @@ -167,7 +171,7 @@ def decode_dxt3(data): return ret -def decode_dxt5(data): +def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]: """ input: one "row" of data (i.e. will produce 4 * width pixels) """ @@ -175,8 +179,8 @@ def decode_dxt5(data): blocks = len(data) // 16 # number of blocks in row ret = (bytearray(), bytearray(), bytearray(), bytearray()) - for block in range(blocks): - idx = block * 16 + for block_index in range(blocks): + idx = block_index * 16 block = data[idx : idx + 16] # Decode next 16-byte block. a0, a1 = struct.unpack_from(" bool: return prefix[:4] in (b"BLP1", b"BLP2") @@ -253,7 +257,7 @@ class BlpImageFile(ImageFile.ImageFile): format = "BLP" format_description = "Blizzard Mipmap Format" - def _open(self): + def _open(self) -> None: self.magic = self.fp.read(4) self.fp.seek(5, os.SEEK_CUR) @@ -275,7 +279,7 @@ class BlpImageFile(ImageFile.ImageFile): class _BLPBaseDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: try: self._read_blp_header() self._load() @@ -284,7 +288,12 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): raise OSError(msg) from e return -1, 0 - def _read_blp_header(self): + @abc.abstractmethod + def _load(self) -> None: + pass + + def _read_blp_header(self) -> None: + assert self.fd is not None self.fd.seek(4) (self._blp_compression,) = struct.unpack(" bytes: return ImageFile._safe_read(self.fd, length) - def _read_palette(self): + def _read_palette(self) -> list[tuple[int, int, int, int]]: ret = [] for i in range(256): try: @@ -316,7 +325,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): ret.append((b, g, r, a)) return ret - def _read_bgra(self, palette): + def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray: data = bytearray() _data = BytesIO(self._safe_read(self._blp_lengths[0])) while True: @@ -325,7 +334,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): except struct.error: break b, g, r, a = palette[offset] - d = (r, g, b) + d: tuple[int, ...] = (r, g, b) if self._blp_alpha_depth: d += (a,) data.extend(d) @@ -333,7 +342,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): class BLP1Decoder(_BLPBaseDecoder): - def _load(self): + def _load(self) -> None: if self._blp_compression == Format.JPEG: self._decode_jpeg_stream() @@ -349,29 +358,30 @@ class BLP1Decoder(_BLPBaseDecoder): msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" raise BLPFormatError(msg) - def _decode_jpeg_stream(self): + def _decode_jpeg_stream(self) -> None: from .JpegImagePlugin import JpegImageFile (jpeg_header_size,) = struct.unpack(" None: palette = self._read_palette() + assert self.fd is not None self.fd.seek(self._blp_offsets[0]) if self._blp_compression == 1: @@ -418,8 +428,9 @@ class BLP2Decoder(_BLPBaseDecoder): class BLPEncoder(ImageFile.PyEncoder): _pushes_fd = True - def _write_palette(self): + def _write_palette(self) -> bytes: data = b"" + assert self.im is not None palette = self.im.getpalette("RGBA", "RGBA") for i in range(len(palette) // 4): r, g, b, a = palette[i * 4 : (i + 1) * 4] @@ -428,12 +439,13 @@ class BLPEncoder(ImageFile.PyEncoder): data += b"\x00" * 4 return data - def encode(self, bufsize): + def encode(self, bufsize: int) -> tuple[int, int, bytes]: palette_data = self._write_palette() offset = 20 + 16 * 4 * 2 + len(palette_data) data = struct.pack("<16I", offset, *((0,) * 15)) + assert self.im is not None w, h = self.im.size data += struct.pack("<16I", w * h, *((0,) * 15)) @@ -446,7 +458,7 @@ class BLPEncoder(ImageFile.PyEncoder): return len(data), 0, data -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode != "P": msg = "Unsupported BLP image mode" raise ValueError(msg) diff --git a/.venv/Lib/site-packages/PIL/BmpImagePlugin.py b/.venv/Lib/site-packages/PIL/BmpImagePlugin.py index 9947f43..ef20453 100644 --- a/.venv/Lib/site-packages/PIL/BmpImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/BmpImagePlugin.py @@ -25,6 +25,7 @@ from __future__ import annotations import os +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -48,12 +49,12 @@ BIT2MODE = { } -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:2] == b"BM" -def _dib_accept(prefix): - return i32(prefix) in [12, 40, 64, 108, 124] +def _dib_accept(prefix: bytes) -> bool: + 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 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 + # 12: BITMAPCOREHEADER/OS21XBITMAPHEADER if file_info["header_size"] == 12: file_info["width"] = i16(header_data, 0) file_info["height"] = i16(header_data, 2) @@ -93,9 +95,14 @@ class BmpImageFile(ImageFile.ImageFile): file_info["compression"] = self.RAW file_info["palette_padding"] = 3 - # --------------------------------------------- Windows Bitmap v2 to v5 - # v3, OS/2 v2, v4, v5 - elif file_info["header_size"] in (40, 64, 108, 124): + # --------------------------------------------- Windows Bitmap v3 to v5 + # 40: BITMAPINFOHEADER + # 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["direction"] = 1 if file_info["y_flip"] else -1 file_info["width"] = i32(header_data, 0) @@ -117,10 +124,13 @@ class BmpImageFile(ImageFile.ImageFile): file_info["palette_padding"] = 4 self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) if file_info["compression"] == self.BITFIELDS: - if len(header_data) >= 52: - for idx, mask in enumerate( - ["r_mask", "g_mask", "b_mask", "a_mask"] - ): + masks = ["r_mask", "g_mask", "b_mask"] + if len(header_data) >= 48: + if len(header_data) >= 52: + masks.append("a_mask") + else: + file_info["a_mask"] = 0x0 + for idx, mask in enumerate(masks): file_info[mask] = i32(header_data, 36 + idx * 4) else: # 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, # and it is not generally an alpha channel 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["rgb_mask"] = ( file_info["r_mask"], @@ -175,9 +185,11 @@ class BmpImageFile(ImageFile.ImageFile): 32: [ (0xFF0000, 0xFF00, 0xFF, 0x0), (0xFF000000, 0xFF0000, 0xFF00, 0x0), + (0xFF000000, 0xFF00, 0xFF, 0x0), (0xFF000000, 0xFF0000, 0xFF00, 0xFF), (0xFF, 0xFF00, 0xFF0000, 0xFF000000), (0xFF0000, 0xFF00, 0xFF, 0xFF000000), + (0xFF000000, 0xFF00, 0xFF, 0xFF0000), (0x0, 0x0, 0x0, 0x0), ], 24: [(0xFF0000, 0xFF00, 0xFF)], @@ -186,9 +198,11 @@ class BmpImageFile(ImageFile.ImageFile): MASK_MODES = { (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", + (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR", (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", + (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", (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""" # read 14 bytes: magic number, filesize, reserved, header final offset head_data = self.fp.read(14) @@ -287,7 +301,8 @@ class BmpImageFile(ImageFile.ImageFile): class BmpRleDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None rle4 = self.args[1] data = bytearray() x = 0 @@ -363,7 +378,7 @@ class DibImageFile(BmpImageFile): format = "DIB" format_description = "Windows Bitmap" - def _open(self): + def _open(self) -> None: 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) -def _save(im, fp, filename, bitmap_header=True): +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True +) -> None: try: rawmode, bits, colors = SAVE[im.mode] except KeyError as e: diff --git a/.venv/Lib/site-packages/PIL/BufrStubImagePlugin.py b/.venv/Lib/site-packages/PIL/BufrStubImagePlugin.py index 60f3ec2..0ee2f65 100644 --- a/.venv/Lib/site-packages/PIL/BufrStubImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/BufrStubImagePlugin.py @@ -10,12 +10,14 @@ # from __future__ import annotations +from typing import IO + from . import Image, ImageFile _handler = None -def register_handler(handler): +def register_handler(handler: ImageFile.StubHandler | None) -> None: """ Install application-specific BUFR image handler. @@ -29,7 +31,7 @@ def register_handler(handler): # Image adapter -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" @@ -37,7 +39,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): format = "BUFR" format_description = "BUFR" - def _open(self): + def _open(self) -> None: offset = self.fp.tell() if not _accept(self.fp.read(4)): @@ -54,11 +56,11 @@ class BufrStubImageFile(ImageFile.StubImageFile): if loader: loader.open(self) - def _load(self): + def _load(self) -> ImageFile.StubHandler | None: return _handler -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if _handler is None or not hasattr(_handler, "save"): msg = "BUFR save handler not installed" raise OSError(msg) diff --git a/.venv/Lib/site-packages/PIL/CurImagePlugin.py b/.venv/Lib/site-packages/PIL/CurImagePlugin.py index 5fb2b01..85e2145 100644 --- a/.venv/Lib/site-packages/PIL/CurImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/CurImagePlugin.py @@ -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" @@ -37,7 +37,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): format = "CUR" format_description = "Windows Cursor" - def _open(self): + def _open(self) -> None: offset = self.fp.tell() # check magic diff --git a/.venv/Lib/site-packages/PIL/DcxImagePlugin.py b/.venv/Lib/site-packages/PIL/DcxImagePlugin.py index f7344df..f67f27d 100644 --- a/.venv/Lib/site-packages/PIL/DcxImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/DcxImagePlugin.py @@ -29,7 +29,7 @@ from .PcxImagePlugin import PcxImageFile MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 4 and i32(prefix) == MAGIC @@ -42,7 +42,7 @@ class DcxImageFile(PcxImageFile): format_description = "Intel DCX" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: # Header s = self.fp.read(4) if not _accept(s): @@ -58,12 +58,12 @@ class DcxImageFile(PcxImageFile): self._offset.append(offset) self._fp = self.fp - self.frame = None + self.frame = -1 self.n_frames = len(self._offset) self.is_animated = self.n_frames > 1 self.seek(0) - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return self.frame = frame @@ -71,7 +71,7 @@ class DcxImageFile(PcxImageFile): self.fp.seek(self._offset[frame]) PcxImageFile._open(self) - def tell(self): + def tell(self) -> int: return self.frame diff --git a/.venv/Lib/site-packages/PIL/DdsImagePlugin.py b/.venv/Lib/site-packages/PIL/DdsImagePlugin.py index 93c8e34..a57e4ae 100644 --- a/.venv/Lib/site-packages/PIL/DdsImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/DdsImagePlugin.py @@ -16,6 +16,7 @@ import io import struct import sys from enum import IntEnum, IntFlag +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i32le as i32 @@ -271,16 +272,16 @@ class D3DFMT(IntEnum): module = sys.modules[__name__] for item in DDSD: 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: 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: 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: 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_RGB = DDPF.RGB @@ -331,7 +332,7 @@ class DdsImageFile(ImageFile.ImageFile): format = "DDS" format_description = "DirectDraw Surface" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) @@ -379,6 +380,7 @@ class DdsImageFile(ImageFile.ImageFile): elif pfflags & DDPF.PALETTEINDEXED8: self._mode = "P" self.palette = ImagePalette.raw("RGBA", self.fp.read(1024)) + self.palette.mode = "RGBA" elif pfflags & DDPF.FOURCC: offset = header_size + 4 if fourcc == D3DFMT.DXT1: @@ -472,14 +474,15 @@ class DdsImageFile(ImageFile.ImageFile): else: self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)] - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass class DdsRgbDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None bitcount, masks = self.args # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 @@ -510,7 +513,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder): return -1, 0 -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode not in ("RGB", "RGBA", "L", "LA"): msg = f"cannot write mode {im.mode} as DDS" raise OSError(msg) @@ -562,7 +565,7 @@ def _save(im, fp, filename): ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"DDS " diff --git a/.venv/Lib/site-packages/PIL/EpsImagePlugin.py b/.venv/Lib/site-packages/PIL/EpsImagePlugin.py index 523ffcb..f31b1c1 100644 --- a/.venv/Lib/site-packages/PIL/EpsImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/EpsImagePlugin.py @@ -27,6 +27,7 @@ import re import subprocess import sys import tempfile +from typing import IO from . import Image, ImageFile from ._binary import i32le as i32 @@ -42,7 +43,7 @@ gs_binary: str | bool | None = None gs_windows_binary = None -def has_ghostscript(): +def has_ghostscript() -> bool: global gs_binary, gs_windows_binary if gs_binary is None: if sys.platform.startswith("win"): @@ -178,7 +179,7 @@ class PSFile: self.char = None self.fp.seek(offset, whence) - def readline(self): + def readline(self) -> str: s = [self.char or b""] self.char = None @@ -195,7 +196,7 @@ class PSFile: 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) @@ -212,7 +213,7 @@ class EpsImageFile(ImageFile.ImageFile): mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} - def _open(self): + def _open(self) -> None: (length, offset) = self._find_offset(self.fp) # go to offset - start of "%!PS" @@ -228,7 +229,12 @@ class EpsImageFile(ImageFile.ImageFile): reading_trailer_comments = False trailer_reached = False - def check_required_header_comments(): + def check_required_header_comments() -> None: + """ + The EPS specification requires that some headers exist. + This should be checked when the header comments formally end, + when image data starts, or when the file ends, whichever comes first. + """ if "PS-Adobe" not in self.info: msg = 'EPS header missing "%!PS-Adobe" comment' raise SyntaxError(msg) @@ -236,7 +242,7 @@ class EpsImageFile(ImageFile.ImageFile): msg = 'EPS header missing "%%BoundingBox" comment' raise SyntaxError(msg) - def _read_comment(s): + def _read_comment(s: str) -> bool: nonlocal reading_trailer_comments try: m = split.match(s) @@ -244,33 +250,33 @@ class EpsImageFile(ImageFile.ImageFile): msg = "not an EPS file" raise SyntaxError(msg) from e - if m: - k, v = m.group(1, 2) - self.info[k] = v - if k == "BoundingBox": - if v == "(atend)": - reading_trailer_comments = True - elif not self._size or ( - trailer_reached and reading_trailer_comments - ): - try: - # Note: The DSC spec says that BoundingBox - # fields should be integers, but some drivers - # put floating point values there anyway. - box = [int(float(i)) for i in v.split()] - self._size = box[2] - box[0], box[3] - box[1] - self.tile = [ - ("eps", (0, 0) + self.size, offset, (length, box)) - ] - except Exception: - pass - return True + if not m: + return False + + k, v = m.group(1, 2) + self.info[k] = v + if k == "BoundingBox": + if v == "(atend)": + reading_trailer_comments = True + elif not self._size or (trailer_reached and reading_trailer_comments): + try: + # Note: The DSC spec says that BoundingBox + # fields should be integers, but some drivers + # put floating point values there anyway. + box = [int(float(i)) for i in v.split()] + self._size = box[2] - box[0], box[3] - box[1] + self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] + except Exception: + pass + return True while True: byte = self.fp.read(1) if byte == b"": # if we didn't read a byte we must be at the end of the file if bytes_read == 0: + if reading_header_comments: + check_required_header_comments() break elif byte in b"\r\n": # if we read a line ending character, ignore it and parse what @@ -366,8 +372,6 @@ class EpsImageFile(ImageFile.ImageFile): trailer_reached = True bytes_read = 0 - check_required_header_comments() - if not self._size: msg = "cannot determine EPS bounding box" raise OSError(msg) @@ -404,7 +408,7 @@ class EpsImageFile(ImageFile.ImageFile): self.tile = [] 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 # use our custom load method by defining this method. 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.""" # make sure image data is available diff --git a/.venv/Lib/site-packages/PIL/ExifTags.py b/.venv/Lib/site-packages/PIL/ExifTags.py index 60a4d97..39b4aa5 100644 --- a/.venv/Lib/site-packages/PIL/ExifTags.py +++ b/.venv/Lib/site-packages/PIL/ExifTags.py @@ -346,7 +346,7 @@ class Interop(IntEnum): InteropVersion = 2 RelatedImageFileFormat = 4096 RelatedImageWidth = 4097 - RleatedImageHeight = 4098 + RelatedImageHeight = 4098 class IFD(IntEnum): diff --git a/.venv/Lib/site-packages/PIL/FitsImagePlugin.py b/.venv/Lib/site-packages/PIL/FitsImagePlugin.py index 0719189..4846054 100644 --- a/.venv/Lib/site-packages/PIL/FitsImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/FitsImagePlugin.py @@ -115,14 +115,18 @@ class FitsImageFile(ImageFile.ImageFile): elif number_of_bits in (-32, -64): self._mode = "F" - args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,) + args: tuple[str | int, ...] + if decoder_name == "raw": + args = (self.mode, 0, -1) + else: + args = (number_of_bits,) return decoder_name, offset, args class FitsGzipDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: assert self.fd is not None value = gzip.decompress(self.fd.read()) diff --git a/.venv/Lib/site-packages/PIL/FliImagePlugin.py b/.venv/Lib/site-packages/PIL/FliImagePlugin.py index f9e4c73..dceb839 100644 --- a/.venv/Lib/site-packages/PIL/FliImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/FliImagePlugin.py @@ -27,7 +27,7 @@ from ._binary import o8 # decoder -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return ( len(prefix) >= 6 and i16(prefix, 4) in [0xAF11, 0xAF12] @@ -123,7 +123,7 @@ class FliImageFile(ImageFile.ImageFile): palette[i] = (r, g, b) i += 1 - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return if frame < self.__frame: @@ -132,7 +132,7 @@ class FliImageFile(ImageFile.ImageFile): for f in range(self.__frame + 1, frame + 1): self._seek(f) - def _seek(self, frame): + def _seek(self, frame: int) -> None: if frame == 0: self.__frame = -1 self._fp.seek(self.__rewind) @@ -162,7 +162,7 @@ class FliImageFile(ImageFile.ImageFile): self.__offset += framesize - def tell(self): + def tell(self) -> int: return self.__frame diff --git a/.venv/Lib/site-packages/PIL/FpxImagePlugin.py b/.venv/Lib/site-packages/PIL/FpxImagePlugin.py index 75680a9..c1927bd 100644 --- a/.venv/Lib/site-packages/PIL/FpxImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/FpxImagePlugin.py @@ -41,7 +41,7 @@ MODES = { # -------------------------------------------------------------------- -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:8] == olefile.MAGIC @@ -70,7 +70,7 @@ class FpxImageFile(ImageFile.ImageFile): self._open_index(1) - def _open_index(self, index=1): + def _open_index(self, index: int = 1) -> None: # # get the Image Contents Property Set @@ -85,7 +85,7 @@ class FpxImageFile(ImageFile.ImageFile): size = max(self.size) i = 1 while size > 64: - size = size / 2 + size = size // 2 i += 1 self.maxid = i - 1 @@ -118,7 +118,7 @@ class FpxImageFile(ImageFile.ImageFile): self._open_subimage(1, self.maxid) - def _open_subimage(self, index=1, subimage=0): + def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: # # setup tile descriptors for a given subimage @@ -237,11 +237,11 @@ class FpxImageFile(ImageFile.ImageFile): return ImageFile.ImageFile.load(self) - def close(self): + def close(self) -> None: self.ole.close() super().close() - def __exit__(self, *args): + def __exit__(self, *args: object) -> None: self.ole.close() super().__exit__() diff --git a/.venv/Lib/site-packages/PIL/FtexImagePlugin.py b/.venv/Lib/site-packages/PIL/FtexImagePlugin.py index b4488e6..5acbb49 100644 --- a/.venv/Lib/site-packages/PIL/FtexImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/FtexImagePlugin.py @@ -71,7 +71,7 @@ class FtexImageFile(ImageFile.ImageFile): format = "FTEX" format_description = "Texture File Format (IW2:EOC)" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not an FTEX file" raise SyntaxError(msg) @@ -103,11 +103,11 @@ class FtexImageFile(ImageFile.ImageFile): self.fp.close() self.fp = BytesIO(data) - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == MAGIC diff --git a/.venv/Lib/site-packages/PIL/GbrImagePlugin.py b/.venv/Lib/site-packages/PIL/GbrImagePlugin.py index 6722fa2..93e89b1 100644 --- a/.venv/Lib/site-packages/PIL/GbrImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/GbrImagePlugin.py @@ -29,7 +29,7 @@ from . import Image, ImageFile 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) @@ -41,7 +41,7 @@ class GbrImageFile(ImageFile.ImageFile): format = "GBR" format_description = "GIMP brush file" - def _open(self): + def _open(self) -> None: header_size = i32(self.fp.read(4)) if header_size < 20: msg = "not a GIMP brush" diff --git a/.venv/Lib/site-packages/PIL/GifImagePlugin.py b/.venv/Lib/site-packages/PIL/GifImagePlugin.py index 6b415d2..284128c 100644 --- a/.venv/Lib/site-packages/PIL/GifImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/GifImagePlugin.py @@ -29,7 +29,10 @@ import itertools import math import os import subprocess +import sys from enum import IntEnum +from functools import cached_property +from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union from . import ( Image, @@ -44,6 +47,9 @@ from ._binary import i16le as i16 from ._binary import o8 from ._binary import o16le as o16 +if TYPE_CHECKING: + from . import _imaging + class LoadingStrategy(IntEnum): """.. versionadded:: 9.1.0""" @@ -60,7 +66,7 @@ LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST # Identify/read GIF files -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] in [b"GIF87a", b"GIF89a"] @@ -76,19 +82,19 @@ class GifImageFile(ImageFile.ImageFile): global_palette = None - def data(self): + def data(self) -> bytes | None: s = self.fp.read(1) if s and s[0]: return self.fp.read(s[0]) return None - def _is_palette_needed(self, p): + def _is_palette_needed(self, p: bytes) -> bool: for i in range(0, len(p), 3): if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): return True return False - def _open(self): + def _open(self) -> None: # Screen s = self.fp.read(13) if not _accept(s): @@ -112,12 +118,11 @@ class GifImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack self.__rewind = self.fp.tell() - self._n_frames = None - self._is_animated = None + self._n_frames: int | None = None self._seek(0) # get ready to read first frame @property - def n_frames(self): + def n_frames(self) -> int: if self._n_frames is None: current = self.tell() try: @@ -128,26 +133,25 @@ class GifImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames - @property - def is_animated(self): - if self._is_animated is None: - if self._n_frames is not None: - self._is_animated = self._n_frames != 1 - else: - current = self.tell() - if current: - self._is_animated = True - else: - try: - self._seek(1, False) - self._is_animated = True - except EOFError: - self._is_animated = False + @cached_property + def is_animated(self) -> bool: + if self._n_frames is not None: + return self._n_frames != 1 - self.seek(current) - return self._is_animated + current = self.tell() + if current: + return True - def seek(self, frame): + try: + self._seek(1, False) + is_animated = True + except EOFError: + is_animated = False + + self.seek(current) + return is_animated + + def seek(self, frame: int) -> None: if not self._seek_check(frame): return if frame < self.__frame: @@ -163,11 +167,11 @@ class GifImageFile(ImageFile.ImageFile): msg = "no more images in GIF file" raise EOFError(msg) from e - def _seek(self, frame, update_image=True): + def _seek(self, frame: int, update_image: bool = True) -> None: if frame == 0: # rewind self.__offset = 0 - self.dispose = None + self.dispose: _imaging.ImagingCore | None = None self.__frame = -1 self._fp.seek(self.__rewind) self.disposal_method = 0 @@ -195,9 +199,9 @@ class GifImageFile(ImageFile.ImageFile): msg = "no more images in GIF file" raise EOFError(msg) - palette = None + palette: ImagePalette.ImagePalette | Literal[False] | None = None - info = {} + info: dict[str, Any] = {} frame_transparency = None interlace = None frame_dispose_extent = None @@ -213,7 +217,7 @@ class GifImageFile(ImageFile.ImageFile): # s = self.fp.read(1) block = self.data() - if s[0] == 249: + if s[0] == 249 and block is not None: # # graphic control extension # @@ -249,14 +253,14 @@ class GifImageFile(ImageFile.ImageFile): info["comment"] = comment s = None continue - elif s[0] == 255 and frame == 0: + elif s[0] == 255 and frame == 0 and block is not None: # # application extension # info["extension"] = block, self.fp.tell() if block[:11] == b"NETSCAPE2.0": block = self.data() - if len(block) >= 3 and block[0] == 1: + if block and len(block) >= 3 and block[0] == 1: self.info["loop"] = i16(block, 1) while self.data(): pass @@ -337,60 +341,60 @@ class GifImageFile(ImageFile.ImageFile): self._mode = "RGB" self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) - def _rgb(color): + def _rgb(color: int) -> tuple[int, int, int]: if self._frame_palette: if color * 3 + 3 > len(self._frame_palette.palette): color = 0 - color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) else: - color = (color, color, color) - return color + return (color, color, color) + self.dispose = None self.dispose_extent = frame_dispose_extent - try: - if self.disposal_method < 2: - # do not dispose or none specified - self.dispose = None - elif self.disposal_method == 2: - # replace with background colour + if self.dispose_extent and self.disposal_method >= 2: + try: + if self.disposal_method == 2: + # replace with background colour - # only dispose the extent in this frame - x0, y0, x1, y1 = self.dispose_extent - dispose_size = (x1 - x0, y1 - y0) - - Image._decompression_bomb_check(dispose_size) - - # by convention, attempt to use transparency first - dispose_mode = "P" - color = self.info.get("transparency", frame_transparency) - if color is not None: - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGBA" - color = _rgb(color) + (0,) - else: - color = self.info.get("background", 0) - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGB" - color = _rgb(color) - self.dispose = Image.core.fill(dispose_mode, dispose_size, color) - else: - # replace with previous contents - if self.im is not None: # only dispose the extent in this frame - self.dispose = self._crop(self.im, self.dispose_extent) - elif frame_transparency is not None: x0, y0, x1, y1 = self.dispose_extent dispose_size = (x1 - x0, y1 - y0) Image._decompression_bomb_check(dispose_size) + + # by convention, attempt to use transparency first dispose_mode = "P" - color = frame_transparency - if self.mode in ("RGB", "RGBA"): - dispose_mode = "RGBA" - color = _rgb(frame_transparency) + (0,) + color = self.info.get("transparency", frame_transparency) + if color is not None: + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGBA" + color = _rgb(color) + (0,) + else: + color = self.info.get("background", 0) + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGB" + color = _rgb(color) self.dispose = Image.core.fill(dispose_mode, dispose_size, color) - except AttributeError: - pass + else: + # replace with previous contents + if self.im is not None: + # only dispose the extent in this frame + self.dispose = self._crop(self.im, self.dispose_extent) + elif frame_transparency is not None: + x0, y0, x1, y1 = self.dispose_extent + dispose_size = (x1 - x0, y1 - y0) + + Image._decompression_bomb_check(dispose_size) + dispose_mode = "P" + color = frame_transparency + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGBA" + color = _rgb(frame_transparency) + (0,) + self.dispose = Image.core.fill( + dispose_mode, dispose_size, color + ) + except AttributeError: + pass if interlace is not None: transparency = -1 @@ -417,7 +421,7 @@ class GifImageFile(ImageFile.ImageFile): elif k in self.info: del self.info[k] - def load_prepare(self): + def load_prepare(self) -> None: temp_mode = "P" if self._frame_palette else "L" self._prev_im = None if self.__frame == 0: @@ -429,7 +433,7 @@ class GifImageFile(ImageFile.ImageFile): self._prev_im = self.im if self._frame_palette: self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) - self.im.putpalette(*self._frame_palette.getdata()) + self.im.putpalette("RGB", *self._frame_palette.getdata()) else: self.im = None self._mode = temp_mode @@ -437,7 +441,7 @@ class GifImageFile(ImageFile.ImageFile): super().load_prepare() - def load_end(self): + def load_end(self) -> None: if self.__frame == 0: if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self._frame_transparency is not None: @@ -454,6 +458,8 @@ class GifImageFile(ImageFile.ImageFile): frame_im = self.im.convert("RGBA") else: frame_im = self.im.convert("RGB") + + assert self.dispose_extent is not None frame_im = self._crop(frame_im, self.dispose_extent) self.im = self._prev_im @@ -463,7 +469,7 @@ class GifImageFile(ImageFile.ImageFile): else: self.im.paste(frame_im, self.dispose_extent) - def tell(self): + def tell(self) -> int: return self.__frame @@ -474,7 +480,7 @@ class GifImageFile(ImageFile.ImageFile): 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 for saving in a Gif. @@ -499,7 +505,12 @@ def _normalize_mode(im): return im.convert("L") -def _normalize_palette(im, palette, info): +_Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette] + + +def _normalize_palette( + im: Image.Image, palette: _Palette | None, info: dict[str, Any] +) -> Image.Image: """ Normalizes the palette for image. - Sets the palette to the incoming palette, if provided. @@ -527,8 +538,10 @@ def _normalize_palette(im, palette, info): source_palette = bytearray(i // 3 for i in range(768)) im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) + used_palette_colors: list[int] | None if palette: used_palette_colors = [] + assert source_palette is not None for i in range(0, len(source_palette), 3): source_color = tuple(source_palette[i : i + 3]) index = im.palette.colors.get(source_color) @@ -559,7 +572,11 @@ def _normalize_palette(im, palette, info): return im -def _write_single_frame(im, fp, palette): +def _write_single_frame( + im: Image.Image, + fp: IO[bytes], + palette: _Palette | None, +) -> None: im_out = _normalize_mode(im) for k, v in im_out.info.items(): im.encoderinfo.setdefault(k, v) @@ -580,7 +597,9 @@ def _write_single_frame(im, fp, palette): fp.write(b"\0") # end of image data -def _getbbox(base_im, im_frame): +def _getbbox( + base_im: Image.Image, im_frame: Image.Image +) -> tuple[Image.Image, tuple[int, int, int, int] | None]: if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): im_frame = im_frame.convert("RGBA") base_im = base_im.convert("RGBA") @@ -588,12 +607,20 @@ def _getbbox(base_im, im_frame): return delta, delta.getbbox(alpha_only=False) -def _write_multiple_frames(im, fp, palette): +class _Frame(NamedTuple): + im: Image.Image + bbox: tuple[int, int, int, int] | None + encoderinfo: dict[str, Any] + + +def _write_multiple_frames( + im: Image.Image, fp: IO[bytes], palette: _Palette | None +) -> bool: duration = im.encoderinfo.get("duration") disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) - im_frames = [] - previous_im = None + im_frames: list[_Frame] = [] + previous_im: Image.Image | None = None frame_count = 0 background_im = None for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): @@ -619,24 +646,22 @@ def _write_multiple_frames(im, fp, palette): frame_count += 1 diff_frame = None - if im_frames: + if im_frames and previous_im: # delta frame delta, bbox = _getbbox(previous_im, im_frame) if not bbox: # This frame is identical to the previous frame if encoderinfo.get("duration"): - im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[ - "duration" - ] + im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] continue - if im_frames[-1]["encoderinfo"].get("disposal") == 2: + if im_frames[-1].encoderinfo.get("disposal") == 2: if background_im is None: color = im.encoderinfo.get( "transparency", im.info.get("transparency", (0, 0, 0)) ) background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) - background_im.putpalette(im_frames[0]["im"].palette) + background_im.putpalette(im_frames[0].im.palette) bbox = _getbbox(background_im, im_frame)[1] elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: @@ -682,39 +707,39 @@ def _write_multiple_frames(im, fp, palette): else: bbox = None previous_im = im_frame - im_frames.append( - {"im": diff_frame or im_frame, "bbox": bbox, "encoderinfo": encoderinfo} - ) + im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo)) if len(im_frames) == 1: if "duration" in im.encoderinfo: # Since multiple frames will not be written, use the combined duration - im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"] - return + im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"] + return False for frame_data in im_frames: - im_frame = frame_data["im"] - if not frame_data["bbox"]: + im_frame = frame_data.im + if not frame_data.bbox: # global header - for s in _get_global_header(im_frame, frame_data["encoderinfo"]): + for s in _get_global_header(im_frame, frame_data.encoderinfo): fp.write(s) offset = (0, 0) else: # compress difference if not palette: - frame_data["encoderinfo"]["include_color_table"] = True + frame_data.encoderinfo["include_color_table"] = True - im_frame = im_frame.crop(frame_data["bbox"]) - offset = frame_data["bbox"][:2] - _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"]) + im_frame = im_frame.crop(frame_data.bbox) + offset = frame_data.bbox[:2] + _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo) return True -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: _save(im, fp, filename, save_all=True) -def _save(im, fp, filename, save_all=False): +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: # header if "palette" in im.encoderinfo or "palette" in im.info: palette = im.encoderinfo.get("palette", im.info.get("palette")) @@ -731,7 +756,7 @@ def _save(im, fp, filename, save_all=False): fp.flush() -def get_interlace(im): +def get_interlace(im: Image.Image) -> int: interlace = im.encoderinfo.get("interlace", 1) # workaround for @PIL153 @@ -741,7 +766,9 @@ def get_interlace(im): return interlace -def _write_local_header(fp, im, offset, flags): +def _write_local_header( + fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int +) -> None: try: transparency = im.encoderinfo["transparency"] except KeyError: @@ -789,7 +816,7 @@ def _write_local_header(fp, im, offset, flags): fp.write(o8(8)) # bits -def _save_netpbm(im, fp, filename): +def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Unused by default. # To use, uncomment the register_save call at the end of the file. # @@ -820,6 +847,7 @@ def _save_netpbm(im, fp, filename): ) # Allow ppmquant to receive SIGPIPE if ppmtogif exits + assert quant_proc.stdout is not None quant_proc.stdout.close() retcode = quant_proc.wait() @@ -841,7 +869,7 @@ def _save_netpbm(im, fp, filename): _FORCE_OPTIMIZE = False -def _get_optimize(im, info): +def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: """ Palette optimization is a potentially expensive operation. @@ -885,9 +913,10 @@ def _get_optimize(im, info): and current_palette_size > 2 ): 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 if not palette_bytes: return 0 @@ -897,7 +926,7 @@ def _get_color_table_size(palette_bytes): 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 suitable for direct inclusion in the GIF header @@ -915,7 +944,7 @@ def _get_header_palette(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 @@ -925,7 +954,10 @@ def _get_palette_bytes(im): return im.palette.palette if im.palette else b"" -def _get_background(im, info_background): +def _get_background( + im: Image.Image, + info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None, +) -> int: background = 0 if info_background: if isinstance(info_background, tuple): @@ -948,7 +980,7 @@ def _get_background(im, info_background): return background -def _get_global_header(im, info): +def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]: """Return a list of strings representing a GIF header""" # Header Block @@ -1010,7 +1042,12 @@ def _get_global_header(im, info): return header -def _write_frame_data(fp, im_frame, offset, params): +def _write_frame_data( + fp: IO[bytes], + im_frame: Image.Image, + offset: tuple[int, int], + params: dict[str, Any], +) -> None: try: im_frame.encoderinfo = params @@ -1030,7 +1067,9 @@ def _write_frame_data(fp, im_frame, offset, params): # Legacy GIF utilities -def getheader(im, palette=None, info=None): +def getheader( + im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None +) -> tuple[list[bytes], list[int] | None]: """ Legacy Method to get Gif data from image. @@ -1042,11 +1081,11 @@ def getheader(im, palette=None, info=None): :returns: tuple of(list of header items, optimized palette) """ - used_palette_colors = _get_optimize(im, info) - if info is None: info = {} + used_palette_colors = _get_optimize(im, info) + if "background" not in info and "background" in im.info: info["background"] = im.info["background"] @@ -1058,7 +1097,9 @@ def getheader(im, palette=None, info=None): return header, used_palette_colors -def getdata(im, offset=(0, 0), **params): +def getdata( + im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any +) -> list[bytes]: """ Legacy Method @@ -1075,12 +1116,23 @@ def getdata(im, offset=(0, 0), **params): :returns: List of bytes containing GIF encoded frame data """ + from io import BytesIO - class Collector: + class Collector(BytesIO): data = [] - def write(self, data): - self.data.append(data) + if sys.version_info >= (3, 12): + from collections.abc import Buffer + + def write(self, data: Buffer) -> int: + self.data.append(data) + return len(data) + + else: + + def write(self, data: Any) -> int: + self.data.append(data) + return len(data) im.load() # make sure raster data is available diff --git a/.venv/Lib/site-packages/PIL/GimpGradientFile.py b/.venv/Lib/site-packages/PIL/GimpGradientFile.py index 2d8c78e..220eac5 100644 --- a/.venv/Lib/site-packages/PIL/GimpGradientFile.py +++ b/.venv/Lib/site-packages/PIL/GimpGradientFile.py @@ -21,6 +21,7 @@ See the GIMP distribution for more information.) from __future__ import annotations from math import log, pi, sin, sqrt +from typing import IO, Callable from ._binary import o8 @@ -28,7 +29,7 @@ EPSILON = 1e-10 """""" # Enable auto-doc for data member -def linear(middle, pos): +def linear(middle: float, pos: float) -> float: if pos <= middle: if middle < EPSILON: return 0.0 @@ -43,19 +44,19 @@ def linear(middle, pos): return 0.5 + 0.5 * pos / middle -def curved(middle, pos): +def curved(middle: float, pos: float) -> float: return pos ** (log(0.5) / log(max(middle, EPSILON))) -def sine(middle, pos): +def sine(middle: float, pos: float) -> float: return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 -def sphere_increasing(middle, pos): +def sphere_increasing(middle: float, pos: float) -> float: return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) -def sphere_decreasing(middle, pos): +def sphere_decreasing(middle: float, pos: float) -> float: return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) @@ -64,9 +65,22 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] class GradientFile: - gradient = None + gradient: ( + list[ + tuple[ + float, + float, + float, + list[float], + list[float], + Callable[[float, float], float], + ] + ] + | None + ) = None - def getpalette(self, entries=256): + def getpalette(self, entries: int = 256) -> tuple[bytes, str]: + assert self.gradient is not None palette = [] ix = 0 @@ -101,7 +115,7 @@ class GradientFile: class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" - def __init__(self, fp): + def __init__(self, fp: IO[bytes]) -> None: if fp.readline()[:13] != b"GIMP Gradient": msg = "not a GIMP gradient file" raise SyntaxError(msg) @@ -114,7 +128,7 @@ class GimpGradientFile(GradientFile): count = int(line) - gradient = [] + self.gradient = [] for i in range(count): s = fp.readline().split() @@ -132,6 +146,4 @@ class GimpGradientFile(GradientFile): msg = "cannot handle HSV colour space" raise OSError(msg) - gradient.append((x0, x1, xm, rgb0, rgb1, segment)) - - self.gradient = gradient + self.gradient.append((x0, x1, xm, rgb0, rgb1, segment)) diff --git a/.venv/Lib/site-packages/PIL/GimpPaletteFile.py b/.venv/Lib/site-packages/PIL/GimpPaletteFile.py index a3109eb..4cad0eb 100644 --- a/.venv/Lib/site-packages/PIL/GimpPaletteFile.py +++ b/.venv/Lib/site-packages/PIL/GimpPaletteFile.py @@ -16,6 +16,7 @@ from __future__ import annotations import re +from typing import IO from ._binary import o8 @@ -25,8 +26,8 @@ class GimpPaletteFile: rawmode = "RGB" - def __init__(self, fp): - self.palette = [o8(i) * 3 for i in range(256)] + def __init__(self, fp: IO[bytes]) -> None: + palette = [o8(i) * 3 for i in range(256)] if fp.readline()[:12] != b"GIMP Palette": msg = "not a GIMP palette file" @@ -49,9 +50,9 @@ class GimpPaletteFile: msg = "bad palette entry" raise ValueError(msg) - self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) + palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) - self.palette = b"".join(self.palette) + self.palette = b"".join(palette) - def getpalette(self): + def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode diff --git a/.venv/Lib/site-packages/PIL/GribStubImagePlugin.py b/.venv/Lib/site-packages/PIL/GribStubImagePlugin.py index f810680..e9aa084 100644 --- a/.venv/Lib/site-packages/PIL/GribStubImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/GribStubImagePlugin.py @@ -10,12 +10,14 @@ # from __future__ import annotations +from typing import IO + from . import Image, ImageFile _handler = None -def register_handler(handler): +def register_handler(handler: ImageFile.StubHandler | None) -> None: """ Install application-specific GRIB image handler. @@ -29,7 +31,7 @@ def register_handler(handler): # Image adapter -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"GRIB" and prefix[7] == 1 @@ -37,7 +39,7 @@ class GribStubImageFile(ImageFile.StubImageFile): format = "GRIB" format_description = "GRIB" - def _open(self): + def _open(self) -> None: offset = self.fp.tell() if not _accept(self.fp.read(8)): @@ -54,11 +56,11 @@ class GribStubImageFile(ImageFile.StubImageFile): if loader: loader.open(self) - def _load(self): + def _load(self) -> ImageFile.StubHandler | None: return _handler -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if _handler is None or not hasattr(_handler, "save"): msg = "GRIB save handler not installed" raise OSError(msg) diff --git a/.venv/Lib/site-packages/PIL/Hdf5StubImagePlugin.py b/.venv/Lib/site-packages/PIL/Hdf5StubImagePlugin.py index 65409e2..cc9e73d 100644 --- a/.venv/Lib/site-packages/PIL/Hdf5StubImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/Hdf5StubImagePlugin.py @@ -10,12 +10,14 @@ # from __future__ import annotations +from typing import IO + from . import Image, ImageFile _handler = None -def register_handler(handler): +def register_handler(handler: ImageFile.StubHandler | None) -> None: """ Install application-specific HDF5 image handler. @@ -29,7 +31,7 @@ def register_handler(handler): # Image adapter -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:8] == b"\x89HDF\r\n\x1a\n" @@ -37,7 +39,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile): format = "HDF5" format_description = "HDF5" - def _open(self): + def _open(self) -> None: offset = self.fp.tell() if not _accept(self.fp.read(8)): @@ -54,11 +56,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile): if loader: loader.open(self) - def _load(self): + def _load(self) -> ImageFile.StubHandler | None: return _handler -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if _handler is None or not hasattr(_handler, "save"): msg = "HDF5 save handler not installed" raise OSError(msg) diff --git a/.venv/Lib/site-packages/PIL/IcnsImagePlugin.py b/.venv/Lib/site-packages/PIL/IcnsImagePlugin.py index d877b4e..2a89d49 100644 --- a/.venv/Lib/site-packages/PIL/IcnsImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/IcnsImagePlugin.py @@ -22,6 +22,7 @@ import io import os import struct import sys +from typing import IO from . import Image, ImageFile, PngImagePlugin, features @@ -252,7 +253,7 @@ class IcnsImageFile(ImageFile.ImageFile): format = "ICNS" format_description = "Mac OS icns resource" - def _open(self): + def _open(self) -> None: self.icns = IcnsFile(self.fp) self._mode = "RGBA" self.info["sizes"] = self.icns.itersizes() @@ -312,7 +313,7 @@ class IcnsImageFile(ImageFile.ImageFile): return px -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: """ Saves the image as a series of PNG files, that are then combined into a .icns file. @@ -346,35 +347,33 @@ def _save(im, fp, filename): entries = [] for type, size in sizes.items(): stream = size_streams[size] - entries.append( - {"type": type, "size": HEADERSIZE + len(stream), "stream": stream} - ) + entries.append((type, HEADERSIZE + len(stream), stream)) # Header fp.write(MAGIC) file_length = HEADERSIZE # Header file_length += HEADERSIZE + 8 * len(entries) # TOC - file_length += sum(entry["size"] for entry in entries) + file_length += sum(entry[1] for entry in entries) fp.write(struct.pack(">i", file_length)) # TOC fp.write(b"TOC ") fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) for entry in entries: - fp.write(entry["type"]) - fp.write(struct.pack(">i", entry["size"])) + fp.write(entry[0]) + fp.write(struct.pack(">i", entry[1])) # Data for entry in entries: - fp.write(entry["type"]) - fp.write(struct.pack(">i", entry["size"])) - fp.write(entry["stream"]) + fp.write(entry[0]) + fp.write(struct.pack(">i", entry[1])) + fp.write(entry[2]) if hasattr(fp, "flush"): fp.flush() -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == MAGIC diff --git a/.venv/Lib/site-packages/PIL/IcoImagePlugin.py b/.venv/Lib/site-packages/PIL/IcoImagePlugin.py index d66fbc2..227fcf3 100644 --- a/.venv/Lib/site-packages/PIL/IcoImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/IcoImagePlugin.py @@ -25,6 +25,7 @@ from __future__ import annotations import warnings from io import BytesIO from math import ceil, log +from typing import IO from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from ._binary import i16le as i16 @@ -39,7 +40,7 @@ from ._binary import o32le as o32 _MAGIC = b"\0\0\1\0" -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(_MAGIC) # (2+2) bmp = im.encoderinfo.get("bitmap_format") == "bmp" sizes = im.encoderinfo.get( @@ -114,7 +115,7 @@ def _save(im, fp, filename): fp.seek(current) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == _MAGIC @@ -194,7 +195,7 @@ class IcoFile: """ return self.frame(self.getentryindex(size, bpp)) - def frame(self, idx): + def frame(self, idx: int) -> Image.Image: """ Get an image from frame idx """ @@ -205,6 +206,7 @@ class IcoFile: data = self.buf.read(8) self.buf.seek(header["offset"]) + im: Image.Image if data[:8] == PngImagePlugin._MAGIC: # png frame im = PngImagePlugin.PngImageFile(self.buf) @@ -302,7 +304,7 @@ class IcoImageFile(ImageFile.ImageFile): format = "ICO" format_description = "Windows Icon" - def _open(self): + def _open(self) -> None: self.ico = IcoFile(self.fp) self.info["sizes"] = self.ico.sizes() self.size = self.ico.entry[0]["dim"] @@ -341,7 +343,7 @@ class IcoImageFile(ImageFile.ImageFile): self.size = im.size - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: # Flag the ImageFile.Parser so that it # just does all the decode at the end. pass diff --git a/.venv/Lib/site-packages/PIL/ImImagePlugin.py b/.venv/Lib/site-packages/PIL/ImImagePlugin.py index 4613e40..2fb7ecd 100644 --- a/.venv/Lib/site-packages/PIL/ImImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/ImImagePlugin.py @@ -28,6 +28,7 @@ from __future__ import annotations import os import re +from typing import IO, Any from . import Image, ImageFile, ImagePalette @@ -78,7 +79,7 @@ OPEN = { "LA image": ("LA", "LA;L"), "PA image": ("LA", "PA;L"), "RGBA image": ("RGBA", "RGBA;L"), - "RGBX image": ("RGBX", "RGBX;L"), + "RGBX image": ("RGB", "RGBX;L"), "CMYK image": ("CMYK", "CMYK;L"), "YCC image": ("YCbCr", "YCbCr;L"), } @@ -103,7 +104,7 @@ for j in range(2, 33): split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") -def number(s): +def number(s: Any) -> float: try: return int(s) except ValueError: @@ -119,7 +120,7 @@ class ImImageFile(ImageFile.ImageFile): format_description = "IFUNC Image Memory" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. @@ -196,7 +197,7 @@ class ImImageFile(ImageFile.ImageFile): n += 1 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) if not n: @@ -271,14 +272,14 @@ class ImImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] @property - def n_frames(self): + def n_frames(self) -> int: return self.info[FRAMES] @property - def is_animated(self): + def is_animated(self) -> bool: return self.info[FRAMES] > 1 - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return @@ -296,7 +297,7 @@ class ImImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] - def tell(self): + def tell(self) -> int: 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: image_type, rawmode = SAVE[im.mode] except KeyError as e: @@ -340,6 +341,8 @@ def _save(im, fp, filename): # or: SyntaxError("not an IM file") # 8 characters are used for "Name: " and "\r\n" # Keep just the filename, ditch the potentially overlong path + if isinstance(filename, bytes): + filename = filename.decode("ascii") name, ext = os.path.splitext(os.path.basename(filename)) name = "".join([name[: 92 - len(ext)], ext]) diff --git a/.venv/Lib/site-packages/PIL/Image.py b/.venv/Lib/site-packages/PIL/Image.py index baef0aa..d41c065 100644 --- a/.venv/Lib/site-packages/PIL/Image.py +++ b/.venv/Lib/site-packages/PIL/Image.py @@ -41,7 +41,16 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from types import ModuleType -from typing import IO, TYPE_CHECKING, Any +from typing import ( + IO, + TYPE_CHECKING, + Any, + Literal, + Protocol, + Sequence, + Tuple, + cast, +) # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -55,7 +64,8 @@ from . import ( _plugins, ) from ._binary import i32le, o32be, o32le -from ._typing import TypeGuard +from ._deprecate import deprecate +from ._typing import StrOrBytesPath, TypeGuard from ._util import DeferredError, is_path ElementTree: ModuleType | None @@ -75,6 +85,8 @@ class DecompressionBombError(Exception): pass +WARN_POSSIBLE_FORMATS: bool = False + # Limit to around a quarter gigabyte for a 24-bit (3 bpp) image MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3) @@ -217,13 +229,13 @@ if hasattr(core, "DEFAULT_STRATEGY"): # Registries if TYPE_CHECKING: - from . import ImageFile + from . import ImageFile, PyAccess ID: list[str] = [] OPEN: dict[ str, tuple[ Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], - Callable[[bytes], bool] | None, + Callable[[bytes], bool | str] | None, ], ] = {} MIME: dict[str, str] = {} @@ -248,7 +260,28 @@ def _conv_type_shape(im): return shape, m.typestr -MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"] +MODES = [ + "1", + "CMYK", + "F", + "HSV", + "I", + "I;16", + "I;16B", + "I;16L", + "I;16N", + "L", + "LA", + "La", + "LAB", + "P", + "PA", + "RGB", + "RGBA", + "RGBa", + "RGBX", + "YCbCr", +] # raw modes that may be memory mapped. NOTE: if you change this, you # may have to modify the stride calculation in map.c too! @@ -357,7 +390,7 @@ def preinit() -> None: _initialized = 1 -def init(): +def init() -> bool: """ Explicitly initializes the Python Imaging Library. This function loads all available file format drivers. @@ -368,7 +401,7 @@ def init(): global _initialized if _initialized >= 2: - return 0 + return False parent_name = __name__.rpartition(".")[0] for plugin in _plugins: @@ -380,14 +413,17 @@ def init(): if OPEN or SAVE: _initialized = 2 - return 1 + return True + return False # -------------------------------------------------------------------- # Codec factories (used by tobytes/frombytes and ImageFile.load) -def _getdecoder(mode, decoder_name, args, extra=()): +def _getdecoder( + mode: str, decoder_name: str, args: Any, extra: tuple[Any, ...] = () +) -> core.ImagingDecoder | ImageFile.PyDecoder: # tweak arguments if args is None: args = () @@ -403,14 +439,16 @@ def _getdecoder(mode, decoder_name, args, extra=()): try: # get decoder - decoder = getattr(core, decoder_name + "_decoder") + decoder = getattr(core, f"{decoder_name}_decoder") except AttributeError as e: msg = f"decoder {decoder_name} not available" raise OSError(msg) from e return decoder(mode, *args + extra) -def _getencoder(mode, encoder_name, args, extra=()): +def _getencoder( + mode: str, encoder_name: str, args: Any, extra: tuple[Any, ...] = () +) -> core.ImagingEncoder | ImageFile.PyEncoder: # tweak arguments if args is None: args = () @@ -426,7 +464,7 @@ def _getencoder(mode, encoder_name, args, extra=()): try: # get encoder - encoder = getattr(core, encoder_name + "_encoder") + encoder = getattr(core, f"{encoder_name}_encoder") except AttributeError as e: msg = f"encoder {encoder_name} not available" raise OSError(msg) from e @@ -480,6 +518,12 @@ def _getscaleoffset(expr): # Implementation wrapper +class SupportsGetData(Protocol): + def getdata( + self, + ) -> tuple[Transform, Sequence[int]]: ... + + class Image: """ This class represents an image object. To create @@ -521,10 +565,10 @@ class Image: return self._size @property - def mode(self): + def mode(self) -> str: return self._mode - def _new(self, im) -> Image: + def _new(self, im: core.ImagingCore) -> Image: new = Image() new.im = im new._mode = im.mode @@ -597,11 +641,11 @@ class Image: self.load() def _dump( - self, file: str | None = None, format: str | None = None, **options + self, file: str | None = None, format: str | None = None, **options: Any ) -> str: suffix = "" if format: - suffix = "." + format + suffix = f".{format}" if not file: f, filename = tempfile.mkstemp(suffix) @@ -620,10 +664,12 @@ class Image: return filename - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return False + assert isinstance(other, Image) return ( - self.__class__ is other.__class__ - and self.mode == other.mode + self.mode == other.mode and self.size == other.size and self.info == other.info and self.getpalette() == other.getpalette() @@ -656,7 +702,7 @@ class Image: ) ) - def _repr_image(self, image_format, **kwargs): + def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: """Helper function for iPython display hook. :param image_format: Image format. @@ -669,14 +715,14 @@ class Image: return None return b.getvalue() - def _repr_png_(self): + def _repr_png_(self) -> bytes | None: """iPython display hook support for PNG format. :returns: PNG version of the image as bytes """ return self._repr_image("PNG", compress_level=1) - def _repr_jpeg_(self): + def _repr_jpeg_(self) -> bytes | None: """iPython display hook support for JPEG format. :returns: JPEG version of the image as bytes @@ -723,7 +769,7 @@ class Image: self.putpalette(palette) self.frombytes(data) - def tobytes(self, encoder_name: str = "raw", *args) -> bytes: + def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: """ Return image as a bytes object. @@ -745,12 +791,13 @@ class Image: :returns: A :py:class:`bytes` object. """ - # may pass tuple instead of argument list - if len(args) == 1 and isinstance(args[0], tuple): - args = args[0] + encoder_args: Any = args + if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple): + # may pass tuple instead of argument list + encoder_args = encoder_args[0] - if encoder_name == "raw" and args == (): - args = self.mode + if encoder_name == "raw" and encoder_args == (): + encoder_args = self.mode self.load() @@ -758,7 +805,7 @@ class Image: return b"" # unpack data - e = _getencoder(self.mode, encoder_name, args) + e = _getencoder(self.mode, encoder_name, encoder_args) e.setimage(self.im) bufsize = max(65536, self.size[0] * 4) # see RawEncode.c @@ -801,7 +848,9 @@ class Image: ] ) - def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: + def frombytes( + self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any + ) -> None: """ Loads this image with pixel data from a bytes object. @@ -812,16 +861,17 @@ class Image: if self.width == 0 or self.height == 0: return - # may pass tuple instead of argument list - if len(args) == 1 and isinstance(args[0], tuple): - args = args[0] + decoder_args: Any = args + if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): + # may pass tuple instead of argument list + decoder_args = decoder_args[0] # default format - if decoder_name == "raw" and args == (): - args = self.mode + if decoder_name == "raw" and decoder_args == (): + decoder_args = self.mode # unpack data - d = _getdecoder(self.mode, decoder_name, args) + d = _getdecoder(self.mode, decoder_name, decoder_args) d.setimage(self.im) s = d.decode(data) @@ -832,7 +882,7 @@ class Image: msg = "cannot decode image data" raise ValueError(msg) - def load(self): + def load(self) -> core.PixelAccess | PyAccess.PyAccess | None: """ Allocates storage for the image and loads the pixel data. In normal cases, you don't need to call this method, since the @@ -845,12 +895,12 @@ class Image: operations. See :ref:`file-handling` for more information. :returns: An image access object. - :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` + :rtype: :py:class:`.PixelAccess` or :py:class:`.PyAccess` """ if self.im is not None and self.palette and self.palette.dirty: # realize palette mode, arr = self.palette.getdata() - self.im.putpalette(mode, arr) + self.im.putpalette(self.palette.mode, mode, arr) self.palette.dirty = 0 self.palette.rawmode = None if "transparency" in self.info and mode in ("LA", "PA"): @@ -860,9 +910,9 @@ class Image: self.im.putpalettealphas(self.info["transparency"]) self.palette.mode = "RGBA" else: - palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB" - self.palette.mode = palette_mode - self.palette.palette = self.im.getpalette(palette_mode, palette_mode) + self.palette.palette = self.im.getpalette( + self.palette.mode, self.palette.mode + ) if self.im is not None: if cffi and USE_CFFI_ACCESS: @@ -874,8 +924,9 @@ class Image: if self.pyaccess: return self.pyaccess return self.im.pixel_access(self.readonly) + return None - def verify(self): + def verify(self) -> None: """ Verifies the contents of a file. For data read from a file, this method attempts to determine if the file is broken, without @@ -938,6 +989,9 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + self.load() has_transparency = "transparency" in self.info @@ -962,9 +1016,11 @@ class Image: if has_transparency and self.im.bands == 3: transparency = new_im.info["transparency"] - def convert_transparency(m, v): - v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 - return max(0, min(255, int(v))) + def convert_transparency( + m: tuple[float, ...], v: tuple[int, int, int] + ) -> int: + value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 + return max(0, min(255, int(value))) if mode == "L": transparency = convert_transparency(matrix, transparency) @@ -1058,7 +1114,10 @@ class Image: del new_im.info["transparency"] if trns is not None: try: - new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) + new_im.info["transparency"] = new_im.palette.getcolor( + cast(Tuple[int, ...], trns), # trns was converted to RGB + new_im, + ) except Exception: # if we can't make a transparent color, don't leave the old # transparency hanging around to mess us up. @@ -1106,7 +1165,7 @@ class Image: # crash fail if we leave a bytes transparency in an rgb/l mode. del new_im.info["transparency"] if trns is not None: - if new_im.mode == "P": + if new_im.mode == "P" and new_im.palette: try: new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) except ValueError as e: @@ -1124,7 +1183,7 @@ class Image: def quantize( self, colors: int = 256, - method: Quantize | None = None, + method: int | None = None, kmeans: int = 0, palette=None, dither: Dither = Dither.FLOYDSTEINBERG, @@ -1216,7 +1275,7 @@ class Image: __copy__ = copy - def crop(self, box: tuple[int, int, int, int] | None = None) -> Image: + def crop(self, box: tuple[float, float, float, float] | None = None) -> Image: """ Returns a rectangular region from this image. The box is a 4-tuple defining the left, upper, right, and lower pixel @@ -1242,7 +1301,9 @@ class Image: self.load() return self._new(self._crop(self.im, box)) - def _crop(self, im, box): + def _crop( + self, im: core.ImagingCore, box: tuple[float, float, float, float] + ) -> core.ImagingCore: """ Returns a rectangular region from the core image object im. @@ -1262,7 +1323,9 @@ class Image: return im.crop((x0, y0, x1, y1)) - def draft(self, mode, size): + def draft( + self, mode: str | None, size: tuple[int, int] | None + ) -> tuple[str, tuple[int, int, float, float]] | None: """ Configures the image file loader so it returns a version of the image that as closely as possible matches the given mode and @@ -1285,13 +1348,16 @@ class Image: """ pass - def _expand(self, xmargin, ymargin=None): + def _expand(self, xmargin: int, ymargin: int | None = None) -> Image: if ymargin is None: ymargin = xmargin self.load() return self._new(self.im.expand(xmargin, ymargin)) - def filter(self, filter): + if TYPE_CHECKING: + from . import ImageFilter + + def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: """ Filters this image using the given filter. For a list of available filters, see the :py:mod:`~PIL.ImageFilter` module. @@ -1303,7 +1369,7 @@ class Image: self.load() - if isinstance(filter, Callable): + if callable(filter): filter = filter() if not hasattr(filter, "filter"): msg = "filter argument should be ImageFilter.Filter instance or class" @@ -1328,7 +1394,7 @@ class Image: """ return ImageMode.getmode(self.mode).bands - def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: + def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None: """ Calculates the bounding box of the non-zero regions in the image. @@ -1408,8 +1474,15 @@ class Image: return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) return self.im.getextrema() - def _getxmp(self, xmp_tags): - def get_name(tag): + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ + + def get_name(tag: str) -> str: return re.sub("^{[^}]+}", "", tag) def get_value(element): @@ -1435,9 +1508,10 @@ class Image: if ElementTree is None: warnings.warn("XMP data cannot be read without defusedxml dependency") return {} - else: - root = ElementTree.fromstring(xmp_tags) - return {get_name(root.tag): get_value(root)} + if "xmp" not in self.info: + return {} + root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00")) + return {get_name(root.tag): get_value(root)} def getexif(self) -> Exif: """ @@ -1480,7 +1554,7 @@ class Image: self._exif._loaded = False self.getexif() - def get_child_images(self): + def get_child_images(self) -> list[ImageFile.ImageFile]: child_images = [] exif = self.getexif() ifds = [] @@ -1504,16 +1578,17 @@ class Image: fp = self.fp thumbnail_offset = ifd.get(513) if thumbnail_offset is not None: - try: - thumbnail_offset += self._exif_offset - except AttributeError: - pass + thumbnail_offset += getattr(self, "_exif_offset", 0) self.fp.seek(thumbnail_offset) data = self.fp.read(ifd.get(514)) fp = io.BytesIO(data) with open(fp) as im: - if thumbnail_offset is None: + from . import TiffImagePlugin + + if thumbnail_offset is None and isinstance( + im, TiffImagePlugin.TiffImageFile + ): im._frame_pos = [ifd_offset] im._seek(0) im.load() @@ -1573,7 +1648,7 @@ class Image: or "transparency" in self.info ) - def apply_transparency(self): + def apply_transparency(self) -> None: """ If a P mode image has a "transparency" key in the info dictionary, remove the key and instead apply the transparency to the palette. @@ -1585,6 +1660,7 @@ class Image: from . import ImagePalette palette = self.getpalette("RGBA") + assert palette is not None transparency = self.info["transparency"] if isinstance(transparency, bytes): for i, alpha in enumerate(transparency): @@ -1596,7 +1672,9 @@ class Image: del self.info["transparency"] - def getpixel(self, xy): + def getpixel( + self, xy: tuple[int, int] | list[int] + ) -> float | tuple[int, ...] | None: """ Returns the pixel value at a given position. @@ -1680,7 +1758,12 @@ class Image: return self.im.entropy(extrema) return self.im.entropy() - def paste(self, im, box=None, mask=None) -> None: + def paste( + self, + im: Image | str | float | tuple[float, ...], + box: Image | tuple[int, int, int, int] | tuple[int, int] | None = None, + mask: Image | None = None, + ) -> None: """ Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the @@ -1708,7 +1791,7 @@ class Image: See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to combine images with respect to their alpha channels. - :param im: Source image or pixel value (integer or tuple). + :param im: Source image or pixel value (integer, float or tuple). :param box: An optional 4-tuple giving the region to paste into. If a 2-tuple is used instead, it's treated as the upper left corner. If omitted or None, the source is pasted into the @@ -1720,10 +1803,14 @@ class Image: :param mask: An optional mask image. """ - if isImageType(box) and mask is None: + if isImageType(box): + if mask is not None: + msg = "If using second argument as mask, third argument must be None" + raise ValueError(msg) # abbreviated paste(im, mask) syntax mask = box box = None + assert not isinstance(box, Image) if box is None: box = (0, 0) @@ -1761,7 +1848,9 @@ class Image: else: self.im.paste(im, box) - def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): + def alpha_composite( + self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0) + ) -> None: """'In-place' analog of Image.alpha_composite. Composites an image onto this image. @@ -1776,32 +1865,35 @@ class Image: """ if not isinstance(source, (list, tuple)): - msg = "Source must be a tuple" + msg = "Source must be a list or tuple" raise ValueError(msg) if not isinstance(dest, (list, tuple)): - msg = "Destination must be a tuple" + msg = "Destination must be a list or tuple" raise ValueError(msg) - if len(source) not in (2, 4): - msg = "Source must be a 2 or 4-tuple" + + if len(source) == 4: + overlay_crop_box = tuple(source) + elif len(source) == 2: + overlay_crop_box = tuple(source) + im.size + else: + msg = "Source must be a sequence of length 2 or 4" raise ValueError(msg) + if not len(dest) == 2: - msg = "Destination must be a 2-tuple" + msg = "Destination must be a sequence of length 2" raise ValueError(msg) if min(source) < 0: msg = "Source must be non-negative" raise ValueError(msg) - if len(source) == 2: - source = source + im.size - - # over image, crop if it's not the whole thing. - if source == (0, 0) + im.size: + # over image, crop if it's not the whole image. + if overlay_crop_box == (0, 0) + im.size: overlay = im else: - overlay = im.crop(source) + overlay = im.crop(overlay_crop_box) # target for the paste - box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) + box = tuple(dest) + (dest[0] + overlay.width, dest[1] + overlay.height) # destination image. don't copy if we're using the whole image. if box == (0, 0) + self.size: @@ -1812,7 +1904,11 @@ class Image: result = alpha_composite(background, overlay) self.paste(result, box) - def point(self, lut, mode: str | None = None) -> Image: + def point( + self, + lut: Sequence[float] | Callable[[int], float] | ImagePointHandler, + mode: str | None = None, + ) -> Image: """ Maps this image through a lookup table or function. @@ -1849,7 +1945,9 @@ class Image: scale, offset = _getscaleoffset(lut) return self._new(self.im.point_transform(scale, offset)) # for other modes, convert the function to a table - lut = [lut(i) for i in range(256)] * self.im.bands + flatLut = [lut(i) for i in range(256)] * self.im.bands + else: + flatLut = lut if self.mode == "F": # FIXME: _imaging returns a confusing error message for this case @@ -1857,18 +1955,17 @@ class Image: raise ValueError(msg) if mode != "F": - lut = [round(i) for i in lut] - return self._new(self.im.point(lut, mode)) + flatLut = [round(i) for i in flatLut] + return self._new(self.im.point(flatLut, mode)) - def putalpha(self, alpha): + def putalpha(self, alpha: Image | int) -> None: """ Adds or replaces the alpha layer in this image. If the image does not have an alpha layer, it's converted to "LA" or "RGBA". The new layer must be either "L" or "1". :param alpha: The new alpha layer. This can either be an "L" or "1" - image having the same size as this image, or an integer or - other color value. + image having the same size as this image, or an integer. """ self._ensure_mutable() @@ -1907,6 +2004,7 @@ class Image: alpha = alpha.convert("L") else: # constant alpha + alpha = cast(int, alpha) # see python/typing#1013 try: self.im.fillband(band, alpha) except (AttributeError, ValueError): @@ -1917,7 +2015,12 @@ class Image: self.im.putband(alpha.im, band) - def putdata(self, data, scale=1.0, offset=0.0): + def putdata( + self, + data: Sequence[float] | Sequence[Sequence[int]], + scale: float = 1.0, + offset: float = 0.0, + ) -> None: """ Copies pixel data from a flattened sequence object into the image. The values should start at the upper left corner (0, 0), continue to the @@ -1967,10 +2070,12 @@ class Image: palette = ImagePalette.raw(rawmode, data) self._mode = "PA" if "A" in self.mode else "P" self.palette = palette - self.palette.mode = "RGB" + self.palette.mode = "RGBA" if "A" in rawmode else "RGB" self.load() # install new palette - def putpixel(self, xy, value): + def putpixel( + self, xy: tuple[int, int], value: float | tuple[int, ...] | list[int] + ) -> None: """ Modifies the pixel at the given position. The color is given as a single numerical value for single-band images, and a tuple for @@ -2008,9 +2113,8 @@ class Image: if self.mode == "PA": alpha = value[3] if len(value) == 4 else 255 value = value[:3] - value = self.palette.getcolor(value, self) - if self.mode == "PA": - value = (value, alpha) + palette_index = self.palette.getcolor(value, self) + value = (palette_index, alpha) if self.mode == "PA" else palette_index return self.im.putpixel(xy, value) def remap_palette(self, dest_map, source_palette=None): @@ -2082,7 +2186,7 @@ class Image: # m_im.putpalette(mapping_palette, 'L') # converts to 'P' # or just force it. # UNDONE -- this is part of the general issue with palettes - m_im.im.putpalette(palette_mode + ";L", m_im.palette.tobytes()) + m_im.im.putpalette(palette_mode, palette_mode + ";L", m_im.palette.tobytes()) m_im = m_im.convert("L") @@ -2115,7 +2219,13 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: + def resize( + self, + size: tuple[int, int], + resample: int | None = None, + box: tuple[float, float, float, float] | None = None, + reducing_gap: float | None = None, + ) -> Image: """ Returns a resized copy of this image. @@ -2173,20 +2283,16 @@ class Image: (Resampling.HAMMING, "Image.Resampling.HAMMING"), ) ] - msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" raise ValueError(msg) if reducing_gap is not None and reducing_gap < 1.0: msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - size = tuple(size) - self.load() if box is None: box = (0, 0) + self.size - else: - box = tuple(box) if self.size == size and box == (0, 0) + self.size: return self.copy() @@ -2221,7 +2327,11 @@ class Image: return self._new(self.im.resize(size, resample, box)) - def reduce(self, factor, box=None): + def reduce( + self, + factor: int | tuple[int, int], + box: tuple[int, int, int, int] | None = None, + ) -> Image: """ Returns a copy of the image reduced ``factor`` times. If the size of the image is not dividable by ``factor``, @@ -2239,8 +2349,6 @@ class Image: if box is None: box = (0, 0) + self.size - else: - box = tuple(box) if factor == (1, 1) and box == (0, 0) + self.size: return self.copy() @@ -2256,13 +2364,13 @@ class Image: def rotate( self, - angle, - resample=Resampling.NEAREST, - expand=0, - center=None, - translate=None, - fillcolor=None, - ): + angle: float, + resample: Resampling = Resampling.NEAREST, + expand: int | bool = False, + center: tuple[float, float] | None = None, + translate: tuple[int, int] | None = None, + fillcolor: float | tuple[float, ...] | str | None = None, + ) -> Image: """ Returns a rotated copy of this image. This method returns a copy of this image, rotated the given number of degrees counter @@ -2327,10 +2435,7 @@ class Image: else: post_trans = translate if center is None: - # FIXME These should be rounded to ints? - rotn_center = (w / 2.0, h / 2.0) - else: - rotn_center = center + center = (w / 2, h / 2) angle = -math.radians(angle) matrix = [ @@ -2347,10 +2452,10 @@ class Image: return a * x + b * y + c, d * x + e * y + f matrix[2], matrix[5] = transform( - -rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix + -center[0] - post_trans[0], -center[1] - post_trans[1], matrix ) - matrix[2] += rotn_center[0] - matrix[5] += rotn_center[1] + matrix[2] += center[0] + matrix[5] += center[1] if expand: # calculate output size @@ -2373,7 +2478,9 @@ class Image: (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor ) - def save(self, fp, format=None, **params) -> None: + def save( + self, fp: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any + ) -> None: """ Saves this image under the given filename. If no format is specified, the format to use is determined from the filename @@ -2422,7 +2529,7 @@ class Image: save_all = params.pop("save_all", False) self.encoderinfo = params - self.encoderconfig = () + self.encoderconfig: tuple[Any, ...] = () preinit() @@ -2454,6 +2561,8 @@ class Image: fp = builtins.open(filename, "r+b") else: fp = builtins.open(filename, "w+b") + else: + fp = cast(IO[bytes], fp) try: save_handler(self, fp, filename) @@ -2565,7 +2674,12 @@ class Image: """ return 0 - def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): + def thumbnail( + self, + size: tuple[float, float], + resample: Resampling = Resampling.BICUBIC, + reducing_gap: float | None = 2.0, + ) -> None: """ Make this image into a thumbnail. This method modifies the image to contain a thumbnail version of itself, no larger than @@ -2625,27 +2739,32 @@ class Image: return x, y box = None + final_size: tuple[int, int] if reducing_gap is not None: - size = preserve_aspect_ratio() - if size is None: + preserved_size = preserve_aspect_ratio() + if preserved_size is None: return + final_size = preserved_size - res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) + res = self.draft( + None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) + ) if res is not None: box = res[1] if box is None: self.load() # load() may have changed the size of the image - size = preserve_aspect_ratio() - if size is None: + preserved_size = preserve_aspect_ratio() + if preserved_size is None: return + final_size = preserved_size - if self.size != size: - im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) + if self.size != final_size: + im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) self.im = im.im - self._size = size + self._size = final_size self._mode = self.im.mode self.readonly = 0 @@ -2655,12 +2774,12 @@ class Image: # instead of bloating the method docs, add a separate chapter. def transform( self, - size, - method, - data=None, - resample=Resampling.NEAREST, - fill=1, - fillcolor=None, + size: tuple[int, int], + method: Transform | ImageTransformHandler | SupportsGetData, + data: Sequence[Any] | None = None, + resample: int = Resampling.NEAREST, + fill: int = 1, + fillcolor: float | tuple[float, ...] | str | None = None, ) -> Image: """ Transforms this image. This method creates a new image with the @@ -2814,7 +2933,7 @@ class Image: (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), ) ] - msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" raise ValueError(msg) image.load() @@ -2824,7 +2943,7 @@ class Image: if image.mode in ("1", "P"): resample = Resampling.NEAREST - self.im.transform2(box, image.im, method, data, resample, fill) + self.im.transform(box, image.im, method, data, resample, fill) def transpose(self, method: Transpose) -> Image: """ @@ -2840,7 +2959,7 @@ class Image: self.load() return self._new(self.im.transpose(method)) - def effect_spread(self, distance): + def effect_spread(self, distance: int) -> Image: """ Randomly spread pixels in an image. @@ -2894,7 +3013,7 @@ class ImageTransformHandler: self, size: tuple[int, int], image: Image, - **options: dict[str, str | int | tuple[int, ...] | list[int]], + **options: Any, ) -> Image: pass @@ -2906,35 +3025,35 @@ class ImageTransformHandler: # Debugging -def _wedge(): +def _wedge() -> Image: """Create grayscale wedge (for debugging only)""" return Image()._new(core.wedge("L")) -def _check_size(size): +def _check_size(size: Any) -> None: """ Common check to enforce type and sanity check on size tuples :param size: Should be a 2 tuple of (width, height) - :returns: True, or raises a ValueError + :returns: None, or raises a ValueError """ if not isinstance(size, (list, tuple)): - msg = "Size must be a tuple" + msg = "Size must be a list or tuple" raise ValueError(msg) if len(size) != 2: - msg = "Size must be a tuple of length 2" + msg = "Size must be a sequence of length 2" raise ValueError(msg) if size[0] < 0 or size[1] < 0: msg = "Width and height must be >= 0" raise ValueError(msg) - return True - def new( - mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0 + mode: str, + size: tuple[int, int] | list[int], + color: float | tuple[float, ...] | str | None = 0, ) -> Image: """ Creates a new image with the given mode and size. @@ -2951,6 +3070,9 @@ def new( :returns: An :py:class:`~PIL.Image.Image` object. """ + if mode in ("BGR;15", "BGR;16", "BGR;24"): + deprecate(mode, 12) + _check_size(size) if color is None: @@ -2965,16 +3087,28 @@ def new( color = ImageColor.getcolor(color, mode) im = Image() - if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]: - # RGB or RGBA value for a P image - from . import ImagePalette + if ( + mode == "P" + and isinstance(color, (list, tuple)) + and all(isinstance(i, int) for i in color) + ): + color_ints: tuple[int, ...] = cast(Tuple[int, ...], tuple(color)) + if len(color_ints) == 3 or len(color_ints) == 4: + # RGB or RGBA value for a P image + from . import ImagePalette - im.palette = ImagePalette.ImagePalette() - color = im.palette.getcolor(color) + im.palette = ImagePalette.ImagePalette() + color = im.palette.getcolor(color_ints) return im._new(core.fill(mode, size, color)) -def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: +def frombytes( + mode: str, + size: tuple[int, int], + data: bytes | bytearray, + decoder_name: str = "raw", + *args: Any, +) -> Image: """ Creates a copy of an image memory from pixel data in a buffer. @@ -3002,18 +3136,21 @@ def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: im = new(mode, size) if im.width != 0 and im.height != 0: - # may pass tuple instead of argument list - if len(args) == 1 and isinstance(args[0], tuple): - args = args[0] + decoder_args: Any = args + if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): + # may pass tuple instead of argument list + decoder_args = decoder_args[0] - if decoder_name == "raw" and args == (): - args = mode + if decoder_name == "raw" and decoder_args == (): + decoder_args = mode - im.frombytes(data, decoder_name, args) + im.frombytes(data, decoder_name, decoder_args) return im -def frombuffer(mode, size, data, decoder_name="raw", *args): +def frombuffer( + mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any +) -> Image: """ Creates an image memory referencing pixel data in a byte buffer. @@ -3069,7 +3206,17 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): return frombytes(mode, size, data, decoder_name, args) -def fromarray(obj, mode=None): +class SupportsArrayInterface(Protocol): + """ + An object that has an ``__array_interface__`` dictionary. + """ + + @property + def __array_interface__(self) -> dict[str, Any]: + raise NotImplementedError() + + +def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: """ Creates an image memory from an object exporting the array interface (using the buffer protocol):: @@ -3148,8 +3295,11 @@ def fromarray(obj, mode=None): if strides is not None: if hasattr(obj, "tobytes"): obj = obj.tobytes() - else: + elif hasattr(obj, "tostring"): obj = obj.tostring() + else: + msg = "'strides' requires either tobytes() or tostring()" + raise ValueError(msg) return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) @@ -3196,8 +3346,8 @@ _fromarray_typemap = { ((1, 1, 3), "|u1"): ("RGB", "RGB"), ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), # shortcuts: - ((1, 1), _ENDIAN + "i4"): ("I", "I"), - ((1, 1), _ENDIAN + "f4"): ("F", "F"), + ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), + ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), } @@ -3222,7 +3372,11 @@ def _decompression_bomb_check(size: tuple[int, int]) -> None: ) -def open(fp, mode="r", formats=None) -> Image: +def open( + fp: StrOrBytesPath | IO[bytes], + mode: Literal["r"] = "r", + formats: list[str] | tuple[str, ...] | None = None, +) -> ImageFile.ImageFile: """ Opens and identifies the given image file. @@ -3253,10 +3407,10 @@ def open(fp, mode="r", formats=None) -> Image: """ if mode != "r": - msg = f"bad mode {repr(mode)}" + msg = f"bad mode {repr(mode)}" # type: ignore[unreachable] raise ValueError(msg) elif isinstance(fp, io.StringIO): - msg = ( + msg = ( # type: ignore[unreachable] "StringIO cannot be used to open an image. " "Binary data must be used instead." ) @@ -3265,7 +3419,7 @@ def open(fp, mode="r", formats=None) -> Image: if formats is None: formats = ID elif not isinstance(formats, (list, tuple)): - msg = "formats must be a list or tuple" + msg = "formats must be a list or tuple" # type: ignore[unreachable] raise TypeError(msg) exclusive_fp = False @@ -3276,6 +3430,8 @@ def open(fp, mode="r", formats=None) -> Image: if filename: fp = builtins.open(filename, "rb") exclusive_fp = True + else: + fp = cast(IO[bytes], fp) try: fp.seek(0) @@ -3287,9 +3443,14 @@ def open(fp, mode="r", formats=None) -> Image: preinit() - accept_warnings = [] + warning_messages: list[str] = [] - def _open_core(fp, filename, prefix, formats): + def _open_core( + fp: IO[bytes], + filename: str | bytes, + prefix: bytes, + formats: list[str] | tuple[str, ...], + ) -> ImageFile.ImageFile | None: for i in formats: i = i.upper() if i not in OPEN: @@ -3297,18 +3458,16 @@ def open(fp, mode="r", formats=None) -> Image: try: factory, accept = OPEN[i] result = not accept or accept(prefix) - if type(result) in [str, bytes]: - accept_warnings.append(result) + if isinstance(result, str): + warning_messages.append(result) elif result: fp.seek(0) im = factory(fp, filename) _decompression_bomb_check(im.size) return im - except (SyntaxError, IndexError, TypeError, struct.error): - # Leave disabled by default, spams the logs with image - # opening failures that are entirely expected. - # logger.debug("", exc_info=True) - continue + except (SyntaxError, IndexError, TypeError, struct.error) as e: + if WARN_POSSIBLE_FORMATS: + warning_messages.append(i + " opening failed. " + str(e)) except BaseException: if exclusive_fp: fp.close() @@ -3318,7 +3477,7 @@ def open(fp, mode="r", formats=None) -> Image: im = _open_core(fp, filename, prefix, formats) if im is None and formats is ID: - checked_formats = formats.copy() + checked_formats = ID.copy() if init(): im = _open_core( fp, @@ -3333,7 +3492,7 @@ def open(fp, mode="r", formats=None) -> Image: if exclusive_fp: fp.close() - for message in accept_warnings: + for message in warning_messages: warnings.warn(message) msg = "cannot identify image file %r" % (filename if filename else fp) raise UnidentifiedImageError(msg) @@ -3414,7 +3573,7 @@ def eval(image, *args): return image.point(args[0]) -def merge(mode, bands): +def merge(mode: str, bands: Sequence[Image]) -> Image: """ Merge a set of single band images into a new multiband image. @@ -3446,9 +3605,9 @@ def merge(mode, bands): def register_open( - id, + id: str, factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], - accept: Callable[[bytes], bool] | None = None, + accept: Callable[[bytes], bool | str] | None = None, ) -> None: """ Register an image file plugin. This function should not be used @@ -3480,7 +3639,9 @@ def register_mime(id: str, mimetype: str) -> None: MIME[id.upper()] = mimetype -def register_save(id: str, driver) -> None: +def register_save( + id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] +) -> None: """ Registers an image save function. This function should not be used in application code. @@ -3491,7 +3652,9 @@ def register_save(id: str, driver) -> None: SAVE[id.upper()] = driver -def register_save_all(id, driver) -> None: +def register_save_all( + id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] +) -> None: """ Registers an image function to save all the frames of a multiframe format. This function should not be @@ -3503,7 +3666,7 @@ def register_save_all(id, driver) -> None: SAVE_ALL[id.upper()] = driver -def register_extension(id, extension) -> None: +def register_extension(id: str, extension: str) -> None: """ Registers an image extension. This function should not be used in application code. @@ -3514,7 +3677,7 @@ def register_extension(id, extension) -> None: EXTENSION[extension.lower()] = id.upper() -def register_extensions(id, extensions) -> None: +def register_extensions(id: str, extensions: list[str]) -> None: """ Registers image extensions. This function should not be used in application code. @@ -3526,7 +3689,7 @@ def register_extensions(id, extensions) -> None: register_extension(id, extension) -def registered_extensions(): +def registered_extensions() -> dict[str, str]: """ Returns a dictionary containing all file extensions belonging to registered plugins @@ -3565,7 +3728,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: # Simple display support. -def _show(image, **options) -> None: +def _show(image: Image, **options: Any) -> None: from . import ImageShow ImageShow.show(image, **options) @@ -3575,7 +3738,9 @@ def _show(image, **options) -> None: # Effects -def effect_mandelbrot(size, extent, quality): +def effect_mandelbrot( + size: tuple[int, int], extent: tuple[float, float, float, float], quality: int +) -> Image: """ Generate a Mandelbrot set covering the given extent. @@ -3588,7 +3753,7 @@ def effect_mandelbrot(size, extent, quality): return Image()._new(core.effect_mandelbrot(size, extent, quality)) -def effect_noise(size, sigma): +def effect_noise(size: tuple[int, int], sigma: float) -> Image: """ Generate Gaussian noise centered around 128. @@ -3599,7 +3764,7 @@ def effect_noise(size, sigma): return Image()._new(core.effect_noise(size, sigma)) -def linear_gradient(mode): +def linear_gradient(mode: str) -> Image: """ Generate 256x256 linear gradient from black to white, top to bottom. @@ -3608,7 +3773,7 @@ def linear_gradient(mode): return Image()._new(core.linear_gradient(mode)) -def radial_gradient(mode): +def radial_gradient(mode: str) -> Image: """ Generate 256x256 radial gradient from black to white, centre to edge. @@ -3621,19 +3786,18 @@ def radial_gradient(mode): # Resources -def _apply_env_variables(env=None) -> None: - if env is None: - env = os.environ +def _apply_env_variables(env: dict[str, str] | None = None) -> None: + env_dict = env if env is not None else os.environ for var_name, setter in [ ("PILLOW_ALIGNMENT", core.set_alignment), ("PILLOW_BLOCK_SIZE", core.set_block_size), ("PILLOW_BLOCKS_MAX", core.set_blocks_max), ]: - if var_name not in env: + if var_name not in env_dict: continue - var = env[var_name].lower() + var = env_dict[var_name].lower() units = 1 for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: @@ -3642,13 +3806,13 @@ def _apply_env_variables(env=None) -> None: var = var[: -len(postfix)] try: - var = int(var) * units + var_int = int(var) * units except ValueError: warnings.warn(f"{var_name} is not int") continue try: - setter(var) + setter(var_int) except ValueError as e: warnings.warn(f"{var_name}: {e}") diff --git a/.venv/Lib/site-packages/PIL/ImageCms.py b/.venv/Lib/site-packages/PIL/ImageCms.py index 3a45572..ec10230 100644 --- a/.venv/Lib/site-packages/PIL/ImageCms.py +++ b/.venv/Lib/site-packages/PIL/ImageCms.py @@ -299,6 +299,31 @@ class ImageCmsTransform(Image.ImagePointHandler): proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, flags: Flags = Flags.NONE, ): + supported_modes = ( + "RGB", + "RGBA", + "RGBX", + "CMYK", + "I;16", + "I;16L", + "I;16B", + "YCbCr", + "LAB", + "L", + "1", + ) + for mode in (input_mode, output_mode): + if mode not in supported_modes: + deprecate( + mode, + 12, + { + "L;16": "I;16 or I;16L", + "L:16B": "I;16B", + "YCCA": "YCbCr", + "YCC": "YCbCr", + }.get(mode), + ) if proof is None: self.transform = core.buildTransform( input.profile, output.profile, input_mode, output_mode, intent, flags @@ -704,12 +729,12 @@ def applyTransform( """ (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. - 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 :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 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 - change the mode in-place (the buffer sizes for some modes are + ``transform.input_mode`` and ``transform.output_mode`` are the same, because we + 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` - 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 - as the ``inMode`` supported by the transform. + :param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same + as the ``input_mode`` supported by the transform. :param transform: A valid CmsTransform class object :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 @@ -754,7 +779,7 @@ def applyTransform( def createProfile( - colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = -1 + colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0 ) -> core.CmsProfile: """ (pyCMS) Creates a profile. @@ -777,7 +802,7 @@ def createProfile( :param colorSpace: String, the color space of the profile you wish to create. Currently only "LAB", "XYZ", and "sRGB" are supported. - :param colorTemp: Positive integer for the white point for the profile, in + :param colorTemp: Positive number for the white point for the profile, in degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50 illuminant if omitted (5000k). colorTemp is ONLY applied to LAB profiles, and is ignored for XYZ and sRGB. @@ -838,8 +863,8 @@ def getProfileName(profile: _CmsProfileCompatible) -> str: if not (model or manufacturer): return (profile.profile.profile_description or "") + "\n" - if not manufacturer or len(model) > 30: # type: ignore[arg-type] - return model + "\n" # type: ignore[operator] + if not manufacturer or (model and len(model) > 30): + return f"{model}\n" return f"{model} - {manufacturer}\n" except (AttributeError, OSError, TypeError, ValueError) as v: @@ -1089,7 +1114,7 @@ def isIntentSupported( raise PyCMSError(v) from v -def versions() -> tuple[str, str, str, str]: +def versions() -> tuple[str, str | None, str, str]: """ (pyCMS) Fetches versions. """ diff --git a/.venv/Lib/site-packages/PIL/ImageColor.py b/.venv/Lib/site-packages/PIL/ImageColor.py index 5fb80b7..9a15a8e 100644 --- a/.venv/Lib/site-packages/PIL/ImageColor.py +++ b/.venv/Lib/site-packages/PIL/ImageColor.py @@ -25,7 +25,7 @@ from . import Image @lru_cache -def getrgb(color): +def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]: """ Convert a color string to an RGB or RGBA tuple. If the string cannot be parsed, this function raises a :py:exc:`ValueError` exception. @@ -44,8 +44,10 @@ def getrgb(color): if rgb: if isinstance(rgb, tuple): return rgb - colormap[color] = rgb = getrgb(rgb) - return rgb + rgb_tuple = getrgb(rgb) + assert len(rgb_tuple) == 3 + colormap[color] = rgb_tuple + return rgb_tuple # check for known string formats if re.match("#[a-f0-9]{3}$", color): @@ -88,15 +90,15 @@ def getrgb(color): if m: from colorsys import hls_to_rgb - rgb = hls_to_rgb( + rgb_floats = hls_to_rgb( float(m.group(1)) / 360.0, float(m.group(3)) / 100.0, float(m.group(2)) / 100.0, ) return ( - int(rgb[0] * 255 + 0.5), - int(rgb[1] * 255 + 0.5), - int(rgb[2] * 255 + 0.5), + int(rgb_floats[0] * 255 + 0.5), + int(rgb_floats[1] * 255 + 0.5), + int(rgb_floats[2] * 255 + 0.5), ) m = re.match( @@ -105,15 +107,15 @@ def getrgb(color): if m: from colorsys import hsv_to_rgb - rgb = hsv_to_rgb( + rgb_floats = hsv_to_rgb( float(m.group(1)) / 360.0, float(m.group(2)) / 100.0, float(m.group(3)) / 100.0, ) return ( - int(rgb[0] * 255 + 0.5), - int(rgb[1] * 255 + 0.5), - int(rgb[2] * 255 + 0.5), + int(rgb_floats[0] * 255 + 0.5), + int(rgb_floats[1] * 255 + 0.5), + int(rgb_floats[2] * 255 + 0.5), ) m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) @@ -124,7 +126,7 @@ def getrgb(color): @lru_cache -def getcolor(color, mode: str) -> tuple[int, ...]: +def getcolor(color: str, mode: str) -> int | tuple[int, ...]: """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is @@ -136,33 +138,34 @@ def getcolor(color, mode: str) -> tuple[int, ...]: :param color: A color string :param mode: Convert result to this mode - :return: ``(graylevel[, alpha]) or (red, green, blue[, alpha])`` + :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])`` """ # same as getrgb, but converts the result to the given mode - color, alpha = getrgb(color), 255 - if len(color) == 4: - color, alpha = color[:3], color[3] + rgb, alpha = getrgb(color), 255 + if len(rgb) == 4: + alpha = rgb[3] + rgb = rgb[:3] if mode == "HSV": from colorsys import rgb_to_hsv - r, g, b = color + r, g, b = rgb h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) return int(h * 255), int(s * 255), int(v * 255) elif Image.getmodebase(mode) == "L": - r, g, b = color + r, g, b = rgb # ITU-R Recommendation 601-2 for nonlinear RGB # scaled to 24 bits to match the convert's implementation. - color = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 + graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 if mode[-1] == "A": - return color, alpha - else: - if mode[-1] == "A": - return color + (alpha,) - return color + return graylevel, alpha + return graylevel + elif mode[-1] == "A": + return rgb + (alpha,) + return rgb -colormap = { +colormap: dict[str, str | tuple[int, int, int]] = { # X11 colour table from https://drafts.csswg.org/css-color-4/, with # gray/grey spelling issues fixed. This is a superset of HTML 4.0 # colour names used in CSS 1. diff --git a/.venv/Lib/site-packages/PIL/ImageDraw.py b/.venv/Lib/site-packages/PIL/ImageDraw.py index d3efe64..244d3d5 100644 --- a/.venv/Lib/site-packages/PIL/ImageDraw.py +++ b/.venv/Lib/site-packages/PIL/ImageDraw.py @@ -34,11 +34,25 @@ from __future__ import annotations import math import numbers 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 ._deprecate import deprecate from ._typing import Coords +# experimental access to the outline API +Outline: Callable[[], Image.core._Outline] | None +try: + Outline = Image.core.outline +except AttributeError: + Outline = None + +if TYPE_CHECKING: + from . import ImageDraw2, ImageFont + +_Ink = Union[float, Tuple[int, ...], str] + """ A simple 2D drawing interface for PIL images.

@@ -48,7 +62,9 @@ directly. class ImageDraw: - font = None + font: ( + ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None + ) = None def __init__(self, im: Image.Image, mode: str | None = None) -> None: """ @@ -92,7 +108,9 @@ class ImageDraw: self.fontmode = "L" # aliasing is okay for other modes self.fill = False - def getfont(self): + def getfont( + self, + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: """ Get the current default font. @@ -117,43 +135,57 @@ class ImageDraw: self.font = ImageFont.load_default() return self.font - def _getfont(self, font_size: float | None): + def _getfont( + self, font_size: float | None + ) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont: if font_size is not None: from . import ImageFont - font = ImageFont.load_default(font_size) + return ImageFont.load_default(font_size) else: - font = self.getfont() - return font + return self.getfont() - def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: + def _getink( + self, ink: _Ink | None, fill: _Ink | None = None + ) -> tuple[int | None, int | None]: + result_ink = None + result_fill = None if ink is None and fill is None: if self.fill: - fill = self.ink + result_fill = self.ink else: - ink = self.ink + result_ink = self.ink else: if ink is not None: if isinstance(ink, str): ink = ImageColor.getcolor(ink, self.mode) if self.palette and not isinstance(ink, numbers.Number): ink = self.palette.getcolor(ink, self._image) - ink = self.draw.draw_ink(ink) + result_ink = self.draw.draw_ink(ink) if fill is not None: if isinstance(fill, str): fill = ImageColor.getcolor(fill, self.mode) if self.palette and not isinstance(fill, numbers.Number): fill = self.palette.getcolor(fill, self._image) - fill = self.draw.draw_ink(fill) - return ink, fill + result_fill = self.draw.draw_ink(fill) + return result_ink, result_fill - def arc(self, xy: Coords, start, end, fill=None, width=1) -> None: + def arc( + self, + xy: Coords, + start: float, + end: float, + fill: _Ink | None = None, + width: int = 1, + ) -> None: """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_arc(xy, start, end, ink, width) - def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None: + def bitmap( + self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None + ) -> None: """Draw a bitmap.""" bitmap.load() ink, fill = self._getink(fill) @@ -162,23 +194,55 @@ class ImageDraw: if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None: + def chord( + self, + xy: Coords, + start: float, + end: float, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: """Draw a chord.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_chord(xy, start, end, fill, 1) - if ink is not None and ink != fill and width != 0: + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_chord(xy, start, end, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None: + def ellipse( + self, + xy: Coords, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: """Draw an ellipse.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_ellipse(xy, fill, 1) - if ink is not None and ink != fill and width != 0: + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_ellipse(xy, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def 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.""" ink = self._getink(fill)[0] if ink is not None: @@ -206,7 +270,9 @@ class ImageDraw: # This is a straight line, so no joint is required continue - def coord_at_angle(coord, angle): + def coord_at_angle( + coord: Sequence[float], angle: float + ) -> tuple[float, ...]: x, y = coord angle -= 90 distance = width / 2 - 1 @@ -247,37 +313,54 @@ class ImageDraw: ] self.line(gap_coords, fill, width=3) - def shape(self, shape, fill=None, outline=None) -> None: + def shape( + self, + shape: Image.core._Outline, + fill: _Ink | None = None, + outline: _Ink | None = None, + ) -> None: """(Experimental) Draw a shape.""" shape.close() - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_outline(shape, fill, 1) - if ink is not None and ink != fill: + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_outline(shape, fill_ink, 1) + if ink is not None and ink != fill_ink: self.draw.draw_outline(shape, ink, 0) def pieslice( - self, xy: Coords, start, end, fill=None, outline=None, width=1 + self, + xy: Coords, + start: float, + end: float, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, ) -> None: """Draw a pieslice.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_pieslice(xy, start, end, fill, 1) - if ink is not None and ink != fill and width != 0: + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_pieslice(xy, start, end, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: self.draw.draw_pieslice(xy, start, end, ink, 0, width) - def point(self, xy: Coords, fill=None) -> None: + def point(self, xy: Coords, fill: _Ink | None = None) -> None: """Draw one or more individual pixels.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None: + def polygon( + self, + xy: Coords, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: """Draw a polygon.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_polygon(xy, fill, 1) - if ink is not None and ink != fill and width != 0: + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_polygon(xy, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: if width == 1: self.draw.draw_polygon(xy, ink, 0, width) elif self.im is not None: @@ -303,22 +386,41 @@ class ImageDraw: self.im.paste(im.im, (0, 0) + im.size, mask.im) def regular_polygon( - self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 + self, + bounding_circle: Sequence[Sequence[float] | float], + n_sides: int, + rotation: float = 0, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, ) -> None: """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline, width) - def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None: + def rectangle( + self, + xy: Coords, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + ) -> None: """Draw a rectangle.""" - ink, fill = self._getink(outline, fill) - if fill is not None: - self.draw.draw_rectangle(xy, fill, 1) - if ink is not None and ink != fill and width != 0: + ink, fill_ink = self._getink(outline, fill) + if fill_ink is not None: + self.draw.draw_rectangle(xy, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None + self, + xy: Coords, + radius: float = 0, + fill: _Ink | None = None, + outline: _Ink | None = None, + width: int = 1, + *, + corners: tuple[bool, bool, bool, bool] | None = None, ) -> None: """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): @@ -360,10 +462,10 @@ class ImageDraw: # that is a rectangle return self.rectangle(xy, fill, outline, width) - r = d // 2 - ink, fill = self._getink(outline, fill) + r = int(d // 2) + ink, fill_ink = self._getink(outline, fill) - def draw_corners(pieslice) -> None: + def draw_corners(pieslice: bool) -> None: parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] if full_x: # Draw top and bottom halves @@ -393,32 +495,32 @@ class ImageDraw: ) for part in parts: if pieslice: - self.draw.draw_pieslice(*(part + (fill, 1))) + self.draw.draw_pieslice(*(part + (fill_ink, 1))) else: self.draw.draw_arc(*(part + (ink, width))) - if fill is not None: + if fill_ink is not None: draw_corners(True) if full_x: - self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1) + self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) else: - self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) + self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) if not full_x and not full_y: left = [x0, y0, x0 + r, y1] if corners[0]: left[1] += r + 1 if corners[3]: left[3] -= r + 1 - self.draw.draw_rectangle(left, fill, 1) + self.draw.draw_rectangle(left, fill_ink, 1) right = [x1 - r, y0, x1, y1] if corners[1]: right[1] += r + 1 if corners[2]: right[3] -= r + 1 - self.draw.draw_rectangle(right, fill, 1) - if ink is not None and ink != fill and width != 0: + self.draw.draw_rectangle(right, fill_ink, 1) + if ink is not None and ink != fill_ink and width != 0: draw_corners(False) if not full_x: @@ -450,15 +552,13 @@ class ImageDraw: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text) -> bool: + def _multiline_check(self, text: AnyStr) -> bool: split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text - def _multiline_split(self, text) -> list[str | bytes]: - split_character = "\n" if isinstance(text, str) else b"\n" - - return text.split(split_character) + def _multiline_split(self, text: AnyStr) -> list[AnyStr]: + return text.split("\n" if isinstance(text, str) else b"\n") def _multiline_spacing(self, font, spacing, stroke_width): return ( @@ -469,10 +569,15 @@ class ImageDraw: def text( self, - xy, - text, + xy: tuple[float, float], + text: str, fill=None, - font=None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, anchor=None, spacing=4, align="left", @@ -510,10 +615,11 @@ class ImageDraw: embedded_color, ) - def getink(fill): - ink, fill = self._getink(fill) + def getink(fill: _Ink | None) -> int: + ink, fill_ink = self._getink(fill) if ink is None: - return fill + assert fill_ink is not None + return fill_ink return ink def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: @@ -526,7 +632,7 @@ class ImageDraw: coord.append(int(xy[i])) start.append(math.modf(xy[i])[0]) try: - mask, offset = font.getmask2( + mask, offset = font.getmask2( # type: ignore[union-attr,misc] text, mode, direction=direction, @@ -542,7 +648,7 @@ class ImageDraw: coord = [coord[0] + offset[0], coord[1] + offset[1]] except AttributeError: try: - mask = font.getmask( + mask = font.getmask( # type: ignore[misc] text, mode, direction, @@ -591,10 +697,15 @@ class ImageDraw: def multiline_text( self, - xy, - text, + xy: tuple[float, float], + text: str, fill=None, - font=None, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, anchor=None, spacing=4, align="left", @@ -624,7 +735,7 @@ class ImageDraw: font = self._getfont(font_size) widths = [] - max_width = 0 + max_width: float = 0 lines = self._multiline_split(text) line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: @@ -678,15 +789,20 @@ class ImageDraw: def textlength( self, - text, - font=None, + text: str, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, direction=None, features=None, language=None, embedded_color=False, *, font_size=None, - ): + ) -> float: """Get the length of a given string, in pixels with 1/64 precision.""" if self._multiline_check(text): msg = "can't measure length of multiline text" @@ -778,7 +894,7 @@ class ImageDraw: font = self._getfont(font_size) widths = [] - max_width = 0 + max_width: float = 0 lines = self._multiline_split(text) line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: @@ -850,7 +966,7 @@ class ImageDraw: return bbox -def Draw(im, mode: str | None = None) -> ImageDraw: +def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: """ A simple 2D drawing interface for PIL images. @@ -862,45 +978,38 @@ def Draw(im, mode: str | None = None) -> ImageDraw: defaults to the mode of the image. """ try: - return im.getdraw(mode) + return getattr(im, "getdraw")(mode) except AttributeError: return ImageDraw(im, mode) -# experimental access to the outline API -try: - Outline = Image.core.outline -except AttributeError: - Outline = None - - -def getdraw(im=None, hints=None): +def getdraw( + im: Image.Image | None = None, hints: list[str] | None = None +) -> tuple[ImageDraw2.Draw | None, ModuleType]: """ - (Experimental) A more advanced 2D drawing interface for PIL images, - based on the WCK interface. - :param im: The image to draw in. - :param hints: An optional list of hints. + :param hints: An optional list of hints. Deprecated. :returns: A (drawing context, drawing resource factory) tuple. """ - # FIXME: this needs more work! - # FIXME: come up with a better 'hints' scheme. - handler = None - if not hints or "nicest" in hints: - try: - from . import _imagingagg as handler - except ImportError: - pass - if handler is None: - from . import ImageDraw2 as handler - if im: - im = handler.Draw(im) - return im, handler + if hints is not None: + deprecate("'hints' parameter", 12) + from . import ImageDraw2 + + draw = ImageDraw2.Draw(im) if im is not None else None + return draw, ImageDraw2 -def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: +def floodfill( + image: Image.Image, + xy: tuple[int, int], + value: float | tuple[int, ...], + border: float | tuple[int, ...] | None = None, + thresh: float = 0, +) -> None: """ - (experimental) Fills a bounded region with a given color. + .. warning:: This method is experimental. + + Fills a bounded region with a given color. :param image: Target image. :param xy: Seed position (a 2-item coordinate tuple). See @@ -918,6 +1027,7 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: # based on an implementation by Eric S. Raymond # amended by yo1995 @20180806 pixel = image.load() + assert pixel is not None x, y = xy try: background = pixel[x, y] @@ -955,12 +1065,12 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: def _compute_regular_polygon_vertices( - bounding_circle, n_sides, rotation + bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float ) -> list[tuple[float, float]]: """ Generate a list of vertices for a 2D regular polygon. - :param bounding_circle: The bounding circle is a tuple defined + :param bounding_circle: The bounding circle is a sequence defined by a point and radius. The polygon is inscribed in this circle. (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) :param n_sides: Number of sides @@ -998,7 +1108,7 @@ def _compute_regular_polygon_vertices( # 1. Error Handling # 1.1 Check `n_sides` has an appropriate value if not isinstance(n_sides, int): - msg = "n_sides should be an int" + msg = "n_sides should be an int" # type: ignore[unreachable] raise TypeError(msg) if n_sides < 3: msg = "n_sides should be an int > 2" @@ -1010,9 +1120,24 @@ def _compute_regular_polygon_vertices( raise TypeError(msg) if len(bounding_circle) == 3: - *centroid, polygon_radius = bounding_circle - elif len(bounding_circle) == 2: - centroid, polygon_radius = bounding_circle + if not all(isinstance(i, (int, float)) for i in bounding_circle): + msg = "bounding_circle should only contain numeric data" + raise ValueError(msg) + + *centroid, polygon_radius = cast(List[float], list(bounding_circle)) + elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)): + if not all( + isinstance(i, (int, float)) for i in bounding_circle[0] + ) or not isinstance(bounding_circle[1], (int, float)): + msg = "bounding_circle should only contain numeric data" + raise ValueError(msg) + + if len(bounding_circle[0]) != 2: + msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" + raise ValueError(msg) + + centroid = cast(List[float], list(bounding_circle[0])) + polygon_radius = cast(float, bounding_circle[1]) else: msg = ( "bounding_circle should contain 2D coordinates " @@ -1020,25 +1145,17 @@ def _compute_regular_polygon_vertices( ) raise ValueError(msg) - if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): - msg = "bounding_circle should only contain numeric data" - raise ValueError(msg) - - if not len(centroid) == 2: - msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" - raise ValueError(msg) - if polygon_radius <= 0: msg = "bounding_circle radius should be > 0" raise ValueError(msg) # 1.3 Check `rotation` has an appropriate value if not isinstance(rotation, (int, float)): - msg = "rotation should be an int or float" + msg = "rotation should be an int or float" # type: ignore[unreachable] raise ValueError(msg) # 2. Define Helper Functions - def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]: + def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]: return ( round( point[0] * math.cos(math.radians(360 - degrees)) @@ -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] return _apply_rotation(start_point, angle) @@ -1077,11 +1194,13 @@ def _compute_regular_polygon_vertices( return [_compute_polygon_vertex(angle) for angle in angles] -def _color_diff(color1, color2: float | tuple[int, ...]) -> float: +def _color_diff( + color1: float | tuple[int, ...], color2: float | tuple[int, ...] +) -> float: """ Uses 1-norm distance to calculate difference between two values. """ - if isinstance(color2, tuple): - return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2))) - else: - return abs(color1 - color2) + first = color1 if isinstance(color1, tuple) else (color1,) + second = color2 if isinstance(color2, tuple) else (color2,) + + return sum(abs(first[i] - second[i]) for i in range(0, len(second))) diff --git a/.venv/Lib/site-packages/PIL/ImageDraw2.py b/.venv/Lib/site-packages/PIL/ImageDraw2.py index 35ee583..e89a78b 100644 --- a/.venv/Lib/site-packages/PIL/ImageDraw2.py +++ b/.venv/Lib/site-packages/PIL/ImageDraw2.py @@ -24,13 +24,16 @@ """ from __future__ import annotations +from typing import BinaryIO + from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath +from ._typing import StrOrBytesPath class Pen: """Stores an outline color and width.""" - def __init__(self, color, width=1, opacity=255): + def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None: self.color = ImageColor.getrgb(color) self.width = width @@ -38,14 +41,16 @@ class Pen: class Brush: """Stores a fill color""" - def __init__(self, color, opacity=255): + def __init__(self, color: str, opacity: int = 255) -> None: self.color = ImageColor.getrgb(color) class Font: """Stores a TrueType font and color""" - def __init__(self, color, file, size=12): + def __init__( + self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12 + ) -> None: # FIXME: add support for bitmap fonts self.color = ImageColor.getrgb(color) self.font = ImageFont.truetype(file, size) @@ -56,14 +61,22 @@ class Draw: (Experimental) WCK-style drawing interface """ - def __init__(self, image, size=None, color=None): - if not hasattr(image, "im"): + def __init__( + self, + image: Image.Image | str, + size: tuple[int, int] | list[int] | None = None, + color: float | tuple[float, ...] | str | None = None, + ) -> None: + if isinstance(image, str): + if size is None: + msg = "If image argument is mode string, size must be a list or tuple" + raise ValueError(msg) image = Image.new(image, size, color) self.draw = ImageDraw.Draw(image) self.image = image self.transform = None - def flush(self): + def flush(self) -> Image.Image: return self.image def render(self, op, xy, pen, brush=None): diff --git a/.venv/Lib/site-packages/PIL/ImageEnhance.py b/.venv/Lib/site-packages/PIL/ImageEnhance.py index 93a50d2..d7e99a9 100644 --- a/.venv/Lib/site-packages/PIL/ImageEnhance.py +++ b/.venv/Lib/site-packages/PIL/ImageEnhance.py @@ -23,7 +23,10 @@ from . import Image, ImageFilter, ImageStat class _Enhance: - def enhance(self, factor): + image: Image.Image + degenerate: Image.Image + + def enhance(self, factor: float) -> Image.Image: """ Returns an enhanced image. @@ -46,7 +49,7 @@ class Color(_Enhance): the original image. """ - def __init__(self, image): + def __init__(self, image: Image.Image) -> None: self.image = image self.intermediate_mode = "L" if "A" in image.getbands(): @@ -63,7 +66,7 @@ class Contrast(_Enhance): gives a solid gray image. A factor of 1.0 gives the original image. """ - def __init__(self, image): + def __init__(self, image: Image.Image) -> None: self.image = image mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) self.degenerate = Image.new("L", image.size, mean).convert(image.mode) @@ -80,7 +83,7 @@ class Brightness(_Enhance): original image. """ - def __init__(self, image): + def __init__(self, image: Image.Image) -> None: self.image = image self.degenerate = Image.new(image.mode, image.size, 0) @@ -96,7 +99,7 @@ class Sharpness(_Enhance): original image, and a factor of 2.0 gives a sharpened image. """ - def __init__(self, image): + def __init__(self, image: Image.Image) -> None: self.image = image self.degenerate = image.filter(ImageFilter.SMOOTH) diff --git a/.venv/Lib/site-packages/PIL/ImageFile.py b/.venv/Lib/site-packages/PIL/ImageFile.py index 0283fa2..69e7ee5 100644 --- a/.venv/Lib/site-packages/PIL/ImageFile.py +++ b/.venv/Lib/site-packages/PIL/ImageFile.py @@ -28,6 +28,7 @@ # from __future__ import annotations +import abc import io import itertools import struct @@ -64,7 +65,7 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`, # Helpers -def _get_oserror(error, *, encoder): +def _get_oserror(error: int, *, encoder: bool) -> OSError: try: msg = Image.core.getcodecstatus(error) except AttributeError: @@ -75,7 +76,7 @@ def _get_oserror(error, *, encoder): return OSError(msg) -def raise_oserror(error): +def raise_oserror(error: int) -> OSError: deprecate( "raise_oserror", 12, @@ -153,17 +154,18 @@ class ImageFile(Image.Image): self.fp.close() raise - def get_format_mimetype(self): + def get_format_mimetype(self) -> str | None: if self.custom_mimetype: return self.custom_mimetype if self.format is not None: return Image.MIME.get(self.format.upper()) + return None def __setstate__(self, state): self.tile = [] super().__setstate__(state) - def verify(self): + def verify(self) -> None: """Check file integrity""" # raise exception if something's wrong. must be called @@ -311,7 +313,7 @@ class ImageFile(Image.Image): return Image.Image.load(self) - def load_prepare(self): + def load_prepare(self) -> None: # create image memory if necessary 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) @@ -319,16 +321,16 @@ class ImageFile(Image.Image): if self.mode == "P": Image.Image.load(self) - def load_end(self): + def load_end(self) -> None: # may be overridden pass # may be defined for contained formats - # def load_seek(self, pos): + # def load_seek(self, pos: int) -> None: # pass # may be defined for blocked formats (e.g. PNG) - # def load_read(self, read_bytes): + # def load_read(self, read_bytes: int) -> bytes: # pass def _seek_check(self, frame): @@ -347,6 +349,15 @@ class ImageFile(Image.Image): return self.tell() != frame +class StubHandler: + def open(self, im: StubImageFile) -> None: + pass + + @abc.abstractmethod + def load(self, im: StubImageFile) -> Image.Image: + pass + + class StubImageFile(ImageFile): """ Base class for stub image loaders. @@ -355,7 +366,7 @@ class StubImageFile(ImageFile): certain format, but relies on external code to load the file. """ - def _open(self): + def _open(self) -> None: msg = "StubImageFile subclass must implement _open" raise NotImplementedError(msg) @@ -371,7 +382,7 @@ class StubImageFile(ImageFile): self.__dict__ = image.__dict__ return image.load() - def _load(self): + def _load(self) -> StubHandler | None: """(Hook) Find actual image loader.""" msg = "StubImageFile subclass must implement _load" raise NotImplementedError(msg) @@ -390,7 +401,7 @@ class Parser: offset = 0 finished = 0 - def reset(self): + def reset(self) -> None: """ (Consumer) Reset the parser. Note that you can only call this method immediately after you've created a parser; parser @@ -477,7 +488,7 @@ class Parser: def __enter__(self): return self - def __exit__(self, *args): + def __exit__(self, *args: object) -> None: self.close() def close(self): @@ -605,13 +616,13 @@ def _safe_read(fp, size): class PyCodecState: - def __init__(self): + def __init__(self) -> None: self.xsize = 0 self.ysize = 0 self.xoff = 0 self.yoff = 0 - def extents(self): + def extents(self) -> tuple[int, int, int, int]: return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize @@ -634,7 +645,7 @@ class PyCodec: """ self.args = args - def cleanup(self): + def cleanup(self) -> None: """ Override to perform codec specific cleanup @@ -651,7 +662,7 @@ class PyCodec: """ self.fd = fd - def setimage(self, im, extents=None): + def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None: """ Called from ImageFile to set the core output image for the codec @@ -700,10 +711,10 @@ class PyDecoder(PyCodec): _pulls_fd = False @property - def pulls_fd(self): + def pulls_fd(self) -> bool: return self._pulls_fd - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: """ Override to perform the decoding process. @@ -728,6 +739,7 @@ class PyDecoder(PyCodec): if not rawmode: rawmode = self.mode d = Image._getdecoder(self.mode, "raw", rawmode) + assert self.im is not None d.setimage(self.im, self.state.extents()) s = d.decode(data) @@ -750,10 +762,10 @@ class PyEncoder(PyCodec): _pushes_fd = False @property - def pushes_fd(self): + def pushes_fd(self) -> bool: return self._pushes_fd - def encode(self, bufsize): + def encode(self, bufsize: int) -> tuple[int, int, bytes]: """ Override to perform the encoding process. @@ -765,7 +777,7 @@ class PyEncoder(PyCodec): msg = "unavailable in base encoder" raise NotImplementedError(msg) - def encode_to_pyfd(self): + def encode_to_pyfd(self) -> tuple[int, int]: """ If ``pushes_fd`` is ``True``, then this method will be used, and ``encode()`` will only be called once. @@ -777,6 +789,7 @@ class PyEncoder(PyCodec): return 0, -8 # bad configuration bytes_consumed, errcode, data = self.encode(0) if data: + assert self.fd is not None self.fd.write(data) return bytes_consumed, errcode diff --git a/.venv/Lib/site-packages/PIL/ImageFilter.py b/.venv/Lib/site-packages/PIL/ImageFilter.py index b2c4950..e18b4a4 100644 --- a/.venv/Lib/site-packages/PIL/ImageFilter.py +++ b/.venv/Lib/site-packages/PIL/ImageFilter.py @@ -16,11 +16,20 @@ # from __future__ import annotations +import abc 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: - pass + @abc.abstractmethod + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: + pass class MultibandFilter(Filter): @@ -28,7 +37,9 @@ class MultibandFilter(Filter): class BuiltinFilter(MultibandFilter): - def filter(self, image): + filterargs: tuple[Any, ...] + + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: if image.mode == "P": msg = "cannot filter palette images" raise ValueError(msg) @@ -53,7 +64,13 @@ class Kernel(BuiltinFilter): name = "Kernel" - def __init__(self, size, kernel, scale=None, offset=0): + def __init__( + self, + size: tuple[int, int], + kernel: Sequence[float], + scale: float | None = None, + offset: float = 0, + ) -> None: if scale is None: # default scale is sum of kernel scale = functools.reduce(lambda a, b: a + b, kernel) @@ -76,11 +93,11 @@ class RankFilter(Filter): name = "Rank" - def __init__(self, size, rank): + def __init__(self, size: int, rank: int) -> None: self.size = size self.rank = rank - def filter(self, image): + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: if image.mode == "P": msg = "cannot filter palette images" raise ValueError(msg) @@ -98,7 +115,7 @@ class MedianFilter(RankFilter): name = "Median" - def __init__(self, size=3): + def __init__(self, size: int = 3) -> None: self.size = size self.rank = size * size // 2 @@ -113,7 +130,7 @@ class MinFilter(RankFilter): name = "Min" - def __init__(self, size=3): + def __init__(self, size: int = 3) -> None: self.size = size self.rank = 0 @@ -128,7 +145,7 @@ class MaxFilter(RankFilter): name = "Max" - def __init__(self, size=3): + def __init__(self, size: int = 3) -> None: self.size = size self.rank = size * size - 1 @@ -144,10 +161,10 @@ class ModeFilter(Filter): name = "Mode" - def __init__(self, size=3): + def __init__(self, size: int = 3) -> None: self.size = size - def filter(self, image): + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: return image.modefilter(self.size) @@ -162,12 +179,12 @@ class GaussianBlur(MultibandFilter): name = "GaussianBlur" - def __init__(self, radius=2): + def __init__(self, radius: float | Sequence[float] = 2) -> None: self.radius = radius - def filter(self, image): + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: xy = self.radius - if not isinstance(xy, (tuple, list)): + if isinstance(xy, (int, float)): xy = (xy, xy) if xy == (0, 0): return image.copy() @@ -190,18 +207,16 @@ class BoxBlur(MultibandFilter): name = "BoxBlur" - def __init__(self, radius): - xy = radius - if not isinstance(xy, (tuple, list)): - xy = (xy, xy) + def __init__(self, radius: float | Sequence[float]) -> None: + xy = radius if isinstance(radius, (tuple, list)) else (radius, radius) if xy[0] < 0 or xy[1] < 0: msg = "radius must be >= 0" raise ValueError(msg) self.radius = radius - def filter(self, image): + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: xy = self.radius - if not isinstance(xy, (tuple, list)): + if isinstance(xy, (int, float)): xy = (xy, xy) if xy == (0, 0): return image.copy() @@ -225,12 +240,14 @@ class UnsharpMask(MultibandFilter): name = "UnsharpMask" - def __init__(self, radius=2, percent=150, threshold=3): + def __init__( + self, radius: float = 2, percent: int = 150, threshold: int = 3 + ) -> None: self.radius = radius self.percent = percent self.threshold = threshold - def filter(self, image): + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: return image.unsharp_mask(self.radius, self.percent, self.threshold) @@ -375,7 +392,14 @@ class Color3DLUT(MultibandFilter): name = "Color 3D LUT" - def __init__(self, size, table, channels=3, target_mode=None, **kwargs): + def __init__( + self, + size: int | tuple[int, int, int], + table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray, + channels: int = 3, + target_mode: str | None = None, + **kwargs: bool, + ) -> None: if channels not in (3, 4): msg = "Only 3 or 4 output channels are supported" raise ValueError(msg) @@ -389,7 +413,7 @@ class Color3DLUT(MultibandFilter): items = size[0] * size[1] * size[2] wrong_size = False - numpy = None + numpy: ModuleType | None = None if hasattr(table, "shape"): try: import numpy @@ -397,15 +421,16 @@ class Color3DLUT(MultibandFilter): pass if numpy and isinstance(table, numpy.ndarray): + numpy_table: NumpyArray = table if copy_table: - table = table.copy() + numpy_table = numpy_table.copy() - if table.shape in [ + if numpy_table.shape in [ (items * channels,), (items, channels), (size[2], size[1], size[0], channels), ]: - table = table.reshape(items * channels) + table = numpy_table.reshape(items * channels) else: wrong_size = True @@ -415,7 +440,8 @@ class Color3DLUT(MultibandFilter): # Convert to a flat list if table and isinstance(table[0], (list, tuple)): - table, raw_table = [], table + raw_table = cast(Sequence[Sequence[int]], table) + flat_table: list[int] = [] for pixel in raw_table: if len(pixel) != channels: msg = ( @@ -423,7 +449,8 @@ class Color3DLUT(MultibandFilter): f"have a length of {channels}." ) raise ValueError(msg) - table.extend(pixel) + flat_table.extend(pixel) + table = flat_table if wrong_size or len(table) != items * channels: msg = ( @@ -436,7 +463,7 @@ class Color3DLUT(MultibandFilter): self.table = table @staticmethod - def _check_size(size): + def _check_size(size: Any) -> tuple[int, int, int]: try: _, _, _ = size except ValueError as e: @@ -444,7 +471,7 @@ class Color3DLUT(MultibandFilter): raise ValueError(msg) from e except TypeError: size = (size, size, size) - size = [int(x) for x in size] + size = tuple(int(x) for x in size) for size_1d in size: if not 2 <= size_1d <= 65: msg = "Size should be in [2, 65] range." @@ -452,7 +479,13 @@ class Color3DLUT(MultibandFilter): return size @classmethod - def generate(cls, size, callback, channels=3, target_mode=None): + def generate( + cls, + size: int | tuple[int, int, int], + callback: Callable[[float, float, float], tuple[float, ...]], + channels: int = 3, + target_mode: str | None = None, + ) -> Color3DLUT: """Generates new LUT using provided callback. :param size: Size of the table. Passed to the constructor. @@ -469,7 +502,7 @@ class Color3DLUT(MultibandFilter): msg = "Only 3 or 4 output channels are supported" raise ValueError(msg) - table = [0] * (size_1d * size_2d * size_3d * channels) + table: list[float] = [0] * (size_1d * size_2d * size_3d * channels) idx_out = 0 for b in range(size_3d): for g in range(size_2d): @@ -487,7 +520,13 @@ class Color3DLUT(MultibandFilter): _copy_table=False, ) - def transform(self, callback, with_normals=False, channels=None, target_mode=None): + def transform( + self, + callback: Callable[..., tuple[float, ...]], + with_normals: bool = False, + channels: int | None = None, + target_mode: str | None = None, + ) -> Color3DLUT: """Transforms the table values using provided callback and returns a new LUT with altered values. @@ -541,7 +580,7 @@ class Color3DLUT(MultibandFilter): _copy_table=False, ) - def __repr__(self): + def __repr__(self) -> str: r = [ f"{self.__class__.__name__} from {self.table.__class__.__name__}", "size={:d}x{:d}x{:d}".format(*self.size), @@ -551,7 +590,7 @@ class Color3DLUT(MultibandFilter): r.append(f"target_mode={self.mode}") return "<{}>".format(" ".join(r)) - def filter(self, image): + def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: from . import Image return image.color_lut_3d( diff --git a/.venv/Lib/site-packages/PIL/ImageFont.py b/.venv/Lib/site-packages/PIL/ImageFont.py index 256c581..d260eef 100644 --- a/.venv/Lib/site-packages/PIL/ImageFont.py +++ b/.venv/Lib/site-packages/PIL/ImageFont.py @@ -33,11 +33,17 @@ import sys import warnings from enum import IntEnum from io import BytesIO -from typing import BinaryIO +from types import ModuleType +from typing import IO, TYPE_CHECKING, Any, BinaryIO from . import Image from ._typing import StrOrBytesPath -from ._util import is_directory, is_path +from ._util import DeferredError, is_path + +if TYPE_CHECKING: + from . import ImageFile + from ._imaging import ImagingFont + from ._imagingft import Font class Layout(IntEnum): @@ -48,15 +54,14 @@ class Layout(IntEnum): MAX_STRING_LENGTH = 1_000_000 +core: ModuleType | DeferredError try: from . import _imagingft as core except ImportError as ex: - from ._util import DeferredError - core = DeferredError.new(ex) -def _string_length_check(text): +def _string_length_check(text: str | bytes | bytearray) -> None: if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: msg = "too many characters in string" raise ValueError(msg) @@ -81,9 +86,11 @@ def _string_length_check(text): class ImageFont: """PIL font wrapper""" - def _load_pilfont(self, filename): + font: ImagingFont + + def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: - image = None + image: ImageFile.ImageFile | None = None for ext in (".png", ".gif", ".pbm"): if image: image.close() @@ -106,7 +113,7 @@ class ImageFont: self._load_pilfont_data(fp, image) image.close() - def _load_pilfont_data(self, file, image): + def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: # read PILfont header if file.readline() != b"PILfont\n": msg = "Not a PILfont file" @@ -153,17 +160,15 @@ class ImageFont: Image._decompression_bomb_check(self.font.getsize(text)) return self.font.getmask(text, mode) - def getbbox(self, text, *args, **kwargs): + def getbbox( + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any + ) -> tuple[int, int, int, int]: """ Returns bounding box (in pixels) of given text. .. versionadded:: 9.2.0 :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 """ @@ -171,7 +176,9 @@ class ImageFont: width, height = self.font.getsize(text) return 0, 0, width, height - def getlength(self, text, *args, **kwargs): + def getlength( + self, text: str | bytes | bytearray, *args: Any, **kwargs: Any + ) -> int: """ Returns length (in pixels) of given text. This is the amount by which following text should be offset. @@ -191,6 +198,9 @@ class ImageFont: class FreeTypeFont: """FreeType font wrapper (requires _imagingft service)""" + font: Font + font_bytes: bytes + def __init__( self, font: StrOrBytesPath | BinaryIO | None = None, @@ -201,6 +211,9 @@ class FreeTypeFont: ) -> None: # FIXME: use service provider instead + if isinstance(core, DeferredError): + raise core.ex + if size <= 0: msg = "font size must be greater than 0" raise ValueError(msg) @@ -254,14 +267,14 @@ class FreeTypeFont: path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def getname(self): + def getname(self) -> tuple[str | None, str | None]: """ :return: A tuple of the font family (e.g. Helvetica) and the font style (e.g. Bold) """ return self.font.family, self.font.style - def getmetrics(self): + def getmetrics(self) -> tuple[int, int]: """ :return: A tuple of the font ascent (the distance from the baseline to the highest outline point) and descent (the distance from the @@ -269,7 +282,9 @@ class FreeTypeFont: """ return self.font.ascent, self.font.descent - def getlength(self, text, mode="", direction=None, features=None, language=None): + def getlength( + self, text: str | bytes, mode="", direction=None, features=None, language=None + ) -> float: """ Returns length (in pixels with 1/64 precision) of given text when rendered in font with provided direction, features, and language. @@ -343,14 +358,14 @@ class FreeTypeFont: def getbbox( self, - text, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ): + text: str | bytes, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ) -> tuple[float, float, float, float]: """ Returns bounding box (in pixels) of given text relative to given anchor when rendered in font with provided direction, features, and language. @@ -500,7 +515,7 @@ class FreeTypeFont: def getmask2( self, - text, + text: str | bytes, mode="", direction=None, features=None, @@ -628,7 +643,7 @@ class FreeTypeFont: layout_engine=layout_engine or self.layout_engine, ) - def get_variation_names(self): + def get_variation_names(self) -> list[bytes]: """ :returns: A list of the named styles in a variation font. :exception OSError: If the font is not a variation font. @@ -670,10 +685,11 @@ class FreeTypeFont: msg = "FreeType 2.9.1 or greater is required" raise NotImplementedError(msg) from e for axis in axes: - axis["name"] = axis["name"].replace(b"\x00", b"") + if axis["name"]: + axis["name"] = axis["name"].replace(b"\x00", b"") return axes - def set_variation_by_axes(self, axes): + def set_variation_by_axes(self, axes: list[float]) -> None: """ :param axes: A list of values for each axis. :exception OSError: If the font is not a variation font. @@ -718,14 +734,14 @@ class TransposedFont: return 0, 0, height, width return 0, 0, width, height - def getlength(self, text, *args, **kwargs): + def getlength(self, text: str | bytes, *args, **kwargs) -> float: if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): msg = "text length is undefined for text rotated by 90 or 270 degrees" raise ValueError(msg) return self.font.getlength(text, *args, **kwargs) -def load(filename): +def load(filename: str) -> ImageFont: """ Load a font file. This function loads a font object from the given bitmap font file, and returns the corresponding font object. @@ -739,7 +755,13 @@ def load(filename): return f -def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): +def truetype( + font: StrOrBytesPath | BinaryIO | None = None, + size: float = 10, + index: int = 0, + encoding: str = "", + layout_engine: Layout | None = None, +) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, and create a font object. @@ -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. If the file is not found in this filename, the loader may also - search in other directories, such as the :file:`fonts/` - directory on Windows or :file:`/Library/Fonts/`, - :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on - macOS. + search in other directories, such as: + + * The :file:`fonts/` directory on Windows, + * :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/` + and :file:`~/Library/Fonts/` on macOS. + * :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`, + and :file:`/usr/share/fonts` on Linux; or those specified by + the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables + for user-installed and system-wide fonts, respectively. :param size: The requested size, in pixels. :param index: Which font face to load (default is first available face). @@ -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. """ - def freetype(font): + def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: return FreeTypeFont(font, size, index, encoding, layout_engine) try: @@ -819,12 +846,21 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): if windir: dirs.append(os.path.join(windir, "fonts")) elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS") - if not lindirs: - # According to the freedesktop spec, XDG_DATA_DIRS should - # default to /usr/share - lindirs = "/usr/share" - dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")] + data_home = os.environ.get("XDG_DATA_HOME") + if not data_home: + # The freedesktop spec defines the following default directory for + # when XDG_DATA_HOME is unset or empty. This user-level directory + # takes precedence over system-level directories. + data_home = os.path.expanduser("~/.local/share") + xdg_dirs = [data_home] + + data_dirs = os.environ.get("XDG_DATA_DIRS") + if not data_dirs: + # Similarly, defaults are defined for the system-level directories + data_dirs = "/usr/local/share:/usr/share" + xdg_dirs += data_dirs.split(":") + + dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs] elif sys.platform == "darwin": dirs += [ "/Library/Fonts", @@ -850,7 +886,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): raise -def load_path(filename): +def load_path(filename: str | bytes) -> ImageFont: """ Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a bitmap font along the Python path. @@ -859,18 +895,153 @@ def load_path(filename): :return: A font object. :exception OSError: If the file could not be read. """ + if not isinstance(filename, str): + filename = filename.decode("utf-8") for directory in sys.path: - if is_directory(directory): - if not isinstance(filename, str): - filename = filename.decode("utf-8") - try: - return load(os.path.join(directory, filename)) - except OSError: - pass + try: + return load(os.path.join(directory, filename)) + except OSError: + pass msg = "cannot find font file" raise OSError(msg) +def load_default_imagefont() -> ImageFont: + f = ImageFont() + f._load_pilfont_data( + # courB08 + BytesIO( + base64.b64decode( + b""" +UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA +BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL +AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA +AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB +ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A +BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB +//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA +AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH +AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA +ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv +AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/ +/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5 +AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA +AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG +AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA +BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA +AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA +2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF +AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA//// ++gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA +////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA +BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv +AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA +AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA +AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA +BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP// +//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA +AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF +AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB +mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn +AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA +AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7 +AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA +Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB +//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA +AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ +AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC +DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ +AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/ ++wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5 +AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/ +///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG +AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA +BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA +Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC +eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG +AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA//// ++gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA +////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA +BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT +AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A +AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA +Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA +Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP// +//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA +AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ +AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA +LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5 +AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA +AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5 +AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA +AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG +AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA +EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK +AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA +pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG +AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// ++QAGAAIAzgAKANUAEw== +""" + ) + ), + Image.open( + BytesIO( + base64.b64decode( + b""" +iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u +Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 +M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g +LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F +IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA +Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791 +NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx +in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9 +SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY +AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt +y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG +ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY +lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H +/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3 +AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47 +c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/ +/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw +pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv +oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR +evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA +AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// +Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR +w7IkEbzhVQAAAABJRU5ErkJggg== +""" + ) + ) + ), + ) + return f + + def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """If FreeType support is available, load a version of Aileron Regular, https://dotcolon.net/font/aileron, with a more limited character set. @@ -885,8 +1056,8 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: :return: A font object. """ - if core.__class__.__name__ == "module" or size is not None: - f = truetype( + if isinstance(core, ModuleType) or size is not None: + return truetype( BytesIO( base64.b64decode( b""" @@ -1116,137 +1287,4 @@ AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ== 10 if size is None else size, layout_engine=Layout.BASIC, ) - else: - f = ImageFont() - f._load_pilfont_data( - # courB08 - BytesIO( - base64.b64decode( - b""" -UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA -BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL -AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA -AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB -ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A -BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB -//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA -AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH -AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA -ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv -AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/ -/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5 -AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA -AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG -AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA -BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA -AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA -2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF -AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA//// -+gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA -////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA -BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv -AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA -AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA -AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA -BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP// -//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA -AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF -AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB -mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn -AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA -AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7 -AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA -Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB -//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA -AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ -AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC -DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ -AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/ -+wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5 -AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/ -///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG -AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA -BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA -Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC -eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG -AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA//// -+gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA -////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA -BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT -AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A -AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA -Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA -Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP// -//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA -AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ -AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA -LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5 -AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA -AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5 -AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA -AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG -AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA -EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK -AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA -pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG -AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// -+QAGAAIAzgAKANUAEw== -""" - ) - ), - Image.open( - BytesIO( - base64.b64decode( - b""" -iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u -Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 -M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g -LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F -IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA -Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791 -NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx -in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9 -SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY -AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt -y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG -ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY -lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H -/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3 -AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47 -c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/ -/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw -pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv -oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR -evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA -AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// -Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR -w7IkEbzhVQAAAABJRU5ErkJggg== -""" - ) - ) - ), - ) - return f + return load_default_imagefont() diff --git a/.venv/Lib/site-packages/PIL/ImageGrab.py b/.venv/Lib/site-packages/PIL/ImageGrab.py index 3f3be70..e27ca7e 100644 --- a/.venv/Lib/site-packages/PIL/ImageGrab.py +++ b/.venv/Lib/site-packages/PIL/ImageGrab.py @@ -26,7 +26,13 @@ import tempfile from . import Image -def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None): +def grab( + bbox: tuple[int, int, int, int] | None = None, + include_layered_windows: bool = False, + all_screens: bool = False, + xdisplay: str | None = None, +) -> Image.Image: + im: Image.Image if xdisplay is None: if sys.platform == "darwin": fh, filepath = tempfile.mkstemp(".png") @@ -63,14 +69,16 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N left, top, right, bottom = bbox im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im + # Cast to Optional[str] needed for Windows and macOS. + display_name: str | None = xdisplay try: if not Image.core.HAVE_XCB: msg = "Pillow was built without XCB support" raise OSError(msg) - size, data = Image.core.grabscreen_x11(xdisplay) + size, data = Image.core.grabscreen_x11(display_name) except OSError: if ( - xdisplay is None + display_name is None and sys.platform not in ("darwin", "win32") and shutil.which("gnome-screenshot") ): @@ -94,7 +102,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N return im -def grabclipboard(): +def grabclipboard() -> Image.Image | list[str] | None: if sys.platform == "darwin": fh, filepath = tempfile.mkstemp(".png") os.close(fh) diff --git a/.venv/Lib/site-packages/PIL/ImageMath.py b/.venv/Lib/site-packages/PIL/ImageMath.py index 77472a2..6664434 100644 --- a/.venv/Lib/site-packages/PIL/ImageMath.py +++ b/.venv/Lib/site-packages/PIL/ImageMath.py @@ -61,7 +61,7 @@ class _Operand: out = Image.new(mode or im_1.mode, im_1.size, None) im_1.load() try: - op = getattr(_imagingmath, op + "_" + im_1.mode) + op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e @@ -89,7 +89,7 @@ class _Operand: im_1.load() im_2.load() try: - op = getattr(_imagingmath, op + "_" + im_1.mode) + op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e diff --git a/.venv/Lib/site-packages/PIL/ImageMode.py b/.venv/Lib/site-packages/PIL/ImageMode.py index 0b31f60..92a08d2 100644 --- a/.venv/Lib/site-packages/PIL/ImageMode.py +++ b/.venv/Lib/site-packages/PIL/ImageMode.py @@ -16,24 +16,19 @@ from __future__ import annotations import sys from functools import lru_cache +from typing import NamedTuple + +from ._deprecate import deprecate -class ModeDescriptor: +class ModeDescriptor(NamedTuple): """Wrapper for mode strings.""" - def __init__( - self, - mode: str, - bands: tuple[str, ...], - basemode: str, - basetype: str, - typestr: str, - ) -> None: - self.mode = mode - self.bands = bands - self.basemode = basemode - self.basetype = basetype - self.typestr = typestr + mode: str + bands: tuple[str, ...] + basemode: str + basetype: str + typestr: str def __str__(self) -> str: return self.mode @@ -42,7 +37,6 @@ class ModeDescriptor: @lru_cache def getmode(mode: str) -> ModeDescriptor: """Gets a mode descriptor for the given mode.""" - # initialize mode cache endian = "<" if sys.byteorder == "little" else ">" modes = { @@ -50,8 +44,8 @@ def getmode(mode: str) -> ModeDescriptor: # Bits need to be extended to bytes "1": ("L", "L", ("1",), "|b1"), "L": ("L", "L", ("L",), "|u1"), - "I": ("L", "I", ("I",), endian + "i4"), - "F": ("L", "F", ("F",), endian + "f4"), + "I": ("L", "I", ("I",), f"{endian}i4"), + "F": ("L", "F", ("F",), f"{endian}f4"), "P": ("P", "L", ("P",), "|u1"), "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), @@ -71,6 +65,8 @@ def getmode(mode: str) -> ModeDescriptor: "PA": ("RGB", "L", ("P", "A"), "|u1"), } 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] return ModeDescriptor(mode, bands, base_mode, base_type, type_str) @@ -82,8 +78,8 @@ def getmode(mode: str) -> ModeDescriptor: "I;16LS": "u2", "I;16BS": ">i2", - "I;16N": endian + "u2", - "I;16NS": endian + "i2", + "I;16N": f"{endian}u2", + "I;16NS": f"{endian}i2", "I;32": "u4", "I;32L": " tuple[int, Image.Image]: """Run a single morphological operation on an image Returns a tuple of the number of changed pixels and the @@ -216,7 +216,7 @@ class MorphOp: count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage - def match(self, image: Image.Image): + def match(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of coordinates matching the morphological operation on an image. @@ -231,7 +231,7 @@ class MorphOp: raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.im.id) - def get_on_pixels(self, image: Image.Image): + def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of all turned on pixels in a binary image Returns a list of tuples of (x,y) coordinates diff --git a/.venv/Lib/site-packages/PIL/ImageOps.py b/.venv/Lib/site-packages/PIL/ImageOps.py index 33db8fa..a84c083 100644 --- a/.venv/Lib/site-packages/PIL/ImageOps.py +++ b/.venv/Lib/site-packages/PIL/ImageOps.py @@ -497,7 +497,7 @@ def expand( color = _color(fill, image.mode) if image.palette: palette = ImagePalette.ImagePalette(palette=image.getpalette()) - if isinstance(color, tuple): + if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4): color = palette.getcolor(color) else: palette = None @@ -709,14 +709,18 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image exif_image.info["exif"] = exif.tobytes() elif "Raw profile type exif" in exif_image.info: exif_image.info["Raw profile type exif"] = exif.tobytes().hex() - elif "XML:com.adobe.xmp" in exif_image.info: - for pattern in ( - r'tiff:Orientation="([0-9])"', - r"([0-9])", - ): - exif_image.info["XML:com.adobe.xmp"] = re.sub( - pattern, "", exif_image.info["XML:com.adobe.xmp"] - ) + for key in ("XML:com.adobe.xmp", "xmp"): + if key in exif_image.info: + for pattern in ( + r'tiff:Orientation="([0-9])"', + r"([0-9])", + ): + value = exif_image.info[key] + exif_image.info[key] = ( + re.sub(pattern, "", value) + if isinstance(value, str) + else re.sub(pattern.encode(), b"", value) + ) if not in_place: return transposed_image elif not in_place: diff --git a/.venv/Lib/site-packages/PIL/ImagePalette.py b/.venv/Lib/site-packages/PIL/ImagePalette.py index 770d100..ed38285 100644 --- a/.venv/Lib/site-packages/PIL/ImagePalette.py +++ b/.venv/Lib/site-packages/PIL/ImagePalette.py @@ -18,10 +18,13 @@ from __future__ import annotations import array -from typing import Sequence +from typing import IO, TYPE_CHECKING, Sequence from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile +if TYPE_CHECKING: + from . import Image + class ImagePalette: """ @@ -35,23 +38,27 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: + def __init__( + self, + mode: str = "RGB", + palette: Sequence[int] | bytes | bytearray | None = None, + ) -> None: self.mode = mode - self.rawmode = None # if set, palette contains raw data + self.rawmode: str | None = None # if set, palette contains raw data self.palette = palette or bytearray() self.dirty: int | None = None @property - def palette(self): + def palette(self) -> Sequence[int] | bytes | bytearray: return self._palette @palette.setter - def palette(self, palette): - self._colors = None + def palette(self, palette: Sequence[int] | bytes | bytearray) -> None: + self._colors: dict[tuple[int, ...], int] | None = None self._palette = palette @property - def colors(self): + def colors(self) -> dict[tuple[int, ...], int]: if self._colors is None: mode_len = len(self.mode) self._colors = {} @@ -63,10 +70,10 @@ class ImagePalette: return self._colors @colors.setter - def colors(self, colors): + def colors(self, colors: dict[tuple[int, ...], int]) -> None: self._colors = colors - def copy(self): + def copy(self) -> ImagePalette: new = ImagePalette() new.mode = self.mode @@ -77,7 +84,7 @@ class ImagePalette: return new - def getdata(self): + def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]: """ Get palette contents in format suitable for the low-level ``im.putpalette`` primitive. @@ -88,7 +95,7 @@ class ImagePalette: return self.rawmode, self.palette return self.mode, self.tobytes() - def tobytes(self): + def tobytes(self) -> bytes: """Convert palette to bytes. .. warning:: This method is experimental. @@ -104,11 +111,13 @@ class ImagePalette: # Declare tostring as an alias for tobytes tostring = tobytes - def _new_color_index(self, image=None, e=None): + def _new_color_index( + self, image: Image.Image | None = None, e: Exception | None = None + ) -> int: if not isinstance(self.palette, bytearray): self._palette = bytearray(self.palette) index = len(self.palette) // 3 - special_colors = () + special_colors: tuple[int | tuple[int, ...] | None, ...] = () if image: special_colors = ( image.info.get("background"), @@ -128,7 +137,11 @@ class ImagePalette: raise ValueError(msg) from e return index - def getcolor(self, color, image=None) -> int: + def getcolor( + self, + color: tuple[int, ...], + image: Image.Image | None = None, + ) -> int: """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. @@ -151,22 +164,23 @@ class ImagePalette: except KeyError as e: # allocate new color slot index = self._new_color_index(image, e) + assert isinstance(self._palette, bytearray) self.colors[color] = index if index * 3 < len(self.palette): self._palette = ( - self.palette[: index * 3] + self._palette[: index * 3] + bytes(color) - + self.palette[index * 3 + 3 :] + + self._palette[index * 3 + 3 :] ) else: self._palette += bytes(color) self.dirty = 1 return index else: - msg = f"unknown color specifier: {repr(color)}" + msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable] raise ValueError(msg) - def save(self, fp): + def save(self, fp: str | IO[str]) -> None: """Save palette to text file. .. warning:: This method is experimental. @@ -193,7 +207,7 @@ class ImagePalette: # Internal -def raw(rawmode, data) -> ImagePalette: +def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data @@ -205,50 +219,57 @@ def raw(rawmode, data) -> ImagePalette: # Factories -def make_linear_lut(black, white): +def make_linear_lut(black: int, white: float) -> list[int]: if black == 0: - return [white * i // 255 for i in range(256)] + return [int(white * i // 255) for i in range(256)] msg = "unavailable when black is non-zero" raise NotImplementedError(msg) # FIXME -def make_gamma_lut(exp): +def make_gamma_lut(exp: float) -> list[int]: return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] -def negative(mode="RGB"): +def negative(mode: str = "RGB") -> ImagePalette: palette = list(range(256 * len(mode))) palette.reverse() return ImagePalette(mode, [i // len(mode) for i in palette]) -def random(mode="RGB"): +def random(mode: str = "RGB") -> ImagePalette: from random import randint palette = [randint(0, 255) for _ in range(256 * len(mode))] return ImagePalette(mode, palette) -def sepia(white="#fff0c0"): +def sepia(white: str = "#fff0c0") -> ImagePalette: bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) -def wedge(mode="RGB"): +def wedge(mode: str = "RGB") -> ImagePalette: palette = list(range(256 * len(mode))) return ImagePalette(mode, [i // len(mode) for i in palette]) -def load(filename): +def load(filename: str) -> tuple[bytes, str]: # FIXME: supports GIMP gradients only with open(filename, "rb") as fp: - for paletteHandler in [ + paletteHandlers: list[ + type[ + GimpPaletteFile.GimpPaletteFile + | GimpGradientFile.GimpGradientFile + | PaletteFile.PaletteFile + ] + ] = [ GimpPaletteFile.GimpPaletteFile, GimpGradientFile.GimpGradientFile, PaletteFile.PaletteFile, - ]: + ] + for paletteHandler in paletteHandlers: try: fp.seek(0) lut = paletteHandler(fp).getpalette() diff --git a/.venv/Lib/site-packages/PIL/ImageQt.py b/.venv/Lib/site-packages/PIL/ImageQt.py index 293ba49..35a3776 100644 --- a/.venv/Lib/site-packages/PIL/ImageQt.py +++ b/.venv/Lib/site-packages/PIL/ImageQt.py @@ -152,7 +152,7 @@ def _toqclass_helper(im): elif im.mode == "RGBA": data = im.tobytes("raw", "BGRA") format = qt_format.Format_ARGB32 - elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + elif im.mode == "I;16": im = im.point(lambda i: i * 256) format = qt_format.Format_Grayscale16 @@ -196,7 +196,7 @@ if qt_is_installed: self.setColorTable(im_data["colortable"]) -def toqimage(im): +def toqimage(im) -> ImageQt: return ImageQt(im) diff --git a/.venv/Lib/site-packages/PIL/ImageShow.py b/.venv/Lib/site-packages/PIL/ImageShow.py index 4e505f2..037d6f4 100644 --- a/.venv/Lib/site-packages/PIL/ImageShow.py +++ b/.venv/Lib/site-packages/PIL/ImageShow.py @@ -118,6 +118,8 @@ class Viewer: """ Display given file. """ + if not os.path.exists(path): + raise FileNotFoundError os.system(self.get_command(path, **options)) # nosec return 1 @@ -142,6 +144,8 @@ class WindowsViewer(Viewer): """ Display given file. """ + if not os.path.exists(path): + raise FileNotFoundError subprocess.Popen( self.get_command(path, **options), shell=True, @@ -171,6 +175,8 @@ class MacViewer(Viewer): """ Display given file. """ + if not os.path.exists(path): + raise FileNotFoundError subprocess.call(["open", "-a", "Preview.app", path]) executable = sys.executable or shutil.which("python3") if executable: @@ -199,7 +205,7 @@ class UnixViewer(Viewer): def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] - return f"({command} {quote(file)}" + return f"{command} {quote(file)}" class XDGViewer(UnixViewer): @@ -215,6 +221,8 @@ class XDGViewer(UnixViewer): """ Display given file. """ + if not os.path.exists(path): + raise FileNotFoundError subprocess.Popen(["xdg-open", path]) return 1 @@ -237,6 +245,8 @@ class DisplayViewer(UnixViewer): """ Display given file. """ + if not os.path.exists(path): + raise FileNotFoundError args = ["display"] title = options.get("title") if title: @@ -259,6 +269,8 @@ class GmDisplayViewer(UnixViewer): """ Display given file. """ + if not os.path.exists(path): + raise FileNotFoundError subprocess.Popen(["gm", "display", path]) return 1 @@ -275,6 +287,8 @@ class EogViewer(UnixViewer): """ Display given file. """ + if not os.path.exists(path): + raise FileNotFoundError subprocess.Popen(["eog", "-n", path]) return 1 @@ -299,6 +313,8 @@ class XVViewer(UnixViewer): """ Display given file. """ + if not os.path.exists(path): + raise FileNotFoundError args = ["xv"] title = options.get("title") if title: diff --git a/.venv/Lib/site-packages/PIL/ImageStat.py b/.venv/Lib/site-packages/PIL/ImageStat.py index 13864e5..8bc5045 100644 --- a/.venv/Lib/site-packages/PIL/ImageStat.py +++ b/.venv/Lib/site-packages/PIL/ImageStat.py @@ -23,35 +23,58 @@ from __future__ import annotations import math +from functools import cached_property + +from . import Image class Stat: - def __init__(self, image_or_list, mask=None): - try: - if mask: - self.h = image_or_list.histogram(mask) - else: - self.h = image_or_list.histogram() - 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" + def __init__( + self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None + ) -> 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) + elif isinstance(image_or_list, list): + self.h = image_or_list + else: + msg = "first argument must be image or list" # type: ignore[unreachable] raise TypeError(msg) self.bands = list(range(len(self.h) // 256)) - def __getattr__(self, id): - """Calculate missing attribute""" - if id[:4] == "_get": - raise AttributeError(id) - # calculate missing attribute - v = getattr(self, "_get" + id)() - setattr(self, id, v) - return v + @cached_property + def extrema(self) -> list[tuple[int, int]]: + """ + Min/max values for each band in the image. - def _getextrema(self): - """Get min/max values for each band in the image""" + .. note:: + 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 for i in range(256): if histogram[i]: @@ -65,12 +88,14 @@ class Stat: return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)] - def _getcount(self): - """Get total number of pixels in each layer""" + @cached_property + 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)] - def _getsum(self): - """Get sum of all pixels in each layer""" + @cached_property + def sum(self) -> list[float]: + """Sum of all pixels for each band in the image.""" v = [] for i in range(0, len(self.h), 256): @@ -80,8 +105,9 @@ class Stat: v.append(layer_sum) return v - def _getsum2(self): - """Get squared sum of all pixels in each layer""" + @cached_property + def sum2(self) -> list[float]: + """Squared sum of all pixels for each band in the image.""" v = [] for i in range(0, len(self.h), 256): @@ -91,12 +117,14 @@ class Stat: v.append(sum2) return v - def _getmean(self): - """Get average pixel level for each layer""" + @cached_property + 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] - def _getmedian(self): - """Get median pixel level for each layer""" + @cached_property + def median(self) -> list[int]: + """Median pixel level for each band in the image.""" v = [] for i in self.bands: @@ -110,19 +138,22 @@ class Stat: v.append(j) return v - def _getrms(self): - """Get RMS for each layer""" + @cached_property + 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] - def _getvar(self): - """Get variance for each layer""" + @cached_property + def var(self) -> list[float]: + """Variance for each band in the image.""" return [ (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] for i in self.bands ] - def _getstddev(self): - """Get standard deviation for each layer""" + @cached_property + def stddev(self) -> list[float]: + """Standard deviation for each band in the image.""" return [math.sqrt(self.var[i]) for i in self.bands] diff --git a/.venv/Lib/site-packages/PIL/ImageTk.py b/.venv/Lib/site-packages/PIL/ImageTk.py index 10b2cc6..90defdb 100644 --- a/.venv/Lib/site-packages/PIL/ImageTk.py +++ b/.venv/Lib/site-packages/PIL/ImageTk.py @@ -37,7 +37,7 @@ from . import Image _pilbitmap_ok = None -def _pilbitmap_check(): +def _pilbitmap_check() -> int: global _pilbitmap_ok if _pilbitmap_ok is None: try: @@ -128,7 +128,7 @@ class PhotoImage: if image: self.paste(image) - def __del__(self): + def __del__(self) -> None: name = self.__photo.name self.__photo.name = None try: @@ -136,7 +136,7 @@ class PhotoImage: except Exception: pass # ignore internal errors - def __str__(self): + def __str__(self) -> str: """ Get the Tkinter photo image identifier. This method is automatically called by Tkinter whenever a PhotoImage object is passed to a Tkinter @@ -146,7 +146,7 @@ class PhotoImage: """ return str(self.__photo) - def width(self): + def width(self) -> int: """ Get the width of the image. @@ -154,7 +154,7 @@ class PhotoImage: """ return self.__size[0] - def height(self): + def height(self) -> int: """ Get the height of the image. @@ -162,7 +162,7 @@ class PhotoImage: """ return self.__size[1] - def paste(self, im): + def paste(self, im: Image.Image) -> None: """ Paste a PIL image into the photo image. Note that this can be very slow if the photo image is displayed. @@ -219,7 +219,7 @@ class BitmapImage: kw["data"] = image.tobitmap() self.__photo = tkinter.BitmapImage(**kw) - def __del__(self): + def __del__(self) -> None: name = self.__photo.name self.__photo.name = None try: @@ -227,7 +227,7 @@ class BitmapImage: except Exception: pass # ignore internal errors - def width(self): + def width(self) -> int: """ Get the width of the image. @@ -235,7 +235,7 @@ class BitmapImage: """ return self.__size[0] - def height(self): + def height(self) -> int: """ Get the height of the image. @@ -243,7 +243,7 @@ class BitmapImage: """ return self.__size[1] - def __str__(self): + def __str__(self) -> str: """ Get the Tkinter bitmap image identifier. This method is automatically called by Tkinter whenever a BitmapImage object is passed to a Tkinter @@ -254,7 +254,7 @@ class BitmapImage: return str(self.__photo) -def getimage(photo): +def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) block = im.im diff --git a/.venv/Lib/site-packages/PIL/ImageTransform.py b/.venv/Lib/site-packages/PIL/ImageTransform.py index 6aa82da..ffd7916 100644 --- a/.venv/Lib/site-packages/PIL/ImageTransform.py +++ b/.venv/Lib/site-packages/PIL/ImageTransform.py @@ -14,7 +14,7 @@ # from __future__ import annotations -from typing import Sequence +from typing import Any, Sequence from . import Image @@ -24,7 +24,7 @@ class Transform(Image.ImageTransformHandler): method: Image.Transform - def __init__(self, data: Sequence[int]) -> None: + def __init__(self, data: Sequence[Any]) -> None: self.data = data def getdata(self) -> tuple[Image.Transform, Sequence[int]]: @@ -34,7 +34,7 @@ class Transform(Image.ImageTransformHandler): self, size: tuple[int, int], image: Image.Image, - **options: dict[str, str | int | tuple[int, ...] | list[int]], + **options: Any, ) -> Image.Image: """Perform the transform. Called from :py:meth:`.Image.transform`.""" # can be overridden diff --git a/.venv/Lib/site-packages/PIL/ImageWin.py b/.venv/Lib/site-packages/PIL/ImageWin.py index 75910d2..978c5a9 100644 --- a/.venv/Lib/site-packages/PIL/ImageWin.py +++ b/.venv/Lib/site-packages/PIL/ImageWin.py @@ -28,10 +28,10 @@ class HDC: methods. """ - def __init__(self, dc): + def __init__(self, dc: int) -> None: self.dc = dc - def __int__(self): + def __int__(self) -> int: return self.dc @@ -42,10 +42,10 @@ class HWND: methods, instead of a DC. """ - def __init__(self, wnd): + def __init__(self, wnd: int) -> None: self.wnd = wnd - def __int__(self): + def __int__(self) -> int: return self.wnd @@ -69,19 +69,22 @@ class Dib: defines the size of the image. """ - def __init__(self, image, size=None): - if hasattr(image, "mode") and hasattr(image, "size"): + def __init__( + self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None + ) -> None: + if isinstance(image, str): + mode = image + image = "" + else: mode = image.mode size = image.size - else: - mode = image - image = None if mode not in ["1", "L", "P", "RGB"]: mode = Image.getmodebase(mode) self.image = Image.core.display(mode, size) self.mode = mode self.size = size if image: + assert not isinstance(image, str) self.paste(image) def expose(self, handle): @@ -149,7 +152,9 @@ class Dib: result = self.image.query_palette(handle) return result - def paste(self, im, box=None): + def paste( + self, im: Image.Image, box: tuple[int, int, int, int] | None = None + ) -> None: """ Paste a PIL image into the bitmap image. @@ -169,16 +174,16 @@ class Dib: else: self.image.paste(im.im) - def frombytes(self, buffer): + def frombytes(self, buffer: bytes) -> None: """ Load display memory contents from byte data. :param buffer: A buffer containing display data (usually data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) """ - return self.image.frombytes(buffer) + self.image.frombytes(buffer) - def tobytes(self): + def tobytes(self) -> bytes: """ Copy display memory contents to bytes object. @@ -190,13 +195,15 @@ class Dib: class Window: """Create a Window with the given title size.""" - def __init__(self, title="PIL", width=None, height=None): + def __init__( + self, title: str = "PIL", width: int | None = None, height: int | None = None + ) -> None: self.hwnd = Image.core.createwindow( title, self.__dispatcher, width or 0, height or 0 ) 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): pass @@ -204,7 +211,7 @@ class Window: def ui_handle_damage(self, x0, y0, x1, y1): pass - def ui_handle_destroy(self): + def ui_handle_destroy(self) -> None: pass def ui_handle_repair(self, dc, x0, y0, x1, y1): @@ -213,7 +220,7 @@ class Window: def ui_handle_resize(self, width, height): pass - def mainloop(self): + def mainloop(self) -> None: Image.core.eventloop() diff --git a/.venv/Lib/site-packages/PIL/IptcImagePlugin.py b/.venv/Lib/site-packages/PIL/IptcImagePlugin.py index 4096094..73df83b 100644 --- a/.venv/Lib/site-packages/PIL/IptcImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/IptcImagePlugin.py @@ -57,7 +57,7 @@ def dump(c: Sequence[int | bytes]) -> None: """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.dump", 12) for i in c: - print("%02x" % _i8(i), end=" ") + print(f"{_i8(i):02x}", end=" ") print() diff --git a/.venv/Lib/site-packages/PIL/Jpeg2KImagePlugin.py b/.venv/Lib/site-packages/PIL/Jpeg2KImagePlugin.py index be000c3..e50cd77 100644 --- a/.venv/Lib/site-packages/PIL/Jpeg2KImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/Jpeg2KImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import io import os import struct +from typing import IO, Tuple, cast from . import Image, ImageFile, ImagePalette, _binary @@ -34,7 +35,7 @@ class BoxReader: self.length = length self.remaining_in_box = -1 - def _can_read(self, num_bytes): + def _can_read(self, num_bytes: int) -> bool: if self.has_length and self.fp.tell() + num_bytes > self.length: # Outside box: ensure we don't read past the known file length return False @@ -44,7 +45,7 @@ class BoxReader: else: return True # No length known, just read - def _read_bytes(self, num_bytes): + def _read_bytes(self, num_bytes: int) -> bytes: if not self._can_read(num_bytes): msg = "Not enough data in header" raise SyntaxError(msg) @@ -58,32 +59,32 @@ class BoxReader: self.remaining_in_box -= num_bytes return data - def read_fields(self, field_format): + def read_fields(self, field_format: str) -> tuple[int | bytes, ...]: size = struct.calcsize(field_format) data = self._read_bytes(size) return struct.unpack(field_format, data) - def read_boxes(self): + def read_boxes(self) -> BoxReader: size = self.remaining_in_box data = self._read_bytes(size) return BoxReader(io.BytesIO(data), size) - def has_next_box(self): + def has_next_box(self) -> bool: if self.has_length: return self.fp.tell() + self.remaining_in_box < self.length else: return True - def next_box_type(self): + def next_box_type(self) -> bytes: # Skip the rest of the box if it has not been read if self.remaining_in_box > 0: self.fp.seek(self.remaining_in_box, os.SEEK_CUR) self.remaining_in_box = -1 # Read the length and type of the next box - lbox, tbox = self.read_fields(">I4s") + lbox, tbox = cast(Tuple[int, bytes], self.read_fields(">I4s")) if lbox == 1: - lbox = self.read_fields(">Q")[0] + lbox = cast(int, self.read_fields(">Q")[0]) hlen = 16 else: hlen = 8 @@ -96,7 +97,7 @@ class BoxReader: return tbox -def _parse_codestream(fp): +def _parse_codestream(fp) -> tuple[tuple[int, int], str]: """Parse the JPEG 2000 codestream to extract the size and component count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" @@ -121,17 +122,19 @@ def _parse_codestream(fp): elif csiz == 4: mode = "RGBA" else: - mode = None + msg = "unable to determine J2K image mode" + raise SyntaxError(msg) return size, mode -def _res_to_dpi(num, denom, exp): +def _res_to_dpi(num: int, denom: int, exp: int) -> float | None: """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, calculated as (num / denom) * 10^exp and stored in dots per meter, to floating-point dots per inch.""" - if denom != 0: - return (254 * num * (10**exp)) / (10000 * denom) + if denom == 0: + return None + return (254 * num * (10**exp)) / (10000 * denom) def _parse_jp2_header(fp): @@ -176,6 +179,10 @@ def _parse_jp2_header(fp): mode = "RGB" elif nc == 4: 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"): ne, npc = header.read_fields(">HB") bitdepths = header.read_fields(">" + ("B" * npc)) @@ -211,7 +218,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): format = "JPEG2000" format_description = "JPEG 2000 (ISO 15444)" - def _open(self): + def _open(self) -> None: sig = self.fp.read(4) if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" @@ -231,10 +238,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile): msg = "not a JPEG 2000 file" raise SyntaxError(msg) - if self.size is None or self.mode is None: - msg = "unable to determine size/mode" - raise SyntaxError(msg) - self._reduce = 0 self.layers = 0 @@ -263,7 +266,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): ) ] - def _parse_comment(self): + def _parse_comment(self) -> None: hdr = self.fp.read(2) length = _binary.i16be(hdr) self.fp.seek(length - 2, os.SEEK_CUR) @@ -313,7 +316,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): return ImageFile.ImageFile.load(self) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return ( prefix[:4] == b"\xff\x4f\xff\x51" or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" @@ -324,11 +327,13 @@ def _accept(prefix): # Save support -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Get the keyword arguments info = im.encoderinfo - if filename.endswith(".j2k") or info.get("no_jp2", False): + if isinstance(filename, str): + filename = filename.encode() + if filename.endswith(b".j2k") or info.get("no_jp2", False): kind = "j2k" else: kind = "jp2" diff --git a/.venv/Lib/site-packages/PIL/JpegImagePlugin.py b/.venv/Lib/site-packages/PIL/JpegImagePlugin.py index 81b8749..b15bf06 100644 --- a/.venv/Lib/site-packages/PIL/JpegImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/JpegImagePlugin.py @@ -42,6 +42,7 @@ import subprocess import sys import tempfile import warnings +from typing import IO, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -54,7 +55,7 @@ from .JpegPresets import presets # Parser -def Skip(self, marker): +def Skip(self: JpegImageFile, marker: int) -> None: n = i16(self.fp.read(2)) - 2 ImageFile._safe_read(self.fp, n) @@ -94,6 +95,8 @@ def APP(self, marker): else: self.info["exif"] = s self._exif_offset = self.fp.tell() - n + 6 + elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00": + self.info["xmp"] = s.split(b"\x00", 1)[1] elif marker == 0xFFE2 and s[:5] == b"FPXR\0": # extract FlashPix information (incomplete) self.info["flashpix"] = s # FIXME: value will change @@ -158,40 +161,8 @@ def APP(self, marker): # plus constant header size self.info["mpoffset"] = self.fp.tell() - n + 4 - # If DPI isn't in JPEG header, fetch from EXIF - if "dpi" not in self.info and "exif" in self.info: - try: - exif = self.getexif() - resolution_unit = exif[0x0128] - x_resolution = exif[0x011A] - try: - dpi = float(x_resolution[0]) / x_resolution[1] - except TypeError: - dpi = x_resolution - if math.isnan(dpi): - msg = "DPI is not a number" - raise ValueError(msg) - if resolution_unit == 3: # cm - # 1 dpcm = 2.54 dpi - dpi *= 2.54 - self.info["dpi"] = dpi, dpi - except ( - struct.error, - KeyError, - SyntaxError, - TypeError, - ValueError, - ZeroDivisionError, - ): - # struct.error for truncated EXIF - # KeyError for dpi not included - # SyntaxError for invalid/unreadable EXIF - # ValueError or TypeError for dpi being an invalid float - # ZeroDivisionError for invalid dpi rational value - self.info["dpi"] = 72, 72 - -def COM(self, marker): +def COM(self: JpegImageFile, marker: int) -> None: # # Comment marker. Store these in the APP dictionary. n = i16(self.fp.read(2)) - 2 @@ -202,7 +173,7 @@ def COM(self, marker): self.applist.append(("COM", s)) -def SOF(self, marker): +def SOF(self: JpegImageFile, marker: int) -> None: # # Start of frame marker. Defines the size and mode of the # image. JPEG is colour blind, so we use some simple @@ -250,7 +221,7 @@ def SOF(self, marker): self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) -def DQT(self, marker): +def DQT(self: JpegImageFile, marker: int) -> None: # # Define quantization table. Note that there might be more # than one table in each marker. @@ -344,7 +315,7 @@ MARKER = { } -def _accept(prefix): +def _accept(prefix: bytes) -> bool: # Magic number was taken from https://en.wikipedia.org/wiki/JPEG return prefix[:3] == b"\xFF\xD8\xFF" @@ -408,7 +379,9 @@ class JpegImageFile(ImageFile.ImageFile): msg = "no marker found" 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 For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker @@ -424,13 +397,15 @@ class JpegImageFile(ImageFile.ImageFile): 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: - return + return None # Protect from second call if self.decoderconfig: - return + return None d, e, o, a = self.tile[0] scale = 1 @@ -460,7 +435,7 @@ class JpegImageFile(ImageFile.ImageFile): box = (0, 0, original_size[0] / scale, original_size[1] / scale) return self.mode, box - def load_djpeg(self): + def load_djpeg(self) -> None: # ALTERNATIVE: handle JPEGs via the IJG command line utilities f, path = tempfile.mkstemp() @@ -491,29 +466,43 @@ class JpegImageFile(ImageFile.ImageFile): self.tile = [] - def _getexif(self): + def _getexif(self) -> dict[str, Any] | None: 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): return _getmp(self) - def getxmp(self): - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - :returns: XMP tags in a dictionary. - """ - - for segment, content in self.applist: - if segment == "APP1": - marker, xmp_tags = content.split(b"\x00")[:2] - if marker == b"http://ns.adobe.com/xap/1.0/": - return self._getxmp(xmp_tags) - return {} - - -def _getexif(self): +def _getexif(self) -> dict[str, Any] | None: if "exif" not in self.info: return None return self.getexif()._get_merged_dict() @@ -641,7 +630,7 @@ def get_sampling(im): return samplings.get(sampling, -1) -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.width == 0 or im.height == 0: msg = "cannot write empty image as JPEG" raise ValueError(msg) @@ -824,7 +813,7 @@ def _save(im, fp, filename): ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) -def _save_cjpeg(im, fp, filename): +def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # ALTERNATIVE: handle JPEGs via the IJG command line utilities. tempfile = im._dump() subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) @@ -841,6 +830,10 @@ def jpeg_factory(fp=None, filename=None): try: mpheader = im._getmp() if mpheader[45057] > 1: + for segment, content in im.applist: + if segment == "APP1" and b' hdrgm:Version="' in content: + # Ultra HDR images are not yet supported + return im # It's actually an MPO from .MpoImagePlugin import MpoImageFile diff --git a/.venv/Lib/site-packages/PIL/MicImagePlugin.py b/.venv/Lib/site-packages/PIL/MicImagePlugin.py index f4529d9..0723988 100644 --- a/.venv/Lib/site-packages/PIL/MicImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/MicImagePlugin.py @@ -25,7 +25,7 @@ from . import Image, TiffImagePlugin # -------------------------------------------------------------------- -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:8] == olefile.MAGIC @@ -38,7 +38,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): format_description = "Microsoft Image Composer" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: # read the OLE directory and see if this is a likely # to be a Microsoft Image Composer file @@ -63,7 +63,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): msg = "not an MIC file; no image entries" raise SyntaxError(msg) - self.frame = None + self.frame = -1 self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 @@ -85,15 +85,15 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self.frame = frame - def tell(self): + def tell(self) -> int: return self.frame - def close(self): + def close(self) -> None: self.__fp.close() self.ole.close() super().close() - def __exit__(self, *args): + def __exit__(self, *args: object) -> None: self.__fp.close() self.ole.close() super().__exit__() diff --git a/.venv/Lib/site-packages/PIL/MpegImagePlugin.py b/.venv/Lib/site-packages/PIL/MpegImagePlugin.py index 1565612..ad4d3e9 100644 --- a/.venv/Lib/site-packages/PIL/MpegImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/MpegImagePlugin.py @@ -53,6 +53,10 @@ class BitStream: 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, # but it cannot read it. @@ -77,7 +81,7 @@ class MpegImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Registry stuff -Image.register_open(MpegImageFile.format, MpegImageFile) +Image.register_open(MpegImageFile.format, MpegImageFile, _accept) Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"]) diff --git a/.venv/Lib/site-packages/PIL/MpoImagePlugin.py b/.venv/Lib/site-packages/PIL/MpoImagePlugin.py index ac9820b..f215706 100644 --- a/.venv/Lib/site-packages/PIL/MpoImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/MpoImagePlugin.py @@ -22,6 +22,7 @@ from __future__ import annotations import itertools import os import struct +from typing import IO from . import ( Image, @@ -32,23 +33,18 @@ from . import ( from ._binary import o32le -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: JpegImagePlugin._save(im, fp, filename) -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: append_images = im.encoderinfo.get("append_images", []) - if not append_images: - try: - animated = im.is_animated - except AttributeError: - animated = False - if not animated: - _save(im, fp, filename) - return + if not append_images and not getattr(im, "is_animated", False): + _save(im, fp, filename) + return mpf_offset = 28 - offsets = [] + offsets: list[int] = [] for imSequence in itertools.chain([im], append_images): for im_frame in ImageSequence.Iterator(imSequence): if not offsets: @@ -100,7 +96,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): format_description = "MPO (CIPA DC-007)" _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 JpegImagePlugin.JpegImageFile._open(self) self._after_jpeg_open() @@ -124,10 +120,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): # for now we can only handle reading and individual frame extraction self.readonly = 1 - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: self._fp.seek(pos) - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return 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.__frame = frame - def tell(self): + def tell(self) -> int: return self.__frame @staticmethod diff --git a/.venv/Lib/site-packages/PIL/MspImagePlugin.py b/.venv/Lib/site-packages/PIL/MspImagePlugin.py index 65cc706..0a75c86 100644 --- a/.venv/Lib/site-packages/PIL/MspImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/MspImagePlugin.py @@ -164,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder) # write MSP files (uncompressed only) -def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/.venv/Lib/site-packages/PIL/PSDraw.py b/.venv/Lib/site-packages/PIL/PSDraw.py index 848fc2f..673eae1 100644 --- a/.venv/Lib/site-packages/PIL/PSDraw.py +++ b/.venv/Lib/site-packages/PIL/PSDraw.py @@ -17,6 +17,7 @@ from __future__ import annotations import sys +from typing import TYPE_CHECKING from . import EpsImagePlugin @@ -38,7 +39,7 @@ class PSDraw: fp = sys.stdout self.fp = fp - def begin_document(self, id=None): + def begin_document(self, id: str | None = None) -> None: """Set up printing of a document. (Write PostScript DSC header.)""" # FIXME: incomplete self.fp.write( @@ -52,30 +53,32 @@ class PSDraw: self.fp.write(EDROFF_PS) self.fp.write(VDI_PS) self.fp.write(b"%%EndProlog\n") - self.isofont = {} + self.isofont: dict[bytes, int] = {} - def end_document(self): + def end_document(self) -> None: """Ends printing. (Write PostScript DSC footer.)""" self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") if hasattr(self.fp, "flush"): self.fp.flush() - def setfont(self, font, size): + def setfont(self, font: str, size: int) -> None: """ Selects which font to use. :param font: A PostScript font name :param size: Size in points. """ - font = bytes(font, "UTF-8") - if font not in self.isofont: + font_bytes = bytes(font, "UTF-8") + if font_bytes not in self.isofont: # reencode font - self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font)) - self.isofont[font] = 1 + self.fp.write( + b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes) + ) + self.isofont[font_bytes] = 1 # rough - self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font)) + self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes)) - def line(self, xy0, xy1): + def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None: """ Draws a line between the two points. Coordinates are given in PostScript point coordinates (72 points per inch, (0, 0) is the lower @@ -83,7 +86,7 @@ class PSDraw: """ self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) - def rectangle(self, box): + def rectangle(self, box: tuple[int, int, int, int]) -> None: """ Draws a rectangle. @@ -92,18 +95,22 @@ class PSDraw: """ self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) - def text(self, xy, text): + def text(self, xy: tuple[int, int], text: str) -> None: """ Draws text at the given position. You must use :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. """ - text = bytes(text, "UTF-8") - text = b"\\(".join(text.split(b"(")) - text = b"\\)".join(text.split(b")")) - xy += (text,) - self.fp.write(b"%d %d M (%s) S\n" % xy) + text_bytes = bytes(text, "UTF-8") + text_bytes = b"\\(".join(text_bytes.split(b"(")) + text_bytes = b"\\)".join(text_bytes.split(b")")) + self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) - def image(self, box, im, dpi=None): + if TYPE_CHECKING: + from . import Image + + def image( + self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None + ) -> None: """Draw a PIL image, centered in the given box.""" # default resolution depends on mode if not dpi: @@ -131,7 +138,7 @@ class PSDraw: sx = x / im.size[0] sy = y / im.size[1] self.fp.write(b"%f %f scale\n" % (sx, sy)) - EpsImagePlugin._save(im, self.fp, None, 0) + EpsImagePlugin._save(im, self.fp, "", 0) self.fp.write(b"\ngrestore\n") diff --git a/.venv/Lib/site-packages/PIL/PaletteFile.py b/.venv/Lib/site-packages/PIL/PaletteFile.py index dc31754..81652e5 100644 --- a/.venv/Lib/site-packages/PIL/PaletteFile.py +++ b/.venv/Lib/site-packages/PIL/PaletteFile.py @@ -14,6 +14,8 @@ # from __future__ import annotations +from typing import IO + from ._binary import o8 @@ -22,8 +24,8 @@ class PaletteFile: rawmode = "RGB" - def __init__(self, fp): - self.palette = [(i, i, i) for i in range(256)] + def __init__(self, fp: IO[bytes]) -> None: + palette = [o8(i) * 3 for i in range(256)] while True: s = fp.readline() @@ -44,9 +46,9 @@ class PaletteFile: g = b = r if 0 <= i <= 255: - self.palette[i] = o8(r) + o8(g) + o8(b) + palette[i] = o8(r) + o8(g) + o8(b) - self.palette = b"".join(self.palette) + self.palette = b"".join(palette) - def getpalette(self): + def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode diff --git a/.venv/Lib/site-packages/PIL/PalmImagePlugin.py b/.venv/Lib/site-packages/PIL/PalmImagePlugin.py index 65be7fe..1735070 100644 --- a/.venv/Lib/site-packages/PIL/PalmImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/PalmImagePlugin.py @@ -8,6 +8,8 @@ ## from __future__ import annotations +from typing import IO + from . import Image, ImageFile from ._binary import o8 from ._binary import o16be as o16b @@ -82,10 +84,10 @@ _Palm8BitColormapValues = ( # so build a prototype image to be used for palette resampling -def build_prototype_image(): +def build_prototype_image() -> Image.Image: image = Image.new("L", (1, len(_Palm8BitColormapValues))) image.putdata(list(range(len(_Palm8BitColormapValues)))) - palettedata = () + palettedata: tuple[int, ...] = () for colormapValue in _Palm8BitColormapValues: palettedata += colormapValue palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) @@ -112,7 +114,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} # (Internal) Image save plugin for the Palm format. -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode == "P": # we assume this is a color Palm image with the standard colormap, # unless the "info" dict has a "custom-colormap" field @@ -127,22 +129,23 @@ def _save(im, fp, filename): # and invert it because # Palm does grayscale from white (0) to black (1) bpp = im.encoderinfo["bpp"] - im = im.point( - lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift) - ) + maxval = (1 << bpp) - 1 + shift = 8 - bpp + im = im.point(lambda x: maxval - (x >> shift)) elif im.info.get("bpp") in (1, 2, 4): # here we assume that even though the inherent mode is 8-bit grayscale, # only the lower bpp bits are significant. # We invert them to match the Palm. bpp = im.info["bpp"] - im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) + maxval = (1 << bpp) - 1 + im = im.point(lambda x: maxval - (x & maxval)) else: msg = f"cannot write mode {im.mode} as Palm" raise OSError(msg) # we ignore the palette here - im.mode = "P" - rawmode = "P;" + str(bpp) + im._mode = "P" + rawmode = f"P;{bpp}" version = 1 elif im.mode == "1": diff --git a/.venv/Lib/site-packages/PIL/PcxImagePlugin.py b/.venv/Lib/site-packages/PIL/PcxImagePlugin.py index 026bfd9..dd42003 100644 --- a/.venv/Lib/site-packages/PIL/PcxImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/PcxImagePlugin.py @@ -144,7 +144,7 @@ SAVE = { } -def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/.venv/Lib/site-packages/PIL/PdfImagePlugin.py b/.venv/Lib/site-packages/PIL/PdfImagePlugin.py index 1777f1f..f0da1e0 100644 --- a/.venv/Lib/site-packages/PIL/PdfImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/PdfImagePlugin.py @@ -25,6 +25,7 @@ import io import math import os import time +from typing import IO from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features @@ -39,7 +40,7 @@ from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features # 5. page contents -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: _save(im, fp, filename, save_all=True) diff --git a/.venv/Lib/site-packages/PIL/PdfParser.py b/.venv/Lib/site-packages/PIL/PdfParser.py index 2542d4e..9e22313 100644 --- a/.venv/Lib/site-packages/PIL/PdfParser.py +++ b/.venv/Lib/site-packages/PIL/PdfParser.py @@ -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 # on page 656 -def encode_text(s): +def encode_text(s: str) -> bytes: return codecs.BOM_UTF16_BE + s.encode("utf_16_be") @@ -76,7 +76,7 @@ class PdfFormatError(RuntimeError): pass -def check_format_condition(condition, error_message): +def check_format_condition(condition: bool, error_message: str) -> None: if not condition: raise PdfFormatError(error_message) @@ -87,28 +87,27 @@ class IndirectReferenceTuple(NamedTuple): class IndirectReference(IndirectReferenceTuple): - def __str__(self): + def __str__(self) -> str: return f"{self.object_id} {self.generation} R" - def __bytes__(self): + def __bytes__(self) -> bytes: return self.__str__().encode("us-ascii") - def __eq__(self, other): - return ( - other.__class__ is self.__class__ - and other.object_id == self.object_id - and other.generation == self.generation - ) + def __eq__(self, other: object) -> bool: + if self.__class__ is not other.__class__: + return False + assert isinstance(other, IndirectReference) + return other.object_id == self.object_id and other.generation == self.generation def __ne__(self, other): return not (self == other) - def __hash__(self): + def __hash__(self) -> int: return hash((self.object_id, self.generation)) class IndirectObjectDef(IndirectReference): - def __str__(self): + def __str__(self) -> str: return f"{self.object_id} {self.generation} obj" @@ -144,15 +143,13 @@ class XrefTable: elif key in self.deleted_entries: generation = self.deleted_entries[key] else: - msg = ( - "object ID " + str(key) + " cannot be deleted because it doesn't exist" - ) + msg = f"object ID {key} cannot be deleted because it doesn't exist" raise IndexError(msg) def __contains__(self, key): return key in self.existing_entries or key in self.new_entries - def __len__(self): + def __len__(self) -> int: return len( set(self.existing_entries.keys()) | set(self.new_entries.keys()) @@ -213,7 +210,7 @@ class PdfName: else: self.name = name.encode("us-ascii") - def name_as_str(self): + def name_as_str(self) -> str: return self.name.decode("us-ascii") def __eq__(self, other): @@ -221,11 +218,11 @@ class PdfName: isinstance(other, PdfName) and other.name == self.name ) or other == self.name - def __hash__(self): + def __hash__(self) -> int: return hash(self.name) - def __repr__(self): - return f"PdfName({repr(self.name)})" + def __repr__(self) -> str: + return f"{self.__class__.__name__}({repr(self.name)})" @classmethod def from_pdf_stream(cls, data): @@ -233,7 +230,7 @@ class PdfName: allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} - def __bytes__(self): + def __bytes__(self) -> bytes: result = bytearray(b"/") for b in self.name: if b in self.allowed_chars: @@ -244,7 +241,7 @@ class PdfName: class PdfArray(List[Any]): - def __bytes__(self): + def __bytes__(self) -> bytes: 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) return value - def __bytes__(self): + def __bytes__(self) -> bytes: out = bytearray(b"<<") for key, value in self.items(): if value is None: @@ -306,7 +303,7 @@ class PdfBinary: def __init__(self, data): self.data = data - def __bytes__(self): + def __bytes__(self) -> bytes: return b"<%s>" % b"".join(b"%02X" % b for b in self.data) @@ -404,41 +401,40 @@ class PdfParser: if f: self.seek_end() - def __enter__(self): + def __enter__(self) -> PdfParser: return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, *args: object) -> None: self.close() - return False # do not suppress exceptions - def start_writing(self): + def start_writing(self) -> None: self.close_buf() self.seek_end() - def close_buf(self): + def close_buf(self) -> None: try: self.buf.close() except AttributeError: pass self.buf = None - def close(self): + def close(self) -> None: if self.should_close_buf: self.close_buf() if self.f is not None and self.should_close_file: self.f.close() self.f = None - def seek_end(self): + def seek_end(self) -> None: self.f.seek(0, os.SEEK_END) - def write_header(self): + def write_header(self) -> None: self.f.write(b"%PDF-1.4\n") def write_comment(self, s): self.f.write(f"% {s}\n".encode()) - def write_catalog(self): + def write_catalog(self) -> IndirectReference: self.del_root() self.root_ref = self.next_object_id(self.f.tell()) self.pages_ref = self.next_object_id(0) @@ -452,7 +448,7 @@ class PdfParser: ) return self.root_ref - def rewrite_pages(self): + def rewrite_pages(self) -> None: pages_tree_nodes_to_delete = [] for i, page_ref in enumerate(self.orig_pages): page_info = self.cached_objects[page_ref] @@ -531,7 +527,7 @@ class PdfParser: f.write(b"endobj\n") return ref - def del_root(self): + def del_root(self) -> None: if self.root_ref is None: return del self.xref_table[self.root_ref.object_id] @@ -549,7 +545,7 @@ class PdfParser: except ValueError: # cannot mmap an empty file return b"" - def read_pdf_info(self): + def read_pdf_info(self) -> None: self.file_size_total = len(self.buf) self.file_size_this = self.file_size_total - self.start_offset self.read_trailer() @@ -825,11 +821,10 @@ class PdfParser: m = cls.re_stream_start.match(data, offset) if m: try: - stream_len = int(result[b"Length"]) - except (TypeError, KeyError, ValueError) as e: - msg = "bad or missing Length in stream dict (%r)" % result.get( - b"Length", None - ) + stream_len_str = result.get(b"Length") + stream_len = int(stream_len_str) + except (TypeError, ValueError) as e: + msg = f"bad or missing Length in stream dict ({stream_len_str})" raise PdfFormatError(msg) from e stream_data = data[m.end() : m.end() + stream_len] m = cls.re_stream_end.match(data, m.end() + stream_len) @@ -884,7 +879,7 @@ class PdfParser: if m: return cls.get_literal_string(data, m.end()) # 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) re_lit_str_token = re.compile( diff --git a/.venv/Lib/site-packages/PIL/PngImagePlugin.py b/.venv/Lib/site-packages/PIL/PngImagePlugin.py index d922bac..d283492 100644 --- a/.venv/Lib/site-packages/PIL/PngImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/PngImagePlugin.py @@ -39,6 +39,7 @@ import struct import warnings import zlib from enum import IntEnum +from typing import IO, TYPE_CHECKING, Any, NoReturn from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -47,6 +48,9 @@ from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +if TYPE_CHECKING: + from . import _imaging + logger = logging.getLogger(__name__) is_cid = re.compile(rb"\w\w\w\w").match @@ -149,14 +153,15 @@ def _crc32(data, seed=0): class ChunkStream: - def __init__(self, fp): - self.fp = fp - self.queue = [] + def __init__(self, fp: IO[bytes]) -> None: + self.fp: IO[bytes] | None = fp + 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.""" cid = None + assert self.fp is not None if self.queue: cid, pos, length = self.queue.pop() self.fp.seek(pos) @@ -173,25 +178,26 @@ class ChunkStream: return cid, pos, length - def __enter__(self): + def __enter__(self) -> ChunkStream: return self - def __exit__(self, *args): + def __exit__(self, *args: object) -> None: self.close() - def close(self): + def close(self) -> 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)) def call(self, cid, pos, length): """Call the appropriate chunk handler""" 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""" # Skip CRC checks for ancillary chunks if allowed to load truncated @@ -201,6 +207,7 @@ class ChunkStream: self.crc_skip(cid, data) return + assert self.fp is not None try: crc1 = _crc32(data, _crc32(cid)) crc2 = i32(self.fp.read(4)) @@ -211,12 +218,13 @@ class ChunkStream: msg = f"broken PNG file (incomplete checksum in {repr(cid)})" raise SyntaxError(msg) from e - def crc_skip(self, cid, data): + def crc_skip(self, cid: bytes, data: bytes) -> None: """Read checksum""" + assert self.fp is not None 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 # blocks. Must be called directly after open. @@ -244,6 +252,9 @@ class iTXt(str): """ + lang: str | bytes | None + tkey: str | bytes | None + @staticmethod def __new__(cls, text, lang=None, tkey=None): """ @@ -265,10 +276,10 @@ class PngInfo: """ - def __init__(self): - self.chunks = [] + def __init__(self) -> None: + self.chunks: list[tuple[bytes, bytes, bool]] = [] - def add(self, cid, data, after_idat=False): + def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None: """Appends an arbitrary chunk. Use with caution. :param cid: a byte string, 4 bytes long. @@ -278,12 +289,16 @@ class PngInfo: """ - chunk = [cid, data] - if after_idat: - chunk.append(True) - self.chunks.append(tuple(chunk)) + self.chunks.append((cid, data, after_idat)) - def add_itxt(self, key, value, lang="", tkey="", zip=False): + def add_itxt( + self, + key: str | bytes, + value: str | bytes, + lang: str | bytes = "", + tkey: str | bytes = "", + zip: bool = False, + ) -> None: """Appends an iTXt chunk. :param key: latin-1 encodable text key name @@ -311,7 +326,9 @@ class PngInfo: else: self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) - def add_text(self, key, value, zip=False): + def add_text( + self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False + ) -> None: """Appends a text chunk. :param key: latin-1 encodable text key name @@ -321,7 +338,13 @@ class PngInfo: """ if isinstance(value, iTXt): - return self.add_itxt(key, value, value.lang, value.tkey, zip=zip) + return self.add_itxt( + key, + value, + value.lang if value.lang is not None else b"", + value.tkey if value.tkey is not None else b"", + zip=zip, + ) # The tEXt chunk stores latin-1 text if not isinstance(value, bytes): @@ -361,7 +384,7 @@ class PngStream(ChunkStream): self.text_memory = 0 - def check_text_memory(self, chunklen): + def check_text_memory(self, chunklen: int) -> None: self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: msg = ( @@ -370,19 +393,19 @@ class PngStream(ChunkStream): ) raise ValueError(msg) - def save_rewind(self): + def save_rewind(self) -> None: self.rewind_state = { "info": self.im_info.copy(), "tile": self.im_tile, "seq_num": self._seq_num, } - def rewind(self): + def rewind(self) -> None: self.im_info = self.rewind_state["info"].copy() self.im_tile = self.rewind_state["tile"] 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 s = ImageFile._safe_read(self.fp, length) # according to PNG spec, the iCCP chunk contains: @@ -409,7 +432,7 @@ class PngStream(ChunkStream): self.im_info["icc_profile"] = icc_profile return s - def chunk_IHDR(self, pos, length): + def chunk_IHDR(self, pos: int, length: int) -> bytes: # image header s = ImageFile._safe_read(self.fp, length) if length < 13: @@ -429,7 +452,7 @@ class PngStream(ChunkStream): raise SyntaxError(msg) return s - def chunk_IDAT(self, pos, length): + def chunk_IDAT(self, pos: int, length: int) -> NoReturn: # image data if "bbox" in self.im_info: tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] @@ -442,18 +465,18 @@ class PngStream(ChunkStream): msg = "image data found" raise EOFError(msg) - def chunk_IEND(self, pos, length): + def chunk_IEND(self, pos: int, length: int) -> NoReturn: msg = "end of PNG image" raise EOFError(msg) - def chunk_PLTE(self, pos, length): + def chunk_PLTE(self, pos: int, length: int) -> bytes: # palette s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": self.im_palette = "RGB", s return s - def chunk_tRNS(self, pos, length): + def chunk_tRNS(self, pos: int, length: int) -> bytes: # transparency s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -473,13 +496,13 @@ class PngStream(ChunkStream): self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) return s - def chunk_gAMA(self, pos, length): + def chunk_gAMA(self, pos: int, length: int) -> bytes: # gamma setting s = ImageFile._safe_read(self.fp, length) self.im_info["gamma"] = i32(s) / 100000.0 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 # 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) return s - def chunk_sRGB(self, pos, length): + def chunk_sRGB(self, pos: int, length: int) -> bytes: # srgb rendering intent, 1 byte # 0 perceptual # 1 relative colorimetric @@ -504,7 +527,7 @@ class PngStream(ChunkStream): self.im_info["srgb"] = s[0] return s - def chunk_pHYs(self, pos, length): + def chunk_pHYs(self, pos: int, length: int) -> bytes: # pixels per unit s = ImageFile._safe_read(self.fp, length) if length < 9: @@ -521,7 +544,7 @@ class PngStream(ChunkStream): self.im_info["aspect"] = px, py return s - def chunk_tEXt(self, pos, length): + def chunk_tEXt(self, pos: int, length: int) -> bytes: # text s = ImageFile._safe_read(self.fp, length) try: @@ -540,7 +563,7 @@ class PngStream(ChunkStream): return s - def chunk_zTXt(self, pos, length): + def chunk_zTXt(self, pos: int, length: int) -> bytes: # compressed text s = ImageFile._safe_read(self.fp, length) try: @@ -574,7 +597,7 @@ class PngStream(ChunkStream): return s - def chunk_iTXt(self, pos, length): + def chunk_iTXt(self, pos: int, length: int) -> bytes: # international text r = s = ImageFile._safe_read(self.fp, length) try: @@ -601,6 +624,8 @@ class PngStream(ChunkStream): return s else: return s + if k == b"XML:com.adobe.xmp": + self.im_info["xmp"] = v try: k = k.decode("latin-1", "strict") lang = lang.decode("utf-8", "strict") @@ -614,13 +639,13 @@ class PngStream(ChunkStream): return s - def chunk_eXIf(self, pos, length): + def chunk_eXIf(self, pos: int, length: int) -> bytes: s = ImageFile._safe_read(self.fp, length) self.im_info["exif"] = b"Exif\x00\x00" + s return s # APNG chunks - def chunk_acTL(self, pos, length): + def chunk_acTL(self, pos: int, length: int) -> bytes: s = ImageFile._safe_read(self.fp, length) if length < 8: if ImageFile.LOAD_TRUNCATED_IMAGES: @@ -640,7 +665,7 @@ class PngStream(ChunkStream): self.im_custom_mimetype = "image/apng" return s - def chunk_fcTL(self, pos, length): + def chunk_fcTL(self, pos: int, length: int) -> bytes: s = ImageFile._safe_read(self.fp, length) if length < 26: if ImageFile.LOAD_TRUNCATED_IMAGES: @@ -669,7 +694,7 @@ class PngStream(ChunkStream): self.im_info["blend"] = s[25] return s - def chunk_fdAT(self, pos, length): + def chunk_fdAT(self, pos: int, length: int) -> bytes: if length < 4: if ImageFile.LOAD_TRUNCATED_IMAGES: s = ImageFile._safe_read(self.fp, length) @@ -689,7 +714,7 @@ class PngStream(ChunkStream): # PNG reader -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:8] == _MAGIC @@ -701,7 +726,7 @@ class PngImageFile(ImageFile.ImageFile): format = "PNG" format_description = "Portable network graphics" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -711,8 +736,8 @@ class PngImageFile(ImageFile.ImageFile): # # Parse headers up to the first IDAT or fDAT chunk - self.private_chunks = [] - self.png = PngStream(self.fp) + self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = [] + self.png: PngStream | None = PngStream(self.fp) while True: # @@ -783,7 +808,7 @@ class PngImageFile(ImageFile.ImageFile): self.seek(frame) return self._text - def verify(self): + def verify(self) -> None: """Verify PNG file""" if self.fp is None: @@ -793,6 +818,7 @@ class PngImageFile(ImageFile.ImageFile): # back up to beginning of IDAT block self.fp.seek(self.tile[0][2] - 8) + assert self.png is not None self.png.verify() self.png.close() @@ -800,7 +826,7 @@ class PngImageFile(ImageFile.ImageFile): self.fp.close() self.fp = None - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return if frame < self.__frame: @@ -815,7 +841,10 @@ class PngImageFile(ImageFile.ImageFile): msg = "no more images in APNG file" raise EOFError(msg) from e - def _seek(self, frame, rewind=False): + def _seek(self, frame: int, rewind: bool = False) -> None: + assert self.png is not None + + self.dispose: _imaging.ImagingCore | None if frame == 0: if rewind: self._fp.seek(self.__rewind) @@ -900,19 +929,19 @@ class PngImageFile(ImageFile.ImageFile): if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: self.dispose_op = Disposal.OP_BACKGROUND + self.dispose = None if self.dispose_op == Disposal.OP_PREVIOUS: - self.dispose = self._prev_im.copy() - self.dispose = self._crop(self.dispose, self.dispose_extent) + if self._prev_im: + self.dispose = self._prev_im.copy() + self.dispose = self._crop(self.dispose, self.dispose_extent) elif self.dispose_op == Disposal.OP_BACKGROUND: self.dispose = Image.core.fill(self.mode, self.size) self.dispose = self._crop(self.dispose, self.dispose_extent) - else: - self.dispose = None - def tell(self): + def tell(self) -> int: return self.__frame - def load_prepare(self): + def load_prepare(self) -> None: """internal: prepare to read PNG file""" if self.info.get("interlace"): @@ -921,9 +950,10 @@ class PngImageFile(ImageFile.ImageFile): self.__idat = self.__prepare_idat # used by load_read() ImageFile.ImageFile.load_prepare(self) - def load_read(self, read_bytes): + def load_read(self, read_bytes: int) -> bytes: """internal: read more image data""" + assert self.png is not None while self.__idat == 0: # end of chunk, skip forward to next one @@ -954,8 +984,9 @@ class PngImageFile(ImageFile.ImageFile): return self.fp.read(read_bytes) - def load_end(self): + def load_end(self) -> None: """internal: finished reading image data""" + assert self.png is not None if self.__idat != 0: self.fp.read(self.__idat) while True: @@ -1011,53 +1042,40 @@ class PngImageFile(ImageFile.ImageFile): if self.pyaccess: self.pyaccess = None - def _getexif(self): + def _getexif(self) -> dict[str, Any] | None: if "exif" not in self.info: self.load() if "exif" not in self.info and "Raw profile type exif" not in self.info: return None return self.getexif()._get_merged_dict() - def getexif(self): + def getexif(self) -> Image.Exif: if "exif" not in self.info: self.load() return super().getexif() - def getxmp(self): - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - - :returns: XMP tags in a dictionary. - """ - return ( - self._getxmp(self.info["XML:com.adobe.xmp"]) - if "XML:com.adobe.xmp" in self.info - else {} - ) - # -------------------------------------------------------------------- # PNG writer _OUTMODES = { - # supported PIL modes, and corresponding rawmodes/bits/color combinations - "1": ("1", b"\x01\x00"), - "L;1": ("L;1", b"\x01\x00"), - "L;2": ("L;2", b"\x02\x00"), - "L;4": ("L;4", b"\x04\x00"), - "L": ("L", b"\x08\x00"), - "LA": ("LA", b"\x08\x04"), - "I": ("I;16B", b"\x10\x00"), - "I;16": ("I;16B", b"\x10\x00"), - "I;16B": ("I;16B", b"\x10\x00"), - "P;1": ("P;1", b"\x01\x03"), - "P;2": ("P;2", b"\x02\x03"), - "P;4": ("P;4", b"\x04\x03"), - "P": ("P", b"\x08\x03"), - "RGB": ("RGB", b"\x08\x02"), - "RGBA": ("RGBA", b"\x08\x06"), + # supported PIL modes, and corresponding rawmode, bit depth and color type + "1": ("1", b"\x01", b"\x00"), + "L;1": ("L;1", b"\x01", b"\x00"), + "L;2": ("L;2", b"\x02", b"\x00"), + "L;4": ("L;4", b"\x04", b"\x00"), + "L": ("L", b"\x08", b"\x00"), + "LA": ("LA", b"\x08", b"\x04"), + "I": ("I;16B", b"\x10", b"\x00"), + "I;16": ("I;16B", b"\x10", b"\x00"), + "I;16B": ("I;16B", b"\x10", b"\x00"), + "P;1": ("P;1", b"\x01", b"\x03"), + "P;2": ("P;2", b"\x02", b"\x03"), + "P;4": ("P;4", b"\x04", b"\x03"), + "P": ("P", b"\x08", b"\x03"), + "RGB": ("RGB", b"\x08", b"\x02"), + "RGBA": ("RGBA", b"\x08", b"\x06"), } @@ -1079,7 +1097,7 @@ class _idat: self.fp = fp self.chunk = chunk - def write(self, data): + def write(self, data: bytes) -> None: self.chunk(self.fp, b"IDAT", data) @@ -1091,13 +1109,13 @@ class _fdat: self.chunk = chunk 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.seq_num += 1 -def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images): - duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) +def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_images): + duration = im.encoderinfo.get("duration") loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) @@ -1111,13 +1129,15 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) frame_count = 0 for im_seq in chain: for im_frame in ImageSequence.Iterator(im_seq): - if im_frame.mode == rawmode: + if im_frame.mode == mode: im_frame = im_frame.copy() else: - im_frame = im_frame.convert(rawmode) + im_frame = im_frame.convert(mode) encoderinfo = im.encoderinfo.copy() if isinstance(duration, (list, tuple)): encoderinfo["duration"] = duration[frame_count] + elif duration is None and "duration" in im_frame.info: + encoderinfo["duration"] = im_frame.info["duration"] if isinstance(disposal, (list, tuple)): encoderinfo["disposal"] = disposal[frame_count] if isinstance(blend, (list, tuple)): @@ -1152,15 +1172,12 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) not bbox and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") + and "duration" in encoderinfo ): - previous["encoderinfo"]["duration"] += encoderinfo.get( - "duration", duration - ) + previous["encoderinfo"]["duration"] += encoderinfo["duration"] continue else: bbox = None - if "duration" not in encoderinfo: - encoderinfo["duration"] = duration im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) if len(im_frames) == 1 and not default_image: @@ -1176,8 +1193,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) # default image IDAT (if it exists) if default_image: - if im.mode != rawmode: - im = im.convert(rawmode) + if im.mode != mode: + im = im.convert(mode) ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) seq_num = 0 @@ -1190,7 +1207,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data["encoderinfo"] - frame_duration = int(round(encoderinfo["duration"])) + frame_duration = int(round(encoderinfo.get("duration", 0))) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control @@ -1226,7 +1243,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) seq_num = fdat_chunks.seq_num -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: _save(im, fp, filename, save_all=True) @@ -1254,6 +1271,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): size = im.size mode = im.mode + outmode = mode if mode == "P": # # attempt to minimize storage requirements for palette images @@ -1274,7 +1292,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): bits = 2 else: bits = 4 - mode = f"{mode};{bits}" + outmode += f";{bits}" # encoder options im.encoderconfig = ( @@ -1286,7 +1304,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): # get the corresponding PNG mode try: - rawmode, mode = _OUTMODES[mode] + rawmode, bit_depth, color_type = _OUTMODES[outmode] except KeyError as e: msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e @@ -1301,7 +1319,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): b"IHDR", o32(size[0]), # 0: size o32(size[1]), - mode, # 8: depth/type + bit_depth, + color_type, b"\0", # 10: compression b"\0", # 11: filter category b"\0", # 12: interlace flag @@ -1337,7 +1356,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): chunk(fp, cid, data) elif cid[1:2].islower(): # Private chunk - after_idat = info_chunk[2:3] + after_idat = len(info_chunk) == 3 and info_chunk[2] if not after_idat: chunk(fp, cid, data) @@ -1406,7 +1425,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): if save_all: im = _write_multiple_frames( - im, fp, chunk, rawmode, default_image, append_images + im, fp, chunk, mode, rawmode, default_image, append_images ) if im: ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) @@ -1416,7 +1435,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): cid, data = info_chunk[:2] if cid[1:2].islower(): # Private chunk - after_idat = info_chunk[2:3] + after_idat = len(info_chunk) == 3 and info_chunk[2] if after_idat: chunk(fp, cid, data) @@ -1436,10 +1455,10 @@ def getchunks(im, **params): class collector: data = [] - def write(self, data): + def write(self, data: bytes) -> None: pass - def append(self, chunk): + def append(self, chunk: bytes) -> None: self.data.append(chunk) def append(fp, cid, *data): diff --git a/.venv/Lib/site-packages/PIL/PpmImagePlugin.py b/.venv/Lib/site-packages/PIL/PpmImagePlugin.py index 94bf430..16c9ccb 100644 --- a/.venv/Lib/site-packages/PIL/PpmImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/PpmImagePlugin.py @@ -328,7 +328,7 @@ class PpmDecoder(ImageFile.PyDecoder): # -------------------------------------------------------------------- -def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": diff --git a/.venv/Lib/site-packages/PIL/PsdImagePlugin.py b/.venv/Lib/site-packages/PIL/PsdImagePlugin.py index d29bcf9..edf698b 100644 --- a/.venv/Lib/site-packages/PIL/PsdImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/PsdImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import io +from functools import cached_property from . import Image, ImageFile, ImagePalette from ._binary import i8 @@ -44,7 +45,7 @@ MODES = { # read PSD images -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"8BPS" @@ -57,7 +58,7 @@ class PsdImageFile(ImageFile.ImageFile): format_description = "Adobe Photoshop" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: read = self.fp.read # @@ -118,18 +119,17 @@ class PsdImageFile(ImageFile.ImageFile): # # layer and mask information - self.layers = [] + self._layers_position = None size = i32(read(4)) if size: end = self.fp.tell() + size size = i32(read(4)) if size: - _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size)) - self.layers = _layerinfo(_layer_data, size) + self._layers_position = self.fp.tell() + self._layers_size = size self.fp.seek(end) - self.n_frames = len(self.layers) - self.is_animated = self.n_frames > 1 + self._n_frames: int | None = None # # image descriptor @@ -141,23 +141,42 @@ class PsdImageFile(ImageFile.ImageFile): self.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): return # seek to given layer (1..max) try: - name, mode, bbox, tile = self.layers[layer - 1] + _, mode, _, tile = self.layers[layer - 1] self._mode = mode self.tile = tile self.frame = layer self.fp = self._fp - return name, bbox except IndexError as e: msg = "no such layer" raise EOFError(msg) from e - def tell(self): + def tell(self) -> int: # return layer number (0=image, 1..max=layers) return self.frame diff --git a/.venv/Lib/site-packages/PIL/PyAccess.py b/.venv/Lib/site-packages/PIL/PyAccess.py index 2c83191..3be1cca 100644 --- a/.venv/Lib/site-packages/PIL/PyAccess.py +++ b/.venv/Lib/site-packages/PIL/PyAccess.py @@ -22,6 +22,7 @@ from __future__ import annotations import logging import sys +from typing import TYPE_CHECKING from ._deprecate import deprecate @@ -48,9 +49,12 @@ except ImportError as ex: logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from . import Image + class PyAccess: - def __init__(self, img, readonly=False): + def __init__(self, img: Image.Image, readonly: bool = False) -> None: deprecate("PyAccess", 11) vals = dict(img.im.unsafe_ptrs) self.readonly = readonly @@ -70,14 +74,19 @@ class PyAccess: # logger.debug("%s", vals) self._post_init() - def _post_init(self): + def _post_init(self) -> None: 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 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 :ref:`coordinate-system`. @@ -102,13 +111,12 @@ class PyAccess: if self._im.mode == "PA": alpha = color[3] if len(color) == 4 else 255 color = color[:3] - color = self._palette.getcolor(color, self._img) - if self._im.mode == "PA": - color = (color, alpha) + palette_index = self._palette.getcolor(color, self._img) + color = (palette_index, alpha) if self._im.mode == "PA" else palette_index 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 value for single band images or a tuple for multiple band @@ -130,13 +138,21 @@ class PyAccess: putpixel = __setitem__ getpixel = __getitem__ - def check_xy(self, xy): + def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]: (x, y) = xy if not (0 <= x < self.xsize and 0 <= y < self.ysize): msg = "pixel location out of range" raise ValueError(msg) return xy + 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): """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): 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] return pixel.r, pixel.a @@ -161,7 +177,7 @@ class _PyAccess32_3(PyAccess): def _post_init(self, *args, **kwargs): 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] return pixel.r, pixel.g, pixel.b @@ -180,7 +196,7 @@ class _PyAccess32_4(PyAccess): def _post_init(self, *args, **kwargs): 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] return pixel.r, pixel.g, pixel.b, pixel.a @@ -199,7 +215,7 @@ class _PyAccess8(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = self.image8 - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: return self.pixels[y][x] def set_pixel(self, x, y, color): @@ -217,7 +233,7 @@ class _PyAccessI16_N(PyAccess): def _post_init(self, *args, **kwargs): 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] def set_pixel(self, x, y, color): @@ -235,7 +251,7 @@ class _PyAccessI16_L(PyAccess): def _post_init(self, *args, **kwargs): 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] return pixel.l + pixel.r * 256 @@ -256,7 +272,7 @@ class _PyAccessI16_B(PyAccess): def _post_init(self, *args, **kwargs): 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] return pixel.l * 256 + pixel.r @@ -277,7 +293,7 @@ class _PyAccessI32_N(PyAccess): def _post_init(self, *args, **kwargs): self.pixels = self.image32 - def get_pixel(self, x, y): + def get_pixel(self, x: int, y: int) -> int: return self.pixels[y][x] 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] 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]) def set_pixel(self, x, y, color): @@ -309,7 +325,7 @@ class _PyAccessF(PyAccess): def _post_init(self, *args, **kwargs): 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] def set_pixel(self, x, y, color): @@ -357,7 +373,7 @@ else: 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) if not access_type: logger.debug("PyAccess Not Implemented: %s", img.mode) diff --git a/.venv/Lib/site-packages/PIL/QoiImagePlugin.py b/.venv/Lib/site-packages/PIL/QoiImagePlugin.py index f8aa720..202ef52 100644 --- a/.venv/Lib/site-packages/PIL/QoiImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/QoiImagePlugin.py @@ -13,7 +13,7 @@ from . import Image, ImageFile from ._binary import i32be as i32 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"qoif" @@ -21,7 +21,7 @@ class QoiImageFile(ImageFile.ImageFile): format = "QOI" format_description = "Quite OK Image" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not a QOI file" raise SyntaxError(msg) @@ -37,17 +37,20 @@ class QoiImageFile(ImageFile.ImageFile): class QoiDecoder(ImageFile.PyDecoder): _pulls_fd = True + _previous_pixel: bytes | bytearray | None = None + _previously_seen_pixels: dict[int, bytes | bytearray] = {} - def _add_to_previous_pixels(self, value): + def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: self._previous_pixel = value r, g, b, a = value hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 self._previously_seen_pixels[hash_value] = value - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + self._previously_seen_pixels = {} - self._previous_pixel = None self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) data = bytearray() @@ -55,7 +58,8 @@ class QoiDecoder(ImageFile.PyDecoder): dest_length = self.state.xsize * self.state.ysize * bands while len(data) < dest_length: byte = self.fd.read(1)[0] - if byte == 0b11111110: # QOI_OP_RGB + value: bytes | bytearray + if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] elif byte == 0b11111111: # QOI_OP_RGBA value = self.fd.read(4) @@ -66,7 +70,7 @@ class QoiDecoder(ImageFile.PyDecoder): value = self._previously_seen_pixels.get( op_index, bytearray((0, 0, 0, 0)) ) - elif op == 1: # QOI_OP_DIFF + elif op == 1 and self._previous_pixel: # QOI_OP_DIFF value = bytearray( ( (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) @@ -77,7 +81,7 @@ class QoiDecoder(ImageFile.PyDecoder): self._previous_pixel[3], ) ) - elif op == 2: # QOI_OP_LUMA + elif op == 2 and self._previous_pixel: # QOI_OP_LUMA second_byte = self.fd.read(1)[0] diff_green = (byte & 0b00111111) - 32 diff_red = ((second_byte & 0b11110000) >> 4) - 8 @@ -90,7 +94,7 @@ class QoiDecoder(ImageFile.PyDecoder): ) ) value += self._previous_pixel[3:] - elif op == 3: # QOI_OP_RUN + elif op == 3 and self._previous_pixel: # QOI_OP_RUN run_length = (byte & 0b00111111) + 1 value = self._previous_pixel if bands == 3: diff --git a/.venv/Lib/site-packages/PIL/SgiImagePlugin.py b/.venv/Lib/site-packages/PIL/SgiImagePlugin.py index 7bd84eb..50d9791 100644 --- a/.venv/Lib/site-packages/PIL/SgiImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/SgiImagePlugin.py @@ -125,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile): ] -def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -171,8 +171,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: # Maximum Byte value (255 = 8bits per pixel) pinmax = 255 # Image name (79 characters max, truncated below in write) - filename = os.path.basename(filename) - img_name = os.path.splitext(filename)[0].encode("ascii", "ignore") + img_name = os.path.splitext(os.path.basename(filename))[0] + if isinstance(img_name, str): + img_name = img_name.encode("ascii", "ignore") # Standard representation of pixel in the file colormap = 0 fp.write(struct.pack(">h", magic_number)) diff --git a/.venv/Lib/site-packages/PIL/SpiderImagePlugin.py b/.venv/Lib/site-packages/PIL/SpiderImagePlugin.py index 86582fb..f5a09c3 100644 --- a/.venv/Lib/site-packages/PIL/SpiderImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/SpiderImagePlugin.py @@ -37,11 +37,12 @@ from __future__ import annotations import os import struct import sys +from typing import IO, TYPE_CHECKING, Any, Tuple, cast from . import Image, ImageFile -def isInt(f): +def isInt(f: Any) -> int: try: i = int(f) if f - i == 0: @@ -61,7 +62,7 @@ iforms = [1, 3, -11, -12, -21, -22] # otherwise returns 0 -def isSpiderHeader(t): +def isSpiderHeader(t: tuple[float, ...]) -> int: h = (99,) + t # add 1 value so can use spider header index start=1 # header values 1,2,5,12,13,22,23 should be integers for i in [1, 2, 5, 12, 13, 22, 23]: @@ -81,7 +82,7 @@ def isSpiderHeader(t): return labbyt -def isSpiderImage(filename): +def isSpiderImage(filename: str) -> int: with open(filename, "rb") as fp: f = fp.read(92) # read 23 * 4 bytes t = struct.unpack(">23f", f) # try big-endian first @@ -97,7 +98,7 @@ class SpiderImageFile(ImageFile.ImageFile): format_description = "Spider 2D image" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: # check header n = 27 * 4 # read 27 float values f = self.fp.read(n) @@ -157,21 +158,21 @@ class SpiderImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack @property - def n_frames(self): + def n_frames(self) -> int: return self._nimages @property - def is_animated(self): + def is_animated(self) -> bool: return self._nimages > 1 # 1st image index is zero (although SPIDER imgnumber starts at 1) - def tell(self): + def tell(self) -> int: if self.imgnumber < 1: return 0 else: return self.imgnumber - 1 - def seek(self, frame): + def seek(self, frame: int) -> None: if self.istack == 0: msg = "attempt to seek in a non-stack file" raise EOFError(msg) @@ -183,16 +184,21 @@ class SpiderImageFile(ImageFile.ImageFile): self._open() # returns a byte image after rescaling to 0..255 - def convert2byte(self, depth=255): - (minimum, maximum) = self.getextrema() - m = 1 + def convert2byte(self, depth: int = 255) -> Image.Image: + extrema = self.getextrema() + assert isinstance(extrema[0], float) + minimum, maximum = cast(Tuple[float, float], extrema) + m: float = 1 if maximum != minimum: m = depth / (maximum - minimum) b = -m * minimum - return self.point(lambda i, m=m, b=b: i * m + b).convert("L") + return self.point(lambda i: i * m + b).convert("L") + + if TYPE_CHECKING: + from . import ImageTk # returns a ImageTk.PhotoImage object, after rescaling to 0..255 - def tkPhotoImage(self): + def tkPhotoImage(self) -> ImageTk.PhotoImage: from . import ImageTk 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 -def loadImageSeries(filelist=None): +def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: - return + return None imglist = [] for img in filelist: @@ -218,7 +224,7 @@ def loadImageSeries(filelist=None): im = im.convert2byte() except Exception: if not isSpiderImage(img): - print(img + " is not a Spider image file") + print(f"{img} is not a Spider image file") continue im.info["filename"] = img imglist.append(im) @@ -229,7 +235,7 @@ def loadImageSeries(filelist=None): # For saving images in Spider format -def makeSpiderHeader(im): +def makeSpiderHeader(im: Image.Image) -> list[bytes]: nsam, nrow = im.size lenbyt = nsam * 4 # There are labrec records in the header labrec = int(1024 / lenbyt) @@ -259,7 +265,7 @@ def makeSpiderHeader(im): return [struct.pack("f", v) for v in hdr] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode[0] != "F": im = im.convert("F") @@ -275,9 +281,10 @@ def _save(im, fp, filename): ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) -def _save_spider(im, fp, filename): +def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # get the filename extension and register it with Image - ext = os.path.splitext(filename)[1] + filename_ext = os.path.splitext(filename)[1] + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext Image.register_extension(SpiderImageFile.format, ext) _save(im, fp, filename) @@ -299,10 +306,10 @@ if __name__ == "__main__": sys.exit() with Image.open(filename) as im: - print("image: " + str(im)) - print("format: " + str(im.format)) - print("size: " + str(im.size)) - print("mode: " + str(im.mode)) + print(f"image: {im}") + print(f"format: {im.format}") + print(f"size: {im.size}") + print(f"mode: {im.mode}") print("max, min: ", end=" ") print(im.getextrema()) diff --git a/.venv/Lib/site-packages/PIL/TarIO.py b/.venv/Lib/site-packages/PIL/TarIO.py index 7470663..cba26d4 100644 --- a/.venv/Lib/site-packages/PIL/TarIO.py +++ b/.venv/Lib/site-packages/PIL/TarIO.py @@ -16,7 +16,6 @@ from __future__ import annotations import io -from types import TracebackType from . import ContainerIO @@ -61,12 +60,7 @@ class TarIO(ContainerIO.ContainerIO[bytes]): def __enter__(self) -> TarIO: return self - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: + def __exit__(self, *args: object) -> None: self.close() def close(self) -> None: diff --git a/.venv/Lib/site-packages/PIL/TgaImagePlugin.py b/.venv/Lib/site-packages/PIL/TgaImagePlugin.py index 401a83f..39104ae 100644 --- a/.venv/Lib/site-packages/PIL/TgaImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/TgaImagePlugin.py @@ -36,7 +36,7 @@ MODES = { (3, 1): "1", (3, 8): "L", (3, 16): "LA", - (2, 16): "BGR;5", + (2, 16): "BGRA;15Z", (2, 24): "BGR", (2, 32): "BGRA", } @@ -87,9 +87,7 @@ class TgaImageFile(ImageFile.ImageFile): elif imagetype in (1, 9): self._mode = "P" if colormaptype else "L" elif imagetype in (2, 10): - self._mode = "RGB" - if depth == 32: - self._mode = "RGBA" + self._mode = "RGB" if depth == 24 else "RGBA" else: msg = "unknown TGA mode" raise SyntaxError(msg) @@ -118,15 +116,16 @@ class TgaImageFile(ImageFile.ImageFile): start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] if mapdepth == 16: self.palette = ImagePalette.raw( - "BGR;15", b"\0" * 2 * start + self.fp.read(2 * size) + "BGRA;15Z", bytes(2 * start) + self.fp.read(2 * size) ) + self.palette.mode = "RGBA" elif mapdepth == 24: self.palette = ImagePalette.raw( - "BGR", b"\0" * 3 * start + self.fp.read(3 * size) + "BGR", bytes(3 * start) + self.fp.read(3 * size) ) elif mapdepth == 32: self.palette = ImagePalette.raw( - "BGRA", b"\0" * 4 * start + self.fp.read(4 * size) + "BGRA", bytes(4 * start) + self.fp.read(4 * size) ) else: msg = "unknown TGA map depth" @@ -178,7 +177,7 @@ SAVE = { } -def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/.venv/Lib/site-packages/PIL/TiffImagePlugin.py b/.venv/Lib/site-packages/PIL/TiffImagePlugin.py index 8bfcd29..ac5b63c 100644 --- a/.venv/Lib/site-packages/PIL/TiffImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/TiffImagePlugin.py @@ -50,12 +50,13 @@ import warnings from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import TYPE_CHECKING, Any, Callable +from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 +from ._deprecate import deprecate from .TiffTags import TYPES logger = logging.getLogger(__name__) @@ -200,12 +201,12 @@ OPEN_INFO = { (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), - (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), - (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), - (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (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"), (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), - (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"), - (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"), + (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"), + (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), (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"), (II, 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"), (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), @@ -276,8 +278,11 @@ PREFIXES = [ 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 @@ -376,13 +381,13 @@ class IFDRational(Rational): f = self._val.limit_denominator(max_denominator) return f.numerator, f.denominator - def __repr__(self): + def __repr__(self) -> str: return str(float(self._val)) - def __hash__(self): + def __hash__(self) -> int: return self._val.__hash__() - def __eq__(self, other): + def __eq__(self, other: object) -> bool: val = self._val if isinstance(other, IFDRational): other = other._val @@ -464,7 +469,7 @@ def _register_basic(idx_fmt_name): idx, fmt, name = idx_fmt_name TYPES[idx] = name - size = struct.calcsize("=" + fmt) + size = struct.calcsize(f"={fmt}") _load_dispatch[idx] = ( # noqa: F821 size, lambda self, data, legacy_api=True: ( @@ -546,7 +551,12 @@ class ImageFileDirectory_v2(_IFDv2Base): _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {} - def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): + def __init__( + self, + ifh: bytes = b"II\052\0\0\0\0\0", + prefix: bytes | None = None, + group: int | None = None, + ) -> None: """Initialize an ImageFileDirectory. To construct an ImageFileDirectory from a real file, pass the 8-byte @@ -570,7 +580,7 @@ class ImageFileDirectory_v2(_IFDv2Base): raise SyntaxError(msg) self._bigtiff = ifh[2] == 43 self.group = group - self.tagtype = {} + self.tagtype: dict[int, int] = {} """ Dictionary of tag types """ self.reset() (self.next,) = ( @@ -582,23 +592,23 @@ class ImageFileDirectory_v2(_IFDv2Base): offset = property(lambda self: self._offset) @property - def legacy_api(self): + def legacy_api(self) -> bool: return self._legacy_api @legacy_api.setter - def legacy_api(self, value): + def legacy_api(self, value: bool) -> NoReturn: msg = "Not allowing setting of legacy api" raise Exception(msg) - def reset(self): - self._tags_v1 = {} # will remain empty if legacy_api is false - self._tags_v2 = {} # main tag storage - self._tagdata = {} + def reset(self) -> None: + self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false + self._tags_v2: dict[int, Any] = {} # main tag storage + self._tagdata: dict[int, bytes] = {} self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None self._offset = None - def __str__(self): + def __str__(self) -> str: return str(dict(self)) def named(self): @@ -612,7 +622,7 @@ class ImageFileDirectory_v2(_IFDv2Base): for code, value in self.items() } - def __len__(self): + def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v2)) def __getitem__(self, tag): @@ -712,7 +722,7 @@ class ImageFileDirectory_v2(_IFDv2Base): # Unspec'd, and length > 1 dest[tag] = values - def __delitem__(self, tag): + def __delitem__(self, tag: int) -> None: self._tags_v2.pop(tag, None) self._tags_v1.pop(tag, None) self._tagdata.pop(tag, None) @@ -982,8 +992,8 @@ ImageFileDirectory_v2._load_dispatch = _load_dispatch ImageFileDirectory_v2._write_dispatch = _write_dispatch for idx, name in TYPES.items(): name = name.replace(" ", "_") - setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1]) - setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx]) + setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1]) + setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx]) 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 return ifd - def to_v2(self): + def to_v2(self) -> ImageFileDirectory_v2: """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` instance with the same data as is contained in the original @@ -1056,7 +1066,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __contains__(self, tag): 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)) def __iter__(self): @@ -1101,7 +1111,7 @@ class TiffImageFile(ImageFile.ImageFile): super().__init__(fp, filename) - def _open(self): + def _open(self) -> None: """Open the first image in a TIFF file""" # Header @@ -1118,8 +1128,8 @@ class TiffImageFile(ImageFile.ImageFile): self.__first = self.__next = self.tag_v2.next self.__frame = -1 self._fp = self.fp - self._frame_pos = [] - self._n_frames = None + self._frame_pos: list[int] = [] + self._n_frames: int | None = None logger.debug("*** TiffImageFile._open ***") logger.debug("- __first: %s", self.__first) @@ -1138,7 +1148,7 @@ class TiffImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames - def seek(self, frame): + def seek(self, frame: int) -> None: """Select a given frame as current image""" if not self._seek_check(frame): return @@ -1149,7 +1159,7 @@ class TiffImageFile(ImageFile.ImageFile): Image._decompression_bomb_check(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 # reset buffered io handle in case fp @@ -1187,25 +1197,20 @@ class TiffImageFile(ImageFile.ImageFile): self.__frame += 1 self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) + if XMP in self.tag_v2: + self.info["xmp"] = self.tag_v2[XMP] + elif "xmp" in self.info: + del self.info["xmp"] self._reload_exif() # fill the legacy tag/ifd entries self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.__frame = frame self._setup() - def tell(self): + def tell(self) -> int: """Return the current frame number""" return self.__frame - def getxmp(self): - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - - :returns: XMP tags in a dictionary. - """ - return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {} - def get_photoshop_blocks(self): """ Returns a dictionary of Photoshop "Image Resource Blocks". @@ -1232,7 +1237,7 @@ class TiffImageFile(ImageFile.ImageFile): return self._load_libtiff() 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 # This is the ImageFile.load path only, libtiff specific below. if not self.is_animated: @@ -1653,6 +1658,20 @@ def _save(im, fp, filename): except Exception: pass # might not be an IFD. Might not have populated type + legacy_ifd = {} + if hasattr(im, "tag"): + legacy_ifd = im.tag.to_v2() + + supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})} + for tag in ( + # IFD offset that may not be correct in the saved image + EXIFIFD, + # Determined by the image format and should not be copied from legacy_ifd. + SAMPLEFORMAT, + ): + if tag in supplied_tags: + del supplied_tags[tag] + # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com if hasattr(im, "tag_v2"): @@ -1666,8 +1685,14 @@ def _save(im, fp, filename): XMP, ): if key in im.tag_v2: - ifd[key] = im.tag_v2[key] - ifd.tagtype[key] = im.tag_v2.tagtype[key] + if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in ( + TiffTags.BYTE, + TiffTags.UNDEFINED, + ): + del supplied_tags[key] + else: + ifd[key] = im.tag_v2[key] + ifd.tagtype[key] = im.tag_v2.tagtype[key] # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech @@ -1807,16 +1832,6 @@ def _save(im, fp, filename): # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. - legacy_ifd = {} - if hasattr(im, "tag"): - legacy_ifd = im.tag.to_v2() - - # SAMPLEFORMAT is determined by the image format and should not be copied - # from legacy_ifd. - supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd} - if SAMPLEFORMAT in supplied_tags: - del supplied_tags[SAMPLEFORMAT] - for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): # Libtiff can only process certain core items without adding # them to the custom dictionary. @@ -1937,7 +1952,7 @@ class AppendingTiffWriter: self.beginning = self.f.tell() self.setup() - def setup(self): + def setup(self) -> None: # Reset everything. self.f.seek(self.beginning, os.SEEK_SET) @@ -1962,7 +1977,7 @@ class AppendingTiffWriter: self.skipIFDs() self.goToEnd() - def finalize(self): + def finalize(self) -> None: if self.isFirst: return @@ -1985,20 +2000,19 @@ class AppendingTiffWriter: self.f.seek(ifd_offset) self.fixIFD() - def newFrame(self): + def newFrame(self) -> None: # Call this to finish a frame. self.finalize() self.setup() - def __enter__(self): + def __enter__(self) -> AppendingTiffWriter: return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, *args: object) -> None: if self.close_fp: self.close() - return False - def tell(self): + def tell(self) -> int: return self.f.tell() - self.offsetOfNewPage def seek(self, offset, whence=io.SEEK_SET): @@ -2008,7 +2022,7 @@ class AppendingTiffWriter: self.f.seek(offset, whence) return self.tell() - def goToEnd(self): + def goToEnd(self) -> None: self.f.seek(0, os.SEEK_END) pos = self.f.tell() @@ -2018,13 +2032,13 @@ class AppendingTiffWriter: self.f.write(bytes(pad_bytes)) self.offsetOfNewPage = self.f.tell() - def setEndian(self, endian): + def setEndian(self, endian: str) -> None: self.endian = endian - self.longFmt = self.endian + "L" - self.shortFmt = self.endian + "H" - self.tagFormat = self.endian + "HHL" + self.longFmt = f"{self.endian}L" + self.shortFmt = f"{self.endian}H" + self.tagFormat = f"{self.endian}HHL" - def skipIFDs(self): + def skipIFDs(self) -> None: while True: ifd_offset = self.readLong() if ifd_offset == 0: @@ -2035,55 +2049,55 @@ class AppendingTiffWriter: num_tags = self.readShort() self.f.seek(num_tags * 12, os.SEEK_CUR) - def write(self, data): + def write(self, data: bytes) -> int | None: return self.f.write(data) - def readShort(self): + def readShort(self) -> int: (value,) = struct.unpack(self.shortFmt, self.f.read(2)) return value - def readLong(self): + def readLong(self) -> int: (value,) = struct.unpack(self.longFmt, self.f.read(4)) return value - def rewriteLastShortToLong(self, value): + def rewriteLastShortToLong(self, value: int) -> None: self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: msg = f"wrote only {bytes_written} bytes but wanted 4" raise RuntimeError(msg) - def rewriteLastShort(self, value): + def rewriteLastShort(self, value: int) -> None: self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.shortFmt, value)) if bytes_written is not None and bytes_written != 2: msg = f"wrote only {bytes_written} bytes but wanted 2" raise RuntimeError(msg) - def rewriteLastLong(self, value): + def rewriteLastLong(self, value: int) -> None: self.f.seek(-4, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: msg = f"wrote only {bytes_written} bytes but wanted 4" raise RuntimeError(msg) - def writeShort(self, value): + def writeShort(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.shortFmt, value)) if bytes_written is not None and bytes_written != 2: msg = f"wrote only {bytes_written} bytes but wanted 2" raise RuntimeError(msg) - def writeLong(self, value): + def writeLong(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: msg = f"wrote only {bytes_written} bytes but wanted 4" raise RuntimeError(msg) - def close(self): + def close(self) -> None: self.finalize() self.f.close() - def fixIFD(self): + def fixIFD(self) -> None: num_tags = self.readShort() for i in range(num_tags): @@ -2092,9 +2106,9 @@ class AppendingTiffWriter: field_size = self.fieldSizes[field_type] total_size = field_size * count is_local = total_size <= 4 + offset: int | None if not is_local: - offset = self.readLong() - offset += self.offsetOfNewPage + offset = self.readLong() + self.offsetOfNewPage self.rewriteLastLong(offset) if tag in self.Tags: @@ -2118,7 +2132,9 @@ class AppendingTiffWriter: # skip the locally stored value that is not an offset self.f.seek(4, os.SEEK_CUR) - def fixOffsets(self, count, isShort=False, isLong=False): + def fixOffsets( + self, count: int, isShort: bool = False, isLong: bool = False + ) -> None: if not isShort and not isLong: msg = "offset is neither short nor long" raise RuntimeError(msg) @@ -2144,7 +2160,7 @@ class AppendingTiffWriter: self.rewriteLastLong(offset) -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoderinfo = im.encoderinfo.copy() encoderconfig = im.encoderconfig append_images = list(encoderinfo.get("append_images", [])) diff --git a/.venv/Lib/site-packages/PIL/TiffTags.py b/.venv/Lib/site-packages/PIL/TiffTags.py index 89fad70..e318c87 100644 --- a/.venv/Lib/site-packages/PIL/TiffTags.py +++ b/.venv/Lib/site-packages/PIL/TiffTags.py @@ -89,7 +89,7 @@ DOUBLE = 12 IFD = 13 LONG8 = 16 -TAGS_V2 = { +_tags_v2 = { 254: ("NewSubfileType", LONG, 1), 255: ("SubfileType", SHORT, 1), 256: ("ImageWidth", LONG, 1), @@ -425,9 +425,11 @@ TAGS = { 50784: "Alias Layer Metadata", } +TAGS_V2: dict[int, TagInfo] = {} + def _populate(): - for k, v in TAGS_V2.items(): + for k, v in _tags_v2.items(): # Populate legacy structure. TAGS[k] = v[0] if len(v) == 4: diff --git a/.venv/Lib/site-packages/PIL/WalImageFile.py b/.venv/Lib/site-packages/PIL/WalImageFile.py index c5bf3e0..fbd7be6 100644 --- a/.venv/Lib/site-packages/PIL/WalImageFile.py +++ b/.venv/Lib/site-packages/PIL/WalImageFile.py @@ -32,7 +32,7 @@ class WalImageFile(ImageFile.ImageFile): format = "WAL" format_description = "Quake2 Texture" - def _open(self): + def _open(self) -> None: self._mode = "P" # read header fields diff --git a/.venv/Lib/site-packages/PIL/WebPImagePlugin.py b/.venv/Lib/site-packages/PIL/WebPImagePlugin.py index c07abca..59be5bf 100644 --- a/.venv/Lib/site-packages/PIL/WebPImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/WebPImagePlugin.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from typing import IO, Any 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_webp_file = prefix[8:12] == b"WEBP" 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" ) return True + return False class WebPImageFile(ImageFile.ImageFile): @@ -42,7 +44,7 @@ class WebPImageFile(ImageFile.ImageFile): __loaded = 0 __logical_frame = 0 - def _open(self): + def _open(self) -> None: if not _webp.HAVE_WEBPANIM: # Legacy mode data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( @@ -94,28 +96,19 @@ class WebPImageFile(ImageFile.ImageFile): # Initialize seek state self._reset(reset=False) - def _getexif(self): + def _getexif(self) -> dict[str, Any] | None: if "exif" not in self.info: return None return self.getexif()._get_merged_dict() - def getxmp(self): - """ - Returns a dictionary containing the XMP tags. - Requires defusedxml to be installed. - - :returns: XMP tags in a dictionary. - """ - return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} - - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return # Set logical frame to requested position self.__logical_frame = frame - def _reset(self, reset=True): + def _reset(self, reset: bool = True) -> None: if reset: self._decoder.reset() self.__physical_frame = 0 @@ -143,7 +136,7 @@ class WebPImageFile(ImageFile.ImageFile): timestamp -= duration return data, timestamp, duration - def _seek(self, frame): + def _seek(self, frame: int) -> None: if self.__physical_frame == frame: return # Nothing to do if frame < self.__physical_frame: @@ -170,17 +163,17 @@ class WebPImageFile(ImageFile.ImageFile): return super().load() - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass - def tell(self): + def tell(self) -> int: if not _webp.HAVE_WEBPANIM: return super().tell() return self.__logical_frame -def _save_all(im, fp, filename): +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoderinfo = im.encoderinfo.copy() append_images = list(encoderinfo.get("append_images", [])) @@ -193,7 +186,7 @@ def _save_all(im, fp, filename): _save(im, fp, filename) return - background = (0, 0, 0, 0) + background: int | tuple[int, ...] = (0, 0, 0, 0) if "background" in encoderinfo: background = encoderinfo["background"] elif "background" in im.info: @@ -323,7 +316,7 @@ def _save_all(im, fp, filename): fp.write(data) -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: lossless = im.encoderinfo.get("lossless", False) quality = im.encoderinfo.get("quality", 80) alpha_quality = im.encoderinfo.get("alpha_quality", 100) diff --git a/.venv/Lib/site-packages/PIL/WmfImagePlugin.py b/.venv/Lib/site-packages/PIL/WmfImagePlugin.py index b5b8c69..3d5cddc 100644 --- a/.venv/Lib/site-packages/PIL/WmfImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/WmfImagePlugin.py @@ -20,6 +20,8 @@ # http://wvware.sourceforge.net/caolan/ora-wmf.html from __future__ import annotations +from typing import IO + from . import Image, ImageFile from ._binary import i16le as word from ._binary import si16le as short @@ -28,7 +30,7 @@ from ._binary import si32le as _long _handler = None -def register_handler(handler): +def register_handler(handler: ImageFile.StubHandler | None) -> None: """ Install application-specific WMF image handler. @@ -41,12 +43,12 @@ def register_handler(handler): if hasattr(Image.core, "drawwmf"): # install default handler (windows only) - class WmfHandler: - def open(self, im): + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: im._mode = "RGB" self.bbox = im.info["wmf_bbox"] - def load(self, im): + def load(self, im: ImageFile.StubImageFile) -> Image.Image: im.fp.seek(0) # rewind return Image.frombytes( "RGB", @@ -65,7 +67,7 @@ if hasattr(Image.core, "drawwmf"): # Read WMF file -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return ( 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_description = "Windows Metafile" - def _open(self): + def _open(self) -> None: self._inch = None # check placable header @@ -147,7 +149,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): if loader: loader.open(self) - def _load(self): + def _load(self) -> ImageFile.StubHandler | None: return _handler def load(self, dpi=None): @@ -161,7 +163,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): return super().load() -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if _handler is None or not hasattr(_handler, "save"): msg = "WMF save handler not installed" raise OSError(msg) diff --git a/.venv/Lib/site-packages/PIL/XbmImagePlugin.py b/.venv/Lib/site-packages/PIL/XbmImagePlugin.py index eee7274..6d11bbf 100644 --- a/.venv/Lib/site-packages/PIL/XbmImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/XbmImagePlugin.py @@ -70,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile): self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] -def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) diff --git a/.venv/Lib/site-packages/PIL/XpmImagePlugin.py b/.venv/Lib/site-packages/PIL/XpmImagePlugin.py index 3125f8d..8d56331 100644 --- a/.venv/Lib/site-packages/PIL/XpmImagePlugin.py +++ b/.venv/Lib/site-packages/PIL/XpmImagePlugin.py @@ -24,7 +24,7 @@ from ._binary import o8 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 */" @@ -36,7 +36,7 @@ class XpmImageFile(ImageFile.ImageFile): format = "XPM" format_description = "X11 Pixel Map" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) @@ -103,16 +103,13 @@ class XpmImageFile(ImageFile.ImageFile): 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 xsize, ysize = self.size - s = [None] * ysize - - for i in range(ysize): - s[i] = self.fp.readline()[1 : xsize + 1].ljust(xsize) + s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] return b"".join(s) diff --git a/.venv/Lib/site-packages/PIL/__pycache__/BdfFontFile.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/BdfFontFile.cpython-311.pyc index dcc9bff..1fd9b7d 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/BdfFontFile.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/BdfFontFile.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-311.pyc index 3c081ad..f958210 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/BlpImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-311.pyc index 8001a83..cff7cf6 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/BmpImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/BufrStubImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/BufrStubImagePlugin.cpython-311.pyc index 66d9275..2bd4792 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/BufrStubImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/BufrStubImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ContainerIO.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ContainerIO.cpython-311.pyc index d1c364b..6b5d525 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ContainerIO.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ContainerIO.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/CurImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/CurImagePlugin.cpython-311.pyc index c68f92d..1c91377 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/CurImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/CurImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-311.pyc index ae621f2..cd10ea5 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/DcxImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-311.pyc index d6bb48d..73088b9 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/DdsImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-311.pyc index 6f3fc7f..aaee6cc 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/EpsImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ExifTags.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ExifTags.cpython-311.pyc index 9a45a5b..eee8297 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ExifTags.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ExifTags.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/FitsImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/FitsImagePlugin.cpython-311.pyc index 4b9e0b3..c861dd2 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/FitsImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/FitsImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/FliImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/FliImagePlugin.cpython-311.pyc index a154034..30272ba 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/FliImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/FliImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/FontFile.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/FontFile.cpython-311.pyc index a497edd..2150040 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/FontFile.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/FontFile.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/FpxImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/FpxImagePlugin.cpython-311.pyc index ba9fcca..63c50ac 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/FpxImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/FpxImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/FtexImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/FtexImagePlugin.cpython-311.pyc index 25a0fbd..ec99100 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/FtexImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/FtexImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/GbrImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/GbrImagePlugin.cpython-311.pyc index 62ca8a2..4f188b0 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/GbrImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/GbrImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/GdImageFile.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/GdImageFile.cpython-311.pyc index 665e2b9..024d77d 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/GdImageFile.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/GdImageFile.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/GifImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/GifImagePlugin.cpython-311.pyc index d9b122f..6cc6133 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/GifImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/GifImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/GimpGradientFile.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/GimpGradientFile.cpython-311.pyc index 2f15536..258428e 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/GimpGradientFile.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/GimpGradientFile.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/GimpPaletteFile.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/GimpPaletteFile.cpython-311.pyc index dc5a089..d4a9738 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/GimpPaletteFile.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/GimpPaletteFile.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/GribStubImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/GribStubImagePlugin.cpython-311.pyc index 99d5f21..ac66d83 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/GribStubImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/GribStubImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/Hdf5StubImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/Hdf5StubImagePlugin.cpython-311.pyc index 94b9a5f..51cbbac 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/Hdf5StubImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/Hdf5StubImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/IcnsImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/IcnsImagePlugin.cpython-311.pyc index bef74f7..e1155a7 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/IcnsImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/IcnsImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/IcoImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/IcoImagePlugin.cpython-311.pyc index 7b50238..208d844 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/IcoImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/IcoImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImImagePlugin.cpython-311.pyc index 7cfbd0f..ffc1725 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/Image.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/Image.cpython-311.pyc index 837c380..c23fdaa 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/Image.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/Image.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageChops.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageChops.cpython-311.pyc index 9603612..a04ed91 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageChops.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageChops.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageCms.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageCms.cpython-311.pyc index 9281dd3..aa410a0 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageCms.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageCms.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageColor.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageColor.cpython-311.pyc index 93afa92..165d8e8 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageColor.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageColor.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageDraw.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageDraw.cpython-311.pyc index 68bdd37..3d2eb22 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageDraw.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageDraw.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageDraw2.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageDraw2.cpython-311.pyc index 344f9d5..d568bab 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageDraw2.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageDraw2.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageEnhance.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageEnhance.cpython-311.pyc index 54a35f8..7500dcc 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageEnhance.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageEnhance.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageFile.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageFile.cpython-311.pyc index 0c8a7e0..f512b70 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageFile.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageFile.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageFilter.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageFilter.cpython-311.pyc index 55efbca..9ecbffe 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageFilter.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageFilter.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageFont.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageFont.cpython-311.pyc index a51c8b1..8e15c61 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageFont.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageFont.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageGrab.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageGrab.cpython-311.pyc index 4aadf12..c3019f8 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageGrab.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageGrab.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageMath.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageMath.cpython-311.pyc index 4e79ca2..955ae8f 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageMath.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageMath.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageMode.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageMode.cpython-311.pyc index 5f3cb4b..b2d8395 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageMode.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageMode.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageMorph.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageMorph.cpython-311.pyc index 0e30b7b..7fd38e3 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageMorph.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageMorph.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageOps.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageOps.cpython-311.pyc index 7c83146..4585e25 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageOps.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageOps.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImagePalette.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImagePalette.cpython-311.pyc index 3376c71..12f1a9d 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImagePalette.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImagePalette.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImagePath.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImagePath.cpython-311.pyc index 181cda6..f50cf0a 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImagePath.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImagePath.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageQt.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageQt.cpython-311.pyc index e364098..27c1d12 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageQt.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageQt.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageSequence.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageSequence.cpython-311.pyc index 51c0b3f..d3a7cab 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageSequence.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageSequence.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageShow.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageShow.cpython-311.pyc index 4d032cc..3f10d5d 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageShow.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageShow.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageStat.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageStat.cpython-311.pyc index cb905ab..1eab330 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageStat.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageStat.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageTk.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageTk.cpython-311.pyc index e1ab57d..a90d3ec 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageTk.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageTk.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageTransform.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageTransform.cpython-311.pyc index c5ea846..f5950fe 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageTransform.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageTransform.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImageWin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImageWin.cpython-311.pyc index 7dc10c4..cb97c9e 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImageWin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImageWin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/ImtImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/ImtImagePlugin.cpython-311.pyc index e1ecffb..55e199c 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/ImtImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/ImtImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/IptcImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/IptcImagePlugin.cpython-311.pyc index 4eea4b4..6c24f83 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/IptcImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/IptcImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/Jpeg2KImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/Jpeg2KImagePlugin.cpython-311.pyc index 1a07215..3802bbf 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/Jpeg2KImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/Jpeg2KImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/JpegImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/JpegImagePlugin.cpython-311.pyc index 6bf1d62..90f0427 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/JpegImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/JpegImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/JpegPresets.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/JpegPresets.cpython-311.pyc index 377acb6..8df3d4d 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/JpegPresets.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/JpegPresets.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/McIdasImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/McIdasImagePlugin.cpython-311.pyc index b4af3ea..a190e28 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/McIdasImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/McIdasImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/MicImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/MicImagePlugin.cpython-311.pyc index 312d8fc..6794f43 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/MicImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/MicImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/MpegImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/MpegImagePlugin.cpython-311.pyc index 89fbc84..ae54fe7 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/MpegImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/MpegImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/MpoImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/MpoImagePlugin.cpython-311.pyc index 165af3e..61f4b4b 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/MpoImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/MpoImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/MspImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/MspImagePlugin.cpython-311.pyc index b00c61e..782fa37 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/MspImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/MspImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PSDraw.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PSDraw.cpython-311.pyc index 4b22eef..eadf934 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PSDraw.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PSDraw.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PaletteFile.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PaletteFile.cpython-311.pyc index 3dc05de..21ed3ec 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PaletteFile.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PaletteFile.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PalmImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PalmImagePlugin.cpython-311.pyc index be6e6e9..93e7c48 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PalmImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PalmImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PcdImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PcdImagePlugin.cpython-311.pyc index 7ee62ae..98536cd 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PcdImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PcdImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PcfFontFile.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PcfFontFile.cpython-311.pyc index 6def733..d884ce6 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PcfFontFile.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PcfFontFile.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PcxImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PcxImagePlugin.cpython-311.pyc index 2618847..12665b0 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PcxImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PcxImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PdfImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PdfImagePlugin.cpython-311.pyc index e12b09a..31c6f35 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PdfImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PdfImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PdfParser.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PdfParser.cpython-311.pyc index bd09b30..301e24f 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PdfParser.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PdfParser.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PixarImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PixarImagePlugin.cpython-311.pyc index 5bd6a62..470c0f3 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PixarImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PixarImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PngImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PngImagePlugin.cpython-311.pyc index 9dcb2bc..647d2aa 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PngImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PngImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PpmImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PpmImagePlugin.cpython-311.pyc index f3550d3..f9ffe6e 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PpmImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PpmImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PsdImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PsdImagePlugin.cpython-311.pyc index 075cd48..1bf2fb2 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PsdImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PsdImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/PyAccess.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/PyAccess.cpython-311.pyc index 66945da..e020be9 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/PyAccess.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/PyAccess.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/QoiImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/QoiImagePlugin.cpython-311.pyc index 5098952..a288d61 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/QoiImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/QoiImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/SgiImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/SgiImagePlugin.cpython-311.pyc index d5ac89f..9218d09 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/SgiImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/SgiImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/SpiderImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/SpiderImagePlugin.cpython-311.pyc index 1c10848..38c91d8 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/SpiderImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/SpiderImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/SunImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/SunImagePlugin.cpython-311.pyc index 47382a7..ba8755e 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/SunImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/SunImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/TarIO.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/TarIO.cpython-311.pyc index 971865a..e606ef2 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/TarIO.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/TarIO.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/TgaImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/TgaImagePlugin.cpython-311.pyc index d19d19e..72fbb93 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/TgaImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/TgaImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/TiffImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/TiffImagePlugin.cpython-311.pyc index 0f42c2a..583229f 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/TiffImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/TiffImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/TiffTags.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/TiffTags.cpython-311.pyc index 378c0ce..0620a35 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/TiffTags.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/TiffTags.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/WalImageFile.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/WalImageFile.cpython-311.pyc index bbb6fdb..b5eb40e 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/WalImageFile.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/WalImageFile.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/WebPImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/WebPImagePlugin.cpython-311.pyc index 7464c60..62c5894 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/WebPImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/WebPImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/WmfImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/WmfImagePlugin.cpython-311.pyc index ba31e2f..2c5f64e 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/WmfImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/WmfImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/XVThumbImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/XVThumbImagePlugin.cpython-311.pyc index d2b1d36..07c1c8e 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/XVThumbImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/XVThumbImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/XbmImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/XbmImagePlugin.cpython-311.pyc index f260e48..4a2dbab 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/XbmImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/XbmImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/XpmImagePlugin.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/XpmImagePlugin.cpython-311.pyc index 8de5a02..0275a7b 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/XpmImagePlugin.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/XpmImagePlugin.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/__init__.cpython-311.pyc index 85bd768..e3f4662 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/__init__.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/__main__.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/__main__.cpython-311.pyc index 1e118f9..98bf372 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/__main__.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/__main__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/_binary.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/_binary.cpython-311.pyc index 271dd92..8e6d7d9 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/_binary.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/_binary.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/_deprecate.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/_deprecate.cpython-311.pyc index 134a018..f441e13 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/_deprecate.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/_deprecate.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/_tkinter_finder.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/_tkinter_finder.cpython-311.pyc index b5b6523..bbf9dc6 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/_tkinter_finder.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/_tkinter_finder.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/_typing.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/_typing.cpython-311.pyc index 7d0d4d4..3bb31bc 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/_typing.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/_typing.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/_util.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/_util.cpython-311.pyc index 42bf261..3b493c1 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/_util.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/_util.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/_version.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/_version.cpython-311.pyc index 51e2c57..9c5d757 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/_version.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/_version.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/features.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/features.cpython-311.pyc index f5bcee3..f7bd6bd 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/features.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/features.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/__pycache__/report.cpython-311.pyc b/.venv/Lib/site-packages/PIL/__pycache__/report.cpython-311.pyc index dddf701..3b09234 100644 Binary files a/.venv/Lib/site-packages/PIL/__pycache__/report.cpython-311.pyc and b/.venv/Lib/site-packages/PIL/__pycache__/report.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/PIL/_imaging.cp311-win_amd64.pyd b/.venv/Lib/site-packages/PIL/_imaging.cp311-win_amd64.pyd index 8b09aa9..e2d2ad8 100644 Binary files a/.venv/Lib/site-packages/PIL/_imaging.cp311-win_amd64.pyd and b/.venv/Lib/site-packages/PIL/_imaging.cp311-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/PIL/_imaging.pyi b/.venv/Lib/site-packages/PIL/_imaging.pyi index e27843e..8cccd3a 100644 --- a/.venv/Lib/site-packages/PIL/_imaging.pyi +++ b/.venv/Lib/site-packages/PIL/_imaging.pyi @@ -1,3 +1,30 @@ from typing import Any +class ImagingCore: + def __getattr__(self, name: str) -> Any: ... + +class ImagingFont: + def __getattr__(self, name: str) -> Any: ... + +class ImagingDraw: + def __getattr__(self, name: str) -> Any: ... + +class PixelAccess: + def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: ... + def __setitem__( + self, xy: tuple[int, int], color: float | tuple[int, ...] + ) -> None: ... + +class ImagingDecoder: + def __getattr__(self, name: str) -> Any: ... + +class ImagingEncoder: + def __getattr__(self, name: str) -> Any: ... + +class _Outline: + def close(self) -> None: ... + def __getattr__(self, name: str) -> Any: ... + +def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ... +def outline() -> _Outline: ... def __getattr__(name: str) -> Any: ... diff --git a/.venv/Lib/site-packages/PIL/_imagingcms.cp311-win_amd64.pyd b/.venv/Lib/site-packages/PIL/_imagingcms.cp311-win_amd64.pyd index feb273d..17b226e 100644 Binary files a/.venv/Lib/site-packages/PIL/_imagingcms.cp311-win_amd64.pyd and b/.venv/Lib/site-packages/PIL/_imagingcms.cp311-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/PIL/_imagingcms.pyi b/.venv/Lib/site-packages/PIL/_imagingcms.pyi index 036521b..2abd6d0 100644 --- a/.venv/Lib/site-packages/PIL/_imagingcms.pyi +++ b/.venv/Lib/site-packages/PIL/_imagingcms.pyi @@ -2,7 +2,7 @@ import datetime import sys from typing import Literal, SupportsFloat, TypedDict -littlecms_version: str +littlecms_version: str | None _Tuple3f = tuple[float, float, float] _Tuple2x3f = tuple[_Tuple3f, _Tuple3f] @@ -108,10 +108,6 @@ class CmsProfile: def is_intent_supported(self, intent: int, direction: int, /) -> int: ... class CmsTransform: - @property - def inputMode(self) -> str: ... - @property - def outputMode(self) -> str: ... def apply(self, id_in: int, id_out: int) -> int: ... def profile_open(profile: str, /) -> CmsProfile: ... diff --git a/.venv/Lib/site-packages/PIL/_imagingft.cp311-win_amd64.pyd b/.venv/Lib/site-packages/PIL/_imagingft.cp311-win_amd64.pyd index 9920493..5e42b18 100644 Binary files a/.venv/Lib/site-packages/PIL/_imagingft.cp311-win_amd64.pyd and b/.venv/Lib/site-packages/PIL/_imagingft.cp311-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/PIL/_imagingft.pyi b/.venv/Lib/site-packages/PIL/_imagingft.pyi index e27843e..5e97b40 100644 --- a/.venv/Lib/site-packages/PIL/_imagingft.pyi +++ b/.venv/Lib/site-packages/PIL/_imagingft.pyi @@ -1,3 +1,69 @@ -from typing import Any +from typing import Any, TypedDict +from . import _imaging + +class _Axis(TypedDict): + minimum: int | None + default: int | None + maximum: int | None + name: bytes | None + +class Font: + @property + def family(self) -> str | None: ... + @property + def style(self) -> str | None: ... + @property + def ascent(self) -> int: ... + @property + def descent(self) -> int: ... + @property + def height(self) -> int: ... + @property + def x_ppem(self) -> int: ... + @property + def y_ppem(self) -> int: ... + @property + def glyphs(self) -> int: ... + def render( + self, + string: str | bytes, + fill, + mode=..., + dir=..., + features=..., + lang=..., + stroke_width=..., + anchor=..., + foreground_ink_long=..., + x_start=..., + y_start=..., + /, + ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... + def getsize( + self, + string: str | bytes | bytearray, + mode=..., + dir=..., + features=..., + lang=..., + anchor=..., + /, + ) -> tuple[tuple[int, int], tuple[int, int]]: ... + def getlength( + self, string: str | bytes, mode=..., dir=..., features=..., lang=..., / + ) -> float: ... + def getvarnames(self) -> list[bytes]: ... + def getvaraxes(self) -> list[_Axis] | None: ... + def setvarname(self, instance_index: int, /) -> None: ... + def setvaraxes(self, axes: list[float], /) -> None: ... + +def getfont( + filename: str | bytes, + size: float, + index=..., + encoding=..., + font_bytes=..., + layout_engine=..., +) -> Font: ... def __getattr__(name: str) -> Any: ... diff --git a/.venv/Lib/site-packages/PIL/_imagingmath.cp311-win_amd64.pyd b/.venv/Lib/site-packages/PIL/_imagingmath.cp311-win_amd64.pyd index ea27a30..d5c20f6 100644 Binary files a/.venv/Lib/site-packages/PIL/_imagingmath.cp311-win_amd64.pyd and b/.venv/Lib/site-packages/PIL/_imagingmath.cp311-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/PIL/_imagingmorph.cp311-win_amd64.pyd b/.venv/Lib/site-packages/PIL/_imagingmorph.cp311-win_amd64.pyd index e82f7f9..b9c223a 100644 Binary files a/.venv/Lib/site-packages/PIL/_imagingmorph.cp311-win_amd64.pyd and b/.venv/Lib/site-packages/PIL/_imagingmorph.cp311-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/PIL/_imagingtk.cp311-win_amd64.pyd b/.venv/Lib/site-packages/PIL/_imagingtk.cp311-win_amd64.pyd index cbe8932..8b2e32f 100644 Binary files a/.venv/Lib/site-packages/PIL/_imagingtk.cp311-win_amd64.pyd and b/.venv/Lib/site-packages/PIL/_imagingtk.cp311-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/PIL/_typing.py b/.venv/Lib/site-packages/PIL/_typing.py index 7075e86..09ece18 100644 --- a/.venv/Lib/site-packages/PIL/_typing.py +++ b/.venv/Lib/site-packages/PIL/_typing.py @@ -2,7 +2,14 @@ from __future__ import annotations import os import sys -from typing import Protocol, Sequence, TypeVar, Union +from typing import Any, Protocol, Sequence, TypeVar, Union + +try: + import numpy.typing as npt + + NumpyArray = npt.NDArray[Any] +except ImportError: + pass if sys.version_info >= (3, 10): from typing import TypeGuard @@ -10,7 +17,6 @@ else: try: from typing_extensions import TypeGuard except ImportError: - from typing import Any class TypeGuard: # type: ignore[no-redef] def __class_getitem__(cls, item: Any) -> type[bool]: diff --git a/.venv/Lib/site-packages/PIL/_version.py b/.venv/Lib/site-packages/PIL/_version.py index dbed769..cebfd86 100644 --- a/.venv/Lib/site-packages/PIL/_version.py +++ b/.venv/Lib/site-packages/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "10.3.0" +__version__ = "10.4.0" diff --git a/.venv/Lib/site-packages/PIL/_webp.cp311-win_amd64.pyd b/.venv/Lib/site-packages/PIL/_webp.cp311-win_amd64.pyd index 62fd33e..03d9166 100644 Binary files a/.venv/Lib/site-packages/PIL/_webp.cp311-win_amd64.pyd and b/.venv/Lib/site-packages/PIL/_webp.cp311-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/PIL/features.py b/.venv/Lib/site-packages/PIL/features.py index 8a0b140..13908c4 100644 --- a/.venv/Lib/site-packages/PIL/features.py +++ b/.venv/Lib/site-packages/PIL/features.py @@ -4,6 +4,7 @@ import collections import os import sys import warnings +from typing import IO import PIL @@ -18,7 +19,7 @@ modules = { } -def check_module(feature): +def check_module(feature: str) -> bool: """ Checks if a module is available. @@ -42,7 +43,7 @@ def check_module(feature): return False -def version_module(feature): +def version_module(feature: str) -> str | None: """ :param feature: The module to check for. :returns: @@ -54,13 +55,10 @@ def version_module(feature): module, ver = modules[feature] - if ver is None: - return None - return getattr(__import__(module, fromlist=[ver]), ver) -def get_supported_modules(): +def get_supported_modules() -> list[str]: """ :returns: A list of all supported modules. """ @@ -75,7 +73,7 @@ codecs = { } -def check_codec(feature): +def check_codec(feature: str) -> bool: """ Checks if a codec is available. @@ -89,10 +87,10 @@ def check_codec(feature): codec, lib = codecs[feature] - return codec + "_encoder" in dir(Image.core) + return f"{codec}_encoder" in dir(Image.core) -def version_codec(feature): +def version_codec(feature: str) -> str | None: """ :param feature: The codec to check for. :returns: @@ -105,7 +103,7 @@ def version_codec(feature): codec, lib = codecs[feature] - version = getattr(Image.core, lib + "_version") + version = getattr(Image.core, f"{lib}_version") if feature == "libtiff": return version.split("\n")[0].split("Version ")[1] @@ -113,7 +111,7 @@ def version_codec(feature): return version -def get_supported_codecs(): +def get_supported_codecs() -> list[str]: """ :returns: A list of all supported codecs. """ @@ -133,7 +131,7 @@ features = { } -def check_feature(feature): +def check_feature(feature: str) -> bool | None: """ Checks if a feature is available. @@ -157,7 +155,7 @@ def check_feature(feature): return None -def version_feature(feature): +def version_feature(feature: str) -> str | None: """ :param feature: The feature to check for. :returns: The version number as a string, or ``None`` if not available. @@ -174,14 +172,14 @@ def version_feature(feature): return getattr(__import__(module, fromlist=[ver]), ver) -def get_supported_features(): +def get_supported_features() -> list[str]: """ :returns: A list of all supported features. """ return [f for f in features if check_feature(f)] -def check(feature): +def check(feature: str) -> bool | None: """ :param feature: A module, codec, or feature name. :returns: @@ -199,7 +197,7 @@ def check(feature): return False -def version(feature): +def version(feature: str) -> str | None: """ :param feature: The module, codec, or feature to check for. @@ -215,7 +213,7 @@ def version(feature): return None -def get_supported(): +def get_supported() -> list[str]: """ :returns: A list of all supported modules, features, and codecs. """ @@ -226,7 +224,7 @@ def get_supported(): return ret -def pilinfo(out=None, supported_formats=True): +def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: """ Prints information about this installation of Pillow. This function can be called with ``python3 -m PIL``. @@ -247,9 +245,9 @@ def pilinfo(out=None, supported_formats=True): print("-" * 68, file=out) print(f"Pillow {PIL.__version__}", file=out) - py_version = sys.version.splitlines() - print(f"Python {py_version[0].strip()}", file=out) - for py_version in py_version[1:]: + py_version_lines = sys.version.splitlines() + print(f"Python {py_version_lines[0].strip()}", file=out) + for py_version in py_version_lines[1:]: print(f" {py_version.strip()}", file=out) print("-" * 68, file=out) print(f"Python executable is {sys.executable or 'unknown'}", file=out) @@ -285,9 +283,12 @@ def pilinfo(out=None, supported_formats=True): ("xcb", "XCB (X protocol)"), ]: if check(name): - if name == "jpg" and check_feature("libjpeg_turbo"): - v = "libjpeg-turbo " + version_feature("libjpeg_turbo") - else: + v: str | None = None + if name == "jpg": + libjpeg_turbo_version = version_feature("libjpeg_turbo") + if libjpeg_turbo_version is not None: + v = "libjpeg-turbo " + libjpeg_turbo_version + if v is None: v = version(name) if v is not None: version_static = name in ("pil", "jpg") diff --git a/.venv/Lib/site-packages/__pycache__/pylab.cpython-311.pyc b/.venv/Lib/site-packages/__pycache__/pylab.cpython-311.pyc index 31ccaa5..b3d40ad 100644 Binary files a/.venv/Lib/site-packages/__pycache__/pylab.cpython-311.pyc and b/.venv/Lib/site-packages/__pycache__/pylab.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/__pycache__/six.cpython-311.pyc b/.venv/Lib/site-packages/__pycache__/six.cpython-311.pyc index 08799ab..9379e05 100644 Binary files a/.venv/Lib/site-packages/__pycache__/six.cpython-311.pyc and b/.venv/Lib/site-packages/__pycache__/six.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc index 6f47798..8ec9b02 100644 Binary files a/.venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc and b/.venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc b/.venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc index 44dfd8a..4b0b672 100644 Binary files a/.venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc and b/.venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/__pycache__/__init__.cpython-311.pyc index 6663bdb..c30855a 100644 Binary files a/.venv/Lib/site-packages/contourpy/__pycache__/__init__.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/__pycache__/_version.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/__pycache__/_version.cpython-311.pyc index fbbdeb9..b08a847 100644 Binary files a/.venv/Lib/site-packages/contourpy/__pycache__/_version.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/__pycache__/_version.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/__pycache__/array.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/__pycache__/array.cpython-311.pyc index 53b9f5b..7001e0a 100644 Binary files a/.venv/Lib/site-packages/contourpy/__pycache__/array.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/__pycache__/array.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/__pycache__/chunk.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/__pycache__/chunk.cpython-311.pyc index 4bde312..6774e65 100644 Binary files a/.venv/Lib/site-packages/contourpy/__pycache__/chunk.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/__pycache__/chunk.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/__pycache__/convert.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/__pycache__/convert.cpython-311.pyc index d2d99ff..eebe820 100644 Binary files a/.venv/Lib/site-packages/contourpy/__pycache__/convert.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/__pycache__/convert.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/__pycache__/dechunk.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/__pycache__/dechunk.cpython-311.pyc index efb94b6..5bb086d 100644 Binary files a/.venv/Lib/site-packages/contourpy/__pycache__/dechunk.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/__pycache__/dechunk.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/__pycache__/enum_util.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/__pycache__/enum_util.cpython-311.pyc index d79c541..bbbacb8 100644 Binary files a/.venv/Lib/site-packages/contourpy/__pycache__/enum_util.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/__pycache__/enum_util.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/__pycache__/typecheck.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/__pycache__/typecheck.cpython-311.pyc index 7d9a26d..af76188 100644 Binary files a/.venv/Lib/site-packages/contourpy/__pycache__/typecheck.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/__pycache__/typecheck.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/__pycache__/types.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/__pycache__/types.cpython-311.pyc index 014c854..842501a 100644 Binary files a/.venv/Lib/site-packages/contourpy/__pycache__/types.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/__pycache__/types.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/util/__pycache__/__init__.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/util/__pycache__/__init__.cpython-311.pyc index ecc6001..e14c18b 100644 Binary files a/.venv/Lib/site-packages/contourpy/util/__pycache__/__init__.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/util/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/util/__pycache__/_build_config.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/util/__pycache__/_build_config.cpython-311.pyc index 89d7108..796e412 100644 Binary files a/.venv/Lib/site-packages/contourpy/util/__pycache__/_build_config.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/util/__pycache__/_build_config.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/util/__pycache__/bokeh_renderer.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/util/__pycache__/bokeh_renderer.cpython-311.pyc index de66e76..6e628b1 100644 Binary files a/.venv/Lib/site-packages/contourpy/util/__pycache__/bokeh_renderer.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/util/__pycache__/bokeh_renderer.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/util/__pycache__/bokeh_util.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/util/__pycache__/bokeh_util.cpython-311.pyc index df33753..14cc7bd 100644 Binary files a/.venv/Lib/site-packages/contourpy/util/__pycache__/bokeh_util.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/util/__pycache__/bokeh_util.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/util/__pycache__/data.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/util/__pycache__/data.cpython-311.pyc index 2d4b8a0..b43513b 100644 Binary files a/.venv/Lib/site-packages/contourpy/util/__pycache__/data.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/util/__pycache__/data.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/util/__pycache__/mpl_renderer.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/util/__pycache__/mpl_renderer.cpython-311.pyc index cdf285b..603607c 100644 Binary files a/.venv/Lib/site-packages/contourpy/util/__pycache__/mpl_renderer.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/util/__pycache__/mpl_renderer.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/util/__pycache__/mpl_util.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/util/__pycache__/mpl_util.cpython-311.pyc index 1ff86c8..1e23335 100644 Binary files a/.venv/Lib/site-packages/contourpy/util/__pycache__/mpl_util.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/util/__pycache__/mpl_util.cpython-311.pyc differ diff --git a/.venv/Lib/site-packages/contourpy/util/__pycache__/renderer.cpython-311.pyc b/.venv/Lib/site-packages/contourpy/util/__pycache__/renderer.cpython-311.pyc index 36a7cec..4b7ae5e 100644 Binary files a/.venv/Lib/site-packages/contourpy/util/__pycache__/renderer.cpython-311.pyc and b/.venv/Lib/site-packages/contourpy/util/__pycache__/renderer.cpython-311.pyc differ