Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added type hints to FontFile and subclasses #7643

Merged
merged 4 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions Tests/test_fontfile.py
@@ -0,0 +1,12 @@
from __future__ import annotations
import pytest

from PIL import FontFile


def test_save(tmp_path):
tempname = str(tmp_path / "temp.pil")

font = FontFile.FontFile()
with pytest.raises(ValueError):
font.save(tempname)
21 changes: 16 additions & 5 deletions src/PIL/BdfFontFile.py
Expand Up @@ -22,6 +22,8 @@
"""
from __future__ import annotations

from typing import BinaryIO

from . import FontFile, Image

bdf_slant = {
Expand All @@ -36,7 +38,17 @@
bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}


def bdf_char(f):
def bdf_char(
f: BinaryIO,
) -> (
tuple[
str,
int,
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]],
Image.Image,
]
| None
):
# skip to STARTCHAR
while True:
s = f.readline()
Expand All @@ -56,13 +68,12 @@ def bdf_char(f):
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")

# load bitmap
bitmap = []
bitmap = bytearray()
while True:
s = f.readline()
if not s or s[:7] == b"ENDCHAR":
break
bitmap.append(s[:-1])
bitmap = b"".join(bitmap)
bitmap += s[:-1]

# The word BBX
# followed by the width in x (BBw), height in y (BBh),
Expand Down Expand Up @@ -92,7 +103,7 @@ def bdf_char(f):
class BdfFontFile(FontFile.FontFile):
"""Font file plugin for the X11 BDF format."""

def __init__(self, fp):
def __init__(self, fp: BinaryIO):
super().__init__()

s = fp.readline()
Expand Down
55 changes: 41 additions & 14 deletions src/PIL/FontFile.py
Expand Up @@ -16,13 +16,16 @@
from __future__ import annotations

import os
from typing import BinaryIO

from . import Image, _binary

WIDTH = 800


def puti16(fp, values):
def puti16(
fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int]
) -> None:
"""Write network order (big-endian) 16-bit sequence"""
for v in values:
if v < 0:
Expand All @@ -33,16 +36,34 @@ def puti16(fp, values):
class FontFile:
"""Base class for raster font file handlers."""

bitmap = None

def __init__(self):
self.info = {}
self.glyph = [None] * 256

def __getitem__(self, ix):
bitmap: Image.Image | None = None

def __init__(self) -> None:
self.info: dict[bytes, bytes | int] = {}
self.glyph: list[
tuple[
tuple[int, int],
tuple[int, int, int, int],
tuple[int, int, int, int],
Image.Image,
]
| None
] = [None] * 256

def __getitem__(
self, ix: int
) -> (
tuple[
tuple[int, int],
tuple[int, int, int, int],
tuple[int, int, int, int],
Image.Image,
]
| None
):
return self.glyph[ix]

def compile(self):
def compile(self) -> None:
"""Create metrics and bitmap"""

if self.bitmap:
Expand All @@ -51,7 +72,7 @@ def compile(self):
# create bitmap large enough to hold all data
h = w = maxwidth = 0
lines = 1
for glyph in self:
for glyph in self.glyph:
if glyph:
d, dst, src, im = glyph
h = max(h, src[3] - src[1])
Expand All @@ -65,13 +86,16 @@ def compile(self):
ysize = lines * h

if xsize == 0 and ysize == 0:
return ""
return

self.ysize = h

# paste glyphs into bitmap
self.bitmap = Image.new("1", (xsize, ysize))
self.metrics = [None] * 256
self.metrics: list[
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]]
| None
] = [None] * 256
x = y = 0
for i in range(256):
glyph = self[i]
Expand All @@ -88,12 +112,15 @@ def compile(self):
self.bitmap.paste(im.crop(src), s)
self.metrics[i] = d, dst, s

def save(self, filename):
def save(self, filename: str) -> None:
"""Save font"""

self.compile()

# font data
if not self.bitmap:
msg = "No bitmap created"
raise ValueError(msg)
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")

# font metrics
Expand All @@ -104,6 +131,6 @@ def save(self, filename):
for id in range(256):
m = self.metrics[id]
if not m:
puti16(fp, [0] * 10)
puti16(fp, (0,) * 10)
else:
puti16(fp, m[0] + m[1] + m[2])
10 changes: 5 additions & 5 deletions src/PIL/Image.py
Expand Up @@ -1194,7 +1194,7 @@ def copy(self) -> Image:

__copy__ = copy

def crop(self, box=None):
def crop(self, box=None) -> Image:
"""
Returns a rectangular region from this image. The box is a
4-tuple defining the left, upper, right, and lower pixel
Expand Down Expand Up @@ -1659,7 +1659,7 @@ def entropy(self, mask=None, extrema=None):
return self.im.entropy(extrema)
return self.im.entropy()

def paste(self, im, box=None, mask=None):
def paste(self, im, box=None, mask=None) -> None:
"""
Pastes another image into this image. The box argument is either
a 2-tuple giving the upper left corner, a 4-tuple defining the
Expand Down Expand Up @@ -2352,7 +2352,7 @@ def transform(x, y, matrix):
(w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor
)

def save(self, fp, format=None, **params):
def save(self, fp, format=None, **params) -> None:
"""
Saves this image under the given filename. If no format is
specified, the format to use is determined from the filename
Expand Down Expand Up @@ -2903,7 +2903,7 @@ def _check_size(size):
return True


