Source code for vibesketch.render.fonts

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