Add font embedding and introspection for HTML export#256
Draft
Add font embedding and introspection for HTML export#256
Conversation
Add FontSource, FontForHtml, FontKey types and extract_text_by_font() function that walks Vega scenegraph JSON to collect unique characters per (font, weight, style) combination. Handles multiline text arrays matching Vega's String([...]) comma-join behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New font_embed module with TTF → WOFF2 → base64 → @font-face CSS pipeline using the font-subset crate. Supports both Google Fonts (downloaded TTF) and local fonts (from fontdb). Includes unit tests with Caveat font for subsetting, encoding, and CSS generation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add format_variant_tuples(), font_cdn_url(), font_link_tag(), and font_import_rule() that generate Google Fonts CSS2 API URLs using actual chart variants (ital,wght@ tuples) instead of hardcoded weight/style combinations. Includes comprehensive unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add FontFormat enum, HtmlFontAnalysis, and the full HTML font pipeline: - classify_and_request_fonts() with prefer_cdn parameter (CDN-first for HTML, local-first for SVG/PNG/PDF) - classify_scenegraph_fonts() with explicit_google_families support to prevent false missing-font warnings for per-call overrides - render_scenegraph_for_html() private helper respecting per-call auto_google_fonts override - analyze_html_fonts() orchestrating font discovery and character extraction from the rendered scenegraph - vega_fonts()/vegalite_fonts() introspection API returning font metadata in 5 formats (Name, Url, LinkTag, ImportRule, FontFace) - build_font_head_html() injecting <link>/<style> tags into HTML output - VlConverterConfig.html_embed_local_fonts opt-in field Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add --embed-local-fonts option to vl2html and vg2html subcommands, wiring through to VlConverterConfig.html_embed_local_fonts for inline @font-face embedding of local system fonts in HTML output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add vegalite_fonts()/vega_fonts() sync and async Python bindings returning font metadata in 5 formats (name, url, link_tag, import_rule, font_face). Add html_embed_local_fonts config option. Update .pyi type stubs with docstrings for all new functions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t API vega_fonts/vegalite_fonts now return Vec<FontInfo> with per-family metadata (name, source, variants, url, link_tag, import_rule) instead of Vec<String> selected by a FontFormat enum. Each FontVariant carries an optional font_face field populated only when include_font_face=true, gating the expensive subsetting pipeline. build_font_head_html now consumes vega_fonts() internally, exercising the public API on every HTML export. Python bindings use pythonize to return list[FontInfo] as TypedDicts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pan/zoom interactions can reveal axis labels with digits that weren't in the initial view, so always include all numeric digits in subsetted fonts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fonts registered via register_google_fonts_font() were classified as local in the HTML pipeline because fontdb has no source tracking. Track registered Google Font families in a separate set so classify_scenegraph_fonts can emit FontSource::GoogleFonts with proper CDN URLs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the stringly-typed `source: String` field with `source:
FontSource`, a serde-tagged enum that serializes as
`{"type": "google", "font_id": "..."}` or `{"type": "local"}`.
Exposes the Google Fonts font ID to API consumers.
Rename `FontSource::GoogleFonts` to `FontSource::Google` for cleaner
serialization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When explicit google_fonts are provided, bundle mode is off, and no local font embedding is needed, build <link> tags directly from the font requests without rendering the scenegraph or invoking V8. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use CSS2 API range syntax (0,100..900;1,100..900) instead of enumerating specific weights — browsers only download variants they need so there is no bandwidth cost. Extend the CDN fast path to cover auto-google-fonts mode using static spec analysis, avoiding V8/scenegraph rendering for all non-embedding HTML exports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two additions to HTML export:
Font embedding — Google Fonts are referenced via CDN
<link>tags by default. In bundle mode, all fonts are subsetted to only the glyphs used in the chart, compressed to WOFF2, and inlined as base64@font-faceCSS for fully offline viewing. Local system fonts can also be embedded via an opt-in flag.Font introspection API — New
vegalite_fonts/vega_fontsfunctions return structuredFontInfometadata for each font used in a rendered spec, including per-variant weight/style data and optional@font-faceCSS blocks (gated byinclude_font_faceto avoid the expensive subsetting pipeline when not needed).Motivation
HTML export (
vl2html/vg2html) currently produces pages with no font information. When a chart uses non-system fonts (e.g. Google Fonts), the browser falls back to a default font or requires internet access. This makes HTML export unreliable for offline viewing, cross-machine consistency, and archival use cases.Font embedding behavior
bundleembed_local_fonts<link>tags<link>tags@font-face@font-face@font-face@font-faceBoth CDN and bundle modes are variant-aware: they use only the weight/style combinations actually present in the chart, via the Google Fonts CSS2 API
ital,wght@parameter syntax.How it works (bundle mode)
font-subsetcrate@font-faceCSS blocks and inject as<style>in the HTML<head>Limitations
font-subsetcrate only supports TrueType (TTF) outline fonts. Local CFF/OTF and TTC fonts are skipped with a warning (or error, depending onmissing_fontspolicy). Google Fonts always serves TTF, so this limitation does not affect them.Changes
Core library (
vl-convert-rs)font_embed.rsmodule — Font subsetting pipeline: TTF → WOFF2 → base64 →@font-faceCSSFontInfo/FontVariantstructs inextract.rs— Structured API types returned byvega_fonts/vegalite_fonts, withSerializefor Python interopFontSource/FontForHtml/FontKeytypes inextract.rs— Classify fonts asGoogleFontsorLocal;extract_text_by_font()Rust scenegraph walker collecting unique chars per (font, weight, style)classify_scenegraph_fonts()— Classifies fonts as Google or Local with CDN-first policy for HTMLvegaToTextByFontJS function — JS-side scenegraph walker for character extractionvega_fonts()/vegalite_fonts()API — ReturnVec<FontInfo>with structured per-family metadata (name, source, variants, url, link_tag, import_rule) and optional@font-faceCSS per variantbuild_font_head_html()consumesvega_fonts()internally — Exercises the public API on every HTML exportindex_font_face_blocks()infont_embed.rs— Parses generated CSS blocks back into a(family, weight, style) → CSSindex for attaching to the correctFontVariantvegalite_to_html/vega_to_htmlnow inject font<link>and/or<style>blocksVlConverterConfig.html_embed_local_fonts— Opt-in for local font embeddinghtml.rs—font_cdn_url(),font_link_tag(),font_import_rule()using actual chart variantsCLI
--embed-local-fontsonvl2html/vg2htmlsubcommands onlyPython
html_embed_local_fontsconfig optionvegalite_fonts()/vega_fonts()sync + async functions returninglist[FontInfo]viapythonizeFontInfoandFontVariantTypedDicts in type stubs (.pyi)include_font_faceparameter to gate subsetting pipelineDependencies
font-subsetv0.1 (withwoff2feature)Review tour
Data flow:
vegalite_to_html→build_font_head_html→vega_fonts→analyze_html_fonts→classify_scenegraph_fonts(font discovery) +extract_text_by_font(character extraction) →build_font_info→generate_font_face_css+index_font_face_blocks(subsetting/encoding)Start here:
vl-convert-rs/src/extract.rs—FontInfo/FontVariantpublic API types,FontSourceenum,FontForHtmlstruct,FontKey, andextract_text_by_font()Rust scenegraph walkerCore pipeline:
vl-convert-rs/src/font_embed.rs—TextByFontEntry/FontKeytypes →aggregate_chars_by_font_key()→generate_google_fonts_css()/generate_local_font_css()→index_font_face_blocks()vl-convert-rs/src/html.rs— Variant-aware CDN URL formatting helpersIntegration (converter.rs is large; focus on these sections):
vegaToTextByFontJS function (~line 1310)classify_scenegraph_fonts()(~line 3085)analyze_html_fonts()(~line 4266)vega_fonts()(~line 4345) /build_font_info()(~line 4375) /vegalite_fonts()(~line 4483)build_font_head_html()(~line 4512) — consumesvega_fonts()internallySurface area:
vl-convert/src/main.rs— CLI (small change)vl-convert-python/src/lib.rs— Python bindings (follows existing patterns)vl-convert-python/vl_convert.pyi— Type stubsTest plan
cargo fmtandcargo clippyclean--embed-local-fontswith system font, verify@font-faceblock in HTML sourceextract_text_by_fontand the fullFontFacepipeline🤖 Generated with Claude Code