Appearance
Embedded Fonts
Status
Shipped (2026-06-25). A deck can carry whole font faces so it renders with them on machines that lack the font, mirroring PowerPoint's Save → Embed fonts in the file. Two independent entry points share one OOXML model:
- Author-side:
await pptx.embedFont({ path | data, typeface, style })embeds a face when generating a deck from scratch. Source:src/presentation.ts(embedFont,_embeddedFonts), write-side wiring insrc/gen/pres/presentation.ts, field on the internal model insrc/types/index.ts. - Import-carry:
importSlide(source, i, { embedFonts: true })brings a source deck's presentation-level embedded fonts across when lifting a slide. Source:src/read/api/ops/embedded-fonts.ts(carryEmbeddedFonts).
Both converge on the shared model and serializer in src/embedded-fonts.ts. See CHANGELOG.md for the import-carry limits. Tests: test/regression/media/embed-font.test.js (author-side), test/read/embedded-fonts.test.js (import-carry, incl. schema validity), and an author-side case in test/schema-cases.js (validator-checked against the oracle).
OOXML target (ECMA-376 transitional)
Embedded fonts are three coordinated pieces:
- Binary font parts (one per face) at
/ppt/fonts/fontN.fntdata. The bytes are the raw TTF/OTF file: PresentationML does not obfuscate embedded fonts (unlike WordprocessingML's.odttf).[Content_Types].xmlcarries a singleDefault Extension="fntdata" ContentType="application/x-fontdata"covering every font part. - Relationships from
presentation.xml(inppt/_rels/presentation.xml.rels), one per face, type…/relationships/font, targetfonts/fontN.fntdata. p:embeddedFontLstinsidepresentation.xml, at index 7 of theCT_Presentationchild sequence: afternotesSz, beforedefaultTextStyle.
xml
<p:embeddedFontLst>
<p:embeddedFont>
<p:font typeface="My Font"/> <!-- CT_TextFont: typeface required (+ optional panose/pitchFamily/charset) -->
<p:regular r:id="rIdN"/> <!-- CT_EmbeddedFontDataId, 0..1 -->
<p:bold r:id="rIdM"/> <!-- 0..1 -->
<p:italic r:id="rIdO"/> <!-- 0..1 -->
<p:boldItalic r:id="rIdP"/> <!-- 0..1 -->
</p:embeddedFont>
</p:embeddedFontLst>CT_EmbeddedFontListEntry child order is font, regular, bold, italic, boldItalic. Only font is required; each of the four face slots is 0..1 and carries the single r:id to its font part. The constant EMBEDDED_FONT_SLOTS fixes this order so the read- and write-side emitters agree.
We embed whole faces, never a glyph subset, so on p:presentation we set embedTrueTypeFonts="1" saveSubsetFonts="0" whenever any face has bytes. When no font is embedded, output is unchanged: the historical inert saveSubsetFonts="1" stays and embedTrueTypeFonts is absent.
Shared model (src/embedded-fonts.ts)
One representation both features build/consume:
ts
interface EmbeddedFontFace { slot: EmbeddedFontSlot; bytes?: Uint8Array }
interface EmbeddedFont { typeface: string; panose?; pitchFamily?; charset?; faces: EmbeddedFontFace[] }The module owns only OOXML-shape knowledge; rId allocation and part placement stay with each caller (their packaging models differ). It exposes:
flattenEmbeddedFaces(fonts, firstRId)→ orderedFlatEmbeddedFace[]assigning sequential 1-based part indices and rIds; faces without bytes are skipped. Used by the write path so the part writer, rels writer, and list emitter all agree.serializeEmbeddedFontLst(fonts, rIdForFace)→ the<p:embeddedFontLst>string (empty when no face has an allocated rId), assuming the enclosing doc declares thep:/r:prefixes.- The constants
FONT_DATA_EXTENSION,FONT_DATA_CONTENT_TYPE,FONT_REL_TYPE,EMBEDDED_FONT_SLOTS.
Author-side: pptx.embedFont
ts
await pptx.embedFont({ path: '/fonts/Silkscreen-Regular.ttf', typeface: 'Silkscreen' })
await pptx.embedFont({ path: '/fonts/Silkscreen-Bold.ttf', typeface: 'Silkscreen', style: 'bold' })
slide.addText('hi', { x: 1, y: 1, w: 4, h: 1, fontFace: 'Silkscreen' })pathis a path/URL loaded via the runtime adapter (loadFontData);datais in-memory bytes:Uint8Array,ArrayBuffer, or a base64 string (with or without a data-URL prefix).styleis one of'regular'(default) |'bold'|'italic'|'boldItalic'. Repeated calls with the sametypefaceand differentstyleaccumulate into onep:embeddedFontentry; a repeat of the sametypeface+stylereplaces the prior bytes (last call wins).- The declared
typefaceMUST match the family name your runs/fontFaceuse or PowerPoint won't bind the embedded face. - Validates input: missing
typeface, missing byte source, and an invalidstyleall throw.
At write time (in src/gen/pres/presentation.ts) the accumulated _embeddedFonts drive: makeXmlContTypes (the fntdata Default), makeXmlPresentationRels (one font rel per face, rIds allocated after the slide/master rels), makeXmlPresentation (the p:embeddedFontLst between notesSz and defaultTextStyle, plus the embedTrueTypeFonts/saveSubsetFonts flags), and the zip.add of each /ppt/fonts/fontN.fntdata part (STORE-compressed, fonts are already compact).
Import-carry: importSlide({ embedFonts: true })
Opt-in (default off, so existing behaviour is unchanged). When set, carryEmbeddedFonts runs a separate traversal of the source presentation.xml (not part of the slide-part copy chain, which still skips p:embeddedFontLst):
- Parse the source
p:embeddedFontLst; for each face resolve itsr:idagainst the sourcepresentation.xml.rels. ensureDefault('fntdata', …)before copying, so#copyPart'saddPartresolves the content type via the Default (one Default, no per-part Override).- Copy each referenced font part via
#copyPart(the per-source registry dedupes, so faces shared across repeated imports copy exactly once) and add a freshfontrelationship in the targetpresentation.xml.rels. - Merge into the target
p:embeddedFontLst(created at index 7 if absent), cloning the sourcep:fontidentity (typeface+ optionalpanose/pitchFamily/charset) and inserting each face slot in schema order. - De-dupe by
typeface+ face slot: a face this deck already embeds is reused, not duplicated, so importing the same slide twice carries each face once.
Oracle & fixtures
Per the project's fixture-gated-work rule, the emitted/merged XML is validated against PowerPoint-authored output, not synthetic XML:
test/read/fixtures/embedded-fonts.pptx: a real PowerPoint deck that embeds Silkscreen regular + bold (whole characters), one slide using the face. Its verbatimembeddedFontLst, font rels, and part list are captured intest/read/fixtures/embedded-fonts.oracle.json, the comparison oracle for both the import-carry merge test and the author-side emit test.test/read/fixtures/fonts/Silkscreen-Regular.ttf/Silkscreen-Bold.ttf: raw redistributable (SIL OFL) faces fed to the author-side API in tests.
pnpm run test:schema (validator installed via ./tools/ooxml-validator/install.sh) confirms the validator accepts the fntdata Default and the embeddedFontLst placement.
Standing caveats
- Font licensing is the caller's responsibility. TTF
OS/2.fsTypecarries embedding-permission bits; v1 does not enforce them: it embeds whatever bytes the caller hands over. - No subsetting. We embed whole faces, so
saveSubsetFonts="0". Subsetting (and the matchingsaveSubsetFonts="1") is not implemented. - No auto-detection of
typeface/style from the font'sname/OS/2tables: the caller declares them (no font-table parser dependency). Revisit if needed. - Import-carry is wired for
importSlide;importShape/importSlideMastersdo not carry fonts (no consumer needs it yet).