@office-kit/pptx-preview 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,17 @@ const svg = renderSlideToSvg(pres, getSlides(pres)[0]);
42
42
  // → '<svg …>…</svg>' (text laid out via <foreignObject> — the browser wraps it)
43
43
  ```
44
44
 
45
+ ### Underlines in custom HTML editors
46
+
47
+ `textUnderlineStyle(underline, color)` returns inline CSS for a DrawingML
48
+ underline, using the same patterns as the HTML preview. Pass a resolved CSS
49
+ color (for example, `#154687`) because SVG background patterns cannot inherit
50
+ the surrounding text color. Apply the style to an inline element; keep any
51
+ strikethrough on an outer element. For `words`, apply it separately to each
52
+ non-whitespace segment. `false`, `none`, and `undefined` return an empty string.
53
+ Pass `false` as the third argument to let native CSS decorations inherit the
54
+ text color; image-based patterns still use the supplied color.
55
+
45
56
  ### PNG / RGBA (Node, no browser)
46
57
 
47
58
  ```ts
@@ -171,3 +182,17 @@ const paragraph = target.closest('[data-pptx-paragraph]')?.getAttribute('data-pp
171
182
  ## License
172
183
 
173
184
  MIT (code). Bundled fonts: OFL-1.1 / Apache-2.0 — see `fonts/LICENSES.md`.
185
+
186
+ ### List numbering in custom editing surfaces
187
+
188
+ `paragraphNumberLabels(paragraphs)` accepts an array of `{ bulletStyle, level }`
189
+ and returns a number label or `null` for each paragraph. It uses the same
190
+ per-level counters as the slide renderer, including restarts after a non-numbered
191
+ paragraph or a change of numbering scheme. Common Arabic, alphabetic and Roman
192
+ schemes are supported; other schemes currently fall back to Arabic numbers
193
+ followed by a period. Labels are display data and should not be inserted into
194
+ the editable paragraph text.
195
+
196
+ ### Text regions for editing overlays
197
+
198
+ `shapeTextRect(shape)` returns where a shape lays out its text, as fractions of its own extent: the `<a:rect>` its custom geometry states, or the one ECMA-376 defines for its preset (an ellipse's inscribed rectangle, a triangle's lower middle), following its adjust values. It is `null` when text takes the whole box. `resolveTextBodyRect(bounds, margins, region)` places that region in `bounds` less the body insets, giving the same inner text rectangle the preview uses before wrapping and autofit. Pass `{x, y, w, h}` and `{left, top, right, bottom}` in the same unit, normally EMU, with inherited/default margins already resolved. Margins that would collapse a region are dropped; a full box with nonpositive width or height stays degenerate so callers can handle it explicitly.
@@ -0,0 +1,236 @@
1
+ import { BulletStyle, PresentationData, SlideData, SlideShapeData, TextFormat, getParagraphBullet, getParagraphIndent, getParagraphPropertiesEffective, getShapeBodyPrEffective, getShapeTextAutoFitParams, getShapeTextColumns, getShapeTextDirection } from "@office-kit/pptx";
2
+
3
+ //#region src/text-layout.d.ts
4
+ /** What a measurer needs to size one run. Pixels at 96 DPI; the caller has
5
+ * already applied EMU→px, pt→px and the autofit fontScale. `family` is the
6
+ * resolved internal family name (see `substituteFamily`), not a CSS list. */
7
+ interface FontSpec {
8
+ readonly family: string;
9
+ readonly sizePx: number;
10
+ readonly bold: boolean;
11
+ readonly italic: boolean;
12
+ readonly letterSpacingPx: number;
13
+ /** Whether the font's OpenType kerning pairs are active for this run. */
14
+ readonly kerning?: boolean;
15
+ }
16
+ /** Advance width of `text` in px, plus optional vertical metrics. A real
17
+ * measurer returns ascent/descent/lineGap so line height matches the font;
18
+ * the heuristic returns width only and the engine falls back to a ratio. */
19
+ interface MeasureResult {
20
+ readonly widthPx: number;
21
+ readonly ascentPx?: number;
22
+ readonly descentPx?: number;
23
+ readonly lineGapPx?: number;
24
+ /** Ink bounds of the measured glyphs, relative to the baseline. These are
25
+ * signed distances, separate from font line metrics, and optional because heuristic
26
+ * measurers cannot determine them. */
27
+ readonly inkAscentPx?: number;
28
+ readonly inkDescentPx?: number;
29
+ /** True when the width is an estimate rather than a glyph measurement —
30
+ * the heuristic measurer always, the fontkit measurer when the resolved
31
+ * font lacks a glyph and per-character ratios filled the gap. The audit
32
+ * API surfaces this so borderline verdicts can be treated as advisory. */
33
+ readonly approximate?: boolean;
34
+ }
35
+ type TextMeasurer = (text: string, spec: FontSpec) => MeasureResult;
36
+ type TextLayoutMode = 'foreignObject' | 'svg';
37
+ interface RenderSlideOptions {
38
+ /** Measurer used by the pure-SVG text path. Custom tabs in browser mode
39
+ * use canvas font metrics, falling back to this measurer outside a browser. */
40
+ readonly measureText?: TextMeasurer;
41
+ /** Which text path to use. Defaults to 'foreignObject' (the browser path).
42
+ * Bodies with custom tab stops use SVG positioning in either mode, since
43
+ * CSS cannot represent their alignment. The harness opts into 'svg'. */
44
+ readonly textLayout?: TextLayoutMode;
45
+ }
46
+ declare const SANS = "Carlito";
47
+ declare const SERIF = "Caladea";
48
+ declare const ARIAL = "Liberation Sans";
49
+ declare const TIMES = "Liberation Serif";
50
+ declare const MONO = "Liberation Mono";
51
+ declare const substituteFamily: (family: string | null | undefined) => string;
52
+ declare const defaultMeasurer: TextMeasurer;
53
+ //#endregion
54
+ //#region src/render-slide.d.ts
55
+ /**
56
+ * Paint only character reflection/inner-shadow effects for an editable text
57
+ * body. The caller supplies the local bounds of the editable box in EMUs. For
58
+ * a table, `cell` selects a zero-based row/column and bounds describe that cell.
59
+ * Body rotation and vertical text handling remain canonical here; the editor only
60
+ * supplies the outer shape/group transform around this SVG.
61
+ */
62
+ declare const renderTextEffectsSvg: (pres: PresentationData, slide: SlideData, shape: SlideShapeData, bounds: {
63
+ w: number;
64
+ h: number;
65
+ }, opts?: Pick<RenderSlideOptions, "measureText"> & {
66
+ cell?: {
67
+ row: number;
68
+ col: number;
69
+ };
70
+ }) => string;
71
+ declare const renderSlideSvg: (pres: PresentationData, slide: SlideData, opts?: RenderSlideOptions) => string;
72
+ //#endregion
73
+ //#region src/audit.d.ts
74
+ interface IssueBase {
75
+ /** 0-based deck position of the slide the shape sits on. */
76
+ readonly slideIndex: number;
77
+ readonly shapeName: string | null;
78
+ /** True when any run was measured by estimate rather than real glyph
79
+ * metrics (heuristic measurer, or a font without the needed glyphs).
80
+ * Treat borderline overflows as advisory when set. */
81
+ readonly approximate: boolean;
82
+ }
83
+ type TextAuditIssue = (IssueBase & {
84
+ readonly kind: 'overflow-x' | 'overflow-y'; /** How far the text ink escapes the box, in px at 96 DPI. */
85
+ readonly overflowPx: number;
86
+ }) | (IssueBase & {
87
+ readonly kind: 'soft-wrap';
88
+ readonly paragraphIndex: number;
89
+ /** Lines beyond the paragraph's authored line count (1 + explicit
90
+ * breaks) — i.e. how many times the text wrapped on its own. */
91
+ readonly extraLines: number;
92
+ });
93
+ interface AuditTextLayoutOptions {
94
+ /** Measurer for run widths / vertical metrics. Defaults to the heuristic
95
+ * measurer (browser-safe, approximate); pass the fontkit measurer from
96
+ * `@office-kit/pptx-preview/node` for real glyph metrics. */
97
+ readonly measureText?: TextMeasurer;
98
+ /** Overflow at or below this many px (96 DPI) is ignored. Default 1 —
99
+ * measurement and PowerPoint disagree by sub-pixel amounts routinely. */
100
+ readonly tolerancePx?: number;
101
+ /** Also report paragraphs that wrap onto more lines than their explicit
102
+ * breaks author (段落ち). Off by default: wrapping is normal for body
103
+ * text, so this is opt-in for single-line-intent content like titles. */
104
+ readonly reportSoftWraps?: boolean;
105
+ }
106
+ /**
107
+ * Measures every text body in the deck and reports text that escapes its box
108
+ * (`overflow-x` / `overflow-y`) and, opt-in, paragraphs that soft-wrap
109
+ * (`soft-wrap`, 段落ち).
110
+ *
111
+ * Table cell text is not audited (v1 covers shape text bodies, including
112
+ * placeholders and shapes inside groups).
113
+ */
114
+ declare const auditTextLayout: (pres: PresentationData, options?: AuditTextLayoutOptions) => ReadonlyArray<TextAuditIssue>;
115
+ //#endregion
116
+ //#region src/paragraph-number-labels.d.ts
117
+ /** Resolve list numbering consistently for rendered slides and editing surfaces. */
118
+ declare const paragraphNumberLabels: (paraData: ReadonlyArray<{
119
+ bulletStyle: BulletStyle | null;
120
+ level: number;
121
+ }>) => Array<string | null>;
122
+ //#endregion
123
+ //#region src/text-body-rect.d.ts
124
+ /** A text rectangle as fractions of the shape's extent. */
125
+ interface TextRectFractions {
126
+ readonly l: number;
127
+ readonly t: number;
128
+ readonly r: number;
129
+ readonly b: number;
130
+ }
131
+ /**
132
+ * Where a shape lays out its text, as fractions of its own extent: the
133
+ * `<a:rect>` its geometry states, written in its `<a:custGeom>` or in
134
+ * ECMA-376's definition of its preset (an ellipse's inscribed rectangle, a
135
+ * triangle's lower middle, an arrow's shaft). `null` lays text into the whole
136
+ * box: a shape that inherits its geometry, or whose rectangle has no room.
137
+ *
138
+ * The rectangle is in the shape's own `<a:ext>` space, so dividing by that —
139
+ * not by on-screen bounds, which carry a group's or autofit's scale — is what
140
+ * lets the result travel with the shape.
141
+ */
142
+ declare function shapeTextRect(shape: SlideShapeData): TextRectFractions | null;
143
+ /**
144
+ * Text layout rectangle used by the preview: `region` (from
145
+ * {@link shapeTextRect}, or `null` for the whole box) placed in `bounds`, less
146
+ * the body insets. All coordinates and margins must use the same unit
147
+ * (normally EMU). When margins collapse a region, the region is kept without
148
+ * them, as PowerPoint does.
149
+ */
150
+ declare function resolveTextBodyRect(bounds: {
151
+ x: number;
152
+ y: number;
153
+ w: number;
154
+ h: number;
155
+ }, margins: {
156
+ left: number;
157
+ top: number;
158
+ right: number;
159
+ bottom: number;
160
+ }, region: TextRectFractions | null): {
161
+ x: number;
162
+ y: number;
163
+ w: number;
164
+ h: number;
165
+ };
166
+ //#endregion
167
+ //#region src/text-body-style.d.ts
168
+ interface VerticalTextStyle {
169
+ /** Declarations to add to the text container, without a trailing `;`. */
170
+ readonly declarations: string;
171
+ /** Transform function to compose with whatever the caller already applies,
172
+ * or `''`. Kept apart so a caller that rotates the box for shape rotation
173
+ * can append it instead of losing one of the two. */
174
+ readonly transform: string;
175
+ }
176
+ /**
177
+ * `<a:bodyPr vert="…"/>` as CSS. `writing-mode` is the right primitive for
178
+ * the East-Asian and Mongolian cases, and `text-orientation` for the WordArt
179
+ * ones, which stack upright glyphs. `vert270` reads bottom-to-top, which no
180
+ * writing-mode expresses on its own — hence the half turn. Its columns still
181
+ * follow the OOXML vertical-rl progression before that half turn; using
182
+ * vertical-lr here reverses the column order relative to the SVG renderer.
183
+ */
184
+ declare function verticalTextStyle(vert: ReturnType<typeof getShapeTextDirection>): VerticalTextStyle;
185
+ /**
186
+ * `<a:bodyPr numCol="N" spcCol="EMU"/>` as CSS declarations, without a
187
+ * trailing `;`. Empty below two columns, which is the same as not splitting
188
+ * the body.
189
+ */
190
+ declare function textColumnsStyle(cols: ReturnType<typeof getShapeTextColumns>): string;
191
+ //#endregion
192
+ //#region src/autofit.d.ts
193
+ interface ShapeAutoFitScaleOptions {
194
+ /** The box to fit into, in EMU. Defaults to the shape's resolved bounds.
195
+ * Pass it for a shape being edited, whose box on screen is not (yet) the
196
+ * one the model carries. */
197
+ readonly bounds?: {
198
+ readonly x: number;
199
+ readonly y: number;
200
+ readonly w: number;
201
+ readonly h: number;
202
+ };
203
+ /** Measurer for run widths, defaulting to the heuristic one the renderer
204
+ * uses in a browser. Pass the same measurer the render call uses, or the
205
+ * two disagree about how much text fits. */
206
+ readonly measureText?: TextMeasurer;
207
+ }
208
+ /**
209
+ * How much the preview shrinks `shape`'s text to fit its box: a multiplier on
210
+ * every authored size — font size, line spacing in points, paragraph spacing
211
+ * and indents.
212
+ *
213
+ * `1` for a shape with no `<a:normAutofit>`: `<a:noAutofit>` overflows and
214
+ * `<a:spAutoFit>` grows the box instead, so neither shrinks the text. A shape
215
+ * with a baked `fontScale` reports that, unchanged — it is what PowerPoint
216
+ * itself applied.
217
+ */
218
+ declare function shapeAutoFitScale(pres: PresentationData, shape: SlideShapeData, options?: ShapeAutoFitScaleOptions): number;
219
+ /** Translation in preview pixels applied to the entire text block by centered
220
+ * text anchoring. Paragraph widths and alignment remain unchanged. Editing
221
+ * surfaces must apply the same offset to keep the caret over the preview. */
222
+ declare function shapeTextAnchorOffset(pres: PresentationData, shape: SlideShapeData, options?: ShapeAutoFitScaleOptions): {
223
+ readonly x: number;
224
+ readonly y: number;
225
+ };
226
+ //#endregion
227
+ //#region src/text-underline-style.d.ts
228
+ /**
229
+ * DrawingML underline as CSS for an inline decoration box. Pass a resolved
230
+ * CSS color for image-based patterns. For `words`, apply only to non-space
231
+ * text segments; keep strikethrough on a separate outer box.
232
+ */
233
+ declare function textUnderlineStyle(underline: TextFormat['underline'], color: string, includeDecorationColor?: boolean): string;
234
+ //#endregion
235
+ export { SERIF as C, defaultMeasurer as D, TextMeasurer as E, substituteFamily as O, SANS as S, TextLayoutMode as T, ARIAL as _, VerticalTextStyle as a, MeasureResult as b, TextRectFractions as c, paragraphNumberLabels as d, AuditTextLayoutOptions as f, renderTextEffectsSvg as g, renderSlideSvg as h, shapeTextAnchorOffset as i, resolveTextBodyRect as l, auditTextLayout as m, ShapeAutoFitScaleOptions as n, textColumnsStyle as o, TextAuditIssue as p, shapeAutoFitScale as r, verticalTextStyle as s, textUnderlineStyle as t, shapeTextRect as u, FontSpec as v, TIMES as w, RenderSlideOptions as x, MONO as y };
236
+ //# sourceMappingURL=index-DUFPGYOf.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as ARIAL, c as MeasureResult, d as SERIF, f as TIMES, g as substituteFamily, h as defaultMeasurer, i as renderSlideSvg, l as RenderSlideOptions, m as TextMeasurer, n as TextAuditIssue, o as FontSpec, p as TextLayoutMode, r as auditTextLayout, s as MONO, t as AuditTextLayoutOptions, u as SANS } from "./index-B6ISDjnu.js";
2
- export { ARIAL, type AuditTextLayoutOptions, type FontSpec, MONO, type MeasureResult, type RenderSlideOptions, SANS, SERIF, TIMES, type TextAuditIssue, type TextLayoutMode, type TextMeasurer, auditTextLayout, defaultMeasurer, renderSlideSvg as renderSlideToSvg, substituteFamily };
1
+ import { C as SERIF, D as defaultMeasurer, E as TextMeasurer, O as substituteFamily, S as SANS, T as TextLayoutMode, _ as ARIAL, a as VerticalTextStyle, b as MeasureResult, c as TextRectFractions, d as paragraphNumberLabels, f as AuditTextLayoutOptions, g as renderTextEffectsSvg, h as renderSlideSvg, i as shapeTextAnchorOffset, l as resolveTextBodyRect, m as auditTextLayout, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as TextAuditIssue, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as FontSpec, w as TIMES, x as RenderSlideOptions, y as MONO } from "./index-DUFPGYOf.js";
2
+ export { ARIAL, type AuditTextLayoutOptions, type FontSpec, MONO, type MeasureResult, type RenderSlideOptions, SANS, SERIF, type ShapeAutoFitScaleOptions, TIMES, type TextAuditIssue, type TextLayoutMode, type TextMeasurer, type TextRectFractions, type VerticalTextStyle, auditTextLayout, defaultMeasurer, paragraphNumberLabels, renderSlideSvg as renderSlideToSvg, renderTextEffectsSvg, resolveTextBodyRect, shapeAutoFitScale, shapeTextAnchorOffset, shapeTextRect, substituteFamily, textColumnsStyle, textUnderlineStyle, verticalTextStyle };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as MONO, c as TIMES, d as substituteFamily, l as defaultMeasurer, n as renderSlideSvg, o as SANS, r as ARIAL, s as SERIF, t as auditTextLayout } from "./src-C2HIGS1C.js";
2
- export { ARIAL, MONO, SANS, SERIF, TIMES, auditTextLayout, defaultMeasurer, renderSlideSvg as renderSlideToSvg, substituteFamily };
1
+ import { _ as textColumnsStyle, a as renderTextEffectsSvg, b as shapeTextRect, c as MONO, d as TIMES, f as defaultMeasurer, g as textUnderlineStyle, h as paragraphNumberLabels, i as renderSlideSvg, l as SANS, m as substituteFamily, n as shapeTextAnchorOffset, o as ARIAL, r as auditTextLayout, t as shapeAutoFitScale, u as SERIF, v as verticalTextStyle, y as resolveTextBodyRect } from "./src-Bwmf324Z.js";
2
+ export { ARIAL, MONO, SANS, SERIF, TIMES, auditTextLayout, defaultMeasurer, paragraphNumberLabels, renderSlideSvg as renderSlideToSvg, renderTextEffectsSvg, resolveTextBodyRect, shapeAutoFitScale, shapeTextAnchorOffset, shapeTextRect, substituteFamily, textColumnsStyle, textUnderlineStyle, verticalTextStyle };
package/dist/node.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ARIAL, c as MeasureResult, d as SERIF, f as TIMES, g as substituteFamily, h as defaultMeasurer, i as renderSlideSvg, l as RenderSlideOptions, m as TextMeasurer, n as TextAuditIssue, o as FontSpec, p as TextLayoutMode, r as auditTextLayout, s as MONO, t as AuditTextLayoutOptions, u as SANS } from "./index-B6ISDjnu.js";
1
+ import { C as SERIF, D as defaultMeasurer, E as TextMeasurer, O as substituteFamily, S as SANS, T as TextLayoutMode, _ as ARIAL, a as VerticalTextStyle, b as MeasureResult, c as TextRectFractions, d as paragraphNumberLabels, f as AuditTextLayoutOptions, g as renderTextEffectsSvg, h as renderSlideSvg, i as shapeTextAnchorOffset, l as resolveTextBodyRect, m as auditTextLayout, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as TextAuditIssue, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as FontSpec, w as TIMES, x as RenderSlideOptions, y as MONO } from "./index-DUFPGYOf.js";
2
2
  import { PresentationData, SlideData } from "@office-kit/pptx";
3
3
 
4
4
  //#region src/measure.d.ts
@@ -73,5 +73,5 @@ declare const renderSlideToRgba: (pres: PresentationData, slide: SlideData, opts
73
73
  readonly png: Uint8Array;
74
74
  };
75
75
  //#endregion
76
- export { ARIAL, type AuditTextLayoutOptions, FONT_DIR, FONT_FILES, type FontSpec, type FontkitMeasurerOptions, MONO, type MeasureResult, type RegisteredFont, RenderImageOptions, type RenderSlideOptions, RgbaImage, SANS, SERIF, TIMES, type TextAuditIssue, type TextLayoutMode, type TextMeasurer, auditTextLayout, buildFontkitMeasurer, defaultMeasurer, renderSlideToImage, renderSlideToRgba, renderSlideSvg as renderSlideToSvg, substituteFamily };
76
+ export { ARIAL, type AuditTextLayoutOptions, FONT_DIR, FONT_FILES, type FontSpec, type FontkitMeasurerOptions, MONO, type MeasureResult, type RegisteredFont, RenderImageOptions, type RenderSlideOptions, RgbaImage, SANS, SERIF, type ShapeAutoFitScaleOptions, TIMES, type TextAuditIssue, type TextLayoutMode, type TextMeasurer, type TextRectFractions, type VerticalTextStyle, auditTextLayout, buildFontkitMeasurer, defaultMeasurer, paragraphNumberLabels, renderSlideToImage, renderSlideToRgba, renderSlideSvg as renderSlideToSvg, renderTextEffectsSvg, resolveTextBodyRect, shapeAutoFitScale, shapeTextAnchorOffset, shapeTextRect, substituteFamily, textColumnsStyle, textUnderlineStyle, verticalTextStyle };
77
77
  //# sourceMappingURL=node.d.ts.map
package/dist/node.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as MONO, c as TIMES, d as substituteFamily, i as AVG_GLYPH_W_RATIO, l as defaultMeasurer, n as renderSlideSvg, o as SANS, r as ARIAL, s as SERIF, t as auditTextLayout, u as isCjk } from "./src-C2HIGS1C.js";
1
+ import { _ as textColumnsStyle, a as renderTextEffectsSvg, b as shapeTextRect, c as MONO, d as TIMES, f as defaultMeasurer, g as textUnderlineStyle, h as paragraphNumberLabels, i as renderSlideSvg, l as SANS, m as substituteFamily, n as shapeTextAnchorOffset, o as ARIAL, p as isCjk, r as auditTextLayout, s as AVG_GLYPH_W_RATIO, t as shapeAutoFitScale, u as SERIF, v as verticalTextStyle, y as resolveTextBodyRect } from "./src-Bwmf324Z.js";
2
2
  import { getSlideSize } from "@office-kit/pptx";
3
3
  import { Resvg } from "@resvg/resvg-js";
4
4
  import * as fontkit from "fontkit";
@@ -73,12 +73,28 @@ const buildFontkitMeasurer = (options = {}) => {
73
73
  let widthPx = 0;
74
74
  let approximate = false;
75
75
  let metricsFont = null;
76
+ let inkTop = Number.NEGATIVE_INFINITY;
77
+ let inkBottom = Number.POSITIVE_INFINITY;
76
78
  let segment = "";
77
79
  let segmentFont = null;
78
80
  const flush = () => {
79
81
  if (segment === "" || segmentFont === null) return;
80
82
  const scale = spec.sizePx / segmentFont.unitsPerEm;
81
- widthPx += segmentFont.layout(segment).advanceWidth * scale;
83
+ const run = segmentFont.layout(segment, { kern: spec.kerning !== false });
84
+ widthPx += run.advanceWidth * scale;
85
+ let penY = 0;
86
+ for (const [index, glyph] of run.glyphs.entries()) {
87
+ const box = glyph.bbox;
88
+ if (!box || !Number.isFinite(box.minY) || !Number.isFinite(box.maxY) || box.maxY <= box.minY) {
89
+ penY += run.positions[index]?.yAdvance ?? 0;
90
+ continue;
91
+ }
92
+ const position = run.positions[index];
93
+ const glyphY = penY + (position?.yOffset ?? 0);
94
+ inkTop = Math.max(inkTop, (box.maxY + glyphY) * scale);
95
+ inkBottom = Math.min(inkBottom, (box.minY + glyphY) * scale);
96
+ penY += position?.yAdvance ?? 0;
97
+ }
82
98
  metricsFont ??= segmentFont;
83
99
  segment = "";
84
100
  };
@@ -104,6 +120,10 @@ const buildFontkitMeasurer = (options = {}) => {
104
120
  ascentPx: vm.ascent * vmScale,
105
121
  descentPx: vm.descent * vmScale,
106
122
  lineGapPx: vm.lineGap * vmScale,
123
+ ...!approximate && metricsFont !== null ? {
124
+ inkAscentPx: inkTop === Number.NEGATIVE_INFINITY ? 0 : inkTop,
125
+ inkDescentPx: inkBottom === Number.POSITIVE_INFINITY || inkBottom === 0 ? 0 : -inkBottom
126
+ } : {},
107
127
  ...approximate ? { approximate } : {}
108
128
  };
109
129
  };
@@ -175,6 +195,6 @@ const renderSlideToRgba = (pres, slide, opts = {}) => {
175
195
  };
176
196
  };
177
197
  //#endregion
178
- export { ARIAL, FONT_DIR, FONT_FILES, MONO, SANS, SERIF, TIMES, auditTextLayout, buildFontkitMeasurer, defaultMeasurer, renderSlideToImage, renderSlideToRgba, renderSlideSvg as renderSlideToSvg, substituteFamily };
198
+ export { ARIAL, FONT_DIR, FONT_FILES, MONO, SANS, SERIF, TIMES, auditTextLayout, buildFontkitMeasurer, defaultMeasurer, paragraphNumberLabels, renderSlideToImage, renderSlideToRgba, renderSlideSvg as renderSlideToSvg, renderTextEffectsSvg, resolveTextBodyRect, shapeAutoFitScale, shapeTextAnchorOffset, shapeTextRect, substituteFamily, textColumnsStyle, textUnderlineStyle, verticalTextStyle };
179
199
 
180
200
  //# sourceMappingURL=node.js.map
package/dist/node.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"node.js","names":[],"sources":["../src/measure.ts","../src/node.ts"],"sourcesContent":["// Node-only fontkit-backed TextMeasurer. NEVER imported by render-slide.ts /\n// text-layout.ts (which must stay browser-safe) — only by the node entry. It\n// measures advance widths and vertical metrics from the same bundled TTFs that\n// resvg rasterizes with (and that LibreOffice renders ground truth with), so\n// the engine's wrap/positioning math agrees with the painted pixels.\n\nimport * as fontkit from 'fontkit';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport {\n ARIAL,\n AVG_GLYPH_W_RATIO,\n isCjk,\n MONO,\n SANS,\n SERIF,\n substituteFamily,\n TIMES,\n type FontSpec,\n type MeasureResult,\n type TextMeasurer,\n} from './text-layout.ts';\n\nexport const FONT_DIR = fileURLToPath(new URL('../fonts/', import.meta.url));\n\n// Internal family name (what the emitter writes + resvg matches) → file prefix.\nconst FAMILY_TO_PREFIX: Record<string, string> = {\n [SANS]: 'Carlito',\n [SERIF]: 'Caladea',\n [ARIAL]: 'LiberationSans',\n [TIMES]: 'LiberationSerif',\n [MONO]: 'LiberationMono',\n};\n\nconst STYLES = ['Regular', 'Bold', 'Italic', 'BoldItalic'] as const;\n\nconst facePath = (prefix: string, style: string): string => `${FONT_DIR}${prefix}-${style}.ttf`;\n\n/** Absolute paths of every bundled face — passed to resvg's `fontFiles`. */\nexport const FONT_FILES: string[] = Object.values(FAMILY_TO_PREFIX).flatMap((prefix) =>\n STYLES.map((s) => facePath(prefix, s)),\n);\n\n// fontkit.create returns Font | FontCollection; our TTFs are single fonts.\nconst openFont = (path: string): fontkit.Font => {\n const font = fontkit.create(readFileSync(path));\n if (!('layout' in font)) {\n throw new Error(`Expected a single font in ${path}, got a collection`);\n }\n return font;\n};\n\nconst styleSuffix = (spec: { bold: boolean; italic: boolean }): (typeof STYLES)[number] => {\n if (spec.bold && spec.italic) return 'BoldItalic';\n if (spec.bold) return 'Bold';\n if (spec.italic) return 'Italic';\n return 'Regular';\n};\n\n/** A font the caller supplies for measurement, keyed by the AUTHORED family\n * name (`<a:latin typeface>` / `<a:ea typeface>` — e.g. \"游ゴシック\",\n * \"Montserrat\"), which routinely differs from the file's internal name. */\nexport interface RegisteredFont {\n readonly family: string;\n /** Path to a ttf/otf file, or its bytes. */\n readonly source: string | Uint8Array;\n readonly bold?: boolean;\n readonly italic?: boolean;\n}\n\nexport interface FontkitMeasurerOptions {\n /** Fonts the deck actually uses. A run whose family matches a registered\n * font measures with it; any registered font also serves as a fallback\n * for glyphs the resolved font lacks (the bundled faces are Latin-only,\n * so this is how CJK text gets real metrics). */\n readonly fonts?: ReadonlyArray<RegisteredFont>;\n}\n\nconst openFontSource = (source: string | Uint8Array): fontkit.Font => {\n if (typeof source === 'string') return openFont(source);\n const font = fontkit.create(Buffer.from(source));\n if (!('layout' in font)) {\n throw new Error('Expected a single font, got a collection');\n }\n return font;\n};\n\n// Per-character width estimate for glyphs no available font covers — the same\n// ratios as the heuristic measurer, so estimates and defaultMeasurer agree.\nconst estimateCharWidth = (ch: string, sizePx: number): number =>\n sizePx * (isCjk(ch.codePointAt(0) ?? 0) ? 1 : AVG_GLYPH_W_RATIO);\n\nexport const buildFontkitMeasurer = (options: FontkitMeasurerOptions = {}): TextMeasurer => {\n const cache = new Map<string, fontkit.Font>();\n // Load + verify every face up front. The familyName assertion guarantees the\n // emit-side family === resvg match === the face we measure, which is the load-\n // bearing invariant for \"x= is where glyphs land\".\n for (const [family, prefix] of Object.entries(FAMILY_TO_PREFIX)) {\n for (const style of STYLES) {\n const path = facePath(prefix, style);\n if (!existsSync(path)) throw new Error(`Missing bundled font: ${path}`);\n const font = openFont(path);\n if (style === 'Regular' && font.familyName !== family) {\n throw new Error(\n `Font family mismatch: ${path} reports \"${font.familyName}\", expected \"${family}\". ` +\n `The substitution map and resvg matching rely on this name.`,\n );\n }\n cache.set(`${prefix}-${style}`, font);\n }\n }\n\n // Registered fonts, keyed by lowercased authored family + style. No\n // familyName assertion here: the registration key is the DECK's name for\n // the font, which legitimately differs from the file's internal name.\n const registered = new Map<string, fontkit.Font>();\n const fallbackFonts: fontkit.Font[] = [];\n for (const reg of options.fonts ?? []) {\n const font = openFontSource(reg.source);\n const style = styleSuffix({ bold: reg.bold ?? false, italic: reg.italic ?? false });\n registered.set(`${reg.family.toLowerCase()}-${style}`, font);\n fallbackFonts.push(font);\n }\n\n const pick = (spec: FontSpec): fontkit.Font => {\n const style = styleSuffix(spec);\n const regKey = spec.family.toLowerCase();\n const reg = registered.get(`${regKey}-${style}`) ?? registered.get(`${regKey}-Regular`);\n if (reg) return reg;\n // The render path pre-substitutes families (spec.family is a bundled\n // internal name); the audit path passes the authored name through, so\n // resolve it with the same map before keying the bundled faces.\n const prefix =\n FAMILY_TO_PREFIX[spec.family] ?? FAMILY_TO_PREFIX[substituteFamily(spec.family)]!;\n return (\n cache.get(`${prefix}-${style}`) ??\n cache.get(`${prefix}-Regular`) ??\n cache.get('Carlito-Regular')!\n );\n };\n\n // Splits `text` into maximal segments coverable by one font: the resolved\n // font first, then each registered font in registration order. Characters\n // no font covers fall back to the per-character estimate (approximate).\n const measureWithFallback = (\n text: string,\n spec: FontSpec,\n primary: fontkit.Font,\n ): MeasureResult => {\n const chain = [primary, ...fallbackFonts.filter((f) => f !== primary)];\n let widthPx = 0;\n let approximate = false;\n let metricsFont: fontkit.Font | null = null;\n let segment = '';\n let segmentFont: fontkit.Font | null = null;\n const flush = (): void => {\n if (segment === '' || segmentFont === null) return;\n const scale = spec.sizePx / segmentFont.unitsPerEm;\n widthPx += segmentFont.layout(segment).advanceWidth * scale;\n metricsFont ??= segmentFont;\n segment = '';\n };\n for (const ch of text) {\n const cp = ch.codePointAt(0) ?? 0;\n const font = chain.find((f) => f.hasGlyphForCodePoint(cp)) ?? null;\n if (font !== segmentFont) {\n flush();\n segmentFont = font;\n }\n if (font === null) {\n widthPx += estimateCharWidth(ch, spec.sizePx);\n approximate = true;\n } else {\n segment += ch;\n }\n }\n flush();\n const vm = verticalMetrics(metricsFont ?? primary);\n const vmScale = spec.sizePx / (metricsFont ?? primary).unitsPerEm;\n const glyphCount = [...text].length;\n const tracking = glyphCount > 1 ? spec.letterSpacingPx * (glyphCount - 1) : 0;\n return {\n widthPx: widthPx + tracking,\n ascentPx: vm.ascent * vmScale,\n descentPx: vm.descent * vmScale,\n lineGapPx: vm.lineGap * vmScale,\n ...(approximate ? { approximate } : {}),\n };\n };\n\n return (text, spec) => measureWithFallback(text, spec, pick(spec));\n};\n\n// Which vertical-metric set a renderer uses depends on the OS/2 fsSelection\n// USE_TYPO_METRICS bit. When it's clear (the common case, incl. Carlito /\n// Liberation), GDI / LibreOffice place the baseline at usWinAscent and size the\n// line box as usWinAscent + usWinDescent (no extra gap). When set, they use the\n// sTypo* metrics plus typoLineGap. fontkit's `.ascent`/`.descent` expose the\n// hhea values, which for these fonts differ from usWinAscent by ~12px at 44pt —\n// exactly the baseline offset that otherwise mismatches ground truth. Returns\n// font-unit values; the caller scales to px.\ninterface VMetrics {\n readonly ascent: number;\n readonly descent: number;\n readonly lineGap: number;\n}\nconst verticalMetrics = (font: fontkit.Font): VMetrics => {\n const os2: unknown = (font as { 'OS/2'?: unknown })['OS/2'];\n if (os2 && typeof os2 === 'object') {\n const o = os2 as {\n fsSelection?: { useTypoMetrics?: boolean };\n typoAscender?: number;\n typoDescender?: number;\n typoLineGap?: number;\n winAscent?: number;\n winDescent?: number;\n };\n if (o.fsSelection?.useTypoMetrics && o.typoAscender !== undefined) {\n return {\n ascent: o.typoAscender,\n descent: Math.abs(o.typoDescender ?? 0),\n lineGap: o.typoLineGap ?? 0,\n };\n }\n if (o.winAscent !== undefined && o.winDescent !== undefined) {\n return { ascent: o.winAscent, descent: Math.abs(o.winDescent), lineGap: 0 };\n }\n }\n // Fall back to hhea metrics if OS/2 is absent.\n return { ascent: font.ascent, descent: Math.abs(font.descent), lineGap: font.lineGap };\n};\n","// `@office-kit/pptx-preview/node` — Node entry: rasterize a slide to a PNG or raw\n// RGBA image, with no browser binary.\n//\n// Pipeline: `renderSlideToSvg(..., { textLayout: 'svg', measureText })` lays\n// text out as pure `<text>`, then resvg paints the SVG to pixels. The fontkit\n// measurer, resvg's `fontFiles`, and the SVG's family names all reference the\n// SAME bundled fonts, so the wrap/positioning math agrees with the rasterized\n// glyphs.\n\nimport { Resvg } from '@resvg/resvg-js';\nimport { getSlideSize, type PresentationData, type SlideData } from '@office-kit/pptx';\nimport { renderSlideSvg } from './render-slide.ts';\nimport { MONO, SANS, SERIF, type TextMeasurer } from './text-layout.ts';\nimport { buildFontkitMeasurer, FONT_FILES } from './measure.ts';\n\n// Re-export the browser-safe surface so a Node consumer gets everything from\n// one import.\nexport * from './index.ts';\n\n// Advanced: pre-build / share a fontkit measurer, or locate the bundled fonts.\nexport { buildFontkitMeasurer, FONT_FILES, FONT_DIR } from './measure.ts';\nexport type { FontkitMeasurerOptions, RegisteredFont } from './measure.ts';\n\n/** Raw, un-encoded raster: RGBA bytes in row-major order. */\nexport interface RgbaImage {\n readonly width: number;\n readonly height: number;\n /** `width * height * 4` bytes, R,G,B,A per pixel. */\n readonly data: Uint8Array;\n}\n\nexport interface RenderImageOptions {\n /**\n * Target raster width in pixels. Height follows the slide's aspect ratio.\n * Defaults to 1280 (matches the fidelity harness baseline).\n */\n readonly width?: number;\n /**\n * Text measurer used for wrap/positioning. Defaults to a shared fontkit\n * measurer over the bundled fonts. Pass {@link buildFontkitMeasurer}'s\n * result to control its lifetime, or a custom measurer to swap fonts.\n */\n readonly measureText?: TextMeasurer;\n /**\n * Extra font files (ttf / otf / ttc paths) loaded into the rasterizer in\n * addition to the bundled Latin faces. The bundled set has no CJK\n * coverage, so decks with Japanese / Chinese / Korean text render as\n * missing-glyph boxes unless the caller supplies a face here (pair it with\n * a {@link buildFontkitMeasurer} `fonts` registration so wrap math uses the\n * same metrics).\n */\n readonly fontFiles?: ReadonlyArray<string>;\n /**\n * Also let the rasterizer discover OS-installed fonts as a fallback for\n * families the bundled + `fontFiles` faces don't cover. Defaults to\n * `false`: system fonts vary per machine, so the default keeps output\n * deterministic.\n */\n readonly loadSystemFonts?: boolean;\n}\n\nconst DEFAULT_WIDTH = 1280;\n\n// The fontkit measurer reads + verifies every bundled face on construction.\n// Build it once and reuse across calls; do it lazily so importing this module\n// (e.g. just for the types) doesn't touch the filesystem.\nlet sharedMeasurer: TextMeasurer | undefined;\nconst getSharedMeasurer = (): TextMeasurer => (sharedMeasurer ??= buildFontkitMeasurer());\n\nconst rasterize = (pres: PresentationData, slide: SlideData, opts: RenderImageOptions) => {\n // `getSlideSize` is only consulted to fail fast on a malformed deck; the SVG\n // carries its own viewBox and resvg fits to the requested width.\n getSlideSize(pres);\n const width = opts.width ?? DEFAULT_WIDTH;\n const measureText = opts.measureText ?? getSharedMeasurer();\n const svg = renderSlideSvg(pres, slide, { measureText, textLayout: 'svg' });\n const resvg = new Resvg(svg, {\n fitTo: { mode: 'width', value: width },\n font: {\n loadSystemFonts: opts.loadSystemFonts ?? false, // default: deterministic bundled set\n fontFiles: [...FONT_FILES, ...(opts.fontFiles ?? [])],\n defaultFontFamily: SANS,\n sansSerifFamily: SANS,\n serifFamily: SERIF,\n monospaceFamily: MONO,\n },\n });\n return resvg.render();\n};\n\n/** Render one slide to PNG-encoded bytes. */\nexport const renderSlideToImage = (\n pres: PresentationData,\n slide: SlideData,\n opts: RenderImageOptions = {},\n): Uint8Array => new Uint8Array(rasterize(pres, slide, opts).asPng());\n\n/**\n * Render one slide to raw RGBA pixels plus the PNG encoding of the same frame.\n * Both come from a single rasterization, so callers needing pixel access (SSIM,\n * diffing) and a saveable file don't pay to render twice.\n */\nexport const renderSlideToRgba = (\n pres: PresentationData,\n slide: SlideData,\n opts: RenderImageOptions = {},\n): { readonly image: RgbaImage; readonly png: Uint8Array } => {\n const rendered = rasterize(pres, slide, opts);\n return {\n image: {\n width: rendered.width,\n height: rendered.height,\n data: new Uint8Array(rendered.pixels),\n },\n png: new Uint8Array(rendered.asPng()),\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"node.js","names":[],"sources":["../src/measure.ts","../src/node.ts"],"sourcesContent":["// Node-only fontkit-backed TextMeasurer. NEVER imported by render-slide.ts /\n// text-layout.ts (which must stay browser-safe) — only by the node entry. It\n// measures advance widths and vertical metrics from the same bundled TTFs that\n// resvg rasterizes with (and that LibreOffice renders ground truth with), so\n// the engine's wrap/positioning math agrees with the painted pixels.\n\nimport * as fontkit from 'fontkit';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport {\n ARIAL,\n AVG_GLYPH_W_RATIO,\n isCjk,\n MONO,\n SANS,\n SERIF,\n substituteFamily,\n TIMES,\n type FontSpec,\n type MeasureResult,\n type TextMeasurer,\n} from './text-layout.ts';\n\nexport const FONT_DIR = fileURLToPath(new URL('../fonts/', import.meta.url));\n\n// Internal family name (what the emitter writes + resvg matches) → file prefix.\nconst FAMILY_TO_PREFIX: Record<string, string> = {\n [SANS]: 'Carlito',\n [SERIF]: 'Caladea',\n [ARIAL]: 'LiberationSans',\n [TIMES]: 'LiberationSerif',\n [MONO]: 'LiberationMono',\n};\n\nconst STYLES = ['Regular', 'Bold', 'Italic', 'BoldItalic'] as const;\n\nconst facePath = (prefix: string, style: string): string => `${FONT_DIR}${prefix}-${style}.ttf`;\n\n/** Absolute paths of every bundled face — passed to resvg's `fontFiles`. */\nexport const FONT_FILES: string[] = Object.values(FAMILY_TO_PREFIX).flatMap((prefix) =>\n STYLES.map((s) => facePath(prefix, s)),\n);\n\n// fontkit.create returns Font | FontCollection; our TTFs are single fonts.\nconst openFont = (path: string): fontkit.Font => {\n const font = fontkit.create(readFileSync(path));\n if (!('layout' in font)) {\n throw new Error(`Expected a single font in ${path}, got a collection`);\n }\n return font;\n};\n\nconst styleSuffix = (spec: { bold: boolean; italic: boolean }): (typeof STYLES)[number] => {\n if (spec.bold && spec.italic) return 'BoldItalic';\n if (spec.bold) return 'Bold';\n if (spec.italic) return 'Italic';\n return 'Regular';\n};\n\n/** A font the caller supplies for measurement, keyed by the AUTHORED family\n * name (`<a:latin typeface>` / `<a:ea typeface>` — e.g. \"游ゴシック\",\n * \"Montserrat\"), which routinely differs from the file's internal name. */\nexport interface RegisteredFont {\n readonly family: string;\n /** Path to a ttf/otf file, or its bytes. */\n readonly source: string | Uint8Array;\n readonly bold?: boolean;\n readonly italic?: boolean;\n}\n\nexport interface FontkitMeasurerOptions {\n /** Fonts the deck actually uses. A run whose family matches a registered\n * font measures with it; any registered font also serves as a fallback\n * for glyphs the resolved font lacks (the bundled faces are Latin-only,\n * so this is how CJK text gets real metrics). */\n readonly fonts?: ReadonlyArray<RegisteredFont>;\n}\n\nconst openFontSource = (source: string | Uint8Array): fontkit.Font => {\n if (typeof source === 'string') return openFont(source);\n const font = fontkit.create(Buffer.from(source));\n if (!('layout' in font)) {\n throw new Error('Expected a single font, got a collection');\n }\n return font;\n};\n\n// Per-character width estimate for glyphs no available font covers — the same\n// ratios as the heuristic measurer, so estimates and defaultMeasurer agree.\nconst estimateCharWidth = (ch: string, sizePx: number): number =>\n sizePx * (isCjk(ch.codePointAt(0) ?? 0) ? 1 : AVG_GLYPH_W_RATIO);\n\nexport const buildFontkitMeasurer = (options: FontkitMeasurerOptions = {}): TextMeasurer => {\n const cache = new Map<string, fontkit.Font>();\n // Load + verify every face up front. The familyName assertion guarantees the\n // emit-side family === resvg match === the face we measure, which is the load-\n // bearing invariant for \"x= is where glyphs land\".\n for (const [family, prefix] of Object.entries(FAMILY_TO_PREFIX)) {\n for (const style of STYLES) {\n const path = facePath(prefix, style);\n if (!existsSync(path)) throw new Error(`Missing bundled font: ${path}`);\n const font = openFont(path);\n if (style === 'Regular' && font.familyName !== family) {\n throw new Error(\n `Font family mismatch: ${path} reports \"${font.familyName}\", expected \"${family}\". ` +\n `The substitution map and resvg matching rely on this name.`,\n );\n }\n cache.set(`${prefix}-${style}`, font);\n }\n }\n\n // Registered fonts, keyed by lowercased authored family + style. No\n // familyName assertion here: the registration key is the DECK's name for\n // the font, which legitimately differs from the file's internal name.\n const registered = new Map<string, fontkit.Font>();\n const fallbackFonts: fontkit.Font[] = [];\n for (const reg of options.fonts ?? []) {\n const font = openFontSource(reg.source);\n const style = styleSuffix({ bold: reg.bold ?? false, italic: reg.italic ?? false });\n registered.set(`${reg.family.toLowerCase()}-${style}`, font);\n fallbackFonts.push(font);\n }\n\n const pick = (spec: FontSpec): fontkit.Font => {\n const style = styleSuffix(spec);\n const regKey = spec.family.toLowerCase();\n const reg = registered.get(`${regKey}-${style}`) ?? registered.get(`${regKey}-Regular`);\n if (reg) return reg;\n // The render path pre-substitutes families (spec.family is a bundled\n // internal name); the audit path passes the authored name through, so\n // resolve it with the same map before keying the bundled faces.\n const prefix =\n FAMILY_TO_PREFIX[spec.family] ?? FAMILY_TO_PREFIX[substituteFamily(spec.family)]!;\n return (\n cache.get(`${prefix}-${style}`) ??\n cache.get(`${prefix}-Regular`) ??\n cache.get('Carlito-Regular')!\n );\n };\n\n // Splits `text` into maximal segments coverable by one font: the resolved\n // font first, then each registered font in registration order. Characters\n // no font covers fall back to the per-character estimate (approximate).\n const measureWithFallback = (\n text: string,\n spec: FontSpec,\n primary: fontkit.Font,\n ): MeasureResult => {\n const chain = [primary, ...fallbackFonts.filter((f) => f !== primary)];\n let widthPx = 0;\n let approximate = false;\n let metricsFont: fontkit.Font | null = null;\n // Keep the bounds signed until the end. Initialising either side at zero\n // would turn a glyph entirely above/below the baseline into a false ink\n // extent on the opposite side.\n let inkTop = Number.NEGATIVE_INFINITY;\n let inkBottom = Number.POSITIVE_INFINITY;\n let segment = '';\n let segmentFont: fontkit.Font | null = null;\n const flush = (): void => {\n if (segment === '' || segmentFont === null) return;\n const scale = spec.sizePx / segmentFont.unitsPerEm;\n const run = segmentFont.layout(segment, { kern: spec.kerning !== false });\n widthPx += run.advanceWidth * scale;\n let penY = 0;\n for (const [index, glyph] of run.glyphs.entries()) {\n const box = glyph.bbox;\n if (\n !box ||\n !Number.isFinite(box.minY) ||\n !Number.isFinite(box.maxY) ||\n box.maxY <= box.minY\n ) {\n penY += run.positions[index]?.yAdvance ?? 0;\n continue;\n }\n // fontkit reports the glyph bbox in font units and shaping can move a\n // glyph vertically (combining marks are the common case). Include the\n // shaped position so the result describes the painted glyph, rather\n // than the unpositioned outline.\n const position = run.positions[index];\n const glyphY = penY + (position?.yOffset ?? 0);\n inkTop = Math.max(inkTop, (box.maxY + glyphY) * scale);\n inkBottom = Math.min(inkBottom, (box.minY + glyphY) * scale);\n penY += position?.yAdvance ?? 0;\n }\n metricsFont ??= segmentFont;\n segment = '';\n };\n for (const ch of text) {\n const cp = ch.codePointAt(0) ?? 0;\n const font = chain.find((f) => f.hasGlyphForCodePoint(cp)) ?? null;\n if (font !== segmentFont) {\n flush();\n segmentFont = font;\n }\n if (font === null) {\n widthPx += estimateCharWidth(ch, spec.sizePx);\n approximate = true;\n } else {\n segment += ch;\n }\n }\n flush();\n const vm = verticalMetrics(metricsFont ?? primary);\n const vmScale = spec.sizePx / (metricsFont ?? primary).unitsPerEm;\n const glyphCount = [...text].length;\n const tracking = glyphCount > 1 ? spec.letterSpacingPx * (glyphCount - 1) : 0;\n return {\n widthPx: widthPx + tracking,\n ascentPx: vm.ascent * vmScale,\n descentPx: vm.descent * vmScale,\n lineGapPx: vm.lineGap * vmScale,\n ...(!approximate && metricsFont !== null\n ? {\n inkAscentPx: inkTop === Number.NEGATIVE_INFINITY ? 0 : inkTop,\n inkDescentPx:\n inkBottom === Number.POSITIVE_INFINITY || inkBottom === 0 ? 0 : -inkBottom,\n }\n : {}),\n ...(approximate ? { approximate } : {}),\n };\n };\n\n return (text, spec) => measureWithFallback(text, spec, pick(spec));\n};\n\n// Which vertical-metric set a renderer uses depends on the OS/2 fsSelection\n// USE_TYPO_METRICS bit. When it's clear (the common case, incl. Carlito /\n// Liberation), GDI / LibreOffice place the baseline at usWinAscent and size the\n// line box as usWinAscent + usWinDescent (no extra gap). When set, they use the\n// sTypo* metrics plus typoLineGap. fontkit's `.ascent`/`.descent` expose the\n// hhea values, which for these fonts differ from usWinAscent by ~12px at 44pt —\n// exactly the baseline offset that otherwise mismatches ground truth. Returns\n// font-unit values; the caller scales to px.\ninterface VMetrics {\n readonly ascent: number;\n readonly descent: number;\n readonly lineGap: number;\n}\nconst verticalMetrics = (font: fontkit.Font): VMetrics => {\n const os2: unknown = (font as { 'OS/2'?: unknown })['OS/2'];\n if (os2 && typeof os2 === 'object') {\n const o = os2 as {\n fsSelection?: { useTypoMetrics?: boolean };\n typoAscender?: number;\n typoDescender?: number;\n typoLineGap?: number;\n winAscent?: number;\n winDescent?: number;\n };\n if (o.fsSelection?.useTypoMetrics && o.typoAscender !== undefined) {\n return {\n ascent: o.typoAscender,\n descent: Math.abs(o.typoDescender ?? 0),\n lineGap: o.typoLineGap ?? 0,\n };\n }\n if (o.winAscent !== undefined && o.winDescent !== undefined) {\n return { ascent: o.winAscent, descent: Math.abs(o.winDescent), lineGap: 0 };\n }\n }\n // Fall back to hhea metrics if OS/2 is absent.\n return { ascent: font.ascent, descent: Math.abs(font.descent), lineGap: font.lineGap };\n};\n","// `@office-kit/pptx-preview/node` — Node entry: rasterize a slide to a PNG or raw\n// RGBA image, with no browser binary.\n//\n// Pipeline: `renderSlideToSvg(..., { textLayout: 'svg', measureText })` lays\n// text out as pure `<text>`, then resvg paints the SVG to pixels. The fontkit\n// measurer, resvg's `fontFiles`, and the SVG's family names all reference the\n// SAME bundled fonts, so the wrap/positioning math agrees with the rasterized\n// glyphs.\n\nimport { Resvg } from '@resvg/resvg-js';\nimport { getSlideSize, type PresentationData, type SlideData } from '@office-kit/pptx';\nimport { renderSlideSvg } from './render-slide.ts';\nimport { MONO, SANS, SERIF, type TextMeasurer } from './text-layout.ts';\nimport { buildFontkitMeasurer, FONT_FILES } from './measure.ts';\n\n// Re-export the browser-safe surface so a Node consumer gets everything from\n// one import.\nexport * from './index.ts';\n\n// Advanced: pre-build / share a fontkit measurer, or locate the bundled fonts.\nexport { buildFontkitMeasurer, FONT_FILES, FONT_DIR } from './measure.ts';\nexport type { FontkitMeasurerOptions, RegisteredFont } from './measure.ts';\n\n/** Raw, un-encoded raster: RGBA bytes in row-major order. */\nexport interface RgbaImage {\n readonly width: number;\n readonly height: number;\n /** `width * height * 4` bytes, R,G,B,A per pixel. */\n readonly data: Uint8Array;\n}\n\nexport interface RenderImageOptions {\n /**\n * Target raster width in pixels. Height follows the slide's aspect ratio.\n * Defaults to 1280 (matches the fidelity harness baseline).\n */\n readonly width?: number;\n /**\n * Text measurer used for wrap/positioning. Defaults to a shared fontkit\n * measurer over the bundled fonts. Pass {@link buildFontkitMeasurer}'s\n * result to control its lifetime, or a custom measurer to swap fonts.\n */\n readonly measureText?: TextMeasurer;\n /**\n * Extra font files (ttf / otf / ttc paths) loaded into the rasterizer in\n * addition to the bundled Latin faces. The bundled set has no CJK\n * coverage, so decks with Japanese / Chinese / Korean text render as\n * missing-glyph boxes unless the caller supplies a face here (pair it with\n * a {@link buildFontkitMeasurer} `fonts` registration so wrap math uses the\n * same metrics).\n */\n readonly fontFiles?: ReadonlyArray<string>;\n /**\n * Also let the rasterizer discover OS-installed fonts as a fallback for\n * families the bundled + `fontFiles` faces don't cover. Defaults to\n * `false`: system fonts vary per machine, so the default keeps output\n * deterministic.\n */\n readonly loadSystemFonts?: boolean;\n}\n\nconst DEFAULT_WIDTH = 1280;\n\n// The fontkit measurer reads + verifies every bundled face on construction.\n// Build it once and reuse across calls; do it lazily so importing this module\n// (e.g. just for the types) doesn't touch the filesystem.\nlet sharedMeasurer: TextMeasurer | undefined;\nconst getSharedMeasurer = (): TextMeasurer => (sharedMeasurer ??= buildFontkitMeasurer());\n\nconst rasterize = (pres: PresentationData, slide: SlideData, opts: RenderImageOptions) => {\n // `getSlideSize` is only consulted to fail fast on a malformed deck; the SVG\n // carries its own viewBox and resvg fits to the requested width.\n getSlideSize(pres);\n const width = opts.width ?? DEFAULT_WIDTH;\n const measureText = opts.measureText ?? getSharedMeasurer();\n const svg = renderSlideSvg(pres, slide, { measureText, textLayout: 'svg' });\n const resvg = new Resvg(svg, {\n fitTo: { mode: 'width', value: width },\n font: {\n loadSystemFonts: opts.loadSystemFonts ?? false, // default: deterministic bundled set\n fontFiles: [...FONT_FILES, ...(opts.fontFiles ?? [])],\n defaultFontFamily: SANS,\n sansSerifFamily: SANS,\n serifFamily: SERIF,\n monospaceFamily: MONO,\n },\n });\n return resvg.render();\n};\n\n/** Render one slide to PNG-encoded bytes. */\nexport const renderSlideToImage = (\n pres: PresentationData,\n slide: SlideData,\n opts: RenderImageOptions = {},\n): Uint8Array => new Uint8Array(rasterize(pres, slide, opts).asPng());\n\n/**\n * Render one slide to raw RGBA pixels plus the PNG encoding of the same frame.\n * Both come from a single rasterization, so callers needing pixel access (SSIM,\n * diffing) and a saveable file don't pay to render twice.\n */\nexport const renderSlideToRgba = (\n pres: PresentationData,\n slide: SlideData,\n opts: RenderImageOptions = {},\n): { readonly image: RgbaImage; readonly png: Uint8Array } => {\n const rendered = rasterize(pres, slide, opts);\n return {\n image: {\n width: rendered.width,\n height: rendered.height,\n data: new Uint8Array(rendered.pixels),\n },\n png: new Uint8Array(rendered.asPng()),\n };\n};\n"],"mappings":"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"}