Skip to content

Commit

Permalink
Merge pull request python-pillow#8234 from radarhere/type_hint
Browse files Browse the repository at this point in the history
Added type hints
  • Loading branch information
radarhere authored Jul 16, 2024
2 parents 2152a17 + 3eeef83 commit f19e07b
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 93 deletions.
3 changes: 3 additions & 0 deletions Tests/test_imagewin.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ def test_dib_mode_string(self) -> None:
# Assert
assert dib.size == (128, 128)

with pytest.raises(ValueError):
ImageWin.Dib(mode)

def test_dib_paste(self) -> None:
# Arrange
im = hopper()
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/EpsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def has_ghostscript() -> bool:
return gs_binary is not False


def Ghostscript(tile, size, fp, scale=1, transparency=False):
def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image:
"""Render an image using Ghostscript"""
global gs_binary
if not has_ghostscript():
Expand Down
86 changes: 46 additions & 40 deletions src/PIL/IcoImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import warnings
from io import BytesIO
from math import ceil, log
from typing import IO
from typing import IO, NamedTuple

from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16
Expand Down Expand Up @@ -119,8 +119,22 @@ def _accept(prefix: bytes) -> bool:
return prefix[:4] == _MAGIC


class IconHeader(NamedTuple):
width: int
height: int
nb_color: int
reserved: int
planes: int
bpp: int
size: int
offset: int
dim: tuple[int, int]
square: int
color_depth: int


class IcoFile:
def __init__(self, buf) -> None:
def __init__(self, buf: IO[bytes]) -> None:
"""
Parse image from file-like object containing ico file data
"""
Expand All @@ -141,51 +155,44 @@ def __init__(self, buf) -> None:
for i in range(self.nb_items):
s = buf.read(16)

icon_header = {
"width": s[0],
"height": s[1],
"nb_color": s[2], # No. of colors in image (0 if >=8bpp)
"reserved": s[3],
"planes": i16(s, 4),
"bpp": i16(s, 6),
"size": i32(s, 8),
"offset": i32(s, 12),
}

# See Wikipedia
for j in ("width", "height"):
if not icon_header[j]:
icon_header[j] = 256

# See Wikipedia notes about color depth.
# We need this just to differ images with equal sizes
icon_header["color_depth"] = (
icon_header["bpp"]
or (
icon_header["nb_color"] != 0
and ceil(log(icon_header["nb_color"], 2))
)
or 256
width = s[0] or 256
height = s[1] or 256

# No. of colors in image (0 if >=8bpp)
nb_color = s[2]
bpp = i16(s, 6)
icon_header = IconHeader(
width=width,
height=height,
nb_color=nb_color,
reserved=s[3],
planes=i16(s, 4),
bpp=i16(s, 6),
size=i32(s, 8),
offset=i32(s, 12),
dim=(width, height),
square=width * height,
# See Wikipedia notes about color depth.
# We need this just to differ images with equal sizes
color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256,
)

icon_header["dim"] = (icon_header["width"], icon_header["height"])
icon_header["square"] = icon_header["width"] * icon_header["height"]

self.entry.append(icon_header)

self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
self.entry = sorted(self.entry, key=lambda x: x.color_depth)
# ICO images are usually squares
self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True)
self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True)

def sizes(self) -> set[tuple[int, int]]:
"""
Get a list of all available icon sizes and color depths.
Get a set of all available icon sizes and color depths.
"""
return {(h["width"], h["height"]) for h in self.entry}
return {(h.width, h.height) for h in self.entry}

def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
for i, h in enumerate(self.entry):
if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
if size == h.dim and (bpp is False or bpp == h.color_depth):
return i
return 0

Expand All @@ -202,9 +209,9 @@ def frame(self, idx: int) -> Image.Image:

header = self.entry[idx]

self.buf.seek(header["offset"])
self.buf.seek(header.offset)
data = self.buf.read(8)
self.buf.seek(header["offset"])
self.buf.seek(header.offset)

im: Image.Image
if data[:8] == PngImagePlugin._MAGIC:
Expand All @@ -222,8 +229,7 @@ def frame(self, idx: int) -> Image.Image:
im.tile[0] = d, (0, 0) + im.size, o, a

# figure out where AND mask image starts
bpp = header["bpp"]
if 32 == bpp:
if header.bpp == 32:
# 32-bit color depth icon image allows semitransparent areas
# PIL's DIB format ignores transparency bits, recover them.
# The DIB is packed in BGRX byte order where X is the alpha
Expand Down Expand Up @@ -253,7 +259,7 @@ def frame(self, idx: int) -> Image.Image:
# padded row size * height / bits per char

total_bytes = int((w * im.size[1]) / 8)
and_mask_offset = header["offset"] + header["size"] - total_bytes
and_mask_offset = header.offset + header.size - total_bytes

self.buf.seek(and_mask_offset)
mask_data = self.buf.read(total_bytes)
Expand Down Expand Up @@ -307,7 +313,7 @@ class IcoImageFile(ImageFile.ImageFile):
def _open(self) -> None:
self.ico = IcoFile(self.fp)
self.info["sizes"] = self.ico.sizes()
self.size = self.ico.entry[0]["dim"]
self.size = self.ico.entry[0].dim
self.load()

@property
Expand Down
14 changes: 7 additions & 7 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -3286,7 +3286,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)


def fromqimage(im):
def fromqimage(im) -> ImageFile.ImageFile:
"""Creates an image instance from a QImage image"""
from . import ImageQt

Expand All @@ -3296,7 +3296,7 @@ def fromqimage(im):
return ImageQt.fromqimage(im)


def fromqpixmap(im):
def fromqpixmap(im) -> ImageFile.ImageFile:
"""Creates an image instance from a QPixmap image"""
from . import ImageQt

