Skip to content

Text that fits ​

fit: 'shrink' and fit: 'resize' ask for text that fits its box. Register the box's font with registerFontMetrics, and the library measures the wrapped text as it writes the deck and stores the result in the file: 'shrink' writes the font scale that fits, and 'resize' writes the height the text needs.

ts
import { TsPptx } from 'pptx-ts'

const pptx = new TsPptx()
await pptx.registerFontMetrics('Aptos', '/fonts/Aptos.ttf')
pptx.addSlide().addText('Quarterly results for the northern region, by product line', {
  x: 1,
  y: 1,
  w: 3,
  h: 1,
  fontFace: 'Aptos',
  fontSize: 28,
  fit: 'shrink',
})
await pptx.writeFile({ fileName: 'fit.pptx' })

With no font registered, 'shrink' and 'resize' write the bare autofit flag, <a:normAutofit/> or <a:spAutoFit/>, with no result in it. PowerPoint computes the fit only after the text or the box is edited.

Options at a glance ​

OptionTypeDefaultEffect
fit on a text box'none' | 'shrink' | 'resize' | TextFitShrinkProps'none''shrink' bakes a font scale, 'resize' bakes a height, and the object form is written as given.
fit on a table or a cell'shrink'noneBakes a smaller font size into a cell whose text overflows its fixed row.
registerFontMetrics(face, …)stringrequiredThe family name your fontFace options use. Case is ignored.
registerFontMetrics(…, source)string | Uint8Array | ArrayBufferrequiredA font file path or URL, or its bytes: .ttf, .otf, .ttc or .otc.
bold, italicbooleanfalseRegisters the metrics for that variant.
fontnumber | stringface for a collection, the only font otherwiseWhich font of a collection: an index from 0, or a name.

measureText and overflowsBox take MeasureTextOptions:

OptionTypeDefaultEffect
wInnumberrequiredWidth available to the text, in inches.
insetInnumber0Taken off both sides of wIn, so wIn can be the box width.
fontSizenumberrequiredSize in points.
fontFacestringnoneThe family to measure. Without it the text cannot be measured.
bold, italicbooleanfalseThe variant to measure.
charSpacingnumber0Points added after every character.
lineSpacingnumbersingle spacingExact line pitch in points. Wins over lineSpacingMultiple.
lineSpacingMultiplenumber1Line pitch as a multiple of single spacing.
paraSpaceBefore, paraSpaceAfternumber0Points before and after each paragraph.
hInnumberrequired by overflowsBoxThe inner height to test against, in inches.

Register font metrics ​

ts
await pptx.registerFontMetrics('Aptos', '/fonts/Aptos.ttf')
await pptx.registerFontMetrics('Aptos', '/fonts/Aptos-Bold.ttf', { bold: true })
  • A string source is a file path, or an http or https URL. It is never base64: pass bytes instead.
  • Register each weight and style your fitted text uses. A bold run with no bold registration is measured with the face's regular metrics, or with any variant registered for that face.
  • registerFontMetrics returns a promise, and the measuring that follows is synchronous. Await every registration before you write the deck or call measureText.
  • One registration turns measuring on for the whole deck. Handle a face with no metrics covers the boxes whose face you did not register.

Register one font from a collection ​

A .ttc or .otc file holds several fonts. msgothic.ttc holds MS Gothic, MS UI Gothic and MS PGothic.

ts
import { readFile } from 'node:fs/promises'
import { listFontFaces } from 'pptx-ts/measure'

const bytes = new Uint8Array(await readFile('C:/Windows/Fonts/msgothic.ttc'))
console.log(listFontFaces(bytes).map((f) => `${f.index} ${f.family}`))
// [ '0 MS Gothic', '1 MS UI Gothic', '2 MS PGothic' ]

