Skip to content

Commit

Permalink
Added type hints
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Jul 8, 2024
1 parent 94a8fcc commit 8a05e32
Show file tree
Hide file tree
Showing 15 changed files with 168 additions and 69 deletions.
16 changes: 12 additions & 4 deletions Tests/test_file_mpo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

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,
Expand All @@ -20,11 +20,11 @@
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)
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion Tests/test_imagefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def test_ico(self) -> None:
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")
Expand All @@ -103,6 +104,7 @@ def test_incremental_webp(self) -> None:
assert not p.image

p.feed(f.read())
assert p.image is not None
assert (128, 128) == p.image.size

@skip_unless_feature("zlib")
Expand Down Expand Up @@ -393,8 +395,9 @@ def test_encode(self) -> None:
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):
Expand Down
3 changes: 3 additions & 0 deletions Tests/test_imagetk.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,6 @@ def test_bitmapimage() -> None:

# reloaded = ImageTk.getimage(im_tk)
# assert_image_equal(reloaded, im)

with pytest.raises(ValueError):
ImageTk.BitmapImage()
1 change: 1 addition & 0 deletions src/PIL/BlpImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ def _read_blp_header(self) -> None:
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]]:
Expand Down
44 changes: 31 additions & 13 deletions src/PIL/BmpImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -92,7 +96,7 @@ def _bitmap(self, header=0, offset=0):
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
Expand Down Expand Up @@ -122,8 +126,9 @@ def _bitmap(self, header=0, offset=0):
)
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:
Expand All @@ -144,6 +149,10 @@ def _bitmap(self, header=0, offset=0):
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"],
Expand All @@ -164,24 +173,26 @@ def _bitmap(self, header=0, offset=0):
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),
Expand Down Expand Up @@ -213,23 +224,28 @@ def _bitmap(self, header=0, offset=0):
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"
raise OSError(msg)
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']})"
Expand All @@ -242,6 +258,7 @@ def _bitmap(self, header=0, offset=0):
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
Expand Down Expand Up @@ -269,10 +286,11 @@ def _bitmap(self, header=0, offset=0):

# ---------------------------- 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 = [
Expand Down
18 changes: 9 additions & 9 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ def feed(self, data):

self.image = im

def __enter__(self):
def __enter__(self) -> Parser:
return self

def __exit__(self, *args: object) -> None:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -636,7 +636,7 @@ def __init__(self, mode, *args):
self.mode = mode
self.init(args)

def init(self, args):
def init(self, args) -> None:
"""
Override to perform codec specific initialization
Expand All @@ -653,7 +653,7 @@ def cleanup(self) -> None:
"""
pass

def setfd(self, fd):
def setfd(self, fd) -> None:
"""
Called from ImageFile to set the Python file-like object
Expand Down Expand Up @@ -793,7 +793,7 @@ def encode_to_pyfd(self) -> tuple[int, int]:
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.
Expand Down
24 changes: 17 additions & 7 deletions src/PIL/ImageTk.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

import tkinter
from io import BytesIO
from typing import Any
from typing import TYPE_CHECKING, Any, cast

from . import Image, ImageFile

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 8a05e32

Please sign in to comment.