Skip to content

feat(preview): add resolveFamily to keep custom font families in svg mode - #462

Merged
baseballyama merged 5 commits into
mainfrom
feat/preview-resolve-family
Oct 16, 2026
Merged

baseballyama merged 5 commits into
mainfrom
feat/preview-resolve-family

Conversation

@baseballyama

@baseballyama baseballyama commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

renderSlideToSvg(…, { textLayout: 'svg' }) wrote every font other than the Calibri / Cambria / Arial / Times / Courier families as Carlito, so a host that shows the SVG as a sanitized <img> and embeds its own @font-face rules could not get Lora / Inter / etc. applied. This adds RenderSlideOptions.resolveFamily, which maps each authored font name to the family written to <text font-family> and passed to measureText.

Motivation

Closes #461

The hook already existed internally (SvgTextArgs.resolveFamily, used by the audit path and the foreignObject path); this exposes it for svg mode, as proposed in the issue. A fonts: [{ family, src }] option that also embeds @font-face was the alternative, but it would put font loading inside the library; the host already owns that.

Changes

  • New optional RenderSlideOptions.resolveFamily?: (family: string | null) => string. Consulted only when textLayout is 'svg'; defaults to substituteFamily, so output is unchanged when it is not passed.
  • It applies to shape text, bullets, table cell text, and the shrink-to-fit (<a:normAutofit/>) search, so the measurer sees the same family for fitting and for laying out.
  • A returned name that is not a sequence of CSS identifiers (e.g. M PLUS 1p) is quoted in the SVG; otherwise the browser would drop the whole font-family value. The measurer still receives the bare name. The default substitute names are identifiers, so default output is unchanged.
  • The foreignObject path ignores it — it already writes the authored name into a CSS font stack.
  • Fix in auditTextLayout: its shrink-to-fit search measured with the substitute family while its layout pass used the authored one; both now use the authored family.
  • Out of scope: the Node raster entry (renderSlideToImage) doesn't take the option yet — resvg matches a font file's internal family name, so keeping the emitted name, the resvg name and the measurer key consistent needs its own design.
  • README: documents svg mode and the new option.

Testing

  • test/preview-resolve-family.test.ts: default substitution unchanged; kept family is written to the SVG and handed to the measurer; table cells; autofit search measures with the resolved family; foreignObject output is identical with or without the option. quoting of non-identifier names; audit fit/layout family consistency. Each fails without the corresponding change (the autofit one fails if only the fit threading is removed).
  • npx vitest run (full suite), oxlint, oxfmt --check, tsc --noEmit pass locally.

Breaking changes

None

Checklist

  • I have read CLAUDE.md and followed the project's conventions.
  • I have added or updated tests for the change.
  • I have added or updated documentation where user-visible behavior changed.
  • If this is a breaking change, I have added a changeset / CHANGELOG entry and
    flagged it above.
  • I have re-read my own diff and removed dead code, debug prints, and stale
    comments.
  • If I used an LLM to draft this PR, I have verified each change myself, this
    PR represents real work that warrants a maintainer's review, and I am
    willing to defend each line in review.

🤖 Generated with Claude Code

…mode

In `textLayout: 'svg'`, every family outside the bundled substitutes was
written as Carlito, so a host embedding its own @font-face rules could not
get its fonts applied. `RenderSlideOptions.resolveFamily` now maps each
authored font name to the family written to the SVG and passed to
`measureText`, including in the shrink-to-fit search.

Closes #461

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 19:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

baseballyama and others added 4 commits October 17, 2026 04:56
… face

A family returned by `resolveFamily` that is not a sequence of CSS
identifiers (e.g. "M PLUS 1p") made the font-family value invalid, so it is
now quoted in the SVG. The audit's shrink-to-fit search now measures with
the same authored family its layout pass uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit quoted every family at emit time, which wrapped the
browser path's whole CSS stack (used by gradient-fill, reflection and bevel
overlays in default mode) as one family name. Quoting now happens where a
single resolved name becomes a piece, and the browser stack is written
verbatim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@baseballyama
baseballyama merged commit f403320 into main Oct 16, 2026
14 checks passed
@baseballyama
baseballyama deleted the feat/preview-resolve-family branch October 16, 2026 20:24
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.

feat: expose resolveFamily in RenderSlideOptions to keep custom font families (textLayout: "svg")

2 participants