Added type hints

This commit is contained in:
Andrew Murray 2024-07-08 20:09:45 +10:00
parent 94a8fccfa7
commit 8a05e32336
15 changed files with 168 additions and 69 deletions

View File

@ -2,11 +2,11 @@ from __future__ import annotations
import warnings
from io import BytesIO
from typing import Any, cast
from typing import Any
import pytest
from PIL import Image, MpoImagePlugin
from PIL import Image, ImageFile, MpoImagePlugin
from .helper import (
assert_image_equal,
@ -20,11 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg")
def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile:
def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
out = BytesIO()
im.save(out, "MPO", **options)
out.seek(0)
return cast(MpoImagePlugin.MpoImageFile, Image.open(out))
return Image.open(out)
@pytest.mark.parametrize("test_file", test_files)
@ -226,6 +226,12 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)
def test_ultra_hdr() -> None:
with Image.open("Tests/images/ultrahdr.jpg") as im:
assert im.format == "JPEG"
@ -275,6 +281,8 @@ def test_save_all() -> None:
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1)

View File

@ -90,6 +90,7 @@ class TestImageFile:
data = f.read()
with ImageFile.Parser() as p:
p.feed(data)
assert p.image is not None
assert (48, 48) == p.image.size
@skip_unless_feature("webp")
@ -103,6 +104,7 @@ class TestImageFile:
assert not p.image
p.feed(f.read())
assert p.image is not None
assert (128, 128) == p.image.size
@skip_unless_feature("zlib")
@ -393,8 +395,9 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd()
fh = BytesIO()
with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None)
encoder.encode_to_file(fh, 0)
def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError):

View File

@ -109,3 +109,6 @@ def test_bitmapimage() -> None:
# reloaded = ImageTk.getimage(im_tk)
# assert_image_equal(reloaded, im)
with pytest.raises(ValueError):
ImageTk.BitmapImage()

View File

@ -313,6 +313,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length: int) -> bytes:
assert self.fd is not None
return ImageFile._safe_read(self.fd, length)
def _read_palette(self) -> list[tuple[int, int, int, int]]:

View File

