Fonts and fidelity
Start with the installed package
Use the coordinated versions in the quickstart. Load fonts once per worker with
loadFonts({pack: 'office', scripts: 'auto', presentation}) from @openpresentation/opf-render/fonts-node,
and pass the same handle as {fonts} to preview and export. When reusing a worker for another deck,
ensure its required faces are loaded before rendering, and bound concurrency. The renderer's
soak benchmark
(npm run bench:soak) measured no memory growth over 500 jobs per worker. A worker holds about 1 GiB, peaking near
2 GiB on script-heavy decks, and its output is byte-identical on every repeat. Measure your own decks the same way.
For an authored Roboto deck, pack: 'base' avoids loading Office replacements. Neither loader reads system fonts by default.
For every catalog scheme's authored and rendered fonts, see the website's generated font scheme and export matrix, including PowerPoint, Keynote, LibreOffice, HTML, SVG, PNG and PDF.
In browsers, serve the renderer's pinned font files from your own origin and await the font handle's ensure(presentation)
before measuring or editing. The renderer README documents font copying and the player integration. Standalone SVG must
carry the used font bytes; script faces require embedScriptFonts: true in the Node loader. The CLI's default SVG mode
requests this automatically (--text fonts); --text system needs matching viewer fonts, and --text paths draws the glyphs as outlines so the SVG needs none. PPTX normally names the authored families
and requires them on the recipient's machine; it does not automatically embed the preview fonts.
The policy below is current. Version-specific measurements and implementation history are evidence for their named checkpoints, not transfer-size budgets for every release. The font roadmap records historical planning; use installed package APIs and measured output when choosing a deployment strategy.
Pass fonts, otherwise the estimate
Text measurement comes from one place: the fonts handle that opf-render's loadFonts() returns. Since RR-55 the same
handle carries it for every step that lays text out:
- core's
validate,paginateand the slide context (resolveSlideContext); - opf-render's
toSvg,toPngandtoPdf; - opf-pptx's
toPptx.
const fonts = await loadFonts({ pack: 'office' });
validate(deck, { fonts });
const { presentation } = paginate(deck, { fonts });
const svg = toSvg(presentation, { fonts });
const pptx = await toPptx(presentation, { fonts });Pass the same handle to preview, validation, pagination and export, so that all of them measure with the faces the
preview draws. Then line breaks, overflow findings, page breaks and the exported text boxes agree, and the same handle
gives identical geometry across engines (opf-pptx's layout-parity test checks that the exported PPTX paragraphs agree with the SVG preview and with
core's composed items, with and without fonts).
A call without fonts is not an error: it uses core's built-in estimate, and says so (checks.layout: "estimated" in
validate, layout: "estimated" in paginate). The estimate is deterministic and needs no font files: 0.54 em per
character, 0.62 em for capital letters and digits, 0.32 em for a space, 1 em for CJK characters, and zero width for
combining marks. It is close for Latin text in an ordinary text face and too narrow for some scripts (the tracking issue
is opf#566), so treat an estimated result as a draft and measure before you rely on a page break or an overflow finding.
Core's Node convert and the CLI's convert and paginate do this for you: they prepare an office-pack fonts handle once per process (opf validate loads none, so it is estimated). A library
call (validate, paginate, toSvg, toPptx) never loads fonts on its own, because that would change every
fonts-less result and pull font files into a browser bundle; pass the handle you want.
Font policy (FF-31)
OPF keeps one machine-readable font policy table, spec/reference/font-policy.json. Core exports it as FONT_POLICY, fontPolicyFor() and applyFontPolicyDecisions() from @openpresentation/opf or @openpresentation/opf/font-policy. Each row gives a family's license class, where viewers get it, whether OPF may ever embed it, and its preview replacement with a measured width difference. It also lists alternates, ending where possible with a face that already ships with opf-render. The licensing table explains the policy families; query FONT_POLICY.families for the installed set rather than relying on a copied count. font-policy.schema.json is its JSON Schema. The measurement evidence explains how each replacement was chosen.
The policy in brief (owner decisions, 2026-09-29). This is the canonical statement; the sibling repositories link here.
- The user's font selection is the source of truth. The user picks a font, for example Aptos or Calibri through a font scheme. That name is what the document, the theme and the PPTX carry.
- License-restricted (proprietary) fonts are never bundled or embedded, so previews cannot draw them. Openly licensed fonts such as Carlito and Roboto are bundled, and "licensed" below always means license-restricted. Live previews, SVG, the editor and gallery thumbnails use an open look-alike instead. The goal is a replacement that looks similar and is metric-compatible (same advance widths and line metrics), so text occupies the same size on screen and wraps as it does in PowerPoint. Calibri→Carlito is the model.
- Where no metric-compatible open replacement exists yet, a visual-only look-alike is a documented fallback. It is reported as visual and is a known layout-fidelity gap to close, not the intended end state. The parity scoreboard counts it as near, not perfect. The Aptos family is no longer such a case: Intos previews it as metric.
- PPTX export always writes the selected font name, for example
typeface="Aptos"in the theme and in runs, never the replacement. PowerPoint then opens the file and shows the actual font, installed or as a Microsoft 365 cloud font. - Only open fonts may be embedded, through the explicit embed path (FF-13).
- Font files are bundled, never hotlinked. Fonts, Google Fonts included, ship as pinned files (an exact npm version, or a vendored file with a recorded sha256) and are never loaded at runtime from a font CDN. That protects visitor privacy (a German court held the Google Fonts CDN a GDPR violation in 2022) and keeps previews offline-capable and audits reproducible. Build-time self-hosting such as
next/font/googleis not a hotlink but is not pinned, so usenext/font/localwith pinned files. Guards fail on CDN references in every repository. - Every bundled face records a verified permissive license. Allowed: exactly OFL-1.1, Apache-2.0, MIT and UFL-1.0; not GPL, LGPL, AGPL, proprietary or unclear public-domain fonts. The record has the SPDX id, any Reserved Font Name the copyright block declares, the source URL, package@version and sha256, all checked against the LICENSE file the font ships with. A modified version (subset, instance, conversion) may not use a Reserved Font Name in its name: a family whose served name contains its reserved name (Carlito, Raleway) is bundled only as the unmodified upstream file, while one that reserves another name (Noto Sans JP reserves "Source") may be modified. Details and enforcement: Font files: bundling and licenses.
Release status. Point 4 is published: opf-pptx 0.10.0 and later write the selected name into the PPTX, and the editor and pptx.gallery builds that depend on the published set export it. opf-pptx 0.9.1 and earlier wrote the substitute.
- Licensed, non-free fonts are never bundled or embedded. This covers Aptos, Calibri, Cambria, Segoe UI, Georgia, Tahoma, Grandview, Seaford, Tenorite, Consolas, Times New Roman, Arial, Courier New and the Windows script fonts. Rendering uses the family's designated open replacement instead:
- Metric-compatible is the goal, used where a replacement exists and measures identical: Calibri→Carlito, Arial→Arimo, Times New Roman→Tinos, Courier New→Cousine and Georgia→Gelasio. A metric row needs an upstream statement and a measurement in all four styles, with a mean width difference below 0.1% and no corpus string more than 0.3% off. Georgia→Gelasio passes only with Gelasio shaped with its
ligaandcligfeatures off (the row lists them asdisabledFeatures): with default features Gelasio ligates fi, fl, ffi and ffl, which Georgia does not, and runs differ by up to 1.02%. With them off every one of the 300 corpus strings matches in all four styles (mean and maximum below 0.01%). opf-render turns those features off in measurement and in SVG, so a renderer that does not is visual against Georgia. - Size adjustment (RR-38, opf#361): a visual replacement whose advances and glyphs are far from the real font's can carry
sizeAdjust, a preview-only font-size multiplier. A renderer scales the replacement's size by it when it measures and when it draws, so lines have the length PowerPoint's have. The exported font sizes and the PPTX font names are unchanged; an export that measures with the renderer's fonts breaks lines at the adjusted widths, as the preview does. It applies to runs of the family's script drawn in the row's replacement; Latin text in a scheme of that family is drawn by the replacement at the composed size. Six rows carry one: Arabic Typesetting 0.64 (RR-38, the corpus advance ratio), and, by the opf#361 rule (the smallest factor at which no line of the script corpus or of the scheme's Latin overhangs more than 3 percent natively), Angsana New 0.75, DilleniaUPC 0.68, Sakkal Majalla 0.89, Malgun Gothic 1.07 and Nirmala UI 1.07. Traditional Arabic, Ebrima and MS Gothic carry none: one factor cannot fit them (measurements).lineAscentandlineAscentMixed(em) say where PowerPoint puts the baseline below the top of a line box, for a line in the real font alone and for a line that also holds other fonts, measured in a native PowerPoint 365 probe: Angsana New 0.83 / 0.86, DilleniaUPC 0.82 / 0.85, Sakkal Majalla 0.76 / 0.81, Nirmala UI 0.97 / 0.94 and Arabic Typesetting 0.74 / 0.80 (corrected in opf#361 from RR-38's 0.70 / 0.78, which an earlier estimator read). A renderer that puts baselines one em below the line top moves such runs up by1 - lineAscentem; Malgun Gothic's baseline (1.06 em) sits below that, so it has none. Decomposed conjoining Hangul jamo (U+1100 to U+11FF, U+A960 to U+A97F, U+D7B0 to U+D7FF) are a known limitation: PowerPoint draws them unshaped and up to 1.9 times wider, which no factor fixes; precomposed Hangul, which real text uses, is what the Malgun Gothic factor is set on. The width that remains against PowerPoint is measured in the Arabic line-width residual. - Alternates are tried, in order, when the declared replacement's pack is not loaded. An alternate is always reported as visual, including on a metric row.
- Aptos family: Aptos→Intos, Aptos Display→Intos Display, Aptos Narrow→Intos Narrow and Aptos Serif→Intos Serif are metric: 0.000% mean and maximum against Aptos 2.01 in all four styles, with equal vertical metrics.
- Otherwise the closest measured open face, marked visual: a documented fallback and a known layout-fidelity gap until a metric-compatible replacement exists. For example, Segoe UI→Red Hat Display measures a 1.72% mean width difference.
- Metric-compatible is the goal, used where a replacement exists and measures identical: Calibri→Carlito, Arial→Arimo, Times New Roman→Tinos, Courier New→Cousine and Georgia→Gelasio. A metric row needs an upstream statement and a measurement in all four styles, with a mean width difference below 0.1% and no corpus string more than 0.3% off. Georgia→Gelasio passes only with Gelasio shaped with its
- The PPTX always names the chosen family, and no font file is included. The exporter writes
Aptoswhen the document chose Aptos. PowerPoint resolves standard fonts on the viewer's machine: those shipped with Office, Windows or macOS, and Microsoft 365 cloud fonts. The replacement never reaches the package (opf-pptxtest/export-chosen-fonts.mjs). - Openly licensed fonts render as themselves. Examples are Roboto, Carlito and the Noto script families. They can reach a PPTX only through an explicit embed path (FF-13), never by default.
- Families that are proprietary and non-standard, or missing from the table, keep their name in the PPTX.
fontAvailabilityDiagnostics()reports that viewers may lack them. It also flags Microsoft 365 cloud-only fonts (such as Aptos) and families that ship only in an optional Windows language feature.
Which faces are available
BUNDLED_FONT_MANIFEST from @openpresentation/opf-render/fonts-node is authoritative for the installed renderer's families, faces, packs, licenses and hashes. Font packages can be optional peers: a declared face still needs its pinned files installed and served. Neither Node nor browser loading discovers system fonts by default.
- Base pack: Roboto and Roboto Mono.
- Office pack: the Office replacements, including Carlito, Caladea, Arimo, Tinos, Cousine and Gelasio, and the Aptos replacements Intos, Intos Display, Intos Narrow and Intos Serif.
- Open pack: the open catalog families and replacement targets, including Red Hat Display for Segoe UI and Red Hat Text for Tahoma. Node
pack: 'office'includes this pack and the base pack by default;includeOpenFonts: falseandincludeBaseFonts: falseopt out. There is no separate proposedcatalogpack to install. - Script packs: pinned Noto and special-purpose faces, loaded for the scripts a presentation needs with
scripts: 'auto', or explicitly by script codes or'all'.
Node loaders verify the bundled manifest; browser hosts serve the package files and load the needed faces on demand, as described below. Optional script-font packages remain explicit host dependencies.
The registry tries an exact supplied family first, then an explicit alias, the permitted policy replacement and its alternates, and an explicit generic fallback if configured. Alternates are visual even when the preferred replacement is metric. Inspect fonts.substitutions for the actual route. substitutionPolicy: 'visual' permits approximate replacements; 'metric' restricts them to metric replacements, with the policy's explicitly documented Cambria fallback; 'none' disables policy substitutions but does not disable explicit aliases or an explicit fallback.
| Environment | Font resolution | What the viewer needs |
|---|---|---|
| Node, cloud or serverless | Supplied exact faces or loaded open replacements, controlled by the registry policy | Pinned font files available to the process |
| Browser/editor | The same registry rules, with the host serving the required faces and awaiting ensure(presentation) | Self-hosted files, not a font CDN |
| PowerPoint, exported PPTX | Authored font names; normal export does not embed fonts | Installed fonts or supported Office cloud fonts; otherwise PowerPoint substitutes |
| Keynote, imported PPTX | Authored names when available to Keynote | Installed fonts; Keynote handles missing-font substitution |
| LibreOffice Impress, PPTX | Authored names, subject to local replacement settings | Installed fonts or system/LibreOffice substitutions |
Native substitutions depend on application version, operating system, installed fonts and user settings. OPF cannot promise one fixed replacement for a missing native font. See PowerPoint cloud fonts, Keynote missing fonts, and LibreOffice replacement settings. There is no native OPF .key exporter: import PPTX into Keynote, then save there if needed.
HTML and rendered exports
The renderer's toHtml() from /element emits inline SVG and supports fontMode: 'standalone' (faces in each slide), 'shared' (rules once for the returned markup), and 'external' (matching self-hosted page CSS). It uses the same rendered faces as toSvg; the file converter and CLI do not have an .html target. Node script faces need embedScriptFonts: true for self-contained SVG or shared HTML. When the element upgrades in a browser, supply the same pinned faces through its font root/handle.
SVG with text: 'fonts' embeds the used faces. text: 'system' names the rendered families without their files and requires matching fonts in the page/viewer. text: 'paths' draws glyph outlines from those faces, so visible text needs no installed font; exceptional runs remain text with a diagnostic. PNG bakes the same rendered faces into pixels. PDF is vector with embedded/subset fonts and selectable text by default; raster PDF contains page images. Vector embedding depends on usable outlines and embedding permissions. These formats preserve the preview's replacement appearance; they do not switch back to the authored proprietary font.
The website's font scheme and export matrix is generated from the installed Gallery catalog and resolved FONT_POLICY on every build. Its rows, counts, code defaults, replacement tiers and size adjustments are not maintained by hand. The hosted guide, Copy as Markdown, raw agent guide and llms-full.txt use the same projection. The marker below is expanded by the website; this repository retains the source prose.
Font scheme and export matrix
Generated from the installed Gallery catalog and core font policy whenever the website builds: 89 schemes, 168 policy families. Heading/body routes: 30 same-font, 5 metric-compatible, 54 with visual replacements. These are policy routes, not a new native-application acceptance run.
What each format uses
| Format or viewer | Font used | Font delivery and missing-font behavior |
|---|---|---|
| PPTX in PowerPoint | Authored heading/body/code families | Normal export names fonts without embedding files. Available installed or supported Office cloud fonts draw as themselves; missing fonts are substituted by PowerPoint. |
| PPTX imported into Keynote | Authored families, when available to Keynote | Keynote handles missing-font substitution. OPF does not prescribe the replacement. |
| PPTX in LibreOffice Impress | Authored families, when available | System and LibreOffice replacement settings control substitution, and can override an installed font. |
| Native .key | No native OPF .key export | Import the PPTX into Keynote. Saving it as .key is a subsequent Keynote conversion. |
| Browser/editor preview | Rendered families in the table below | The renderer loads supplied/self-hosted faces; it does not discover system fonts by default. |
| HTML, toHtml() | Rendered families in inline SVG | standalone embeds faces per slide; shared emits rules once for the returned markup; external needs matching page CSS and self-hosted files. The converter/CLI does not have an .html file target. |
| SVG, text: fonts (default) | Rendered families | Embeds used font bytes. Node script faces need embedScriptFonts: true; the CLI prepares this automatically. |
| SVG, text: system | Rendered family names | No font bytes; matching viewer/page fonts are required or the viewer substitutes. |
| SVG, text: paths | Glyph outlines from rendered families | Visible glyphs need no installed font. Outlined glyphs cannot be edited as text; exceptional runs can remain text with a diagnostic. |
| PNG | Rendered families baked into pixels | No recipient font dependency. |
| Vector PDF (default) | Rendered families embedded/subset in the PDF | Selectable text; font files must permit embedding and be supported by the PDF writer. |
| Raster PDF | Rendered families baked into page images | No recipient font dependency; page text is an image. |
See PowerPoint cloud fonts, Keynote missing fonts, and LibreOffice replacement settings.
Every supported scheme
One family means both heading and body; otherwise the order is heading / body. Same font means no family replacement. Metric means qualified compatibility for measured faces and features, not identical letter shapes. Visual is an approximate look-alike and can differ in widths and wrapping. Unlisted requires caller-supplied files or an explicit host alias; it is never counted as exact.
The rendered column applies when the designated target faces are installed and loaded, with visual substitution enabled where needed. Custom exact faces take precedence. A smaller pack can choose a declared alternate (visual) or fail. Font weights/styles, language-specific slots and glyph fallback can add or change faces. The code column includes the default Roboto Mono when a scheme omits its code role. Size adjustments affect applicable script runs in previews/rendered exports; PPTX keeps authored sizes and names. Adjustment labels H/B mean heading/body; replacement weights are before bold styling. Scroll the table horizontally to see every column on a narrow screen.
| Scheme · ID | PPTX heading / body | Preview, HTML, SVG, PNG, PDF heading / body | Match heading / body | Code: PPTX → rendered | Adjustments |
|---|---|---|---|---|---|
Angsana New · angsana-new | Angsana New | Noto Sans Thai | Visual | Roboto Mono → Roboto Mono | H/B script ×0.75 |
Aparajita · aparajita | Aparajita | Noto Sans Devanagari | Visual | Roboto Mono → Roboto Mono | — |
Aptos · aptos | Aptos Display / Aptos | Intos Display / Intos | Metric | Roboto Mono → Roboto Mono | — |
Arabic Typesetting · arabic-typesetting | Arabic Typesetting | Noto Naskh Arabic | Visual | Roboto Mono → Roboto Mono | H/B script ×0.64 |
Arial · arial | Arial Black / Arial | Montserrat / Arimo | Visual / Metric | Roboto Mono → Roboto Mono | H weight 900 |
Batang · batang | BatangChe / Batang | Noto Sans KR | Visual | Roboto Mono → Roboto Mono | — |
Bookman · bookman | Bookman Old Style | Libre Caslon Text | Visual | Roboto Mono → Roboto Mono | — |
Calibri · calibri | Calibri | Carlito | Metric | Roboto Mono → Roboto Mono | — |
Century Schoolbook · century-schoolbook | Century Schoolbook | Gelasio | Visual | Roboto Mono → Roboto Mono | — |
Consolas · consolas | Consolas | Cousine | Visual | Consolas → Cousine | — |
Constantia · constantia | Constantia | PT Serif | Visual | Roboto Mono → Roboto Mono | — |
Courier New · courier-new | Courier New | Cousine | Metric | Courier New → Cousine | — |
DaunPenh · daunpenh | DaunPenh | Noto Sans Khmer | Visual | Roboto Mono → Roboto Mono | — |
David · david | David | Noto Serif Hebrew | Visual | Roboto Mono → Roboto Mono | — |
DilleniaUPC · dilleniaupc | DilleniaUPC | Noto Sans Thai | Visual | Roboto Mono → Roboto Mono | H/B script ×0.68 |
FangSong · fangsong | FangSong | Noto Sans SC | Visual | Roboto Mono → Roboto Mono | — |
Garamond · garamond | Garamond | EB Garamond | Visual | Roboto Mono → Roboto Mono | — |
Gautami · gautami | Gautami | Noto Sans Telugu | Visual | Roboto Mono → Roboto Mono | — |
Georgia · georgia | Georgia | Gelasio | Metric | Roboto Mono → Roboto Mono | — |
Gisha · gisha | Gisha | Noto Sans Hebrew | Visual | Roboto Mono → Roboto Mono | — |
Grandview · grandview | Grandview Display / Grandview | Barlow | Visual | Roboto Mono → Roboto Mono | — |
Gungsuh · gungsuh | GungsuhChe / Gungsuh | Noto Sans KR | Visual | Roboto Mono → Roboto Mono | — |
Impact · impact | Impact / Grandview | Anton / Barlow | Visual | Roboto Mono → Roboto Mono | — |
Kalinga · kalinga | Kalinga | Noto Sans Oriya | Visual | Roboto Mono → Roboto Mono | — |
Kartika · kartika | Kartika | Noto Sans Malayalam | Visual | Roboto Mono → Roboto Mono | — |
Khmer UI · khmer-ui | Khmer UI | Noto Sans Khmer | Visual | Roboto Mono → Roboto Mono | — |
Latha · latha | Latha | Noto Sans Tamil | Visual | Roboto Mono → Roboto Mono | — |
Lucida Sans · lucida-sans | Lucida Sans | Work Sans | Visual | Roboto Mono → Roboto Mono | — |
Malgun Gothic · malgun-gothic | Malgun Gothic | Noto Sans KR | Visual | Roboto Mono → Roboto Mono | H/B script ×1.07 |
Mangal · mangal | Mangal | Noto Sans Devanagari | Visual | Roboto Mono → Roboto Mono | — |
Meiryo · meiryo | Meiryo | Noto Sans JP | Visual | Roboto Mono → Roboto Mono | — |
Microsoft JhengHei · microsoft-jhenghei | Microsoft JhengHei | Noto Sans TC | Visual | Roboto Mono → Roboto Mono | — |
Microsoft Yahei · microsoft-yahei | Microsoft YaHei | Noto Sans SC | Visual | Roboto Mono → Roboto Mono | — |
MingLiU · mingliu | MingLiU | Noto Sans TC | Visual | Roboto Mono → Roboto Mono | — |
Miriam · miriam | Miriam | Noto Sans Hebrew | Visual | Roboto Mono → Roboto Mono | — |
Montserrat · montserrat | Montserrat | Montserrat | Same font | Roboto Mono → Roboto Mono | — |
MS Mincho · ms-mincho | MS Mincho | Noto Sans JP | Visual | Roboto Mono → Roboto Mono | — |
Nirmala UI · nirmala-ui | Nirmala UI | Noto Sans Devanagari | Visual | Roboto Mono → Roboto Mono | H/B script ×1.07 |
Noto Naskh Arabic · noto-naksh-arabic | Noto Naskh Arabic | Noto Naskh Arabic | Same font | Roboto Mono → Roboto Mono | — |
Noto Nastaliq Urdu · noto-nastaliq-urdu | Noto Nastaliq Urdu | Noto Nastaliq Urdu | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans · noto-sans | Noto Sans | Noto Sans | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Arabic · noto-sans-arabic | Noto Naskh Arabic | Noto Naskh Arabic | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Armenian · noto-sans-armenian | Noto Sans Armenian | Noto Sans Armenian | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Bengali · noto-sans-bengali | Noto Sans Bengali | Noto Sans Bengali | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Devanagari · noto-sans-devangari | Noto Sans Devanagari | Noto Sans Devanagari | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Ethiopic · noto-sans-ethiopic | Noto Sans Ethiopic | Noto Sans Ethiopic | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Georgian · noto-sans-georgian | Noto Sans Georgian | Noto Sans Georgian | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Gujarati · noto-sans-gujarati | Noto Sans Gujarati | Noto Sans Gujarati | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Gurmukhi · noto-sans-gurmukhi | Noto Sans Gurmukhi | Noto Sans Gurmukhi | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Hebrew · noto-sans-hebrew | Noto Sans Hebrew | Noto Sans Hebrew | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans JP · noto-sans-jp | Noto Sans JP | Noto Sans JP | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Kannada · noto-sans-kannada | Noto Sans Kannada | Noto Sans Kannada | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Khmer · noto-sans-khmer | Noto Sans Khmer | Noto Sans Khmer | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans KR · noto-sans-kr | Noto Sans KR | Noto Sans KR | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Malayalam · noto-sans-malayalam | Noto Sans Malayalam | Noto Sans Malayalam | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Mongolian · noto-sans-mongolian | Noto Sans Mongolian | Noto Sans Mongolian | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Oriya · noto-sans-oriya | Noto Sans Oriya | Noto Sans Oriya | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans SC · noto-sans-sc | Noto Sans SC | Noto Sans SC | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Tamil · noto-sans-tamil | Noto Sans Tamil | Noto Sans Tamil | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans TC · noto-sans-tc | Noto Sans TC | Noto Sans TC | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Telugu · noto-sans-telugu | Noto Sans Telugu | Noto Sans Telugu | Same font | Roboto Mono → Roboto Mono | — |
Noto Sans Thai · noto-sans-thai | Noto Sans Thai | Noto Sans Thai | Same font | Roboto Mono → Roboto Mono | — |
Nyala · nyala | Nyala | Noto Sans Ethiopic | Visual | Roboto Mono → Roboto Mono | — |
Open Sans · open-sans | Open Sans | Open Sans | Same font | Roboto Mono → Roboto Mono | — |
PMingLiU · pmingliu | PMingLiU | Noto Sans TC | Visual | Roboto Mono → Roboto Mono | — |
Poppins · poppins | Poppins | Poppins | Same font | Roboto Mono → Roboto Mono | — |
PT Serif · pt-serif | PT Serif | PT Serif | Same font | Roboto Mono → Roboto Mono | — |
Raavi · raavi | Raavi | Noto Sans Gurmukhi | Visual | Roboto Mono → Roboto Mono | — |
Raleway · raleway | Raleway | Raleway | Same font | Roboto Mono → Roboto Mono | — |
Roboto · roboto | Roboto | Roboto | Same font | Roboto Mono → Roboto Mono | — |
Rockwell · rockwell | Rockwell | Bitter | Visual | Roboto Mono → Roboto Mono | — |
Sakkal Majalla · sakkal-majalla | Sakkal Majalla | Noto Naskh Arabic | Visual | Roboto Mono → Roboto Mono | H/B script ×0.89 |
Seaford · seaford | Seaford Display / Seaford | Source Sans 3 | Visual | Roboto Mono → Roboto Mono | — |
Segoe UI · segoe-ui | Segoe UI Semibold / Segoe UI | Red Hat Display | Visual | Roboto Mono → Roboto Mono | H weight 600 |
Segoe UI Light · segoe-ui-light | Segoe UI Semilight / Segoe UI Light | Red Hat Display | Visual | Roboto Mono → Roboto Mono | B weight 300 |
Shonar Bangla · shonar-bangla | Shonar Bangla | Noto Sans Bengali | Visual | Roboto Mono → Roboto Mono | — |
Shruti · shruti | Shruti | Noto Sans Gujarati | Visual | Roboto Mono → Roboto Mono | — |
SimSun · simsun | SimSun | Noto Sans SC | Visual | Roboto Mono → Roboto Mono | — |
Skeena · skeena | Skeena Display / Skeena | Open Sans | Visual | Roboto Mono → Roboto Mono | — |
Sylfaen · sylfaen | Sylfaen | Noto Sans | Visual | Roboto Mono → Roboto Mono | — |
Tahoma · tahoma | Tahoma | Red Hat Text | Visual | Roboto Mono → Roboto Mono | — |
Tenorite · tenorite | Tenorite Display / Tenorite | Figtree | Visual | Roboto Mono → Roboto Mono | — |
Times New Roman · times-new-roman | Times New Roman | Tinos | Metric | Roboto Mono → Roboto Mono | — |
Traditional Arabic · traditional-arabic | Traditional Arabic | Noto Naskh Arabic | Visual | Roboto Mono → Roboto Mono | — |
Trebuchet MS · trebuchet-ms | Trebuchet MS | Figtree | Visual | Roboto Mono → Roboto Mono | — |
Tunga · tunga | Tunga | Noto Sans Kannada | Visual | Roboto Mono → Roboto Mono | — |
Verdana · verdana | Verdana | Montserrat | Visual | Roboto Mono → Roboto Mono | — |
Vrinda · vrinda | Vrinda | Noto Sans Bengali | Visual | Roboto Mono → Roboto Mono | — |
Yu Gothic · yu-gothic | Yu Gothic | Noto Sans JP | Visual | Roboto Mono → Roboto Mono | — |
The catalog's names and IDs are shown literally. For example, a scheme's display name can differ from its selected families. For CSS-only pptx.gallery cards, the original family precedes its replacement in the CSS stack, so a locally installed original can win; the controlled OPF renderer uses its supplied font registry.
Aptos, the default scheme. Aptos is a Microsoft 365 cloud font and is not redistributable. Under the owner policy of 2026-09-29 it previews with Intos, an OFL font whose advance widths, kerning and vertical metrics equal Aptos 2.01: 0.000% mean and maximum width difference over the 300-string corpus in regular, bold, italic and bold italic, for Aptos, Aptos Display, Aptos Narrow and Aptos Serif (Aptos Serif measured from Microsoft's standalone Aptos Fonts download, the others from the Microsoft 365 cloud fonts). Line breaks, line heights and text sizes therefore agree with Aptos. The letter shapes are Intos's own (Inter-derived, Gelasio-derived for the serif), not Aptos's. The exported PPTX still names Aptos, Aptos Display, Aptos Narrow or Aptos Serif, and no Aptos file is bundled or embedded. Deployments that hold an Aptos license can pass the real files through faces for exact previews.
Intos ships in opf-render's default office pack (loadFonts({pack: 'office'})), so the default metric policy previews the Aptos family without asking for visual mode. Without those faces, previews can use the alternates Roboto and Carlito under visual policy. Like the open families, Intos is an embed: "used" face: the fonts handle supplies the faces a slide actually draws, and subsetting can reduce the embedded bytes further. Pack size is not SVG or HTML transfer size; measure the installed renderer's output and selected delivery mode rather than using historical eager-embedding totals. Intos files are pinned by commit and SHA-256, and the previous replacements stay as alternates. The pptx.gallery parity scoreboard's fontResolution check counts the Aptos family as perfect because the replacement is metric-compatible.
Browser hosts load the vendored faces on demand. The eager list is what a host puts in one fonts.json (12.6 MB); the vendored faces (Intos and the open families, registry.lazyFonts, 51 faces) would add 19.4 MB, so they ship as separate hash-pinned files at their package-relative paths (fonts/intos/..., fonts/<family>/...) and load through loadFonts({faces, lazyFontsBaseUrl}) from /fonts-browser. await fonts.ensure(presentation) (the registry's ensureLazyFonts and ensureScripts are the lower-level calls) fetches and verifies only the faces the document draws (renderer 0.11.5, face level: a plain Aptos deck needs Intos Display Bold and Intos Regular, 2 files, 1.5 MB; an italic or bold run adds one face; before 0.11.5 it was every face of the resolved families, 8 files, 5.9 MB), then adds them to the document and the registry together, so the editor never measures with a face it paints as a fallback. The gallery commits only a pinned manifest, lazy-fonts.json (scripts/gallery-lazy-fonts.mjs, copied by prepare-gallery-editor): exact renderer version, SPDX license, license-file hash and every face's SHA-256, no bytes. The gallery's own build copies the faces from the pinned renderer package's fonts/ directory into an untracked path, verifying each hash, the way it does for script fonts. The local editor demo (build-editor-demo, through scripts/emit-lazy-fonts.mjs) copies the files beside the page instead. The editor playground calls fonts.ensure(presentation) when a document needs them.
The Windows reference-font advance study is exploratory source-checkpoint evidence, not an additional compatibility certification. It measures 1,024 cases across regular/bold Calibri, Arial, Times New Roman and Courier New, recording local reference-file versions/hashes and native font-slot names. Disabling optional ligatures and rounding base glyph advances to eighth-point steps predicts 949 observations within 0.02pt; 75 outliers remain, including combining marks, Arabic and Calibri kerning. Office theme tokens and per-glyph fallback are not resolved to exact native files by these name properties. No runtime provider or open-font mapping changes from this hypothesis, and no reference font is redistributed.
The composition API accepts a textMeasurement provider. A provider resolves font faces and returns actual text widths; callers pass the same provider to pagination (as { fonts: { textMeasurement } }), editor geometry, SVG rendering, and PPTX export. Without one, the existing deterministic character-width estimate remains available. A provider with forScripts(profile) (the renderer's registry measurement) is a script planner: validate and paginate ask it for each slide's script-aware measurement, built from the slide's resolveScriptFonts profile and a run language's own profile, so they measure Arabic or Japanese runs in the script faces preview and export draw.
loadFonts(options) from /fonts-node returns the fonts handle: the same registry measurement (textMeasurement), the faces to embed in SVG (embeddedFonts) and the explicit raster files (fontFiles), with system and bundled fallback disabled for raster calls (useBundledFonts: false, loadSystemFonts: false). Pass it as { fonts } to every deck-level call: paginate, editor geometry, toSvg, toPng, toPdf and toPptx. pack: 'base' is the default for authored Roboto decks; pack: 'office' adds the Office replacements and open families, and retains metric policy unless visual substitution is explicitly requested. Core's file API and CLI explicitly use visual policy. fonts.substitutions records actual substitutions; the loader does not rewrite the authored document or add native embedding.
Renderer 0.8.0 groups static files by their OpenType preferred family while retaining legacy family names and explicit custom namespaces. Roboto requests at 500/600/800 now select the actual Medium/SemiBold/ExtraBold files instead of nearby 400/700 faces. Optional TextStyle.fontFace carries the physical legacy family and native bold/italic flags independently of CSS numeric weight: SemiBold/ExtraBold are regular within their legacy families. The converter consumes this metadata; providers without it retain their prior behavior. Nine actual base faces pass metadata/measurement/outline checks and offline Chromium advances on Node 20/24. Seven payload slides cover serialized native selectors, deterministic output and source/reimport. These checks do not establish native Office paint, embedding or broader script coverage; see the Mac candidate evidence.
Renderer 0.8.0 uses adjacent SVG spans when no measurement provider is supplied. This closes the visible gaps caused by estimated fragment widths while retaining estimated line breaks and all run text/style/source offsets. Supplied providers and accepted placements still use exact fragment origins. Measured SVG requests geometric precision; accepted outline placements also constrain horizontal advances with textLength/spacingAndGlyphs to avoid browser quantization drift. This can scale glyphs horizontally while retaining nominal font size and baseline. Width-only providers do not receive that constraint, and constrained widths do not certify raw font-metric equivalence. The compatible editor supports both forms. This is a spacing improvement, not evidence that unmeasured wrapping or glyph coverage is accurate; see candidate evidence.
The Node loader verifies the pinned package versions, font-file hashes, license notices and vendored provenance in the installed BUNDLED_FONT_MANIFEST; the browser loader verifies the served faces against the same pins. Missing or modified resources reject with actionable errors. Font files must remain available and unchanged between preparation and export. HarfBuzz shaping and subsetting are provided by the fonts handle when its pinned WASM resources are available; inspect diagnostics if a deployment omits them. Browser loading, glyph coverage, style selection and native compatibility remain separate requirements. The version-specific evidence below records historical checkpoints, not the current manifest's face counts or API boundaries.
The renderer's optional font registry uses Fontkit to shape text and measure glyph advances from local font bytes. It does not discover system fonts or fetch fonts. The Node loader loads the renderer's bundled Roboto and Roboto Mono faces:
import { loadFonts } from '@openpresentation/opf-render/fonts-node';
import { toSvg, toPng } from '@openpresentation/opf-render';
import { paginate } from '@openpresentation/opf/pagination';
import { toPptx } from '@openpresentation/opf-pptx';
const fonts = await loadFonts(); // pack: 'base', the bundled Roboto faces
// Use design.fontScheme: 'roboto', or supply the document's actual font files.
const { presentation } = paginate(deck, { fonts });
const svg = toSvg(presentation, 1, { fonts }); // slide 1; embeds the faces it draws (text: 'paths' draws outlines instead)
const png = await toPng(svg, { fonts }); // draws with fonts.fontFiles only
const pptx = await toPptx(presentation, { fonts });loadFonts({ faces }) (from /fonts-node or /fonts-browser) accepts {data: Uint8Array, weight, italic?, family?, postscriptName?, license?} entries in Node or the browser, and {path} entries in Node. Weights are explicit, with 400 as the default. Supply each style that the document uses. Missing font families and unsupported glyphs fail with OPFFontError, including the source path where available. Collection fonts require a postscriptName selecting one face.
Aliases and fallback families are explicit choices:
const fonts = await loadFonts({
pack: 'none', faces,
aliases: { Aptos: 'Roboto', 'Aptos Display': 'Roboto' },
fallbackFamily: 'Roboto',
});
console.log(fonts.substitutions);
fonts.registry.clearSubstitutions(); // Start a fresh render's diagnostic collection.An available exact family takes precedence over aliases. The registry resolves a requested weight to the closest supplied weight, reports the substitution, and makes the resolved style available to rendering. Missing italic/upright styles fail instead of synthesizing an unmeasured style. strictGlyphs: false is an explicit escape hatch for hosts with their own glyph-fallback policy; it is unsuitable for fidelity verification.
SVG embeds supplied fonts using data URIs and includes supplied license notices as metadata. The bundled loader carries the fonts' SIL Open Font License notices. For PNG/PDF, pass the same font files to the rasterizer; its native font loader does not depend on browser CSS font loading. In a browser, wait for document.fonts.ready before measuring or taking a screenshot. The editor playground loads and embeds bundled fonts and displays substitutions.
toPdf (svgToPdf before 0.18) in renderers up to 0.11.9 was image-only: each slide was rasterized and embedded as a PNG on a PDF page. From opf-render 0.12.0 (opf-render#90, roadmap) the default vector mode writes PDF text objects in embedded TrueType subsets of the fonts you supply or the bundled open pack (the same files the PNG preview uses; system fonts are rejected, a face whose OS/2 fsType forbids embedding is never embedded, the report names requested and resolved faces), with ToUnicode maps and /ActualText where the glyph map cannot give the text, vector shapes, gradients and images, and no second layout pass. raster: true (mode: "raster" before 0.18) keeps the image-per-slide output as an explicit compatibility mode. Extraction was checked in pdf.js, PDFium and poppler on Latin, CJK, right-to-left and Indic samples and all 805 example slides; PDFium misreads some Thai and Burmese marks, as it does in Chrome's own PDFs. No PDF/UA or PDF/A claim.
Office compatibility pack
loadFonts({ pack: 'office' }) from @openpresentation/opf-render/fonts-node supplies regular, bold, italic, and bold italic faces of Carlito, Caladea, Arimo, Tinos, Cousine, and Gelasio, plus the base Roboto pack. Package versions are pinned and each face carries its distribution's license notice. includeBaseFonts: false omits Roboto. Loading never installs fonts into the operating system or downloads fonts at render time.
const fonts = await loadFonts({
pack: 'office',
substitutionPolicy: 'metric', // Default for the office pack; no visual fallback.
});
fonts.registry.resolveFont({fontFamily: 'Calibri', fontWeight: 400});
// requestedFamily: Calibri, resolvedFamily: Carlito, compatibility: metricloadFonts defaults to substitutionPolicy: 'none' (the office pack to 'metric'). Policies are none, metric, and visual; visual permits both curated tiers. An explicit fallbackFamily is a separate, reported generic fallback. Aliases are explicit visual substitutions and never establish metric compatibility. resolveFont reports exact resolutions as well; substitutions only collects changes. Resolution records include requested/resolved weights, italic, source path, and supporting upstream information where available.
| Requested family | Bundled substitute | Current automatic tier |
|---|---|---|
| Calibri | Carlito | Metric intent, standard 400/700 styles |
| Cambria | Caladea | Visual: advances differ from Cambria 6.99 by a mean of 2.7% (FF-31 measurement). Metric-mode registries still use it, reported as visual (metricModeFallback) |
| Arial | Arimo | Metric, standard 400/700 styles |
| Times New Roman | Tinos | Metric, standard 400/700 styles |
| Courier New | Cousine | Metric, standard 400/700 styles |
| Georgia | Gelasio | Metric with liga and clig off (disabledFeatures, applied by opf-render): advances identical on all 300 corpus strings in four styles. With default features, ligature runs differ by up to 1.02% |
| Calibri Light | Carlito | Visual: the bundle has no Carlito Light face |
| Aptos, Aptos Display, Aptos Narrow, Aptos Serif | Intos, Intos Display, Intos Narrow, Intos Serif (office pack) | Metric: 0.000% mean and maximum against Aptos 2.01, all four styles; Roboto and Carlito are visual alternates |
Upstream evidence: Carlito, Fontconfig mappings, Arimo, Tinos, Cousine, and Gelasio. Metric classification describes compatibility intent within the stated style scope, not universal identical output. Missing matching weights cannot silently qualify for the metric tier.
RR-17: Liberation Sans, Serif and Mono are not bundled (Reserved Font Name, about 4.4 MB); a document that names them previews with Arimo, Tinos and Cousine, the Croscore faces Liberation 2 is built from (metric, 0.0000% in four styles). Each Latin replacement has a per-family qualification and a fixture in every host; see the font tracker.
The exported FONT_COMPATIBILITY list also contains optional visual candidates and CJK families. Listing a candidate does not bundle it or imply complete character coverage. Liberation Sans Narrow is a separate legacy distribution with a different license history; it is not part of this bundle. Wingdings, Webdings, and Symbol require character mapping before substitution; an ordinary fallback fails with font-encoding-required. Cambria Math previews with STIX Two Math and Segoe UI Emoji with Noto Color Emoji once opf-render's optional math and emoji packs are loaded (FF-45, special families: emoji and math); without the pack a renderer reports font-unavailable naming it, like any other routed family (the former math-font-required failure is gone). OPF has no equation model: a Cambria Math run is text drawn per character in a math face, not MATH-table layout.
DrawingML tokens such as +mn-lt resolve through the registry's explicit themeFonts option before substitution. Supply concrete majorLatin, minorLatin, and, where used, majorEastAsia, minorEastAsia, majorComplexScript, or minorComplexScript families. Missing theme mappings fail. This helper does not yet extract theme font records or embedded fonts from imported PPTX files.
Measured results and experimental fonts
node scripts/test-office-fonts.mjs --system compares the bundle to reference fonts already installed in macOS's Supplemental directory. It does not redistribute reference fonts. The report records source-file hashes and individual shaped widths. Across four samples and four styles, Arimo/Arial, Tinos/Times New Roman, and Cousine/Courier New matched exactly on 48 runs. Gelasio/Georgia differed on ligature-containing runs, with a maximum difference of 2.0125%. Individual basic-Latin advances matched; disabling optional ligatures removed the tested difference. That earlier comparison predated the current disabled-feature qualification: the policy now marks Georgia→Gelasio metric when liga and clig are off, as described above. Calibri and Cambria reference fonts were not available for this comparison.
Akasia, assessed earlier (v0.0.2 open-file assessment), is dropped: its repository is no longer available, and Intos replaces it. EXPERIMENTAL_FONT_CANDIDATES now records Microsoft's Selawik, measured for Segoe UI on 2026-09-29 and rejected: 0.16% mean and 2.5% maximum in regular, no italic faces, 349 code points, lowercase 4.8% shorter. The acceptance rules for replacement fonts are in the licensing table.
An original OPF font project is technically feasible: independently designed or suitably open-licensed glyph outlines can be fitted to target advance widths, placement, vertical metrics, and shaping behavior. A successful font needs a reproducible source build, provenance, style/coverage tests, visual review, and cross-renderer conformance. Matching bounding boxes alone is insufficient: OpenType horizontal metrics and glyph positioning jointly control text placement. Universal pixel identity across rasterizers is not the acceptance criterion; measured layout preservation over an explicit test matrix is.
Verification and remaining work
Script fonts (CJK, Arabic, Hebrew, Indic, Thai, Khmer, Myanmar and the rest) have their own shaping corpora and per-family qualification (FF-44, RR-17): see script-corpora.md. It records, per script, glyph coverage, fontkit against HarfBuzz and Chromium, the installed originals measured in place, and dated shaping/raster limitations and their fixes. Read each result with its exact renderer and rasterizer versions; historical resvg-js failures do not describe every later PNG path or establish native acceptance.
Arabic Typesetting and Traditional Arabic: line-width residual (opf#523)
Neither family is bundled; both preview and compose with Noto Naskh Arabic (OFL-1.1). Arabic Typesetting carries the RR-38 sizeAdjust of 0.64, and Traditional Arabic has none. Native PowerPoint draws lines in both families narrower than the shared composition measures them. This is a known, accepted residual.
Measured. All widths are PowerPoint 365 TextRange.Lines bounds or ink, against the composed line.
| Family | Native lines | Native / composed width | Largest gap |
|---|---|---|---|
| Arabic Typesetting (proxy at 0.64) | 16, in RR-59 and FF-46 set 2 | 14 lines at 0.86 to 0.97 (headings 0.86 to 0.92, body 0.90 to 0.97); one FF-46 body line at 1.04; one FF-46 heading at 0.55; pooled 0.94 without that heading | 56.6 pt narrower on an 856 pt body line (284 pt on the 0.55 heading); 31 pt wider on the 1.04 line |
| Traditional Arabic (proxy at 1) | 10, in the same two runs | 0.68 to 0.90, pooled 0.79 | 223.6 pt narrower (an 842 pt body line) |
The RR-38 probe agrees for Arabic Typesetting. Its 45-character line is 216.9, 289.0 and 386.1 pt at 18, 24 and 32 pt in PowerPoint, and composes at 222.1, 298.6 and 396.9 pt.
Why it exists. Three reasons:
- No published metrics. Microsoft's font pages for Arabic Typesetting and Traditional Arabic give only the file names, styles, vendor, scripts and code pages. They publish no advance widths,
xAvgCharWidthor vertical metrics. - No metric-compatible open face. None exists for either family: no fontconfig metric alias, no Croscore-style clone. OPF does not take metrics from the installed proprietary files.
- One factor cannot fit. Per glyph, Noto Naskh Arabic's advances differ from these fonts' by more than any scale can absorb. Ligatures and letter widths vary by text. Arabic Typesetting also has only a regular face, so PowerPoint emboldens bold text synthetically, while the preview draws Noto Naskh Arabic Bold.
One factor per family does not fix it. For Traditional Arabic, the native / Noto Naskh ratio of the 10 lines spans 0.68 to 0.90, so any one factor leaves large gaps:
| Factor | Largest gap, composed wider | Largest gap, native wider |
|---|---|---|
| 0.79 (the pooled ratio; the opf#361 proposal) | 47 pt | 48 pt |
| 0.90 | 139 pt | none |
Latin text in a deck that uses the Traditional Arabic scheme also draws in Noto Naskh Arabic's Latin, where the policy measures Traditional Arabic at 0.86 of the replacement's width. At 0.79, those lines would run about 9 percent past their composed width in PowerPoint. Arabic Typesetting's 0.64 already sits near the upper edge of its native ratios. A lower factor would let more lines run past their boxes.
Practical impact.
- Line counts agree. The PPTX export writes each composed line as its own text box, and PowerPoint does not re-wrap it. Line counts, the order of lines and the right edges of right-to-left lines therefore agree: within 0.13 pt in RR-59.
- Left edges differ. The left edge of a right-to-left line (the right edge of a left-to-right one) sits closer to the text in PowerPoint. The same text could fit in fewer lines natively than the preview shows.
- Overhang is rare. Because the proxy errs wide, a Traditional Arabic line keeps slack in its box. For Arabic Typesetting, an occasional line runs a few percent past its composed width: 3.78 pt past its box in RR-59
ar-run-in-en, not clipped. - Wrapping can differ near the edge. Text that PowerPoint lays out itself wraps where the real font's widths put it, which can differ from the preview. That covers a line box retyped in PowerPoint, and text another application re-flows.
For layouts that must wrap exactly, choose a family with a metric replacement, or supply licensed font files through loadFonts({faces}).
pnpm test:fonts checks that editor and SVG geometry match and that every native PPTX text box has the same coordinates and measured line breaks. With opf-pptx FF-31 (opf-pptx#63), export names the chosen family, not the preview substitute. It writes artifacts to artifacts/fonts/. A real-browser check of the same Roboto run measured 324.032 pixels versus the font engine's 324.170 pixels at 25 pixels, a difference of 0.138 pixels. These are measured tolerances, not a promise of pixel identity.
PPTX records the chosen font family (FF-31); it never records a preview replacement and never embeds a proprietary font binary. PowerPoint still needs those fonts installed, through Office, the OS or Microsoft 365 cloud fonts, or it substitutes them. Line height remains the shared 1.22 multiplier, rather than a complete ascent/descent model. Rich-text font overrides, mixed-script fallback and bidi layout, specialized payload internals, and native font embedding remain active fidelity work. Passing a width provider does not remove those limits.