await pptx.registerFontMetrics('MS PGothic', bytes)
await pptx.registerFontMetrics('Gothic body', bytes, { font: 0 })
  • In a collection, face picks the font when font is not set. It matches the family, full or PostScript name, ignoring case.
  • font picks by index from 0, or by the same names. Use it when your fontFace differs from the name inside the file.
  • A name or index that matches no font throws. The library does not fall back to the first font, because the wrong font's widths look as plausible as the right one's: MS Gothic sets an English pangram 15% wider than MS PGothic at the same size.
  • A plain .ttf or .otf counts as a list of one. It registers under any face, and a font other than 0 or the file's own name throws.
  • isFontCollection(bytes) tells a collection from a plain font.

Embed the font too ​

registerFontMetrics and embedFont both take a font file and a family name, and they do different jobs:

registerFontMetrics(face, source, options)embedFont({ path, data, typeface, style })
Jobreads advance widths to measure textcopies the font into the .pptx
Adds to the filenothingthe font part
A string sourcea path or URLpath is a path or URL, data is base64
Variantsbold and italic flagsstyle: 'regular', 'bold', 'italic' or 'boldItalic'

Registering does not make a machine without the font draw it, and embedding does not turn on measuring. To fit text and ship its font, call both with the same family name. See Embedded fonts.

Choose shrink or resize ​

  • The box has a fixed place in the layout, such as a card or a column: use 'shrink'. The box stays as drawn and the text gets smaller.
  • The text size is fixed and the box can change height: use 'resize'. The box takes the height of its text, taller or shorter than you drew it.
  • Other shapes are laid out around the box, such as a card background or an icon: 'resize' moves only the text box. Measure the text first and size every shape from the result, as in Measure text before export.
  • Neither the size nor the box may change: leave fit off and check the text with overflowsBox.
  • A table cell: 'shrink' is the only choice. See Shrink text in a table cell.

When the deck is written, one pass runs before any XML is built:

Shrink text into its box ​

  • The pass lays the text out at 100%, 97.5%, 95% and so on, and writes the first scale at which it fits, such as <a:normAutofit fontScale="85000"/> for 85%.
  • Text that fits at 100% keeps the bare <a:normAutofit/>.
  • The scale stops at 25%. Text that still overflows there gets 25%.
  • Only the font scale changes. PowerPoint also reduces line spacing by up to 20% to keep a larger font, and the pass does not, so its scale is never larger than PowerPoint's and can be smaller.
  • With wrap: false, each paragraph is one line, and the widest line also has to fit the box width.
  • The vertical anchor does not change the scale.
  • Text boxes inside a group are measured at their authored size, like any other.
  • fit: { type: 'shrink', fontScale: 85 } is written as given and never measured.

Resize the box to its text ​

ts
slide.addText(body, { x: 1, y: 1, w: 4, h: 1, fontFace: 'Aptos', fontSize: 14, fit: 'resize', valign: 'top' })
  • The pass sets h to the measured text height plus the top and bottom insets, and keeps <a:spAutoFit/> beside it.
  • The box grows when the text needs more room and shrinks when it needs less.
  • valign decides which edge stays put:
valigny moves byThe box
'top'nothinggrows down
'middle', or unsethalf the height changegrows both ways
'bottom'the whole height changegrows up
  • Nothing holds a bottom-anchored box at the slide edge. A box that grows by more than its distance from the top gets a negative y.
  • The width never changes. With wrap: false, a line wider than the box runs past its edge, where PowerPoint would widen the box.

Shrink text in a table cell ​

ts
await pptx.registerFontMetrics('Aptos', fontBytes)
slide.addTable(rows, { x: 1, y: 1, w: 8, rowH: 0.4, fontFace: 'Aptos', fit: 'shrink' })
  • PowerPoint has no autofit inside a table cell, so the pass writes a smaller font size into the cell's runs, rounded down to 0.1 pt.
  • Only cells in fixed-height rows shrink. A row with no rowH entry and no table h grows instead.
  • 'resize' and the object form are ignored on cells.
  • Tables covers which rows are fixed and how fit passes from the table to its cells.

Handle a face with no metrics ​