Expand Down Expand Up @@ -3867,7 +3867,7 @@ def _fixup_dict(self, src_dict):
# returns a dict with any single item tuples/lists as individual values
return {k: self._fixup(v) for k, v in src_dict.items()}

def _get_ifd_dict(self, offset, group=None):
def _get_ifd_dict(self, offset: int, group=None):
try:
# an offset pointer to the location of the nested embedded IFD.
# It should be a long, but may be corrupted.
Expand All @@ -3881,7 +3881,7 @@ def _get_ifd_dict(self, offset, group=None):
info.load(self.fp)
return self._fixup_dict(info)

def _get_head(self):
def _get_head(self) -> bytes:
version = b"\x2B" if self.bigtiff else b"\x2A"
if self.endian == "<":
head = b"II" + version + b"\x00" + o32le(8)
Expand Down Expand Up @@ -4102,16 +4102,16 @@ def __len__(self) -> int:
keys.update(self._info)
return len(keys)

def __getitem__(self, tag):
def __getitem__(self, tag: int):
if self._info is not None and tag not in self._data and tag in self._info:
self._data[tag] = self._fixup(self._info[tag])
del self._info[tag]
return self._data[tag]

def __contains__(self, tag) -> bool:
def __contains__(self, tag: object) -> bool:
return tag in self._data or (self._info is not None and tag in self._info)

def __setitem__(self, tag, value) -> None:
def __setitem__(self, tag: int, value) -> None:
if self._info is not None and tag in self._info:
del self._info[tag]
self._data[tag] = value
Expand Down
6 changes: 4 additions & 2 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,9 @@ def _save(im, fp, tile, bufsize: int = 0) -> None:
fp.flush()


def _encode_tile(im, fp, tile: list[_Tile], bufsize: int, fh, exc=None) -> None:
def _encode_tile(
im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None
) -> None:
for encoder_name, extents, offset, args in tile:
if offset > 0:
fp.seek(offset)
Expand Down Expand Up @@ -653,7 +655,7 @@ def cleanup(self) -> None:
"""
pass

def setfd(self, fd) -> None:
def setfd(self, fd: IO[bytes]) -> None:
"""
Called from ImageFile to set the Python file-like object
Expand Down
11 changes: 7 additions & 4 deletions src/PIL/ImageQt.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@

import sys
from io import BytesIO
from typing import Callable
from typing import TYPE_CHECKING, Callable

from . import Image
from ._util import is_path

if TYPE_CHECKING:
from . import ImageFile

qt_version: str | None
qt_versions = [
["6", "PyQt6"],
Expand Down Expand Up @@ -90,11 +93,11 @@ def fromqimage(im):
return Image.open(b)


def fromqpixmap(im):
def fromqpixmap(im) -> ImageFile.ImageFile:
return fromqimage(im)


def align8to32(bytes, width, mode):
def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
"""
converts each scanline of data from 8 bit to 32 bit aligned
"""
Expand Down Expand Up @@ -172,7 +175,7 @@ def _toqclass_helper(im):
if qt_is_installed:

class ImageQt(QImage):
def __init__(self, im):
def __init__(self, im) -> None:
"""
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
class.
Expand Down
28 changes: 18 additions & 10 deletions src/PIL/ImageWin.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@ class Dib:
"""

def __init__(
self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None
self, image: Image.Image | str, size: tuple[int, int] | None = None
) -> None:
if isinstance(image, str):
mode = image
image = ""
if size is None:
msg = "If first argument is mode, size is required"
raise ValueError(msg)
else:
mode = image.mode
size = image.size
Expand Down Expand Up @@ -105,7 +108,12 @@ def expose(self, handle):
result = self.image.expose(handle)
return result

def draw(self, handle, dst, src=None):
def draw(
self,
handle,
dst: tuple[int, int, int, int],
src: tuple[int, int, int, int] | None = None,
):
"""
Same as expose, but allows you to specify where to draw the image, and
what part of it to draw.
Expand All @@ -115,7 +123,7 @@ def draw(self, handle, dst, src=None):
the destination have different sizes, the image is resized as
necessary.
"""
if not src:
if src is None:
src = (0, 0) + self.size
if isinstance(handle, HWND):
dc = self.image.getdc(handle)
Expand Down Expand Up @@ -202,22 +210,22 @@ def __init__(
title, self.__dispatcher, width or 0, height or 0
)

def __dispatcher(self, action, *args):
def __dispatcher(self, action: str, *args):
return getattr(self, f"ui_handle_{action}")(*args)

def ui_handle_clear(self, dc, x0, y0, x1, y1):
def ui_handle_clear(self, dc, x0, y0, x1, y1) -> None:
pass

def ui_handle_damage(self, x0, y0, x1, y1):
def ui_handle_damage(self, x0, y0, x1, y1) -> None:
pass

def ui_handle_destroy(self) -> None:
pass

def ui_handle_repair(self, dc, x0, y0, x1, y1):
def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None:
pass

def ui_handle_resize(self, width, height):
def ui_handle_resize(self, width, height) -> None:
pass

def mainloop(self) -> None:
Expand All @@ -227,12 +235,12 @@ def mainloop(self) -> None:
class ImageWindow(Window):
"""Create an image window which displays the given image."""

def __init__(self, image, title="PIL"):
def __init__(self, image, title: str = "PIL") -> None:
if not isinstance(image, Dib):
image = Dib(image)
self.image = image
width, height = image.size
super().__init__(title, width=width, height=height)

def ui_handle_repair(self, dc, x0, y0, x1, y1):
def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None:
self.image.draw(dc, (x0, y0, x1, y1))
Loading

0 comments on commit f19e07b

Please sign in to comment.