text
Line spacing, wrapping, autofit, bullets, fields and font substitution, the way PowerPoint lays them out.
Line spacing, wrapping, autofit, bullets, fields and font substitution, the way PowerPoint actually lays them out.
npm install @pptx-studio/text
What it is
A browser will lay text out for you, but not the way PowerPoint does, and not the same way on every operating system. @pptx-studio/text measures runs through a canvas and does the rest itself: the 1.2-times line box, the break opportunities, the fifteen-rung autofit ladder, the bullet numbering that is not in the file, the date fields, and which face a run actually gets when the one it asked for is missing.
30-second example
const measure = createCanvasMeasurer(); // needs OffscreenCanvas: a tab or a Worker
const text = 'Measured against PowerPoint, not derived from the standard.';
const font = { family: 'Arial', sz: 1800 }; // sz in hundredths of a point
const lines = wrapText({
text,
widthPt: 200,
hyphenWidthPt: measure.measure('-', font).width,
measure: (start, end) => measure.measure(text.slice(start, end), font).width,
});
lines.length; // how many lines the package broke it into
lineAdvance(1800, { kind: 'percent', value: 150000 }); // 32.4 - the line box, not the font size
formatAutonumber('romanLcParenBoth', 4); // '(iv)'Key concepts
Never line-height: normal
It resolves from different font-table fields per operating system, so the same HTML wraps differently on macOS and Windows. The line box here is a font-independent 1.2 times the size, measured across thirteen faces and seven sizes.
Line breaking is smaller than UAX #14
PowerPoint's breaker is simpler than the Unicode algorithm, and the package matches the simpler one: 2212 probes fitted. Thai does not break, and the package says so rather than guessing.
The fifteen-rung autofit ladder
Shrink-on-overflow snaps to discrete rungs of font scale and line-spacing reduction. fitAutofit walks them in the measured order; a scale off the ladder is re-snapped by PowerPoint and the text visibly jumps.
The numbers are not in the file
An autonumbered list stores the scheme and a start value; the number itself is counted. numberParagraphs and formatAutonumber count the way PowerPoint does, for 39 of the 41 schemes - two are shaped into glyph ids before drawing and are refused rather than guessed.
Which typeface a run actually gets
fontReport probes the browser for each face the deck names and reports present, substituted or missing, with the metric-compatible substitute where one is known.
API reference
162 exports of @pptx-studio/text 0.2.2, read from the package this site installed. Click a name for its declaration.
src/errors.ts
TextErrorCodetypeTyped failures, as everywhere else in this repository.type TextErrorCode = /** A font size that is not a positive, finite number of hundredths of a point. */ 'TEXT_SIZE' | /** An `a:lnSpc` value outside what `ST_TextSpacing` can hold. */ 'TEXT_LINE_SPACING' | /** A line index or count that is not a non-negative integer. */ 'TEXT_LINE_COUNT' | /** A typeface name that cannot be written into a CSS font shorthand. */ 'TEXT_FONT_FAMILY' | /** A wrap width or hyphen advance that is not a usable length. */ 'TEXT_BREAK_WIDTH' | /** An `a:normAutofit` scale outside what `ST_TextFontScalePercentOrPercentString` can hold. */ 'TEXT_AUTOFIT_SCALE' | /** A body height or inset that is not a usable length. */ 'TEXT_AUTOFIT_HEIGHT' | /** * A typeface whose per-face line metrics were never measured. * * Two of the numbers the last line of a block needs belong to the typeface * and are not derivable from anything a browser reports - see `autofit.ts`. * A caller that reaches this either passes `APPROXIMATE_FACE_METRICS` on * purpose, with a diagnostic, or supplies its own; what it must not do is * get a plausible number from here without having asked for one. */ 'TEXT_FACE_METRICS' | /** An `@u` or `@strike` value that is not in its ST_ simple type. */ 'TEXT_DECORATION' | /** An `a:buAutoNum/@type` that is not one of the 41 schemes PowerPoint accepts. */ 'TEXT_AUTONUMBER_SCHEME' | /** * One of the two schemes T5 measured but could not read. * * `arabic1Minus` and `arabic2Minus` are shaped into glyph ids before they are * drawn, so the measurement holds indices into a font rather than characters. * Distinct from `TEXT_AUTONUMBER_SCHEME` because the scheme is real and the * gap is ours - "not a scheme" would send somebody to go and check the file. */ 'TEXT_AUTONUMBER_UNMEASURED' | /** * A bullet number outside `ST_TextBulletStartAtNum`, which is 1 to 32767. * * Not pedantry: `startAt="0"`, `"-1"`, `"32768"` and `"65536"` are each a * package PowerPoint **repairs** rather than reads, measured one probe to a * package. A reader that quietly renders one has accepted a file the format's * own author refuses. */ 'TEXT_AUTONUMBER_VALUE' | /** A bullet whose kind and content disagree - an `autonum` with no scheme, say. */ 'TEXT_BULLET' | /** * A date field whose `@lang` and `@type` were never measured. * * PowerPoint formats dates with Windows' own per-locale patterns, and * `Intl.DateTimeFormat` reproduces them on a quarter of the readings. So the * patterns are a measured table, and a cell outside it is a gap the caller * has to handle - by showing the cached text, which is what `renderField` * does - rather than a licence to invent a plausible date. */ 'TEXT_FIELD_LOCALE' | /** A field kind or slide number that cannot be rendered as asked. */ 'TEXT_FIELD' | /** A code point outside Unicode, handed to the script-run splitter. */ 'TEXT_SCRIPT_RUN' | /** * An `a:bodyPr` value the text frame cannot lay out. * * A `@numCol` outside `ST_TextColumnCount`, a `@vert` or `@anchor` that is not * one of the enumerated values, or a length that is not finite. Each is a * package PowerPoint repairs rather than reads. */ 'TEXT_FRAME' | /** * No `OffscreenCanvas`, or no 2d context on one. * * The measurer is built for a Web Worker, where `OffscreenCanvas` is the only * canvas there is. A caller that reaches this on the main thread has imported * the Worker-side measurer into the document side by mistake, which is worth * saying rather than silently falling back to a DOM canvas the plan * deliberately does not use. */ 'TEXT_NO_CANVAS';
TextErrorclassclass TextError extends Error { readonly code: TextErrorCode; /** The offending value, when the failure is about one. */ readonly token: string | null; constructor(code: TextErrorCode, message: string, token?: string | null); }
src/lines/line-model.ts
LineSpacingtypea:lnSpc, as the file states it.interface LineSpacing { readonly kind: 'percent' | 'points'; /** * For `percent`, `a:spcPct/@val` in thousandths of a percent - the value * `ST_Percentage` parses to, so both the `150000` and the `150%` spellings * arrive here as 150000. For `points`, `a:spcPts/@val` in hundredths of a * point. */ readonly value: number; }NATURAL_LINE_FACTORconstThe line box at single spacing, as a multiple of the font size.const NATURAL_LINE_FACTOR = 1.2;
quantisePercentfunctiona:spcPct/@valto a multiple, rounded half up to a whole percent.function quantisePercent(value: number): number;
naturalLineHeightfunctionThe line box at single spacing, in points.function naturalLineHeight(sz: number): number;
lineAdvancefunctionBaseline-to-baseline distance, in points.function lineAdvance(sz: number, lnSpc?: LineSpacing): number;
lineTopfunctionThe top of lineindex, in points from the top of the text.function lineTop(index: number, advance: number): number;
blockHeightfunctionThe height of a block oflineCountlines, in points.function blockHeight(lineCount: number, advance: number, lastLineHeight: number, spcFirstLastPara?: boolean): number;
src/runs/measure.ts
RunFonttypeThe properties of a run that change its width.interface RunFont { /** `a:latin/@typeface`, already resolved through any `+mj-lt`/`+mn-lt`. */ readonly family: string; /** `a:rPr/@sz`, hundredths of a point. */ readonly sz: number; readonly bold?: boolean | undefined; readonly italic?: boolean | undefined; /** `a:rPr/@spc`, hundredths of a point. May be negative. */ readonly spc?: number | undefined; /** `a:rPr/@kern`: the minimum font size at which kerning happens. */ readonly kern?: number | undefined; }kerningEnabledfunctionWhether kerning applies to a run.function kerningEnabled(kern: number | undefined, sz: number): boolean;
cssFamilyfunctionA family name, quoted for a CSS font shorthand.function cssFamily(family: string): string;
cssFontfunctionA CSSfontshorthand, in the order the property requires: style, weight, size, family.function cssFont(font: RunFont): string;
cssLetterSpacingfunctiona:rPr/@spcas a CSSletterSpacing.function cssLetterSpacing(spc: number | undefined): string;
AdvancetypeWhat a measurement returns. Points, in the fixed 960x540pt layout space.interface Advance { /** The pen advance across the run: what a line's width is made of. */ readonly width: number; }TextMeasurertypeinterface TextMeasurer { /** The advance of `text` set in `font`, in points. */ measure(text: string, font: RunFont): Advance; }MeasuringContexttypeinterface MeasuringContext { font: string; letterSpacing: string; fontKerning: string; measureText(text: string): { readonly width: number; readonly fontBoundingBoxAscent: number; readonly fontBoundingBoxDescent: number; /** Negative where the ideographic baseline is below the alphabetic one. */ readonly ideographicBaseline: number; }; }createMeasuringContextfunctionThe one place in the package that acquires a measuring surface.function createMeasuringContext(): MeasuringContext;
createCanvasMeasurerfunctionA measurer over a 1x1OffscreenCanvas.function createCanvasMeasurer(): TextMeasurer;
src/lines/baseline.ts
FaceBoxtypeA typeface's own box, as fractions of the em.interface FaceBox { readonly ascent: number; readonly descent: number; /** * The ideographic baseline below the alphabetic one, as a fraction of the em. * * Only vertical text reads it: an upright glyph's baseline sits `1 - this` * from the leading edge of its cell. ADR 0039. */ readonly ideographic: number; }baselineDropfunctionHow far below the top of the line box the baseline sits, in points.function baselineDrop(lineHeightPt: number, box: FaceBox): number;
FACE_BOX_PXconstThe size a face box is measured at.const FACE_BOX_PX = 1000;
FaceBoxProbetypeMeasures a typeface's box on the same surface the measurer uses.interface FaceBoxProbe { box(family: string): FaceBox; }createFaceBoxProbefunctionA probe over oneOffscreenCanvas, caching each face.function createFaceBoxProbe(context?: MeasuringContext): FaceBoxProbe;
src/runs/face-rules.gen.ts
RuleMetricstypeOne rule, as fractions of the em: how far below the baseline, and how thick.interface RuleMetrics { /** Below the baseline is positive, so a strikethrough's offset is negative. */ readonly offset: number; readonly thickness: number; }FaceRulestypeThe two rules a typeface carries.interface FaceRules { readonly underline: RuleMetrics; readonly strike: RuleMetrics; }MEASURED_FACE_RULESconstWhere PowerPoint drew each rule, measured at 120pt on a 1920-pixel export.const MEASURED_FACE_RULES: Readonly<Record<string, FaceRules>>;
src/runs/decoration.ts
UnderlinetypeST_TextUnderlineType.type Underline = 'none' | 'words' | 'sng' | 'dbl' | 'heavy' | 'dotted' | 'dottedHeavy' | 'dash' | 'dashHeavy' | 'dashLong' | 'dashLongHeavy' | 'dotDash' | 'dotDashHeavy' | 'dotDotDash' | 'dotDotDashHeavy' | 'wavy' | 'wavyHeavy' | 'wavyDbl';
StriketypeST_TextStrikeType.type Strike = 'noStrike' | 'sngStrike' | 'dblStrike';
RulePatterntypeHow a rule is painted along its length.type RulePattern = 'solid' | 'dotted' | 'dashed' | 'longDashed' | 'dotDash' | 'dotDotDash';
DrawnRuletypeOne drawn rule, in points relative to the baseline.interface DrawnRule { /** The rule's top edge below the baseline; a strikethrough's is negative. */ readonly top: number; readonly thickness: number; readonly pattern: RulePattern; /** Drawn as a wave rather than a straight rail. */ readonly wavy: boolean; }APPROXIMATE_FACE_RULESconstWhat to use for a typeface nobody measured.const APPROXIMATE_FACE_RULES: FaceRules;
faceRulesfunctionThe rules measured for a typeface, or an error naming what is missing.function faceRules(typeface: string): FaceRules;
hasFaceRulesfunctionWhether this typeface was one of the eight measured.function hasFaceRules(typeface: string): boolean;
underlineRulesfunctionThe rules an@uvalue draws, in points relative to the baseline.function underlineRules(underline: Underline | undefined, sizePt: number, rules: FaceRules): readonly DrawnRule[];
strikeRulesfunctionThe rules a@strikevalue draws.function strikeRules(strike: Strike | undefined, sizePt: number, rules: FaceRules): readonly DrawnRule[];
decoratedStretchesfunctionThe text a rule is drawn under, which is not always the run's text.function decoratedStretches(text: string, underline: Underline | undefined): readonly (readonly [number, number])[];
src/lines/kinsoku.ts
KinsokuSetstypeThe two sets, as characters.interface KinsokuSets { /** Characters that may not begin a line. */ readonly noStart: ReadonlySet<string>; /** Characters that may not end a line. */ readonly noEnd: ReadonlySet<string>; }BUILT_IN_KINSOKUconstPowerPoint's own sets, used unless a file replaces them.const BUILT_IN_KINSOKU: KinsokuSets;
HANGING_PUNCTUATIONconstThe characters that hang outside the measure whenhangingPunctis on.const HANGING_PUNCTUATION: ReadonlySet<string>;
kinsokuInForcefunctionThe sets in force, given what the package states.function kinsokuInForce(stated: KinsokuSets | undefined, strictFirstAndLastChars: boolean | undefined): KinsokuSets;
src/lines/break.ts
BreakTailoringtypeThe threea:pPrattributes that tailor line breaking.interface BreakTailoring { /** * `@latinLnBrk`. **Absent behaves as `false`.** * * ECMA-376 gives the attribute a default of `true`. PowerPoint does not: * measured over 24 box widths on a 28-code-point string, a paragraph stating * nothing lays out identically to one stating `0` and differently from one * stating `1` at every width. Following the standard here breaks every long * word in every deck. */ readonly latinLnBrk?: boolean | undefined; /** * `@eaLnBrk`. * * Measured **inert**: all three of absent, `1` and `0` lay out identically on * a nine-ideograph string at every box width. It is accepted and ignored * rather than absent from this interface, so that a caller passing it is not * quietly getting a different rule than they think, and so the next person to * wonder finds the answer here. */ readonly eaLnBrk?: boolean | undefined; /** * `@hangingPunct`. **Absent behaves as `true`.** * * Only affects the six characters in `HANGING_PUNCTUATION`, and only the fit * test - it never adds or removes an opportunity. Measured inert on Latin * punctuation: a Latin comma hangs by way of the space after it, not by way * of this attribute. */ readonly hangingPunct?: boolean | undefined; }BreakOptionstypeinterface BreakOptions { readonly tailoring?: BreakTailoring | undefined; /** * The kinsoku sets in force. Defaults to PowerPoint's built-in list, which is * what applies unless a package both states `p:kinsoku` and turns * `strictFirstAndLastChars` off - see `kinsokuInForce`. */ readonly kinsoku?: KinsokuSets | undefined; }isEastAsianfunctionfunction isEastAsian(ch: string): boolean;
toCodePointsfunctionCode points, not UTF-16 units. Every index in this module counts these.function toCodePoints(text: string): readonly string[];
breakOpportunitiesfunctionEvery position at which a line may end, ascending.function breakOpportunities(text: string, options?: BreakOptions): readonly number[];
RangeMeasurertypeThe width in points of the code points in[start, end).type RangeMeasurer = (start: number, end: number) => number;
WrapInputtypeinterface WrapInput { readonly text: string; /** The usable width, in points: the frame width less `lIns` and `rIns`. */ readonly widthPt: number; readonly measure: RangeMeasurer; /** * The advance of a hyphen in the run's face, in points. * * Charged to a line that ends on a soft hyphen, because that line draws one. * Measured: at a box sized for exactly the code points up to and including a * soft hyphen, PowerPoint refused the break and fell back to the space before * it. The soft hyphen has no advance of its own, so the only thing that can * have overflowed is the hyphen it drew. */ readonly hyphenWidthPt: number; readonly tailoring?: BreakTailoring | undefined; readonly kinsoku?: KinsokuSets | undefined; }LineBoxtypeinterface LineBox { /** Index of the first code point on the line. */ readonly start: number; /** One past the last code point on the line, including anything that hangs. */ readonly end: number; /** * One past the last code point charged to the width. * * Equal to `end` unless something hangs. The difference is what a renderer * must let overflow the frame, and what an alignment calculation must ignore. */ readonly measuredEnd: number; /** Whether the line draws a hyphen after its last code point. */ readonly hyphen: boolean; /** The text on the line, hanging characters included. */ readonly text: string; }wrapTextfunctionBreaktextinto lines.function wrapText(input: WrapInput): readonly LineBox[];
src/lines/autofit.ts
AutofitScaletypeOne rung: the pair of attributes PowerPoint writes intoa:normAutofit.interface AutofitScale { /** `a:normAutofit/@fontScale`. */ readonly fontScale: number; /** `a:normAutofit/@lnSpcReduction`. */ readonly lnSpcReduction: number; }AUTOFIT_LADDERconstThe ladder, in the order PowerPoint tries it.const AUTOFIT_LADDER: readonly AutofitScale[];
FaceLineMetricstypeThe two numbers the last-line rule needs that belong to the typeface rather than to the format.interface FaceLineMetrics { /** * `b`: the per-size term in `lastLine = 0.75 x advance + b x size`. * * Spans 0.2053 on Tahoma to 0.3000 on Courier New. Fitted across seven sizes * and two line spacings a face, to a worst residual of 0.0043pt. */ readonly lastLineCoefficient: number; /** * `C`: the multiple of the effective size up to which the last line takes the * whole advance instead of the fitted value. * * Indistinguishable from the 1.2 line box on five of the six faces measured, * and 1.2698 on Courier New - which is the only reason it is a field rather * than the constant 1.2. */ readonly floorRatio: number; }MEASURED_FACE_METRICSconstThe six faces T4 measured, by the name a run states ina:latin/@typeface.const MEASURED_FACE_METRICS: Readonly<Record<string, FaceLineMetrics>>;
APPROXIMATE_FACE_METRICSconstWhat to use for a typeface nobody measured.const APPROXIMATE_FACE_METRICS: FaceLineMetrics;
faceLineMetricsfunctionThe measured metrics for a typeface, or a typed failure naming it.function faceLineMetrics(typeface: string): FaceLineMetrics;
hasFaceLineMetricsfunctionWhether this typeface was one of the six measured.function hasFaceLineMetrics(typeface: string): boolean;
effectiveFontSizefunctionThe size a run is actually drawn at, in points.function effectiveFontSize(sz: number, fontScale?: number): number;
autofitAdvancefunctionBaseline-to-baseline distance under an autofit scale, in points.function autofitAdvance(effectiveSizePt: number, lnSpcReduction?: number, lnSpc?: LineSpacing): number;
lastLineHeightfunctionThe height of the last line of a block, in points.function lastLineHeight(advancePt: number, effectiveSizePt: number, metrics: FaceLineMetrics): number;
ParagraphBoxtypeOne paragraph, already laid out at some candidate scale.interface ParagraphBox { /** How many lines it wrapped into at this scale. At least one. */ readonly lines: number; /** `a:rPr/@sz` in hundredths of a point, before any scale. */ readonly sz: number; /** The metrics of the face that governs the paragraph's last line. */ readonly metrics: FaceLineMetrics; readonly lnSpc?: LineSpacing | undefined; readonly spcBef?: LineSpacing | undefined; readonly spcAft?: LineSpacing | undefined; }textBodyHeightfunctionThe height a laid-out text body occupies, in points.function textBodyHeight(paragraphs: readonly ParagraphBox[], scale: AutofitScale, spcFirstLastPara?: boolean): number;
storedScalefunctionView mode: the scale a file states, applied as written.function storedScale(fontScale?: number, lnSpcReduction?: number): AutofitScale;
AutofitRequesttypeinterface AutofitRequest { /** * The height the text has to fit in, in points: the shape height less `tIns` * and `bIns`. * * Measured 20/20 against 12/20 for testing against the shape height itself, * so the insets are the caller's to subtract and are named here rather than * passed as a rectangle, to make forgetting them harder. */ readonly bodyHeightPt: number; /** `a:bodyPr/@spcFirstLastPara`, resolved. Absent behaves as false. */ readonly spcFirstLastPara?: boolean | undefined; /** * Lay the text out at a candidate scale and report each paragraph. * * A callback rather than a fixed list because the line count is a consequence * of the scale: shrinking the font rewraps the text, and a fit test that used * one line count for every rung would be wrong on every paragraph that wraps. * Use `effectiveFontSize` to get the size to wrap at. */ readonly layout: (scale: AutofitScale) => readonly ParagraphBox[]; }AutofitResulttypeinterface AutofitResult { readonly scale: AutofitScale; /** The paragraphs as `layout` reported them at the chosen scale. */ readonly paragraphs: readonly ParagraphBox[]; /** The height those paragraphs occupy, in points. */ readonly heightPt: number; /** True when even the last rung overflows, so the text is drawn overflowing. */ readonly overflows: boolean; }fitAutofitfunctionEdit mode: recompute the scale the way PowerPoint does.function fitAutofit(request: AutofitRequest): AutofitResult;
SHAPE_AUTOFIT_SLACKconstThe heighta:spAutoFitgives a shape, in points.const SHAPE_AUTOFIT_SLACK: number;
requiredShapeHeightfunctionfunction requiredShapeHeight(paragraphs: readonly ParagraphBox[], insets: { readonly topPt: number; readonly bottomPt: number; }, scale?: AutofitScale, spcFirstLastPara?: boolean): number;
src/bullets/autonumber.ts
AUTONUMBER_SCHEMESconstST_TextAutonumberScheme, inPpNumberedBulletStyleorder.const AUTONUMBER_SCHEMES: readonly ["alphaLcPeriod", "alphaUcPeriod", "arabicParenR", "arabicPeriod", "romanLcParenBoth", "romanLcParenR", "romanLcPeriod", "romanUcPeriod", "alphaLcParenBoth", "alphaLcParenR", "alphaUcParenBoth", "alphaUcParenR", "arabicParenBoth", "arabicPlain", "romanUcParenBoth", "romanUcParenR", "ea1ChsPlain", "ea1ChsPeriod", "circleNumDbPlain", "circleNumWdWhitePlain", "circleNumWdBlackPlain", "ea1ChtPlain", "ea1ChtPeriod", "arabic2Minus", "arabic1Minus", "hebrew2Minus", "ea1JpnKorPlain", "ea1JpnKorPeriod", "arabicDbPlain", "arabicDbPeriod", "thaiAlphaPeriod", "thaiAlphaParenR", "thaiAlphaParenBoth", "thaiNumPeriod", "thaiNumParenR", "thaiNumParenBoth", "hindiAlphaPeriod", "hindiNumPeriod", "ea1JpnChsDbPeriod", "hindiNumParenR", "hindiAlpha1Period"];
AutonumberSchemetypetype AutonumberScheme = (typeof AUTONUMBER_SCHEMES)[number];
isAutonumberSchemefunctionWhether a string is one of the 41 values PowerPoint's own UI can produce.function isAutonumberScheme(value: string): value is AutonumberScheme;
START_AT_MINconstST_TextBulletStartAtNum, which the schema bounds at 1 to 32767.const START_AT_MIN = 1;
START_AT_MAXconstconst START_AT_MAX = 32767;
AlphabettypeAn alphabet, and the three numbers measured with it.interface Alphabet { readonly letters: string; readonly wrap: number; readonly bias: number; readonly fillWithLast: boolean; }ALPHABETSconstThe six alphabets, read off the sweep one value at a time.const ALPHABETS: Readonly<Record<string, Alphabet>>;
AutonumberRuletypeA scheme split into its numeral system and the characters around it.interface AutonumberRule { readonly numeral: string; readonly prefix: string; readonly suffix: string; }UNMEASURED_SCHEMESconstThe two of the 41 whose rendering T5 could not read.const UNMEASURED_SCHEMES: readonly string[];
autonumberRulefunctionSplit a scheme name into a numeral system and a decoration.function autonumberRule(scheme: string): AutonumberRule;
formatAutonumberfunctionThe string PowerPoint draws forschemeatn.function formatAutonumber(scheme: string, n: number): string;
autonumberTypefacefunctionThe typeface a scheme needs, where it needs one.function autonumberTypeface(scheme: string): string | null;
src/bullets/bullets.ts
BulletKindtypeThe exclusive group inCT_TextParagraphProperties: one of four, or nothing.type BulletKind = 'none' | 'char' | 'autonum' | 'blip';
BulletSizetypea:buSzPct/a:buSzPts/a:buSzTx, which are exclusive of each other.type BulletSize = { readonly kind: 'percent'; readonly value: number; } | { readonly kind: 'points'; readonly value: number; } | { readonly kind: 'text'; };BulletColortypea:buClr/a:buClrTx. The colour itself is the model's, not this package's.type BulletColor<TColor> = { readonly kind: 'color'; readonly value: TColor; } | { readonly kind: 'text'; };BulletFonttypea:buFont/a:buFontTx.type BulletFont = { readonly kind: 'typeface'; readonly value: string; } | { readonly kind: 'text'; };ResolvedBullettypeA paragraph's bullet, after the cascade has merged it.interface ResolvedBullet<TColor> { readonly kind: BulletKind; /** `a:buChar/@char`, as written. The private-use mapping happens at draw time. */ readonly char?: string | undefined; /** `a:buAutoNum/@type`. */ readonly scheme?: string | undefined; /** `a:buAutoNum/@startAt`. Absent is measured to behave exactly as `1`. */ readonly startAt?: number | undefined; /** The relationship id of an `a:buBlip`'s image. */ readonly blip?: string | undefined; readonly font?: BulletFont | undefined; readonly size?: BulletSize | undefined; readonly color?: BulletColor<TColor> | undefined; }NumberedParagraphtypeWhatnumberParagraphsneeds to know about one paragraph.interface NumberedParagraph { /** `a:pPr/@lvl`, 0 through 8. */ readonly level: number; /** The resolved `a:buAutoNum/@type`, or `null` for any other bullet kind. */ readonly scheme: string | null; /** The resolved `a:buAutoNum/@startAt`; absent behaves as 1. */ readonly startAt: number; }numberParagraphsfunctionThe number each paragraph's bullet carries, ornullwhere it has none.function numberParagraphs(paragraphs: readonly NumberedParagraph[]): readonly (number | null)[];
SYMBOL_TYPEFACESconstThe symbol faces this experiment measured, and no more.const SYMBOL_TYPEFACES: readonly string[];
isSymbolTypefacefunctionWhethertypefaceis one of the symbol faces T5 measured.function isSymbolTypeface(typeface: string): boolean;
symbolBulletCharfunctionThe character a symbol face actually draws forchar.function symbolBulletChar(char: string, typeface: string | undefined): string;
BulletContexttypeThe run properties a bullet takes its face, size and colour from.interface BulletContext<TColor> { /** The **first** run's typeface. Measured: not the largest run's, not the paragraph's. */ readonly typeface: string; /** The first run's `@sz`, hundredths of a point. */ readonly sz: number; readonly color: TColor; }DrawableBullettypeA bullet resolved to the thing a renderer draws.type DrawableBullet<TColor> = { readonly kind: 'none'; } | { readonly kind: 'text'; /** The characters to draw, already mapped for a symbol face. */ readonly text: string; readonly typeface: string; /** Hundredths of a point. */ readonly sz: number; readonly color: TColor; } | { readonly kind: 'picture'; readonly blip: string; /** The height to scale the picture to, in hundredths of a point. */ readonly sz: number; };drawableBulletfunctionWhat to draw for one paragraph's bullet.function drawableBullet<TColor>(bullet: ResolvedBullet<TColor>, context: BulletContext<TColor>, number?: number): DrawableBullet<TColor>;
BLIP_BULLET_HEIGHTconstThe height a picture bullet is scaled to, as a fraction of the font size.const BLIP_BULLET_HEIGHT = 0.7;
BulletLayouttypeWhere the bullet is drawn and where the paragraph's text starts, in points.interface BulletLayout { /** The bullet's left edge, from the text frame's left inset. */ readonly bulletLeftPt: number; /** The first line's text left edge. */ readonly firstLineLeftPt: number; /** Every line after the first. */ readonly wrappedLeftPt: number; }bulletLayoutfunctionWhere a bulleted paragraph's bullet and text sit, in points from the inset.function bulletLayout(marLPt: number, indentPt: number, bulletAdvancePt: number): BulletLayout;
blipBulletWidthfunctionThe width a picture bullet occupies, in points.function blipBulletWidth(sz: number, aspect: number): number;
isStartAtInRangefunctionWhetherstartAtis insideST_TextBulletStartAtNum, which PowerPoint enforces.function isStartAtInRange(startAt: number): boolean;
src/fields/fields.ts
RESERVED_FIELD_TYPESconstThe eighteen@typevalues PowerPoint treats as fields rather than as text.const RESERVED_FIELD_TYPES: readonly ["slidenum", "datetime", "datetime1", "datetime2", "datetime3", "datetime4", "datetime5", "datetime6", "datetime7", "datetime8", "datetime9", "datetime10", "datetime11", "datetime12", "datetime13", "datetimeFigureOut", "uaqdatetime1", "uaqdatetime2"];
ReservedFieldTypetypetype ReservedFieldType = (typeof RESERVED_FIELD_TYPES)[number];
isReservedFieldTypefunctionWhether PowerPoint recomputes this@typerather than showing the cache.function isReservedFieldType(type: string): type is ReservedFieldType;
renderDatePatternfunctionRender one measured Windows NLS pattern for an instant, in a locale.function renderDatePattern(pattern: string, at: Date, lang: string): string;
datePatternForfunctionThe measured pattern for a@typein a@lang, ornullwhere none was.function datePatternFor(type: string, lang: string): string | null;
formatDateTimeFieldfunctionThe text a date field shows, formatted the way PowerPoint formats it.function formatDateTimeField(type: string, lang: string, at: Date): string;
FieldRequesttypeEverythingrenderFieldneeds about onea:fld.interface FieldRequest { /** `a:fld/@type`. An unreserved value is the cache's cue, not an error. */ readonly type: string; /** The `@lang` of the field's own `a:rPr`, which is what drives the format. */ readonly lang: string; /** The `a:t` PowerPoint last wrote, which is the fallback and never the answer. */ readonly cachedText: string; /** The slide's one-based position, for `slidenum`. */ readonly slideNumber?: number | undefined; /** The instant to render a date at. Passed in so a test is not a clock. */ readonly now: Date; }RenderedFieldtypeWhat was shown, and which of the three branches produced it.interface RenderedField { readonly text: string; /** * - `computed`: a reserved type with a measured pattern, recomputed as * PowerPoint recomputes it. * - `cached`: the `a:t`, because the type is not reserved or its locale was * never measured. Diagnosable, and the right thing to show. */ readonly source: 'computed' | 'cached'; /** Why the cache was used, for the diagnostics panel. Empty when it was not. */ readonly reason: string; }renderFieldfunctionWhat to display for onea:fld.function renderField(request: FieldRequest): RenderedField;
src/fields/field-formats.gen.ts
FIELD_DATE_PATTERNSconstWindows' own date patterns, per locale and pera:fld/@type.const FIELD_DATE_PATTERNS: Readonly<Record<string, Readonly<Record<string, string>>>>;
src/runs/script-runs.ts
ScriptSlottypeThe four typeface slots aCT_TextCharacterPropertiescan name.type ScriptSlot = 'latin' | 'ea' | 'cs' | 'sym';
RunTypefacestypeThe four slots as a run states them. An absent slot falls back tolatin.interface RunTypefaces { readonly latin: string; readonly ea?: string | undefined; readonly cs?: string | undefined; readonly sym?: string | undefined; }scriptSlotOffunctionWhich slot drawscode.function scriptSlotOf(code: number): ScriptSlot;
typefaceForfunctionThe typeface a character is drawn in, given what the run names.function typefaceFor(code: number, faces: RunTypefaces): string;
ScriptRuntypeOne maximal stretch of a string drawn in a single typeface.interface ScriptRun { /** Index of the first code unit, into the original string. */ readonly start: number; /** Index one past the last code unit. */ readonly end: number; readonly slot: ScriptSlot; readonly typeface: string; readonly text: string; }splitScriptRunsfunctionSplit a run of text into the stretches PowerPoint draws in one face each.function splitScriptRuns(text: string, faces: RunTypefaces): readonly ScriptRun[];
src/frames/frame.ts
AnchortypeST_TextAnchoringType.type Anchor = 't' | 'ctr' | 'b' | 'just' | 'dist';
VerticalTexttypeST_TextVerticalType.type VerticalText = 'horz' | 'vert' | 'vert270' | 'wordArtVert' | 'eaVert' | 'mongolianVert' | 'wordArtVertRtl';
VertOverflowtypeST_TextHorzOverflowTypeandST_TextVertOverflowType.type VertOverflow = 'overflow' | 'ellipsis' | 'clip';
EdgetypeWhich frame edge an axis runs from.type Edge = 'left' | 'top' | 'right' | 'bottom';
Insetstypea:bodyPr's four insets, in points.interface Insets { readonly left: number; readonly top: number; readonly right: number; readonly bottom: number; }DEFAULT_INSETSconstThe inset defaults, per attribute rather than per element.const DEFAULT_INSETS: Insets;
BoxtypeA rectangle in points, relative to the shape's own top-left corner.interface Box { readonly leftPt: number; readonly topPt: number; readonly widthPt: number; readonly heightPt: number; }contentBoxfunctionThe box the text is laid out in, after the insets.function contentBox(frame: { widthPt: number; heightPt: number; }, insets: Insets): Box;turnedInsetsfunctionThe insets as the frame sees them once the block has been turned.function turnedInsets(insets: Insets, quarterDeg: number): Insets;
FrameAxestypeWhich axis the anchor and the alignment govern, and from which edge.interface FrameAxes { readonly stacking: 'x' | 'y'; readonly anchorFrom: Edge; readonly alignFrom: Edge; /** GDI is asked for the `@`-prefixed vertical face. */ readonly verticalFace: boolean; /** Each character occupies a line of its own. */ readonly stackedGlyphs: boolean; }VERTICAL_AXESconstThe axes for eachST_TextVerticalType.const VERTICAL_AXES: Readonly<Record<VerticalText, FrameAxes>>;
frameAxesfunctionThe axes an absent or stated@vertselects.function frameAxes(vert?: VerticalText): FrameAxes;
anchorFractionfunctionHow far along its axis each anchor puts the block.function anchorFraction(anchor?: Anchor): number;
offsetAlongfunctionThe block's offset from the start of its axis, in points.function offsetAlong(from: Edge, fraction: number, availablePt: number, blockPt: number): number;
blockOriginfunctionWhere the block's near corner sits inside the content box, in points.function blockOrigin(input: { readonly content: Box; readonly axes: FrameAxes; readonly anchor?: Anchor | undefined; readonly anchorCtr?: boolean | undefined; readonly blockAcrossPt: number; readonly blockAlongPt: number; }): { readonly leftPt: number; readonly topPt: number; };emptyParagraphHeightfunctionThe height of ana:pwith no runs in it, in points.function emptyParagraphHeight(sz: number, lnSpcMultiple?: number): number;
MAX_COLUMNSconstST_TextColumnCount. PowerPoint repairs a package outside it.const MAX_COLUMNS = 16;
columnBoxfunctionThe box of one column, in points.function columnBox(input: { readonly content: Box; readonly index: number; readonly numCol?: number | undefined; readonly spcColPt?: number | undefined; readonly rtlCol?: boolean | undefined; }): Box;ELLIPSISconstThe character@vertOverflow="ellipsis"substitutes for a line that does not fit.const ELLIPSIS = "…";
OverflowLinetypeOne laid-out line, as much of it as the overflow rule needs.interface OverflowLine { readonly topPt: number; readonly heightPt: number; }DrawnLinestypeWhat an overflow rule leaves on the slide.interface DrawnLines { /** How many of the lines are drawn as they are. */ readonly count: number; /** An ellipsis is drawn as the next line, in place of the one that did not fit. */ readonly ellipsis: boolean; }drawnLinesfunctionWhich lines@vertOverflowdraws.function drawnLines(lines: readonly OverflowLine[], availableHeightPt: number, overflow?: VertOverflow): DrawnLines;
UprightPentypeWhere an upright glyph's pen goes, in the line's own coordinates.interface UprightPen { /** The glyph's alphabetic baseline, from the leading edge of its cell. */ readonly alongPt: number; /** The glyph's own x origin, from the leading edge of the line box. */ readonly acrossPt: number; }uprightPenfunctionThe pen an upright glyph is drawn from, turned a quarter back out of the line.function uprightPen(cell: { readonly sizePt: number; readonly advancePt: number; readonly lineHeightPt: number; readonly ideographic: number; }): UprightPen;wordArtCellfunctionThe square cell a WordArt column stacks one character in.function wordArtCell(sizePt: number, face: { readonly ascent: number; readonly descent: number; }): number;stackedPenfunctionWhere a glyph stacked in a WordArt cell is drawn from.function stackedPen(cell: { readonly sizePt: number; readonly advancePt: number; readonly cellPt: number; readonly columnPt: number; readonly descent: number; }): UprightPen;
src/fonts/presence.ts
ANCHORSconstThe generics a family is stacked on so that absence becomes observable.const ANCHORS: readonly string[];
FINGERPRINT_TEXTconstThe string a fingerprint is taken over.const FINGERPRINT_TEXT = "Hamburgefonstiv";
FINGERPRINT_PXconstThe size a fingerprint is taken at.const FINGERPRINT_PX = 100;
FontProbetypeA measurement over one CSS font stack.interface FontProbe { /** The advance of `text` set at `px` in `stack`, which is a CSS family list. */ advance(stack: string, px: number, text: string): number; }createFontProbefunctionA probe over the sameOffscreenCanvasroute the measurer uses.function createFontProbe(): FontProbe;
anchoredStackfunction"Family", monospace- the family with one generic behind it.function anchoredStack(family: string, anchor: string): string;
isFontAvailablefunctionWhether this machine resolvesfamilyto something of its own.function isFontAvailable(family: string, probe: FontProbe): boolean;
fontFingerprintfunctionThe advance that identifies whatfamilyresolved to, atFINGERPRINT_PX.function fontFingerprint(family: string, probe: FontProbe): number;
metricsAgreefunctionWhether two families lay out identically, which is what metric-compatible means.function metricsAgree(a: string, b: string, probe: FontProbe): boolean;
src/fonts/substitute.ts
POWERPOINT_LAST_RESORTconstWhat PowerPoint draws a Latin face it does not have in.const POWERPOINT_LAST_RESORT = "Calibri";
LAST_RESORT_FAMILIESconstChased in order whenPOWERPOINT_LAST_RESORTis itself absent.const LAST_RESORT_FAMILIES: readonly string[];
LAST_RESORT_GENERICconstThe one generic every stack ends on, so a stack is never open-ended.const LAST_RESORT_GENERIC = "sans-serif";
Substitutetypeinterface Substitute { /** The typeface a deck names. */ readonly missing: string; /** What to render with instead. */ readonly use: string; /** Whether `use` was designed to the same advances as `missing`. */ readonly metricCompatible: boolean; readonly note?: string; }SUBSTITUTESconstThe substitutions, most specific first.const SUBSTITUTES: readonly Substitute[];
substituteForfunctionfunction substituteFor(family: string): Substitute | undefined;
fontStackfunctionThe CSS family list a run is set in.function fontStack(family: string, drawn?: string): string;
verifySubstitutefunctionWhether a substitution'smetricCompatibleclaim holds on this machine.function verifySubstitute(substitute: Substitute, probe: FontProbe, available: (family: string) => boolean): boolean | null;
src/fonts/report.ts
FontStatustypepresent- the machine resolved the name to something of its own.substituted- absent, and the table names what to use instead.missing- absent, with nothing better than what PowerPoint itself would do.type FontStatus = 'present' | 'substituted' | 'missing';
FontFindingtypeinterface FontFinding { /** The typeface as the deck names it, after any `+mj-lt` was resolved. */ readonly typeface: string; readonly status: FontStatus; /** The family the text will be laid out in. */ readonly rendersAs: string; /** The whole CSS family list, which is what a renderer sets. */ readonly stack: string; /** `null` when the claim could not be checked because a face is absent. */ readonly metricCompatible: boolean | null; /** Whether `metricCompatible` was measured here rather than taken from the table. */ readonly verified: boolean; /** How many runs asked for this typeface. */ readonly runs: number; }fontReportfunctionOne finding per distinct typeface, ordered by run count and then by name.function fontReport(requested: Iterable<string>, probe?: FontProbe): readonly FontFinding[];
substitutedFontsfunctionThe findings a user needs to be told about: everything not drawn as asked.function substitutedFonts(findings: readonly FontFinding[]): readonly FontFinding[];
Errors
Throws TextError, whose code is one of:
- TEXT_SIZE
- TEXT_LINE_SPACING
- TEXT_LINE_COUNT
- TEXT_FONT_FAMILY
- TEXT_BREAK_WIDTH
- TEXT_AUTOFIT_SCALE
- TEXT_AUTOFIT_HEIGHT
- TEXT_FACE_METRICS
- TEXT_DECORATION
- TEXT_AUTONUMBER_SCHEME
- TEXT_AUTONUMBER_UNMEASURED
- TEXT_AUTONUMBER_VALUE
- TEXT_BULLET
- TEXT_FIELD_LOCALE
- TEXT_FIELD
- TEXT_SCRIPT_RUN
- TEXT_FRAME
- TEXT_NO_CANVAS
Try it live
Reading the deck…