CaseResultWarning
The deck registered no facebare flags on every text box, table cells unchangednone
The box's fontFace is registeredmeasurednone
Another face is registered, but not the box's fontFacemeasured with average character widths, biased widemeasure/heuristic-metrics
Another face is registered, and the box has no fontFacebare flag, table cell unchangedmeasure/shrink-unmeasured or measure/resize-unmeasured
A registered face has no glyph for a charactermeasured with the font's missing-glyph widthmeasure/uncovered-codepoints
  • Each warning fires once per write and lists the faces or characters.
  • Text with no fontFace takes the theme font, and the pass does not work out which face the theme names. Set fontFace on fitted text.
  • PowerPoint draws a character the named font lacks from a substitute font, at that font's width. The missing-glyph width can be wider or narrower. Narrower loses a line, and the text overflows. Register a face that covers the text.

Measure text before export ​

pptx.measureText(text, options) lays text out with the same model the pass uses, so you can size shapes before you add them:

ts
import { TsPptx } from 'pptx-ts'

const pptx = new TsPptx()
await pptx.registerFontMetrics('Aptos', '/fonts/Aptos.ttf')

const title = 'Quarterly results for the northern region'
const m = pptx.measureText(title, { wIn: 4, insetIn: 0.1, fontSize: 24, fontFace: 'Aptos' })
const textH = m.heightIn + 0.1 // plus the default top and bottom insets

const slide = pptx.addSlide()
slide.addShape('roundRect', { x: 1, y: 1, w: 4, h: textH + 0.4, fill: { color: 'F2F2F2' } })
slide.addText(title, { x: 1, y: 1.2, w: 4, h: textH, fontFace: 'Aptos', fontSize: 24 })
MemberReturns
heightInthe laid-out height in inches, without insets
lineCountthe number of wrapped lines
widestLineInthe widest line in inches. With a very large wIn, the width of the text on one line
measurablefalse when there is no fontFace or the run array is empty
approximatedFacesthe named faces measured with average character widths
uncoveredCodepointssorted code points a registered face has no glyph for
fitsBox(hIn)whether the text fits an inner height of hIn inches at full size
shrinkScaleFor(hIn)the percent scale 'shrink' would bake for that inner height, 100 when it fits

pptx.overflowsBox(text, { ...options, hIn }) is true when the text can be measured and does not fit hIn. Text it cannot measure returns false.

  • wIn is the width inside the insets. To pass the box width, add insetIn. A text box's default insets are 0.1 in on the left and right and 0.05 in at the top and bottom.
  • Results err tall: each width counts 3% wide and the height 4% tall, the same margins the pass bakes with. overflowsBox can report overflow for text PowerPoint fits, so treat it as a warning.
  • A non-empty uncoveredCodepoints means the height can come out short.
  • With at least one face registered, measureText and the pass share one run converter, one font lookup and one layout. shrinkScaleFor(h) equals the scale the pass bakes for that inner box, and heightIn equals the height 'resize' bakes, less the insets.
  • They differ when no face is registered. measureText still measures every named face with average character widths and lists it in approximatedFaces, while the pass writes bare flags. Check approximatedFaces when you need an exact number.

measureText, overflowsBox and tableLayout come from the measure family. new TsPptx() has them. A deck from createPresentation has them only when it asks, and without them the call throws family/not-composed. The fit pass runs either way.

ts
import { createPresentation } from 'pptx-ts'
import { measure } from 'pptx-ts/families'

const deck = createPresentation({ use: [measure] })

Measure without a presentation ​

pptx-ts/measure exports the model as plain functions, for layout code that has no TsPptx:

ts
import { readFile } from 'node:fs/promises'
import { FontMetricsRegistry, measureText, parseFontMetrics } from 'pptx-ts/measure'