def new(mode, size, color=0):
def new(mode, size, color=0) -> Image:
"""
Creates a new image with the given mode and size.

Expand Down Expand Up @@ -2942,7 +2942,7 @@ def new(mode, size, color=0):
return im._new(core.fill(mode, size, color))


def frombytes(mode, size, data, decoder_name="raw", *args):
def frombytes(mode, size, data, decoder_name="raw", *args) -> Image:
"""
Creates a copy of an image memory from pixel data in a buffer.

Expand Down
31 changes: 17 additions & 14 deletions src/PIL/PcfFontFile.py
Expand Up @@ -18,6 +18,7 @@
from __future__ import annotations

import io
from typing import BinaryIO, Callable

from . import FontFile, Image
from ._binary import i8
Expand Down Expand Up @@ -49,7 +50,7 @@
]


def sz(s, o):
def sz(s: bytes, o: int) -> bytes:
return s[o : s.index(b"\0", o)]


Expand All @@ -58,7 +59,7 @@ class PcfFontFile(FontFile.FontFile):

name = "name"

def __init__(self, fp, charset_encoding="iso8859-1"):
def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"):
self.charset_encoding = charset_encoding

magic = l32(fp.read(4))
Expand Down Expand Up @@ -104,7 +105,9 @@ def __init__(self, fp, charset_encoding="iso8859-1"):
bitmaps[ix],
)

def _getformat(self, tag):
def _getformat(
self, tag: int
) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]:
format, size, offset = self.toc[tag]

fp = self.fp
Expand All @@ -119,7 +122,7 @@ def _getformat(self, tag):

return fp, format, i16, i32

def _load_properties(self):
def _load_properties(self) -> dict[bytes, bytes | int]:
#
# font properties

Expand All @@ -138,18 +141,16 @@ def _load_properties(self):
data = fp.read(i32(fp.read(4)))

for k, s, v in p:
k = sz(data, k)
if s:
v = sz(data, v)
properties[k] = v
property_value: bytes | int = sz(data, v) if s else v
properties[sz(data, k)] = property_value

return properties

def _load_metrics(self):
def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]:
#
# font metrics

metrics = []
metrics: list[tuple[int, int, int, int, int, int, int, int]] = []

fp, format, i16, i32 = self._getformat(PCF_METRICS)

Expand Down Expand Up @@ -182,7 +183,9 @@ def _load_metrics(self):

return metrics

def _load_bitmaps(self, metrics):
def _load_bitmaps(
self, metrics: list[tuple[int, int, int, int, int, int, int, int]]
) -> list[Image.Image]:
#
# bitmap data

Expand All @@ -207,7 +210,7 @@ def _load_bitmaps(self, metrics):

data = fp.read(bitmapsize)

pad = BYTES_PER_ROW[padindex]
pad: Callable[[int], int] = BYTES_PER_ROW[padindex]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why this one is needed?

Checking with typing.reveal_type, I see that it can be inferred if you add the following at the top of the file:

BYTES_PER_ROW: list[Callable[[int], int]] = [ ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without it, python3 -m mypy --strict src/PIL/PcfFontFile.py gives

src/PIL/PcfFontFile.py:223: error: Call to untyped function (unknown) in typed context  [no-untyped-call]
                    Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize))
                                                                                 ^~~~~~~~~~
Found 1 error in 1 file (checked 1 source file)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your suggestion also works however, so I've pushed a commit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I was missing the --strict.

mode = "1;R"
if bitorder:
mode = "1"
Expand All @@ -222,7 +225,7 @@ def _load_bitmaps(self, metrics):

return bitmaps

def _load_encoding(self):
def _load_encoding(self) -> list[int | None]:
fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS)

first_col, last_col = i16(fp.read(2)), i16(fp.read(2))
Expand All @@ -233,7 +236,7 @@ def _load_encoding(self):
nencoding = (last_col - first_col + 1) * (last_row - first_row + 1)

# map character code to bitmap index
encoding = [None] * min(256, nencoding)
encoding: list[int | None] = [None] * min(256, nencoding)

encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)]

Expand Down
6 changes: 3 additions & 3 deletions src/PIL/_binary.py
Expand Up @@ -18,7 +18,7 @@
from struct import pack, unpack_from


def i8(c):
def i8(c) -> int:
return c if c.__class__ is int else c[0]


Expand Down Expand Up @@ -57,7 +57,7 @@ def si16be(c, o=0):
return unpack_from(">h", c, o)[0]


def i32le(c, o=0):
def i32le(c, o=0) -> int:
"""
Converts a 4-bytes (32 bits) string to an unsigned integer.

Expand Down Expand Up @@ -94,7 +94,7 @@ def o32le(i):
return pack("<I", i)


def o16be(i):
def o16be(i) -> bytes:
return pack(">H", i)


Expand Down