mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 09:26:16 +03:00
Merge pull request #8214 from radarhere/type_hint
This commit is contained in:
commit
6a9acfa5ca
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -109,3 +109,6 @@ def test_bitmapimage() -> None:
|
|||
|
||||
# reloaded = ImageTk.getimage(im_tk)
|
||||
# assert_image_equal(reloaded, im)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageTk.BitmapImage()
|
||||
|
|
|
@ -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]]:
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,12 +133,13 @@ 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),
|
||||
}
|
||||
else:
|
||||
photoshop[code] = data
|
||||
offset += size
|
||||
offset += offset & 1 # align
|
||||
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
3
src/PIL/_imagingtk.pyi
Normal file
|
@ -0,0 +1,3 @@
|
|||
from typing import Any
|
||||
|
||||
def __getattr__(name: str) -> Any: ...
|
Loading…
Reference in New Issue
Block a user