OPF
DocsReferenceView source

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, paginate and the slide context (resolveSlideContext);
  • opf-render's toSvg, toPng and toPdf;
  • opf-pptx's toPptx.
js
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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Only open fonts may be embedded, through the explicit embed path (FF-13).
  6. 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/google is not a hotlink but is not pinned, so use next/font/local with pinned files. Guards fail on CDN references in every repository.
  7. 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.

  1. 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 liga and clig features off (the row lists them as disabledFeatures): 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). lineAscent and lineAscentMixed (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 by 1 - lineAscent em; 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.
  2. The PPTX always names the chosen family, and no font file is included. The exporter writes Aptos when 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-pptx test/export-chosen-fonts.mjs).
  3. 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.
  4. 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: false and includeBaseFonts: false opt out. There is no separate proposed catalog pack 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.

EnvironmentFont resolutionWhat the viewer needs
Node, cloud or serverlessSupplied exact faces or loaded open replacements, controlled by the registry policyPinned font files available to the process
Browser/editorThe same registry rules, with the host serving the required faces and awaiting ensure(presentation)Self-hosted files, not a font CDN
PowerPoint, exported PPTXAuthored font names; normal export does not embed fontsInstalled fonts or supported Office cloud fonts; otherwise PowerPoint substitutes
Keynote, imported PPTXAuthored names when available to KeynoteInstalled fonts; Keynote handles missing-font substitution
LibreOffice Impress, PPTXAuthored names, subject to local replacement settingsInstalled 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 viewerFont usedFont delivery and missing-font behavior
PPTX in PowerPointAuthored heading/body/code familiesNormal 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 KeynoteAuthored families, when available to KeynoteKeynote handles missing-font substitution. OPF does not prescribe the replacement.
PPTX in LibreOffice ImpressAuthored families, when availableSystem and LibreOffice replacement settings control substitution, and can override an installed font.
Native .keyNo native OPF .key exportImport the PPTX into Keynote. Saving it as .key is a subsequent Keynote conversion.
Browser/editor previewRendered families in the table belowThe renderer loads supplied/self-hosted faces; it does not discover system fonts by default.
HTML, toHtml()Rendered families in inline SVGstandalone 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 familiesEmbeds used font bytes. Node script faces need embedScriptFonts: true; the CLI prepares this automatically.
SVG, text: systemRendered family namesNo font bytes; matching viewer/page fonts are required or the viewer substitutes.
SVG, text: pathsGlyph outlines from rendered familiesVisible glyphs need no installed font. Outlined glyphs cannot be edited as text; exceptional runs can remain text with a diagnostic.
PNGRendered families baked into pixelsNo recipient font dependency.
Vector PDF (default)Rendered families embedded/subset in the PDFSelectable text; font files must permit embedding and be supported by the PDF writer.
Raster PDFRendered families baked into page imagesNo 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 · IDPPTX heading / bodyPreview, HTML, SVG, PNG, PDF heading / bodyMatch heading / bodyCode: PPTX → renderedAdjustments
Angsana New · angsana-newAngsana NewNoto Sans ThaiVisualRoboto Mono → Roboto MonoH/B script ×0.75
Aparajita · aparajitaAparajitaNoto Sans DevanagariVisualRoboto Mono → Roboto Mono—
Aptos · aptosAptos Display / AptosIntos Display / IntosMetricRoboto Mono → Roboto Mono—
Arabic Typesetting · arabic-typesettingArabic TypesettingNoto Naskh ArabicVisualRoboto Mono → Roboto MonoH/B script ×0.64
Arial · arialArial Black / ArialMontserrat / ArimoVisual / MetricRoboto Mono → Roboto MonoH weight 900
Batang · batangBatangChe / BatangNoto Sans KRVisualRoboto Mono → Roboto Mono—
Bookman · bookmanBookman Old StyleLibre Caslon TextVisualRoboto Mono → Roboto Mono—
Calibri · calibriCalibriCarlitoMetricRoboto Mono → Roboto Mono—
Century Schoolbook · century-schoolbookCentury SchoolbookGelasioVisualRoboto Mono → Roboto Mono—
Consolas · consolasConsolasCousineVisualConsolas → Cousine—
Constantia · constantiaConstantiaPT SerifVisualRoboto Mono → Roboto Mono—
Courier New · courier-newCourier NewCousineMetricCourier New → Cousine—
DaunPenh · daunpenhDaunPenhNoto Sans KhmerVisualRoboto Mono → Roboto Mono—
David · davidDavidNoto Serif HebrewVisualRoboto Mono → Roboto Mono—
DilleniaUPC · dilleniaupcDilleniaUPCNoto Sans ThaiVisualRoboto Mono → Roboto MonoH/B script ×0.68
FangSong · fangsongFangSongNoto Sans SCVisualRoboto Mono → Roboto Mono—
Garamond · garamondGaramondEB GaramondVisualRoboto Mono → Roboto Mono—
Gautami · gautamiGautamiNoto Sans TeluguVisualRoboto Mono → Roboto Mono—
Georgia · georgiaGeorgiaGelasioMetricRoboto Mono → Roboto Mono—
Gisha · gishaGishaNoto Sans HebrewVisualRoboto Mono → Roboto Mono—
Grandview · grandviewGrandview Display / GrandviewBarlowVisualRoboto Mono → Roboto Mono—
Gungsuh · gungsuhGungsuhChe / GungsuhNoto Sans KRVisualRoboto Mono → Roboto Mono—
Impact · impactImpact / GrandviewAnton / BarlowVisualRoboto Mono → Roboto Mono—
Kalinga · kalingaKalingaNoto Sans OriyaVisualRoboto Mono → Roboto Mono—
Kartika · kartikaKartikaNoto Sans MalayalamVisualRoboto Mono → Roboto Mono—
Khmer UI · khmer-uiKhmer UINoto Sans KhmerVisualRoboto Mono → Roboto Mono—
Latha · lathaLathaNoto Sans TamilVisualRoboto Mono → Roboto Mono—
Lucida Sans · lucida-sansLucida SansWork SansVisualRoboto Mono → Roboto Mono—
Malgun Gothic · malgun-gothicMalgun GothicNoto Sans KRVisualRoboto Mono → Roboto MonoH/B script ×1.07
Mangal · mangalMangalNoto Sans DevanagariVisualRoboto Mono → Roboto Mono—
Meiryo · meiryoMeiryoNoto Sans JPVisualRoboto Mono → Roboto Mono—
Microsoft JhengHei · microsoft-jhengheiMicrosoft JhengHeiNoto Sans TCVisualRoboto Mono → Roboto Mono—
Microsoft Yahei · microsoft-yaheiMicrosoft YaHeiNoto Sans SCVisualRoboto Mono → Roboto Mono—
MingLiU · mingliuMingLiUNoto Sans TCVisualRoboto Mono → Roboto Mono—
Miriam · miriamMiriamNoto Sans HebrewVisualRoboto Mono → Roboto Mono—
Montserrat · montserratMontserratMontserratSame fontRoboto Mono → Roboto Mono—
MS Mincho · ms-minchoMS MinchoNoto Sans JPVisualRoboto Mono → Roboto Mono—
Nirmala UI · nirmala-uiNirmala UINoto Sans DevanagariVisualRoboto Mono → Roboto MonoH/B script ×1.07
Noto Naskh Arabic · noto-naksh-arabicNoto Naskh ArabicNoto Naskh ArabicSame fontRoboto Mono → Roboto Mono—
Noto Nastaliq Urdu · noto-nastaliq-urduNoto Nastaliq UrduNoto Nastaliq UrduSame fontRoboto Mono → Roboto Mono—
Noto Sans · noto-sansNoto SansNoto SansSame fontRoboto Mono → Roboto Mono—
Noto Sans Arabic · noto-sans-arabicNoto Naskh ArabicNoto Naskh ArabicSame fontRoboto Mono → Roboto Mono—
Noto Sans Armenian · noto-sans-armenianNoto Sans ArmenianNoto Sans ArmenianSame fontRoboto Mono → Roboto Mono—
Noto Sans Bengali · noto-sans-bengaliNoto Sans BengaliNoto Sans BengaliSame fontRoboto Mono → Roboto Mono—
Noto Sans Devanagari · noto-sans-devangariNoto Sans DevanagariNoto Sans DevanagariSame fontRoboto Mono → Roboto Mono—
Noto Sans Ethiopic · noto-sans-ethiopicNoto Sans EthiopicNoto Sans EthiopicSame fontRoboto Mono → Roboto Mono—
Noto Sans Georgian · noto-sans-georgianNoto Sans GeorgianNoto Sans GeorgianSame fontRoboto Mono → Roboto Mono—
Noto Sans Gujarati · noto-sans-gujaratiNoto Sans GujaratiNoto Sans GujaratiSame fontRoboto Mono → Roboto Mono—
Noto Sans Gurmukhi · noto-sans-gurmukhiNoto Sans GurmukhiNoto Sans GurmukhiSame fontRoboto Mono → Roboto Mono—
Noto Sans Hebrew · noto-sans-hebrewNoto Sans HebrewNoto Sans HebrewSame fontRoboto Mono → Roboto Mono—
Noto Sans JP · noto-sans-jpNoto Sans JPNoto Sans JPSame fontRoboto Mono → Roboto Mono—
Noto Sans Kannada · noto-sans-kannadaNoto Sans KannadaNoto Sans KannadaSame fontRoboto Mono → Roboto Mono—
Noto Sans Khmer · noto-sans-khmerNoto Sans KhmerNoto Sans KhmerSame fontRoboto Mono → Roboto Mono—
Noto Sans KR · noto-sans-krNoto Sans KRNoto Sans KRSame fontRoboto Mono → Roboto Mono—
Noto Sans Malayalam · noto-sans-malayalamNoto Sans MalayalamNoto Sans MalayalamSame fontRoboto Mono → Roboto Mono—
Noto Sans Mongolian · noto-sans-mongolianNoto Sans MongolianNoto Sans MongolianSame fontRoboto Mono → Roboto Mono—
Noto Sans Oriya · noto-sans-oriyaNoto Sans OriyaNoto Sans OriyaSame fontRoboto Mono → Roboto Mono—
Noto Sans SC · noto-sans-scNoto Sans SCNoto Sans SCSame fontRoboto Mono → Roboto Mono—
Noto Sans Tamil · noto-sans-tamilNoto Sans TamilNoto Sans TamilSame fontRoboto Mono → Roboto Mono—
Noto Sans TC · noto-sans-tcNoto Sans TCNoto Sans TCSame fontRoboto Mono → Roboto Mono—
Noto Sans Telugu · noto-sans-teluguNoto Sans TeluguNoto Sans TeluguSame fontRoboto Mono → Roboto Mono—
Noto Sans Thai · noto-sans-thaiNoto Sans ThaiNoto Sans ThaiSame fontRoboto Mono → Roboto Mono—
Nyala · nyalaNyalaNoto Sans EthiopicVisualRoboto Mono → Roboto Mono—
Open Sans · open-sansOpen SansOpen SansSame fontRoboto Mono → Roboto Mono—
PMingLiU · pmingliuPMingLiUNoto Sans TCVisualRoboto Mono → Roboto Mono—
Poppins · poppinsPoppinsPoppinsSame fontRoboto Mono → Roboto Mono—
PT Serif · pt-serifPT SerifPT SerifSame fontRoboto Mono → Roboto Mono—
Raavi · raaviRaaviNoto Sans GurmukhiVisualRoboto Mono → Roboto Mono—
Raleway · ralewayRalewayRalewaySame fontRoboto Mono → Roboto Mono—
Roboto · robotoRobotoRobotoSame fontRoboto Mono → Roboto Mono—
Rockwell · rockwellRockwellBitterVisualRoboto Mono → Roboto Mono—
Sakkal Majalla · sakkal-majallaSakkal MajallaNoto Naskh ArabicVisualRoboto Mono → Roboto MonoH/B script ×0.89
Seaford · seafordSeaford Display / SeafordSource Sans 3VisualRoboto Mono → Roboto Mono—
Segoe UI · segoe-uiSegoe UI Semibold / Segoe UIRed Hat DisplayVisualRoboto Mono → Roboto MonoH weight 600
Segoe UI Light · segoe-ui-lightSegoe UI Semilight / Segoe UI LightRed Hat DisplayVisualRoboto Mono → Roboto MonoB weight 300
Shonar Bangla · shonar-banglaShonar BanglaNoto Sans BengaliVisualRoboto Mono → Roboto Mono—
Shruti · shrutiShrutiNoto Sans GujaratiVisualRoboto Mono → Roboto Mono—
SimSun · simsunSimSunNoto Sans SCVisualRoboto Mono → Roboto Mono—
Skeena · skeenaSkeena Display / SkeenaOpen SansVisualRoboto Mono → Roboto Mono—
Sylfaen · sylfaenSylfaenNoto SansVisualRoboto Mono → Roboto Mono—
Tahoma · tahomaTahomaRed Hat TextVisualRoboto Mono → Roboto Mono—
Tenorite · tenoriteTenorite Display / TenoriteFigtreeVisualRoboto Mono → Roboto Mono—
Times New Roman · times-new-romanTimes New RomanTinosMetricRoboto Mono → Roboto Mono—
Traditional Arabic · traditional-arabicTraditional ArabicNoto Naskh ArabicVisualRoboto Mono → Roboto Mono—
Trebuchet MS · trebuchet-msTrebuchet MSFigtreeVisualRoboto Mono → Roboto Mono—
Tunga · tungaTungaNoto Sans KannadaVisualRoboto Mono → Roboto Mono—
Verdana · verdanaVerdanaMontserratVisualRoboto Mono → Roboto Mono—
Vrinda · vrindaVrindaNoto Sans BengaliVisualRoboto Mono → Roboto Mono—
Yu Gothic · yu-gothicYu GothicNoto Sans JPVisualRoboto 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:

js
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:

js
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.

js
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: metric

loadFonts 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 familyBundled substituteCurrent automatic tier
CalibriCarlitoMetric intent, standard 400/700 styles
CambriaCaladeaVisual: 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)
ArialArimoMetric, standard 400/700 styles
Times New RomanTinosMetric, standard 400/700 styles
Courier NewCousineMetric, standard 400/700 styles
GeorgiaGelasioMetric 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 LightCarlitoVisual: the bundle has no Carlito Light face
Aptos, Aptos Display, Aptos Narrow, Aptos SerifIntos, 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.

FamilyNative linesNative / composed widthLargest gap
Arabic Typesetting (proxy at 0.64)16, in RR-59 and FF-46 set 214 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 heading56.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 runs0.68 to 0.90, pooled 0.79223.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, xAvgCharWidth or 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:

FactorLargest gap, composed widerLargest gap, native wider
0.79 (the pooled ratio; the opf#361 proposal)47 pt48 pt
0.90139 ptnone

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.