"""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:]