PPTX Studio
Packages

text

Line spacing, wrapping, autofit, bullets, fields and font substitution, the way PowerPoint lays them out.

@pptx-studio/text0.2.2runs inTabWorkerNodeLive demo →npm →

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

wrap.ts
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';
  • TextErrorclass
    class 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/@val to 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 line index, in points from the top of the text.
    function lineTop(index: number, advance: number): number;
  • blockHeightfunctionThe height of a block of lineCount lines, 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 CSS font shorthand, in the order the property requires: style, weight, size, family.
    function cssFont(font: RunFont): string;
  • cssLetterSpacingfunctiona:rPr/@spc as a CSS letterSpacing.
    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;
    }
  • TextMeasurertype
    interface TextMeasurer {
      /** The advance of `text` set in `font`, in points. */
      measure(text: string, font: RunFont): Advance;
    }
  • MeasuringContexttype
    interface 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 1x1 OffscreenCanvas.
    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;
    }
  • baselineSharefunctionThe fraction of the line box that sits above the baseline.
    function baselineShare(box: FaceBox): 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 one OffscreenCanvas, 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 @u value draws, in points relative to the baseline.
    function underlineRules(underline: Underline | undefined, sizePt: number, rules: FaceRules): readonly DrawnRule[];
  • strikeRulesfunctionThe rules a @strike value 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 when hangingPunct is 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 three a:pPr attributes 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;
    }
  • BreakOptionstype
    interface 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;
    }
  • isEastAsianfunction
    function 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;
  • WrapInputtype
    interface 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;
    }
  • LineBoxtype
    interface 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;
    }
  • wrapTextfunctionBreak text into lines.
    function wrapText(input: WrapInput): readonly LineBox[];

src/lines/autofit.ts

  • AutofitScaletypeOne rung: the pair of attributes PowerPoint writes into a: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 in a: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;
  • AutofitRequesttype
    interface 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[];
    }
  • AutofitResulttype
    interface 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 height a:spAutoFit gives a shape, in points.
    const SHAPE_AUTOFIT_SLACK: number;
  • requiredShapeHeightfunction
    function requiredShapeHeight(paragraphs: readonly ParagraphBox[], insets: {
      readonly topPt: number;
      readonly bottomPt: number;
    }, scale?: AutofitScale, spcFirstLastPara?: boolean): number;

src/bullets/autonumber.ts

  • AUTONUMBER_SCHEMESconstST_TextAutonumberScheme, in PpNumberedBulletStyle order.
    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"];
  • AutonumberSchemetype
    type 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_MAXconst
    const 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 for scheme at n.
    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 in CT_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;
    }
  • NumberedParagraphtypeWhat numberParagraphs needs 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, or null where 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[];
  • isSymbolTypefacefunctionWhether typeface is one of the symbol faces T5 measured.
    function isSymbolTypeface(typeface: string): boolean;
  • symbolBulletCharfunctionThe character a symbol face actually draws for char.
    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;
  • isStartAtInRangefunctionWhether startAt is inside ST_TextBulletStartAtNum, which PowerPoint enforces.
    function isStartAtInRange(startAt: number): boolean;

src/fields/fields.ts

  • RESERVED_FIELD_TYPESconstThe eighteen @type values 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"];
  • ReservedFieldTypetype
    type ReservedFieldType = (typeof RESERVED_FIELD_TYPES)[number];
  • isReservedFieldTypefunctionWhether PowerPoint recomputes this @type rather 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 @type in a @lang, or null where 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;
  • FieldRequesttypeEverything renderField needs about one a: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 one a:fld.
    function renderField(request: FieldRequest): RenderedField;

src/fields/field-formats.gen.ts

  • FIELD_DATE_PATTERNSconstWindows' own date patterns, per locale and per a:fld/@type.
    const FIELD_DATE_PATTERNS: Readonly<Record<string, Readonly<Record<string, string>>>>;

src/runs/script-runs.ts

  • ScriptSlottypeThe four typeface slots a CT_TextCharacterProperties can name.
    type ScriptSlot = 'latin' | 'ea' | 'cs' | 'sym';
  • RunTypefacestypeThe four slots as a run states them. An absent slot falls back to latin.
    interface RunTypefaces {
      readonly latin: string;
      readonly ea?: string | undefined;
      readonly cs?: string | undefined;
      readonly sym?: string | undefined;
    }
  • scriptSlotOffunctionWhich slot draws code.
    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_TextHorzOverflowType and ST_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 each ST_TextVerticalType.
    const VERTICAL_AXES: Readonly<Record<VerticalText, FrameAxes>>;
  • frameAxesfunctionThe axes an absent or stated @vert selects.
    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 an a:p with 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 @vertOverflow draws.
    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 same OffscreenCanvas route 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 resolves family to something of its own.
    function isFontAvailable(family: string, probe: FontProbe): boolean;
  • fontFingerprintfunctionThe advance that identifies what family resolved to, at FINGERPRINT_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 when POWERPOINT_LAST_RESORT is 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";
  • Substitutetype
    interface 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[];
  • substituteForfunction
    function substituteFor(family: string): Substitute | undefined;
  • fontStackfunctionThe CSS family list a run is set in.
    function fontStack(family: string, drawn?: string): string;
  • verifySubstitutefunctionWhether a substitution's metricCompatible claim 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';
  • FontFindingtype
    interface 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…

Open the full demo →

Read more

On this page