Skip to content

feat: embed font via @font-face with pyftsubset subsetting - #8

Merged
jackspirou merged 1 commit into
agentstation:mainfrom
xav-ie:feat/svg-font-embedding
Oct 8, 2026
Merged

jackspirou merged 1 commit into
agentstation:mainfrom
xav-ie:feat/svg-font-embedding

Conversation

@xav-ie

@xav-ie xav-ie commented Feb 26, 2026

Copy link
Copy Markdown

Summary

  • Resolve the configured font family using fc-match, subset it to only the glyphs used in the SVG frames via pyftsubset (if available), and embed the result as a base64 @font-face rule
  • Falls back to embedding the full font file when pyftsubset is not installed
  • Only embeds when the user explicitly sets a font via Set FontFamily — the default comma-separated fallback stack is skipped to avoid a CSS @font-face name mismatch
  • Validates font file exists after fc-match for clear error messages
  • Sanitizes font family names in CSS against injection (\, ", {}, ;, <>, newlines)
  • Correct CSS format() hints for woff2, woff, opentype, and truetype

Test plan

  • TestSVGGenerator_FontFaceEmbed — @font-face present/absent based on FontData
  • TestSVGGenerator_FontFormatHint — correct format hint for all MIME types
  • TestSVGGenerator_FontFamilyEscaping — all special characters escaped correctly
  • TestResolveFont_EarlyReturns — skips empty, monospace, comma-separated, and default font family
  • TestResolveFont_WithFcMatch — resolves real fonts, handles nonexistent gracefully
  • Full test suite passes (go test ./...)

Resolve the configured font family using fc-match, subset it to only
the glyphs used in the SVG frames via pyftsubset (if available), and
embed the result as a base64 @font-face rule. Falls back to embedding
the full font file when pyftsubset is not installed.
@jackspirou

Copy link
Copy Markdown
Member

Closing as superseded for portable font embedding by the merged exact-font implementation.
Use --svg-font-file or VHS_SVG_FONT_FILE to select an embeddable WOFF2, WOFF, TrueType, or OpenType file.
VHS loads those exact bytes into the capture browser before recording and embeds them in the SVG.
The command-line option takes precedence over the environment variable.

Automatic fc-match lookup and pyftsubset subsetting were not merged.
The reviewed lookup selected a fallback with different character widths from the capture browser.
The subset omitted a glyph used by the window title.
The explicit file contract avoids these lookup and subset errors, without fontconfig or FontTools dependencies.
The caller remains responsible for the font embedding license.

@jackspirou jackspirou closed this Oct 8, 2026
@jackspirou jackspirou reopened this Oct 8, 2026
@jackspirou

Copy link
Copy Markdown
Member

Thank you, @xav-ie, for proposing automatic font discovery, glyph subsetting, graceful tool fallback, and safer font-family handling. I closed this PR too early: the manual font-file option does not provide the automatic setup or file-size benefits you proposed. I have reopened it. We are completing automatic selection before capture so the browser and SVG use the same face, optional subsetting with full-font fallback, title glyph coverage, and font-family escaping. Your contribution will remain in the replacement branch history. I will link the tested replacement and coverage evidence here before closing this PR again.

@jackspirou
jackspirou merged commit 60080a9 into agentstation:main Oct 8, 2026
@jackspirou

Copy link
Copy Markdown
Member

Thank you, @xav-ie, for proposing automatic font discovery and subsetting. The audit showed that explicit font files alone did not deliver those benefits. We reopened this PR and completed the missing behavior in PR 15.

The merged repair provides:

  • Automatic discovery of a named installed font through optional Fontconfig, with the selected bytes loaded before terminal sizing and capture.
  • Optional FontTools subsetting for terminal, cursor, and title glyphs. Missing, failed, or invalid subset output falls back to the full selected face.
  • Separate title fonts and the correct face from font collections. Generic families and fallback stacks keep their current meaning.
  • Safe font names across JavaScript, CSS, and XML. Resolution failures provide diagnostics.
  • Exact explicit file overrides, with flag priority over the environment path. Video-only capture also retains this existing behavior.

A real installed-font CLI capture produces a 9,601-byte SVG with two WOFF2 subsets. Glyphs and advances match the selected faces. A separate real evaluation reduces the base64 font payload from 231,000 to 5,000 bytes.

The final source passes 336 native race cases, 106 tape checks, and all eight hosted build and lint checks. Vet, lint, build, tidy, formatting, and pre-PR review also pass. Native rendering evidence covers macOS and Chrome. Linux and Windows native rendering remains unverified.

The replacement preserves your original commits. We used an owned branch because the contributor fork rejected the authenticated LFS lock check. Thank you for identifying the setup and file-size improvements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants