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