File size: 4,643 Bytes
4dce182 e24bf15 4dce182 e24bf15 4dce182 e24bf15 4dce182 9d054d4 4dce182 e24bf15 4dce182 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | """Font bank over the openly licensed fonts fetched by scripts/fetch_fonts.py.
Families are split deterministically into train / val so evaluation measures unseen fonts.
"""
import zlib
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from fontTools.ttLib import TTFont
from PIL import ImageFont
MONO_HINTS = ("mono", "courier", "vt323", "inconsolata", "anonymouspro", "cousine", "sharetech", "dotgothic", "spacemono", "ptmono")
VAL_PERCENT = 15
# Maths / symbol fonts: never a line's text font, only the fallback for glyphs it lacks (in every split).
FALLBACK_FAMILIES = ("notosansmath", "stixtwotext")
@dataclass(frozen=True)
class FontFace:
path: str
family: str
mono: bool
italic: bool
variable: bool
bold_file: bool
@lru_cache(maxsize=None)
def coverage(path: str) -> frozenset:
font = TTFont(path, lazy=True, fontNumber=0)
cmap = font.getBestCmap() or {}
return frozenset(chr(c) for c in cmap)
@lru_cache(maxsize=4096)
def load_font(path: str, size: int, bold: bool) -> ImageFont.FreeTypeFont:
font = ImageFont.truetype(path, size)
if "[" in Path(path).name:
try:
axes = font.get_variation_axes()
values = []
for ax in axes:
name = ax.get("name", b"")
name = name.decode() if isinstance(name, bytes) else str(name)
if name.lower().startswith("weight"):
values.append(max(ax["minimum"], min(ax["maximum"], 700 if bold else 400)))
else:
values.append(ax["default"])
font.set_variation_by_axes(values)
except (OSError, KeyError):
pass
return font
class FontBank:
def __init__(self, root="fonts", split="train"):
faces, self.fallbacks = [], []
for fam_dir in sorted(p for p in Path(root).glob("*/*") if p.is_dir()):
fam = fam_dir.name
if fam in FALLBACK_FAMILIES:
self.fallbacks += [FontFace(str(f), fam, False, False, "[" in f.name, False)
for f in sorted(fam_dir.glob("*.[ot]tf")) if not f.name.startswith("._")
and "Italic" not in f.name and ("Bold" not in f.name or "[" in f.name)]
continue
is_val = zlib.crc32(fam.encode()) % 100 < VAL_PERCENT
if split != "all" and is_val != (split == "val"):
continue
for f in sorted(f for f in fam_dir.glob("*.[ot]tf") if not f.name.startswith("._")): # skip macOS metadata files
n = f.name
faces.append(FontFace(
path=str(f), family=fam,
mono=any(h in fam for h in MONO_HINTS),
italic="Italic" in n,
variable="[" in n,
bold_file="Bold" in n and not any(k in n for k in ("Semi", "Extra", "Ultra")),
))
if not faces:
raise FileNotFoundError(f"no fonts under {root}/ for split={split}; run scripts/fetch_fonts.py")
self.faces = faces
self.families = sorted({f.family for f in faces})
def pick(self, rng, mono=None, bold=False, italic_ok=False):
"""Returns (face, bold_flag). Prefers a real bold file, else a variable font's weight axis."""
fams = [f for f in self.families if mono is None or any(h in f for h in MONO_HINTS) == mono] or self.families
fam = fams[rng.integers(len(fams))]
cands = [f for f in self.faces if f.family == fam and (italic_ok or not f.italic)] or \
[f for f in self.faces if f.family == fam]
if bold:
bolds = [f for f in cands if f.bold_file]
if bolds:
return bolds[rng.integers(len(bolds))], False
var = [f for f in cands if f.variable]
if var:
return var[0], True
regular = [f for f in cands if not f.bold_file] or cands
return regular[rng.integers(len(regular))], False
@staticmethod
def font(face: FontFace, size: int, bold_axis: bool = False):
return load_font(face.path, int(size), bold_axis)
def fallback(self, ch: str):
"""A maths / symbol face that has `ch`, for glyphs the text font lacks (None when none has it)."""
for f in self.fallbacks:
if ch in coverage(f.path):
return f
return None
@staticmethod
def supported(face: FontFace, text: str) -> str:
cov = coverage(face.path)
return "".join(c if (c in cov or c == " ") else "" for c in text)
|