const registry = new FontMetricsRegistry()
registry.set('Aptos', await parseFontMetrics(new Uint8Array(await readFile('/fonts/Aptos.ttf'))))
const m = measureText(registry, 'Quarterly results', { wIn: 2.8, fontSize: 24, fontFace: 'Aptos' })
ExportUse
measureText(registry, text, options)the measurement pptx.measureText makes, against your registry
FontMetricsRegistrymetrics by face: set(face, metrics, { bold, italic }), get, hasFace, hasCodepoint
parseFontMetrics(bytes, { font })parses a font file, or one font of a collection
listFontFaces(bytes), isFontCollection(bytes)what a font file holds
getHeuristicFontMetrics()the average character widths used for a face without metrics
makeRegistryResolver(registry), buildFitParagraphs(runs, options)the font lookup and run converter the pass uses
measureLayout, measureHeightPt, solveShrink, solveResizethe layout and both solvers, in points
SINGLE_LINE_PITCH, FONT_SCALE_STEP_PCT, MIN_FONT_SCALE_PCT, WIDTH_SAFETY_FACTOR, HEIGHT_SAFETY_FACTORthe model's constants

parseFontMetrics loads opentype.js the first time it runs. Importing the subpath does not load it.

Invalid input ​

ConditionResultCode
source is not a string, Uint8Array or ArrayBufferthrows InvalidOptionErrorfont/missing-source
a path that cannot be read, or base64 text, under Nodethrows MediaErrorfont/read-failed
a URL that does not loadthrows MediaErrorfont/fetch-failed
bytes that are not a font the parser readsthrows MediaErrorfont/parse-failed
a font index outside the filethrows InvalidOptionErrorfont/collection-index-out-of-range
a font name, or a collection's face, that names no font in the filethrows InvalidOptionErrorfont/collection-face-not-found
a face that is empty or not a stringthrows InvalidOptionErrorfont/missing-typeface
a measureText fontSize that is not a positive numberthrows InvalidOptionErrorfont/size-not-positive
fitted text with no fontFace, in a deck with a registered facewarns, bare flagmeasure/shrink-unmeasured, measure/resize-unmeasured
a fontFace with no metrics, in a deck with a registered facewarns, measured with average widthsmeasure/heuristic-metrics
a character the registered face has no glyph forwarns, measured with the missing-glyph widthmeasure/uncovered-codepoints
wIn, insetIn or hIn that is not a finite numberthrows InvalidOptionErrorcoord/non-finite
wIn not wider than twice insetIn, or hIn not above 0throws InvalidOptionErrorcoord/not-positive
measureText, overflowsBox or tableLayout on a deck composed without measurethrows UnsupportedFeatureErrorfamily/not-composed

Limits ​

Chinese and Japanese text breaks between any two characters and Korean text breaks at spaces, as in PowerPoint. Everything else below is an approximation:

ApproximationWhich way it errs
Widths add up each character's advance, with no kerning or ligatureswide: text shrinks a little more, or grows a little taller, than in PowerPoint
Widths count 3% wide and heights 4% tallwide and tall
Single line spacing is 1.2117 times the font size, measured on Aptos, Aptos SemiBold, Calibri, Tahoma and Arialnot checked on other fonts
'shrink' never reduces line spacingsmall: a lower scale than PowerPoint picks
A face with no metrics uses average character widthswide
A character the registered face lacks takes the missing-glyph widtheither way, and it is reported
No kinsoku: the model breaks before 、 where PowerPoint hangs it past the edgethe same line count, and a narrower widestLineIn
A tab counts as one spacenarrow: a line with tabs can measure short
Bullets and paragraph indents are not readnarrow: indented text can measure short
Text columns and vertical text are not readeither way
Right-to-left text and scripts that need shaping are not modeledeither way
'resize' never changes the widtha wrap: false line wider than the box runs past its edge
The theme font is not resolvedtext with no fontFace is not measured

The model and its calibration against PowerPoint-authored decks are described in the design notes.

Reading it back ​

A deck opened through pptx-ts/read reports what was baked on a shape's textFrame:

MemberReturns
autofit'none', 'normAutofit' or 'spAutoFit'
autofitFontScalethe baked font scale in percent, or null for a bare flag
autofitLineSpaceReductionthe baked line spacing reduction in percent, or null

A resized box's baked height is the shape's height, in EMU. See Read object model.

See also ​