@ -25,7 +25,7 @@
from __future__ import annotations
import os
from typing import IO
from typing import IO, Any
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@ -72,16 +72,20 @@ class BmpImageFile(ImageFile.ImageFile):
for k, v in COMPRESSIONS.items():
vars()[k] = v
def _bitmap(self, header=0, offset=0):
def _bitmap(self, header: int = 0, offset: int = 0) -> None:
"""Read relevant info about the BMP"""
read, seek = self.fp.read, self.fp.seek
if header:
seek(header)
# read bmp header size @offset 14 (this is part of the header size)
file_info = {"header_size": i32(read(4)), "direction": -1}
file_info: dict[str, bool | int | tuple[int, ...]] = {
"header_size": i32(read(4)),
"direction": -1,
}
# -------------------- If requested, read header at a specific position
# read the rest of the bmp header, without its size
assert isinstance(file_info["header_size"], int)
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
# ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
@ -92,7 +96,7 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["height"] = i16(header_data, 2)
file_info["planes"] = i16(header_data, 4)
file_info["bits"] = i16(header_data, 6)
file_info["compression"] = self.RAW
file_info["compression"] = self.COMPRESSIONS["RAW"]
file_info["palette_padding"] = 3
# --------------------------------------------- Windows Bitmap v3 to v5
@ -122,8 +126,9 @@ class BmpImageFile(ImageFile.ImageFile):
)
file_info["colors"] = i32(header_data, 28)
file_info["palette_padding"] = 4
assert isinstance(file_info["pixels_per_meter"], tuple)
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
if file_info["compression"] == self.BITFIELDS:
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
masks = ["r_mask", "g_mask", "b_mask"]
if len(header_data) >= 48:
if len(header_data) >= 52:
@ -144,6 +149,10 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["a_mask"] = 0x0
for mask in masks:
file_info[mask] = i32(read(4))
assert isinstance(file_info["r_mask"], int)
assert isinstance(file_info["g_mask"], int)
assert isinstance(file_info["b_mask"], int)
assert isinstance(file_info["a_mask"], int)
file_info["rgb_mask"] = (
file_info["r_mask"],
file_info["g_mask"],
@ -164,24 +173,26 @@ class BmpImageFile(ImageFile.ImageFile):
self._size = file_info["width"], file_info["height"]
# ------- If color count was not found in the header, compute from bits
assert isinstance(file_info["bits"], int)
file_info["colors"] = (
file_info["colors"]
if file_info.get("colors", 0)
else (1 << file_info["bits"])
)
assert isinstance(file_info["colors"], int)
if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
offset += 4 * file_info["colors"]
# ---------------------- Check bit depth for unusual unsupported values
self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
if self.mode is None:
self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", ""))
if not self.mode:
msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
raise OSError(msg)
# ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw"
if file_info["compression"] == self.BITFIELDS:
SUPPORTED = {
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
SUPPORTED: dict[int, list[tuple[int, ...]]] = {
32: [
(0xFF0000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
@ -213,12 +224,14 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["bits"] == 32
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
):
assert isinstance(file_info["rgba_mask"], tuple)
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
self._mode = "RGBA" if "A" in raw_mode else self.mode
elif (
file_info["bits"] in (24, 16)
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
):
assert isinstance(file_info["rgb_mask"], tuple)
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
else:
msg = "Unsupported BMP bitfields layout"
@ -226,10 +239,13 @@ class BmpImageFile(ImageFile.ImageFile):
else:
msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
elif file_info["compression"] == self.RAW:
elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in (self.RLE8, self.RLE4):
elif file_info["compression"] in (
self.COMPRESSIONS["RLE8"],
self.COMPRESSIONS["RLE4"],
):
decoder_name = "bmp_rle"
else:
msg = f"Unsupported BMP compression ({file_info['compression']})"
@ -242,6 +258,7 @@ class BmpImageFile(ImageFile.ImageFile):
msg = f"Unsupported BMP Palette size ({file_info['colors']})"
raise OSError(msg)
else:
assert isinstance(file_info["palette_padding"], int)
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
grayscale = True
@ -269,10 +286,11 @@ class BmpImageFile(ImageFile.ImageFile):
# ---------------------------- Finally set the tile data for the plugin
self.info["compression"] = file_info["compression"]
args = [raw_mode]
args: list[Any] = [raw_mode]
if decoder_name == "bmp_rle":
args.append(file_info["compression"] == self.RLE4)
args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"])
else:
assert isinstance(file_info["width"], int)
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
args.append(file_info["direction"])
self.tile = [

View File

@ -485,7 +485,7 @@ class Parser:
self.image = im
def __enter__(self):
def __enter__(self) -> Parser:
return self
def __exit__(self, *args: object) -> None:
@ -580,7 +580,7 @@ def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
encoder.cleanup()
def _safe_read(fp, size):
def _safe_read(fp: IO[bytes], size: int) -> bytes:
"""
Reads large blocks in a safe way. Unlike fp.read(n), this function
doesn't trust the user. If the requested size is larger than
@ -601,18 +601,18 @@ def _safe_read(fp, size):
msg = "Truncated File Read"
raise OSError(msg)
return data
data = []
blocks: list[bytes] = []
remaining_size = size
while remaining_size > 0:
block = fp.read(min(remaining_size, SAFEBLOCK))
if not block:
break
data.append(block)
blocks.append(block)
remaining_size -= len(block)
if sum(len(d) for d in data) < size:
if sum(len(block) for block in blocks) < size:
msg = "Truncated File Read"
raise OSError(msg)
return b"".join(data)
return b"".join(blocks)
class PyCodecState:
@ -636,7 +636,7 @@ class PyCodec:
self.mode = mode
self.init(args)
def init(self, args):
def init(self, args) -> None:
"""
Override to perform codec specific initialization
@ -653,7 +653,7 @@ class PyCodec:
"""
pass
def setfd(self, fd):
def setfd(self, fd) -> None:
"""
Called from ImageFile to set the Python file-like object
@ -793,7 +793,7 @@ class PyEncoder(PyCodec):
self.fd.write(data)
return bytes_consumed, errcode
def encode_to_file(self, fh, bufsize):
def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
"""
:param fh: File handle.
:param bufsize: Buffer size.

View File

@ -28,7 +28,7 @@ from __future__ import annotations
import tkinter
from io import BytesIO
from typing import Any
from typing import TYPE_CHECKING, Any, cast
from . import Image, ImageFile
@ -61,7 +61,9 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
return Image.open(source)
def _pyimagingtkcall(command, photo, id):
def _pyimagingtkcall(
command: str, photo: PhotoImage | tkinter.PhotoImage, id: int
) -> None:
tk = photo.tk
try:
tk.call(command, photo, id)
@ -215,11 +217,14 @@ class BitmapImage:
:param image: A PIL image.
"""
def __init__(self, image=None, **kw):
def __init__(self, image: Image.Image | None = None, **kw: Any) -> None:
# Tk compatibility: file or data
if image is None:
image = _get_image_from_kw(kw)
if image is None:
msg = "Image is required"
raise ValueError(msg)
self.__mode = image.mode
self.__size = image.size
@ -278,18 +283,23 @@ def getimage(photo: PhotoImage) -> Image.Image:
return im
def _show(image, title):
def _show(image: Image.Image, title: str | None) -> None:
"""Helper for the Image.show method."""
class UI(tkinter.Label):
def __init__(self, master, im):
def __init__(self, master: tkinter.Toplevel, im: Image.Image) -> None:
self.image: BitmapImage | PhotoImage
if im.mode == "1":
self.image = BitmapImage(im, foreground="white", master=master)
else:
self.image = PhotoImage(im, master=master)
super().__init__(master, image=self.image, bg="black", bd=0)
if TYPE_CHECKING:
image = cast(tkinter._Image, self.image)
else:
image = self.image
super().__init__(master, image=image, bg="black", bd=0)
if not tkinter._default_root:
if not getattr(tkinter, "_default_root"):
msg = "tkinter not initialized"
raise OSError(msg)
top = tkinter.Toplevel()

View File

@ -29,7 +29,7 @@ class BoxReader:
and to easily step into and read sub-boxes.
"""
def __init__(self, fp, length=-1):
def __init__(self, fp: IO[bytes], length: int = -1) -> None:
self.fp = fp
self.has_length = length >= 0
self.length = length
@ -97,7 +97,7 @@ class BoxReader:
return tbox
def _parse_codestream(fp) -> tuple[tuple[int, int], str]:
def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]:
"""Parse the JPEG 2000 codestream to extract the size and component
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
@ -137,7 +137,15 @@ def _res_to_dpi(num: int, denom: int, exp: int) -> float | None:
return (254 * num * (10**exp)) / (10000 * denom)
def _parse_jp2_header(fp):
def _parse_jp2_header(
fp: IO[bytes],
) -> tuple[
tuple[int, int],
str,
str | None,
tuple[float, float] | None,
ImagePalette.ImagePalette | None,
]:
"""Parse the JP2 header box to extract size, component count,
color space information, and optionally DPI information,
returning a (size, mode, mimetype, dpi) tuple."""
@ -155,6 +163,7 @@ def _parse_jp2_header(fp):
elif tbox == b"ftyp":
if reader.read_fields(">4s")[0] == b"jpx ":
mimetype = "image/jpx"
assert header is not None
size = None
mode = None
@ -168,6 +177,9 @@ def _parse_jp2_header(fp):
if tbox == b"ihdr":
height, width, nc, bpc = header.read_fields(">IIHB")
assert isinstance(height, int)
assert isinstance(width, int)
assert isinstance(bpc, int)
size = (width, height)
if nc == 1 and (bpc & 0x7F) > 8:
mode = "I;16"
@ -185,11 +197,21 @@ def _parse_jp2_header(fp):
mode = "CMYK"
elif tbox == b"pclr" and mode in ("L", "LA"):
ne, npc = header.read_fields(">HB")
bitdepths = header.read_fields(">" + ("B" * npc))
if max(bitdepths) <= 8:
assert isinstance(ne, int)
assert isinstance(npc, int)
max_bitdepth = 0
for bitdepth in header.read_fields(">" + ("B" * npc)):
assert isinstance(bitdepth, int)
if bitdepth > max_bitdepth:
max_bitdepth = bitdepth
if max_bitdepth <= 8:
palette = ImagePalette.ImagePalette()
for i in range(ne):
palette.getcolor(header.read_fields(">" + ("B" * npc)))
color: list[int] = []
for value in header.read_fields(">" + ("B" * npc)):
assert isinstance(value, int)
color.append(value)
palette.getcolor(tuple(color))
mode = "P" if mode == "L" else "PA"
elif tbox == b"res ":
res = header.read_boxes()
@ -197,6 +219,12 @@ def _parse_jp2_header(fp):
tres = res.next_box_type()
if tres == b"resc":
vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
assert isinstance(vrcn, int)
assert isinstance(vrcd, int)
assert isinstance(hrcn, int)
assert isinstance(hrcd, int)
assert isinstance(vrce, int)
assert isinstance(hrce, int)
hres = _res_to_dpi(hrcn, hrcd, hrce)
vres = _res_to_dpi(vrcn, vrcd, vrce)
if hres is not None and vres is not None:

View File

@ -60,7 +60,7 @@ def Skip(self: JpegImageFile, marker: int) -> None:
ImageFile._safe_read(self.fp, n)
def APP(self, marker):
def APP(self: JpegImageFile, marker: int) -> None:
#
# Application marker. Store these in the APP dictionary.
# Also look for well-known application markers.
@ -133,13 +133,14 @@ def APP(self, marker):
offset += 4
data = s[offset : offset + size]
if code == 0x03ED: # ResolutionInfo
data = {
photoshop[code] = {
"XResolution": i32(data, 0) / 65536,
"DisplayedUnitsX": i16(data, 4),
"YResolution": i32(data, 8) / 65536,
"DisplayedUnitsY": i16(data, 12),
}
photoshop[code] = data
else:
photoshop[code] = data
offset += size
offset += offset & 1 # align
except struct.error:
@ -338,6 +339,7 @@ class JpegImageFile(ImageFile.ImageFile):
# Create attributes
self.bits = self.layers = 0
self._exif_offset = 0
# JPEG specifics (internal)
self.layer = []
@ -498,17 +500,17 @@ class JpegImageFile(ImageFile.ImageFile):
):
self.info["dpi"] = 72, 72
def _getmp(self):
def _getmp(self) -> dict[int, Any] | None:
return _getmp(self)
def _getexif(self) -> dict[str, Any] | None:
def _getexif(self: JpegImageFile) -> dict[str, Any] | None:
if "exif" not in self.info:
return None
return self.getexif()._get_merged_dict()
def _getmp(self):
def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
# Extract MP information. This method was inspired by the "highly
# experimental" _getexif version that's been in use for years now,
# itself based on the ImageFileDirectory class in the TIFF plugin.
@ -616,7 +618,7 @@ samplings = {
# fmt: on
def get_sampling(im):
def get_sampling(im: Image.Image) -> int:
# There's no subsampling when images have only 1 layer
# (grayscale images) or when they are CMYK (4 layers),
# so set subsampling to the default value.
@ -624,7 +626,7 @@ def get_sampling(im):
# NOTE: currently Pillow can't encode JPEG to YCCK format.
# If YCCK support is added in the future, subsampling code will have
# to be updated (here and in JpegEncode.c) to deal with 4 layers.
if not hasattr(im, "layers") or im.layers in (1, 4):
if not isinstance(im, JpegImageFile) or im.layers in (1, 4):
return -1
sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
return samplings.get(sampling, -1)

View File

@ -22,7 +22,7 @@ from __future__ import annotations
import itertools
import os
import struct
from typing import IO
from typing import IO, Any, cast
from . import (
Image,
@ -101,8 +101,11 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
JpegImagePlugin.JpegImageFile._open(self)
self._after_jpeg_open()
def _after_jpeg_open(self, mpheader=None):
def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
self.mpinfo = mpheader if mpheader is not None else self._getmp()
if self.mpinfo is None:
msg = "Image appears to be a malformed MPO file"
raise ValueError(msg)
self.n_frames = self.mpinfo[0xB001]
self.__mpoffsets = [
mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
@ -149,7 +152,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
return self.__frame
@staticmethod
def adopt(jpeg_instance, mpheader=None):
def adopt(
jpeg_instance: JpegImagePlugin.JpegImageFile,
mpheader: dict[int, Any] | None = None,
) -> MpoImageFile:
"""
Transform the instance of JpegImageFile into
an instance of MpoImageFile.
@ -161,8 +167,9 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
double call to _open.
"""
jpeg_instance.__class__ = MpoImageFile
jpeg_instance._after_jpeg_open(mpheader)
return jpeg_instance
mpo_instance = cast(MpoImageFile, jpeg_instance)
mpo_instance._after_jpeg_open(mpheader)
return mpo_instance
# ---------------------------------------------------------------------

View File

@ -230,6 +230,7 @@ class ChunkStream:
cids = []
assert self.fp is not None
while True:
try:
cid, pos, length = self.read()
@ -407,6 +408,7 @@ class PngStream(ChunkStream):
def chunk_iCCP(self, pos: int, length: int) -> bytes:
# ICC profile
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
# according to PNG spec, the iCCP chunk contains:
# Profile name 1-79 bytes (character string)
@ -434,6 +436,7 @@ class PngStream(ChunkStream):
def chunk_IHDR(self, pos: int, length: int) -> bytes:
# image header
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 13:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -471,6 +474,7 @@ class PngStream(ChunkStream):
def chunk_PLTE(self, pos: int, length: int) -> bytes:
# palette
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
self.im_palette = "RGB", s
@ -478,6 +482,7 @@ class PngStream(ChunkStream):
def chunk_tRNS(self, pos: int, length: int) -> bytes:
# transparency
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
if _simple_palette.match(s):
@ -498,6 +503,7 @@ class PngStream(ChunkStream):
def chunk_gAMA(self, pos: int, length: int) -> bytes:
# gamma setting
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
self.im_info["gamma"] = i32(s) / 100000.0
return s
@ -506,6 +512,7 @@ class PngStream(ChunkStream):
# chromaticity, 8 unsigned ints, actual value is scaled by 100,000
# WP x,y, Red x,y, Green x,y Blue x,y
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
raw_vals = struct.unpack(">%dI" % (len(s) // 4), s)
self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
@ -518,6 +525,7 @@ class PngStream(ChunkStream):
# 2 saturation
# 3 absolute colorimetric
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 1:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -529,6 +537,7 @@ class PngStream(ChunkStream):
def chunk_pHYs(self, pos: int, length: int) -> bytes:
# pixels per unit
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 9:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -546,6 +555,7 @@ class PngStream(ChunkStream):
def chunk_tEXt(self, pos: int, length: int) -> bytes:
# text
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
try:
k, v = s.split(b"\0", 1)
@ -554,17 +564,18 @@ class PngStream(ChunkStream):
k = s
v = b""
if k:
k = k.decode("latin-1", "strict")
k_str = k.decode("latin-1", "strict")
v_str = v.decode("latin-1", "replace")
self.im_info[k] = v if k == "exif" else v_str
self.im_text[k] = v_str
self.im_info[k_str] = v if k == b"exif" else v_str
self.im_text[k_str] = v_str
self.check_text_memory(len(v_str))
return s
def chunk_zTXt(self, pos: int, length: int) -> bytes:
# compressed text
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
try:
k, v = s.split(b"\0", 1)
@ -589,16 +600,17 @@ class PngStream(ChunkStream):
v = b""
if k:
k = k.decode("latin-1", "strict")
v = v.decode("latin-1", "replace")
k_str = k.decode("latin-1", "strict")
v_str = v.decode("latin-1", "replace")
self.im_info[k] = self.im_text[k] = v
self.check_text_memory(len(v))
self.im_info[k_str] = self.im_text[k_str] = v_str
self.check_text_memory(len(v_str))
return s
def chunk_iTXt(self, pos: int, length: int) -> bytes:
# international text
assert self.fp is not None
r = s = ImageFile._safe_read(self.fp, length)
try:
k, r = r.split(b"\0", 1)
@ -627,25 +639,27 @@ class PngStream(ChunkStream):
if k == b"XML:com.adobe.xmp":
self.im_info["xmp"] = v
try:
k = k.decode("latin-1", "strict")
lang = lang.decode("utf-8", "strict")
tk = tk.decode("utf-8", "strict")
v = v.decode("utf-8", "strict")
k_str = k.decode("latin-1", "strict")
lang_str = lang.decode("utf-8", "strict")
tk_str = tk.decode("utf-8", "strict")
v_str = v.decode("utf-8", "strict")
except UnicodeError:
return s
self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)
self.check_text_memory(len(v))
self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str)
self.check_text_memory(len(v_str))
return s
def chunk_eXIf(self, pos: int, length: int) -> bytes:
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
self.im_info["exif"] = b"Exif\x00\x00" + s
return s
# APNG chunks
def chunk_acTL(self, pos: int, length: int) -> bytes:
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 8:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -666,6 +680,7 @@ class PngStream(ChunkStream):
return s
def chunk_fcTL(self, pos: int, length: int) -> bytes:
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 26:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -695,6 +710,7 @@ class PngStream(ChunkStream):
return s
def chunk_fdAT(self, pos: int, length: int) -> bytes:
assert self.fp is not None
if length < 4:
if ImageFile.LOAD_TRUNCATED_IMAGES:
s = ImageFile._safe_read(self.fp, length)

View File

@ -185,7 +185,7 @@ def _layerinfo(fp, ct_bytes):
# read layerinfo block
layers = []
def read(size):
def read(size: int) -> bytes:
return ImageFile._safe_read(fp, size)
ct = si16(read(2))

View File

@ -115,7 +115,7 @@ class WebPImageFile(ImageFile.ImageFile):
self.__loaded = -1
self.__timestamp = 0
def _get_next(self):
def _get_next(self) -> tuple[bytes, int, int]:
# Get next frame
ret = self._decoder.get_next()
self.__physical_frame += 1

View File

@ -152,7 +152,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
def _load(self) -> ImageFile.StubHandler | None:
return _handler
def load(self, dpi=None):
def load(self, dpi: int | None = None) -> Image.core.PixelAccess | None:
if dpi is not None and self._inch is not None:
self.info["dpi"] = dpi
x0, y0, x1, y1 = self.info["wmf_bbox"]

3
src/PIL/_imagingtk.pyi Normal file
View File

@ -0,0 +1,3 @@
from typing import Any
def __getattr__(name: str) -> Any: ...