"""The reviewed Roboto font shared by publication SVG, PNG and PDF."""
from __future__ import annotations
import base64
import struct
import xml.etree.ElementTree as ET
from functools import lru_cache
from pathlib import Path
FONT = Path(__file__).with_name("fonts") / "Roboto-Regular.ttf"
NS = "{http://www.w3.org/2000/svg}"
@lru_cache(maxsize=1)
def _font() -> tuple[bytes, frozenset[int]]:
"""The packaged font's bytes and the code points it has glyphs for."""
data, glyphs, _, _ = _read()
return data, frozenset(glyphs)
@lru_cache(maxsize=1)
def _read() -> tuple[bytes, dict[int, int], list[int], int]:
"""Read Unicode cmap 4/12, horizontal metrics and units per em (OpenType).
https://learn.microsoft.com/en-us/typography/opentype/spec/cmap,
.../hhea, .../hmtx and .../head. Only this packaged, reviewed font is
read, never an input document's font. Returns the bytes, each code
point's glyph, each glyph's advance width and the units per em.
"""
data = FONT.read_bytes()
def u16(offset: int) -> int:
return int(struct.unpack_from(">H", data, offset)[0])
tables = {}
for i in range(u16(4)):
tag, _, offset, _ = struct.unpack_from(">4sIII", data, 12 + 16 * i)
tables[tag] = offset
cmap = tables[b"cmap"]
glyphs: dict[int, int] = {}
for i in range(u16(cmap + 2)):
platform, encoding, relative = struct.unpack_from(">HHI", data, cmap + 4 + 8 * i)
if platform != 0 and (platform, encoding) not in ((3, 1), (3, 10)):
continue
start = cmap + relative
fmt = u16(start)
if fmt == 4:
segments = u16(start + 6) // 2
for segment in range(segments):
last = u16(start + 14 + 2 * segment)
first = u16(start + 16 + 2 * segments + 2 * segment)
delta = u16(start + 16 + 4 * segments + 2 * segment)
address = start + 16 + 6 * segments + 2 * segment
distance = u16(address)
for code in range(first, last + 1):
glyph = u16(address + distance + 2 * (code - first)) if distance else code
if (glyph + delta) & 0xFFFF and (not distance or glyph):
glyphs[code] = (glyph + delta) & 0xFFFF
elif fmt == 12:
count = struct.unpack_from(">I", data, start + 12)[0]
for group in range(count):
first, last, glyph = struct.unpack_from(">III", data, start + 16 + 12 * group)
for code in range(first + (glyph == 0), last + 1):
glyphs[code] = glyph + code - first
metrics = u16(tables[b"hhea"] + 34)
advances = [u16(tables[b"hmtx"] + 4 * i) for i in range(metrics)]
return data, glyphs, advances, u16(tables[b"head"] + 18)
[docs]
def advance(text: str) -> float:
"""The width of one line of ``text`` in the publication font, in ems.
Advance widths only, without kerning; a character the font lacks counts
as a full em, as the renderer's own bounds do.
"""
_, glyphs, advances, units = _read()
width = 0
for char in text:
glyph = glyphs.get(ord(char))
if glyph is None:
width += units
else:
width += advances[min(glyph, len(advances) - 1)]
return width / units
[docs]
def check_text(svg: bytes) -> None:
from vibesketch.render import RenderRefused, RenderUnavailable
try:
_, glyphs = _font()
except (OSError, KeyError, struct.error) as error:
raise RenderUnavailable(
"The publication font is missing or damaged; reinstall vibesketch"
) from error
root = ET.fromstring(svg)
characters = set("".join("".join(node.itertext()) for node in root.iter(f"{NS}text")))
missing = sorted(
ord(char) for char in characters if char not in "\n\r\t" and ord(char) not in glyphs
)
if missing:
names = ", ".join(f"U+{code:04X}" for code in missing[:8])
raise RenderRefused(
f"publication font lacks glyphs {names}; replace the unsupported annotation characters"
)
[docs]
def embed_font(svg: bytes) -> bytes:
"""Make SVG annotations portable without relying on the viewer's fonts."""
if b"<text " not in svg:
return svg
check_text(svg)
encoded = base64.b64encode(_font()[0]).decode("ascii")
style = (
"<defs><style type='text/css'>@font-face {font-family: Roboto; "
f"src: url(data:font/ttf;base64,{encoded}) format('truetype'); "
"font-weight: 100 900;}</style></defs>"
)
position = svg.index(b">", svg.index(b"<svg")) + 1
return svg[:position] + style.encode("ascii") + svg[position:]
#: A glyph contour: points in font units, each with whether it is on the curve.
Contour = list[tuple[float, float, bool]]
[docs]
@lru_cache(maxsize=512)
def outline(glyph: int) -> tuple[Contour, ...]:
"""The contours of ``glyph`` in font units, composite glyphs resolved.
TrueType ``glyf``/``loca`` as the OpenType specification gives them:
https://learn.microsoft.com/en-us/typography/opentype/spec/glyf and
.../loca. Off-curve points are quadratic B-spline controls; two in a row
imply an on-curve point midway.
"""
data = _read()[0]
def u16(offset: int) -> int:
return int(struct.unpack_from(">H", data, offset)[0])
def i16(offset: int) -> int:
return int(struct.unpack_from(">h", data, offset)[0])
tables = {}
for i in range(u16(4)):
tag, _, offset, _ = struct.unpack_from(">4sIII", data, 12 + 16 * i)
tables[tag] = offset
long_offsets = i16(tables[b"head"] + 50) == 1
loca = tables[b"loca"]
if long_offsets:
start, end = struct.unpack_from(">II", data, loca + 4 * glyph)
else:
start, end = (2 * u16(loca + 2 * glyph), 2 * u16(loca + 2 * glyph + 2))
if start == end:
return ()
at = tables[b"glyf"] + start
contours = i16(at)
if contours >= 0:
ends = [u16(at + 10 + 2 * i) for i in range(contours)]
count = ends[-1] + 1 if ends else 0
cursor = at + 10 + 2 * contours
cursor += 2 + u16(cursor) # instructions
flags: list[int] = []
while len(flags) < count:
flag = data[cursor]
cursor += 1
repeat = 0
if flag & 0x08:
repeat = data[cursor]
cursor += 1
flags.extend([flag] * (repeat + 1))
values: list[list[int]] = []
for short, same in ((0x02, 0x10), (0x04, 0x20)):
position, coordinates = 0, []
for flag in flags[:count]:
if flag & short:
step = data[cursor]
cursor += 1
position += step if flag & same else -step
elif not flag & same:
position += i16(cursor)
cursor += 2
coordinates.append(position)
values.append(coordinates)
points = [
(float(x), float(y), bool(flag & 0x01))
for x, y, flag in zip(values[0], values[1], flags[:count], strict=True)
]
result, first = [], 0
for last in ends:
result.append(points[first : last + 1])
first = last + 1
return tuple(result)
found: list[Contour] = []
cursor = at + 10
while True:
flags_, component = u16(cursor), u16(cursor + 2)
cursor += 4
if flags_ & 0x01:
dx, dy = i16(cursor), i16(cursor + 2)
cursor += 4
else:
dx, dy = struct.unpack_from(">bb", data, cursor)
cursor += 2
if not flags_ & 0x02:
raise ValueError("composite glyphs placed by point matching are not supported")
a, b, c, d = 1.0, 0.0, 0.0, 1.0
if flags_ & 0x08:
a = d = i16(cursor) / 16384
cursor += 2
elif flags_ & 0x40:
a, d = i16(cursor) / 16384, i16(cursor + 2) / 16384
cursor += 4
elif flags_ & 0x80:
a, b, c, d = (i16(cursor + 2 * k) / 16384 for k in range(4))
cursor += 8
for contour in outline(component):
found.append([(a * x + c * y + dx, b * x + d * y + dy, on) for x, y, on in contour])
if not flags_ & 0x20:
return tuple(found)
[docs]
def glyph_of(char: str) -> tuple[int | None, float]:
"""The glyph of ``char`` and its advance width, in font units; None when missing."""
_, glyphs, advances, units = _read()
glyph = glyphs.get(ord(char))
if glyph is None:
return None, float(units)
return glyph, float(advances[min(glyph, len(advances) - 1)])
[docs]
def units_per_em() -> int:
return _read()[3]