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

View File

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

View File

@ -109,3 +109,6 @@ def test_bitmapimage() -> None:
# reloaded = ImageTk.getimage(im_tk) # reloaded = ImageTk.getimage(im_tk)
# assert_image_equal(reloaded, im) # 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)) self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length: int) -> bytes: def _safe_read(self, length: int) -> bytes:
assert self.fd is not None
return ImageFile._safe_read(self.fd, length) return ImageFile._safe_read(self.fd, length)
def _read_palette(self) -> list[tuple[int, int, int, int]]: def _read_palette(self) -> list[tuple[int, int, int, int]]:

View File

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

View File

@ -485,7 +485,7 @@ class Parser:
self.image = im self.image = im
def __enter__(self): def __enter__(self) -> Parser:
return self return self
def __exit__(self, *args: object) -> None: def __exit__(self, *args: object) -> None:
@ -580,7 +580,7 @@ def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
encoder.cleanup() 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 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 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" msg = "Truncated File Read"
raise OSError(msg) raise OSError(msg)
return data return data
data = [] blocks: list[bytes] = []
remaining_size = size remaining_size = size
while remaining_size > 0: while remaining_size > 0:
block = fp.read(min(remaining_size, SAFEBLOCK)) block = fp.read(min(remaining_size, SAFEBLOCK))
if not block: if not block:
break break
data.append(block) blocks.append(block)
remaining_size -= len(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" msg = "Truncated File Read"
raise OSError(msg) raise OSError(msg)
return b"".join(data) return b"".join(blocks)
class PyCodecState: class PyCodecState:
@ -636,7 +636,7 @@ class PyCodec:
self.mode = mode self.mode = mode
self.init(args) self.init(args)
def init(self, args): def init(self, args) -> None:
""" """
Override to perform codec specific initialization Override to perform codec specific initialization
@ -653,7 +653,7 @@ class PyCodec:
""" """
pass pass
def setfd(self, fd): def setfd(self, fd) -> None:
""" """
Called from ImageFile to set the Python file-like object Called from ImageFile to set the Python file-like object
@ -793,7 +793,7 @@ class PyEncoder(PyCodec):
self.fd.write(data) self.fd.write(data)
return bytes_consumed, errcode 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 fh: File handle.
:param bufsize: Buffer size. :param bufsize: Buffer size.

View File

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

View File

@ -29,7 +29,7 @@ class BoxReader:
and to easily step into and read sub-boxes. 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.fp = fp
self.has_length = length >= 0 self.has_length = length >= 0
self.length = length self.length = length
@ -97,7 +97,7 @@ class BoxReader:
return tbox 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 """Parse the JPEG 2000 codestream to extract the size and component
count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
@ -137,7 +137,15 @@ def _res_to_dpi(num: int, denom: int, exp: int) -> float | None:
return (254 * num * (10**exp)) / (10000 * denom) 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, """Parse the JP2 header box to extract size, component count,
color space information, and optionally DPI information, color space information, and optionally DPI information,
returning a (size, mode, mimetype, dpi) tuple.""" returning a (size, mode, mimetype, dpi) tuple."""
@ -155,6 +163,7 @@ def _parse_jp2_header(fp):
elif tbox == b"ftyp": elif tbox == b"ftyp":
if reader.read_fields(">4s")[0] == b"jpx ": if reader.read_fields(">4s")[0] == b"jpx ":
mimetype = "image/jpx" mimetype = "image/jpx"
assert header is not None
size = None size = None
mode = None mode = None
@ -168,6 +177,9 @@ def _parse_jp2_header(fp):
if tbox == b"ihdr": if tbox == b"ihdr":
height, width, nc, bpc = header.read_fields(">IIHB") height, width, nc, bpc = header.read_fields(">IIHB")
assert isinstance(height, int)
assert isinstance(width, int)
assert isinstance(bpc, int)
size = (width, height) size = (width, height)
if nc == 1 and (bpc & 0x7F) > 8: if nc == 1 and (bpc & 0x7F) > 8:
mode = "I;16" mode = "I;16"
@ -185,11 +197,21 @@ def _parse_jp2_header(fp):
mode = "CMYK" mode = "CMYK"
elif tbox == b"pclr" and mode in ("L", "LA"): elif tbox == b"pclr" and mode in ("L", "LA"):
ne, npc = header.read_fields(">HB") ne, npc = header.read_fields(">HB")
bitdepths = header.read_fields(">" + ("B" * npc)) assert isinstance(ne, int)
if max(bitdepths) <= 8: 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() palette = ImagePalette.ImagePalette()
for i in range(ne): 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" mode = "P" if mode == "L" else "PA"
elif tbox == b"res ": elif tbox == b"res ":
res = header.read_boxes() res = header.read_boxes()
@ -197,6 +219,12 @@ def _parse_jp2_header(fp):
tres = res.next_box_type() tres = res.next_box_type()
if tres == b"resc": if tres == b"resc":
vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") 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) hres = _res_to_dpi(hrcn, hrcd, hrce)
vres = _res_to_dpi(vrcn, vrcd, vrce) vres = _res_to_dpi(vrcn, vrcd, vrce)
if hres is not None and vres is not None: 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) 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. # Application marker. Store these in the APP dictionary.
# Also look for well-known application markers. # Also look for well-known application markers.
@ -133,12 +133,13 @@ def APP(self, marker):
offset += 4 offset += 4
data = s[offset : offset + size] data = s[offset : offset + size]
if code == 0x03ED: # ResolutionInfo if code == 0x03ED: # ResolutionInfo
data = { photoshop[code] = {
"XResolution": i32(data, 0) / 65536, "XResolution": i32(data, 0) / 65536,
"DisplayedUnitsX": i16(data, 4), "DisplayedUnitsX": i16(data, 4),
"YResolution": i32(data, 8) / 65536, "YResolution": i32(data, 8) / 65536,
"DisplayedUnitsY": i16(data, 12), "DisplayedUnitsY": i16(data, 12),
} }
else:
photoshop[code] = data photoshop[code] = data
offset += size offset += size
offset += offset & 1 # align offset += offset & 1 # align
@ -338,6 +339,7 @@ class JpegImageFile(ImageFile.ImageFile):
# Create attributes # Create attributes
self.bits = self.layers = 0 self.bits = self.layers = 0
self._exif_offset = 0
# JPEG specifics (internal) # JPEG specifics (internal)
self.layer = [] self.layer = []
@ -498,17 +500,17 @@ class JpegImageFile(ImageFile.ImageFile):
): ):
self.info["dpi"] = 72, 72 self.info["dpi"] = 72, 72
def _getmp(self): def _getmp(self) -> dict[int, Any] | None:
return _getmp(self) return _getmp(self)
def _getexif(self) -> dict[str, Any] | None: def _getexif(self: JpegImageFile) -> dict[str, Any] | None:
if "exif" not in self.info: if "exif" not in self.info:
return None return None
return self.getexif()._get_merged_dict() return self.getexif()._get_merged_dict()
def _getmp(self): def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
# Extract MP information. This method was inspired by the "highly # Extract MP information. This method was inspired by the "highly
# experimental" _getexif version that's been in use for years now, # experimental" _getexif version that's been in use for years now,
# itself based on the ImageFileDirectory class in the TIFF plugin. # itself based on the ImageFileDirectory class in the TIFF plugin.
@ -616,7 +618,7 @@ samplings = {
# fmt: on # fmt: on
def get_sampling(im): def get_sampling(im: Image.Image) -> int:
# There's no subsampling when images have only 1 layer # There's no subsampling when images have only 1 layer
# (grayscale images) or when they are CMYK (4 layers), # (grayscale images) or when they are CMYK (4 layers),
# so set subsampling to the default value. # 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. # NOTE: currently Pillow can't encode JPEG to YCCK format.
# If YCCK support is added in the future, subsampling code will have # 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. # 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 return -1
sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
return samplings.get(sampling, -1) return samplings.get(sampling, -1)

View File

@ -22,7 +22,7 @@ from __future__ import annotations
import itertools import itertools
import os import os
import struct import struct
from typing import IO from typing import IO, Any, cast
from . import ( from . import (
Image, Image,
@ -101,8 +101,11 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
JpegImagePlugin.JpegImageFile._open(self) JpegImagePlugin.JpegImageFile._open(self)
self._after_jpeg_open() 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() 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.n_frames = self.mpinfo[0xB001]
self.__mpoffsets = [ self.__mpoffsets = [
mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
@ -149,7 +152,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
return self.__frame return self.__frame
@staticmethod @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 Transform the instance of JpegImageFile into
an instance of MpoImageFile. an instance of MpoImageFile.
@ -161,8 +167,9 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
double call to _open. double call to _open.
""" """
jpeg_instance.__class__ = MpoImageFile jpeg_instance.__class__ = MpoImageFile
jpeg_instance._after_jpeg_open(mpheader) mpo_instance = cast(MpoImageFile, jpeg_instance)
return jpeg_instance mpo_instance._after_jpeg_open(mpheader)
return mpo_instance
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

View File

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

View File

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

View File

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

View File

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