@office-kit/pptx-preview 0.19.0 → 0.20.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 +11 -0
- package/dist/{index-C8dvep3H.d.ts → index-D3rjsHQW.d.ts} +10 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.d.ts +4 -2
- package/dist/node.js +33 -3
- package/dist/node.js.map +1 -1
- package/dist/{src-BGzSNWjt.js → src-BObFGqe2.js} +84 -19
- package/dist/src-BObFGqe2.js.map +1 -0
- package/package.json +3 -3
- package/dist/src-BGzSNWjt.js.map +0 -1
package/README.md
CHANGED
|
@@ -64,6 +64,12 @@ const svg = renderSlideToSvg(pres, slide, {
|
|
|
64
64
|
});
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
The bundled substitutes have no Japanese, Chinese or Korean glyphs. Pass
|
|
68
|
+
`eastAsianFamily` to write a run's East Asian characters (kana, kanji, Hangul,
|
|
69
|
+
and CJK and full-width punctuation) in a family the viewer has, in the weights
|
|
70
|
+
the deck uses; otherwise the glyphs are left to the rasterizer's fallback,
|
|
71
|
+
which in resvg ignores bold.
|
|
72
|
+
|
|
67
73
|
`renderSlideLayoutToSvg(pres, layout, { master })` draws what a slide master
|
|
68
74
|
editor shows behind the placeholders: the layout's background and decorative
|
|
69
75
|
shapes over its master's (the master's are left out when the layout hides
|
|
@@ -105,6 +111,11 @@ Cambria, Liberation ≈ Arial/Times/Courier; OFL / Apache-2.0, see
|
|
|
105
111
|
all reference the same fonts, so wrap/positioning math agrees with the painted
|
|
106
112
|
glyphs and the result is deterministic (no system fonts).
|
|
107
113
|
|
|
114
|
+
For Japanese, Chinese or Korean text, pass the faces in `fontFiles` — the
|
|
115
|
+
regular and the bold one — and register them with `buildFontkitMeasurer({ fonts })`
|
|
116
|
+
for the measurer. East Asian characters are drawn in the family of the first
|
|
117
|
+
`fontFiles` face that covers kana, in the face closest to each run's weight.
|
|
118
|
+
|
|
108
119
|
### Text-layout audit (overflow / soft-wrap detection)
|
|
109
120
|
|
|
110
121
|
```ts
|
|
@@ -58,6 +58,15 @@ interface RenderSlideOptions {
|
|
|
58
58
|
* `@font-face` rule the host embeds), and pair it with a `measureText` that
|
|
59
59
|
* knows its metrics so line breaks match the glyphs. */
|
|
60
60
|
readonly resolveFamily?: (family: string | null) => string;
|
|
61
|
+
/** Family written for East Asian characters (kana, kanji, Hangul, and CJK
|
|
62
|
+
* and full-width punctuation) with `textLayout: 'svg'`, split from the
|
|
63
|
+
* run's Latin text the way presentation apps switch to a run's East Asian
|
|
64
|
+
* font. Unset keeps them in the run's family and leaves the face to the
|
|
65
|
+
* rasterizer's fallback; resvg's fallback ignores the weight, so bold
|
|
66
|
+
* Japanese text there paints with whichever face it finds first. Name a
|
|
67
|
+
* family the rasterizer has loaded in the weights the deck uses. The
|
|
68
|
+
* `/node` entry sets it from the first `fontFiles` face that covers kana. */
|
|
69
|
+
readonly eastAsianFamily?: string;
|
|
61
70
|
}
|
|
62
71
|
declare const SANS = "Carlito";
|
|
63
72
|
declare const SERIF = "Caladea";
|
|
@@ -295,4 +304,4 @@ declare function shapeTextAnchorOffset(pres: PresentationData, shape: SlideShape
|
|
|
295
304
|
declare function textUnderlineStyle(underline: TextFormat['underline'], color: string, includeDecorationColor?: boolean): string;
|
|
296
305
|
//#endregion
|
|
297
306
|
export { substituteFamily as A, RenderSlideOptions as C, TextLayoutMode as D, TIMES as E, TextMeasurer as O, MeasureResult as S, SERIF as T, renderSlideSvg as _, VerticalTextStyle as a, FontSpec as b, TextRectFractions as c, decimalSeparatorOf as d, paragraphNumberLabels as f, renderSlideLayoutSvg as g, auditTextLayout as h, shapeTextAnchorOffset as i, defaultMeasurer as k, resolveTextBodyRect as l, TextAuditIssue as m, ShapeAutoFitScaleOptions as n, textColumnsStyle as o, AuditTextLayoutOptions as p, shapeAutoFitScale as r, verticalTextStyle as s, textUnderlineStyle as t, shapeTextRect as u, renderTextEffectsSvg as v, SANS as w, MONO as x, ARIAL as y };
|
|
298
|
-
//# sourceMappingURL=index-
|
|
307
|
+
//# sourceMappingURL=index-D3rjsHQW.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as substituteFamily, C as RenderSlideOptions, D as TextLayoutMode, E as TIMES, O as TextMeasurer, S as MeasureResult, T as SERIF, _ as renderSlideSvg, a as VerticalTextStyle, b as FontSpec, c as TextRectFractions, d as decimalSeparatorOf, f as paragraphNumberLabels, g as renderSlideLayoutSvg, h as auditTextLayout, i as shapeTextAnchorOffset, k as defaultMeasurer, l as resolveTextBodyRect, m as TextAuditIssue, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as AuditTextLayoutOptions, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as renderTextEffectsSvg, w as SANS, x as MONO, y as ARIAL } from "./index-
|
|
1
|
+
import { A as substituteFamily, C as RenderSlideOptions, D as TextLayoutMode, E as TIMES, O as TextMeasurer, S as MeasureResult, T as SERIF, _ as renderSlideSvg, a as VerticalTextStyle, b as FontSpec, c as TextRectFractions, d as decimalSeparatorOf, f as paragraphNumberLabels, g as renderSlideLayoutSvg, h as auditTextLayout, i as shapeTextAnchorOffset, k as defaultMeasurer, l as resolveTextBodyRect, m as TextAuditIssue, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as AuditTextLayoutOptions, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as renderTextEffectsSvg, w as SANS, x as MONO, y as ARIAL } from "./index-D3rjsHQW.js";
|
|
2
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, decimalSeparatorOf, defaultMeasurer, paragraphNumberLabels, renderSlideLayoutSvg as renderSlideLayoutToSvg, renderSlideSvg as renderSlideToSvg, renderTextEffectsSvg, resolveTextBodyRect, shapeAutoFitScale, shapeTextAnchorOffset, shapeTextRect, substituteFamily, textColumnsStyle, textUnderlineStyle, verticalTextStyle };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { S as shapeTextRect, _ as paragraphNumberLabels, a as renderSlideSvg, b as verticalTextStyle, d as SERIF, f as TIMES, g as decimalSeparatorOf, h as substituteFamily, i as renderSlideLayoutSvg, l as MONO, n as shapeTextAnchorOffset, o as renderTextEffectsSvg, p as defaultMeasurer, r as auditTextLayout, s as ARIAL, t as shapeAutoFitScale, u as SANS, v as textUnderlineStyle, x as resolveTextBodyRect, y as textColumnsStyle } from "./src-
|
|
1
|
+
import { S as shapeTextRect, _ as paragraphNumberLabels, a as renderSlideSvg, b as verticalTextStyle, d as SERIF, f as TIMES, g as decimalSeparatorOf, h as substituteFamily, i as renderSlideLayoutSvg, l as MONO, n as shapeTextAnchorOffset, o as renderTextEffectsSvg, p as defaultMeasurer, r as auditTextLayout, s as ARIAL, t as shapeAutoFitScale, u as SANS, v as textUnderlineStyle, x as resolveTextBodyRect, y as textColumnsStyle } from "./src-BObFGqe2.js";
|
|
2
2
|
export { ARIAL, MONO, SANS, SERIF, TIMES, auditTextLayout, decimalSeparatorOf, defaultMeasurer, paragraphNumberLabels, renderSlideLayoutSvg as renderSlideLayoutToSvg, 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 substituteFamily, C as RenderSlideOptions, D as TextLayoutMode, E as TIMES, O as TextMeasurer, S as MeasureResult, T as SERIF, _ as renderSlideSvg, a as VerticalTextStyle, b as FontSpec, c as TextRectFractions, d as decimalSeparatorOf, f as paragraphNumberLabels, g as renderSlideLayoutSvg, h as auditTextLayout, i as shapeTextAnchorOffset, k as defaultMeasurer, l as resolveTextBodyRect, m as TextAuditIssue, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as AuditTextLayoutOptions, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as renderTextEffectsSvg, w as SANS, x as MONO, y as ARIAL } from "./index-
|
|
1
|
+
import { A as substituteFamily, C as RenderSlideOptions, D as TextLayoutMode, E as TIMES, O as TextMeasurer, S as MeasureResult, T as SERIF, _ as renderSlideSvg, a as VerticalTextStyle, b as FontSpec, c as TextRectFractions, d as decimalSeparatorOf, f as paragraphNumberLabels, g as renderSlideLayoutSvg, h as auditTextLayout, i as shapeTextAnchorOffset, k as defaultMeasurer, l as resolveTextBodyRect, m as TextAuditIssue, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as AuditTextLayoutOptions, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as renderTextEffectsSvg, w as SANS, x as MONO, y as ARIAL } from "./index-D3rjsHQW.js";
|
|
2
2
|
import { PresentationData, SlideData } from "@office-kit/pptx";
|
|
3
3
|
|
|
4
4
|
//#region src/measure.d.ts
|
|
@@ -50,7 +50,9 @@ interface RenderImageOptions {
|
|
|
50
50
|
* coverage, so decks with Japanese / Chinese / Korean text render as
|
|
51
51
|
* missing-glyph boxes unless the caller supplies a face here (pair it with
|
|
52
52
|
* a {@link buildFontkitMeasurer} `fonts` registration so wrap math uses the
|
|
53
|
-
* same metrics).
|
|
53
|
+
* same metrics). East Asian text is drawn in the family of the first face
|
|
54
|
+
* here that covers kana, in the weight closest to the run's, so supply that
|
|
55
|
+
* family's bold face too for bold Japanese text.
|
|
54
56
|
*/
|
|
55
57
|
readonly fontFiles?: ReadonlyArray<string>;
|
|
56
58
|
/**
|
package/dist/node.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as shapeTextRect, _ as paragraphNumberLabels, a as renderSlideSvg, b as verticalTextStyle, c as AVG_GLYPH_W_RATIO, d as SERIF, f as TIMES, g as decimalSeparatorOf, h as substituteFamily, i as renderSlideLayoutSvg, l as MONO, m as isCjk, n as shapeTextAnchorOffset, o as renderTextEffectsSvg, p as defaultMeasurer, r as auditTextLayout, s as ARIAL, t as shapeAutoFitScale, u as SANS, v as textUnderlineStyle, x as resolveTextBodyRect, y as textColumnsStyle } from "./src-
|
|
1
|
+
import { S as shapeTextRect, _ as paragraphNumberLabels, a as renderSlideSvg, b as verticalTextStyle, c as AVG_GLYPH_W_RATIO, d as SERIF, f as TIMES, g as decimalSeparatorOf, h as substituteFamily, i as renderSlideLayoutSvg, l as MONO, m as isCjk, n as shapeTextAnchorOffset, o as renderTextEffectsSvg, p as defaultMeasurer, r as auditTextLayout, s as ARIAL, t as shapeAutoFitScale, u as SANS, v as textUnderlineStyle, x as resolveTextBodyRect, y as textColumnsStyle } from "./src-BObFGqe2.js";
|
|
2
2
|
import { getSlideSize } from "@office-kit/pptx";
|
|
3
3
|
import { Resvg } from "@resvg/resvg-js";
|
|
4
4
|
import * as fontkit from "fontkit";
|
|
@@ -155,12 +155,42 @@ const verticalMetrics = (font) => {
|
|
|
155
155
|
const DEFAULT_WIDTH = 1280;
|
|
156
156
|
let sharedMeasurer;
|
|
157
157
|
const getSharedMeasurer = () => sharedMeasurer ??= buildFontkitMeasurer();
|
|
158
|
+
const KANA_PROBE = 12354;
|
|
159
|
+
const eastAsianFamilyCache = /* @__PURE__ */ new Map();
|
|
160
|
+
const matchFamilyOf = (font) => font.getName("preferredFamily", "en") ?? font.familyName;
|
|
161
|
+
const openFontFile = (path) => {
|
|
162
|
+
if (!existsSync(path)) return null;
|
|
163
|
+
try {
|
|
164
|
+
return fontkit.create(readFileSync(path));
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const eastAsianFamilyOf = (fontFiles) => {
|
|
170
|
+
const key = fontFiles.join("\n");
|
|
171
|
+
if (eastAsianFamilyCache.has(key)) return eastAsianFamilyCache.get(key);
|
|
172
|
+
let family;
|
|
173
|
+
for (const path of fontFiles) {
|
|
174
|
+
const opened = openFontFile(path);
|
|
175
|
+
if (opened === null) continue;
|
|
176
|
+
const face = ("fonts" in opened ? opened.fonts : [opened]).find((f) => f.hasGlyphForCodePoint(KANA_PROBE));
|
|
177
|
+
if (face) {
|
|
178
|
+
family = matchFamilyOf(face);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
eastAsianFamilyCache.set(key, family);
|
|
183
|
+
return family;
|
|
184
|
+
};
|
|
158
185
|
const rasterize = (pres, slide, opts) => {
|
|
159
186
|
getSlideSize(pres);
|
|
160
187
|
const width = opts.width ?? DEFAULT_WIDTH;
|
|
188
|
+
const measureText = opts.measureText ?? getSharedMeasurer();
|
|
189
|
+
const eastAsianFamily = eastAsianFamilyOf(opts.fontFiles ?? []);
|
|
161
190
|
return new Resvg(renderSlideSvg(pres, slide, {
|
|
162
|
-
measureText
|
|
163
|
-
textLayout: "svg"
|
|
191
|
+
measureText,
|
|
192
|
+
textLayout: "svg",
|
|
193
|
+
...eastAsianFamily === void 0 ? {} : { eastAsianFamily }
|
|
164
194
|
}), {
|
|
165
195
|
fitTo: {
|
|
166
196
|
mode: "width",
|
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 // 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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"}
|
|
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 * as fontkit from 'fontkit';\nimport { existsSync, readFileSync } from 'node:fs';\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). East Asian text is drawn in the family of the first face\n * here that covers kana, in the weight closest to the run's, so supply that\n * family's bold face too for bold Japanese text.\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\n// Hiragana あ: a face that has it is a Japanese (or pan-CJK) face.\nconst KANA_PROBE = 0x3042;\n\n// Reading a face's names means parsing the file, so remember the answer per\n// list of files: a deck renders every slide with the same list.\nconst eastAsianFamilyCache = new Map<string, string | undefined>();\n\n// The family resvg matches a face by: the typographic family when the face\n// has one (\"Hiragino Sans\" for \"Hiragino Sans W6\"), else the family name.\nconst matchFamilyOf = (font: fontkit.Font): string =>\n font.getName('preferredFamily', 'en') ?? font.familyName;\n\n// resvg skips a font file it cannot find or parse, and `fontFiles` promises\n// the same, so such a file names no family here either.\nconst openFontFile = (path: string): fontkit.Font | fontkit.FontCollection | null => {\n if (!existsSync(path)) return null;\n try {\n return fontkit.create(readFileSync(path));\n } catch {\n return null;\n }\n};\n\nconst eastAsianFamilyOf = (fontFiles: ReadonlyArray<string>): string | undefined => {\n const key = fontFiles.join('\\n');\n if (eastAsianFamilyCache.has(key)) return eastAsianFamilyCache.get(key);\n let family: string | undefined;\n for (const path of fontFiles) {\n const opened = openFontFile(path);\n if (opened === null) continue;\n const faces = 'fonts' in opened ? opened.fonts : [opened];\n const face = faces.find((f) => f.hasGlyphForCodePoint(KANA_PROBE));\n if (face) {\n family = matchFamilyOf(face);\n break;\n }\n }\n eastAsianFamilyCache.set(key, family);\n return family;\n};\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 // resvg's own fallback for glyphs the run's family lacks ignores the weight\n // (bold Japanese paints with the regular face), so name the face instead.\n const eastAsianFamily = eastAsianFamilyOf(opts.fontFiles ?? []);\n const svg = renderSlideSvg(pres, slide, {\n measureText,\n textLayout: 'svg',\n ...(eastAsianFamily === undefined ? {} : { eastAsianFamily }),\n });\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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"}
|
|
@@ -1058,6 +1058,30 @@ const emitPlacements = (placements, effectsOnly = false, effectKind = "all") =>
|
|
|
1058
1058
|
close();
|
|
1059
1059
|
return paragraphs.join("");
|
|
1060
1060
|
};
|
|
1061
|
+
const familyOf = (g) => g.piece.cssFamily ?? g.piece.family;
|
|
1062
|
+
const lineText = (groups, textAnchor, x0, span) => {
|
|
1063
|
+
if (new Set(groups.filter((g) => !g.isTab).map(familyOf)).size < 2 || !groups.some((g) => EAST_ASIAN_CHAR.test(g.text))) return {
|
|
1064
|
+
x: x0,
|
|
1065
|
+
anchor: textAnchor,
|
|
1066
|
+
body: groups.map((g) => span(g)).join("")
|
|
1067
|
+
};
|
|
1068
|
+
const totalWidth = groups.reduce((sum, g) => sum + g.width, 0);
|
|
1069
|
+
const start = x0 - (textAnchor === "middle" ? totalWidth / 2 : textAnchor === "end" ? totalWidth : 0);
|
|
1070
|
+
let cursor = start;
|
|
1071
|
+
let family = null;
|
|
1072
|
+
const parts = [];
|
|
1073
|
+
for (const g of groups) {
|
|
1074
|
+
const changes = !g.isTab && familyOf(g) !== family;
|
|
1075
|
+
parts.push(span(g, changes && family !== null ? cursor : void 0));
|
|
1076
|
+
if (changes) family = familyOf(g);
|
|
1077
|
+
cursor += g.width;
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
x: start,
|
|
1081
|
+
anchor: "start",
|
|
1082
|
+
body: parts.join("")
|
|
1083
|
+
};
|
|
1084
|
+
};
|
|
1061
1085
|
const topPad = (line) => {
|
|
1062
1086
|
const lead = line.advance - (line.ascent + line.descent);
|
|
1063
1087
|
return lead > 0 ? lead / 2 : 0;
|
|
@@ -1076,24 +1100,25 @@ const emitLine = (line, baselineY, dx, descent, nextEffectId, effectsOnly, effec
|
|
|
1076
1100
|
const content = toks.filter((t) => !t.isBreak);
|
|
1077
1101
|
if (content.length === 0) return "";
|
|
1078
1102
|
const groups = groupTokens(content);
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1103
|
+
const x0 = line.anchorX + dx + GRID_NUDGE_X;
|
|
1104
|
+
const linked = lineText(groups, line.textAnchor, x0, (g, x) => {
|
|
1105
|
+
const span = tspan(g, x);
|
|
1081
1106
|
const href = g.piece.href ? safeLinkHref(g.piece.href) : void 0;
|
|
1082
1107
|
if (!href) return span;
|
|
1083
1108
|
const target = href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
|
|
1084
1109
|
const title = g.piece.hrefTip ? `<title>${escapeXml$1(g.piece.hrefTip)}</title>` : "";
|
|
1085
1110
|
return `<a href="${escapeXml$1(href)}"${target}>${title}${span}</a>`;
|
|
1086
|
-
})
|
|
1087
|
-
if (
|
|
1088
|
-
const
|
|
1089
|
-
const text = `<text x="${fmt(x0)}" y="${fmt(baselineY)}" text-anchor="${line.textAnchor}" xml:space="preserve">${tspans}</text>`;
|
|
1111
|
+
});
|
|
1112
|
+
if (linked.body === "") return "";
|
|
1113
|
+
const text = `<text x="${fmt(linked.x)}" y="${fmt(baselineY)}" text-anchor="${linked.anchor}" xml:space="preserve">${linked.body}</text>`;
|
|
1090
1114
|
if (effectsOnly && effectKind === "fill") {
|
|
1091
1115
|
const painted = groups.map((g) => g.piece.fillPaint ? g : {
|
|
1092
1116
|
...g,
|
|
1093
1117
|
piece: unpaintedPiece(g.piece)
|
|
1094
1118
|
});
|
|
1095
1119
|
if (!painted.some((g) => g.piece.fillPaint)) return "";
|
|
1096
|
-
const
|
|
1120
|
+
const paintedText = lineText(painted, line.textAnchor, x0, tspan);
|
|
1121
|
+
const glyphs = `<text x="${fmt(paintedText.x)}" y="${fmt(baselineY)}" text-anchor="${paintedText.anchor}" xml:space="preserve" aria-hidden="true" pointer-events="none">${paintedText.body}</text>`;
|
|
1097
1122
|
return emitTextOuterEffects(painted, line.textAnchor, x0, baselineY, nextEffectId) + glyphs + emitTextDecorations(painted, line.textAnchor, x0, baselineY);
|
|
1098
1123
|
}
|
|
1099
1124
|
const reflections = effectKind === "reflection" || effectKind === "all" ? emitTextReflections(groups, line.textAnchor, x0, baselineY, descent, nextEffectId) : "";
|
|
@@ -1258,11 +1283,11 @@ const emitTextBevel = (groups, textAnchor, x0, baselineY, nextId) => {
|
|
|
1258
1283
|
const light = `<feDistantLight azimuth="${fmt(bevel.azimuthDeg)}" elevation="${fmt(bevel.elevationDeg)}"/>`;
|
|
1259
1284
|
const specular = bevel.specular ? `<feSpecularLighting in="bevelHeight" surfaceScale="${fmt(surfaceScale)}" specularConstant="${fmt(bevel.specular.constant)}" specularExponent="${fmt(bevel.specular.exponent)}" lighting-color="#fff" result="bevelSpecular">${light}</feSpecularLighting>` : "";
|
|
1260
1285
|
const defs = `<defs><filter id="${id}" x="-25%" y="-25%" width="150%" height="150%" color-interpolation-filters="sRGB" data-pptx-bevel="1"><feGaussianBlur in="SourceAlpha" stdDeviation="${fmt(widthPx / 2)}" result="bevelBlur"/><feComponentTransfer in="bevelBlur" result="bevelHeight"><feFuncA type="table" tableValues="${table.map(fmt).join(" ")}"/></feComponentTransfer><feDiffuseLighting in="bevelHeight" surfaceScale="${fmt(surfaceScale)}" diffuseConstant="1" lighting-color="#fff" result="bevelDiffuse">${light}</feDiffuseLighting><feColorMatrix in="bevelDiffuse" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 ${fmt(lightGain)} 0 0 0 ${fmt(-lightGain * flat)}" result="bevelLight"/><feColorMatrix in="bevelDiffuse" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ${fmt(-shadeGain)} 0 0 0 ${fmt(shadeGain * flat)}" result="bevelShade"/>` + specular + `<feMerge result="bevelMerged"><feMergeNode in="bevelShade"/><feMergeNode in="bevelLight"/>${specular ? "<feMergeNode in=\"bevelSpecular\"/>" : ""}</feMerge><feComposite in="bevelMerged" in2="SourceAlpha" operator="in"/></filter></defs>`;
|
|
1261
|
-
const
|
|
1286
|
+
const beveledText = lineText(groups.map((g) => g.piece.bevel && !g.isTab ? g : {
|
|
1262
1287
|
...g,
|
|
1263
1288
|
piece: unpaintedPiece(g.piece)
|
|
1264
|
-
});
|
|
1265
|
-
return `${defs}<text x="${fmt(
|
|
1289
|
+
}), textAnchor, x0, tspan);
|
|
1290
|
+
return `${defs}<text x="${fmt(beveledText.x)}" y="${fmt(baselineY)}" text-anchor="${beveledText.anchor}" xml:space="preserve" filter="url(#${id})" aria-hidden="true" pointer-events="none" data-pptx-bevel="text">${beveledText.body}</text>`;
|
|
1266
1291
|
};
|
|
1267
1292
|
const baselineShiftPxOf = (p) => p.sizePx * p.baseline;
|
|
1268
1293
|
const SUPER_SUB_SIZE_RATIO = .65;
|
|
@@ -1408,11 +1433,12 @@ const sameGlow = (a, b) => {
|
|
|
1408
1433
|
return a.color === b.color && a.radiusEmu === b.radiusEmu && a.opacity === b.opacity;
|
|
1409
1434
|
};
|
|
1410
1435
|
const samePiece = (a, b) => a.family === b.family && a.sizePx === b.sizePx && a.bold === b.bold && a.italic === b.italic && a.letterSpacingPx === b.letterSpacingPx && a.kerning === b.kerning && a.fillHex === b.fillHex && a.outlineHex === b.outlineHex && a.outlineWidthPx === b.outlineWidthPx && a.underlineHex === b.underlineHex && a.highlightHex === b.highlightHex && a.underline === b.underline && a.strike === b.strike && a.baseline === b.baseline && a.smallCaps === b.smallCaps && sameShadow(a.shadow, b.shadow) && sameGlow(a.glow, b.glow) && sameInnerShadow(a.innerShadow, b.innerShadow) && sameReflection(a.reflection, b.reflection) && a.href === b.href && a.hrefTip === b.hrefTip;
|
|
1411
|
-
const tspan = (g) => {
|
|
1436
|
+
const tspan = (g, x) => {
|
|
1412
1437
|
if (g.isTab) return `<tspan dx="${fmt(g.width)}">​</tspan>`;
|
|
1413
1438
|
const p = g.piece;
|
|
1414
1439
|
const sizePx = renderedSizePxOf(p);
|
|
1415
1440
|
const attrs = [
|
|
1441
|
+
...x === void 0 ? [] : [`x="${fmt(x)}"`],
|
|
1416
1442
|
`font-family="${escapeXml$1(p.cssFamily ?? p.family)}"`,
|
|
1417
1443
|
`font-size="${fmt(sizePx)}"`,
|
|
1418
1444
|
`fill="${p.fillHex}"`
|
|
@@ -1756,6 +1782,10 @@ const BROWSER_FAMILIES = {
|
|
|
1756
1782
|
resolveFamily: browserFontFamily,
|
|
1757
1783
|
familyIsCssList: true
|
|
1758
1784
|
};
|
|
1785
|
+
const svgFamilies = (ctx) => ({
|
|
1786
|
+
resolveFamily: ctx.resolveFamily,
|
|
1787
|
+
...ctx.eastAsianFamily === void 0 ? {} : { eastAsianFamily: ctx.eastAsianFamily }
|
|
1788
|
+
});
|
|
1759
1789
|
const DEFAULT_BULLET_FONT = "Arial";
|
|
1760
1790
|
const DEFAULT_INSET_X = 91440;
|
|
1761
1791
|
const DEFAULT_INSET_Y = 45720;
|
|
@@ -2607,6 +2637,36 @@ const faceOf = (a, font) => {
|
|
|
2607
2637
|
cssFamily: cssFamilyName(family)
|
|
2608
2638
|
};
|
|
2609
2639
|
};
|
|
2640
|
+
const eastAsianParts = (piece, family) => {
|
|
2641
|
+
if (family === void 0 || !EAST_ASIAN_CHAR.test(piece.text)) return [piece];
|
|
2642
|
+
const face = {
|
|
2643
|
+
family,
|
|
2644
|
+
cssFamily: cssFamilyName(family)
|
|
2645
|
+
};
|
|
2646
|
+
const parts = [];
|
|
2647
|
+
let current = "";
|
|
2648
|
+
let currentEastAsian = false;
|
|
2649
|
+
const flush = () => {
|
|
2650
|
+
if (current === "") return;
|
|
2651
|
+
parts.push(currentEastAsian ? {
|
|
2652
|
+
...piece,
|
|
2653
|
+
...face,
|
|
2654
|
+
text: current
|
|
2655
|
+
} : {
|
|
2656
|
+
...piece,
|
|
2657
|
+
text: current
|
|
2658
|
+
});
|
|
2659
|
+
current = "";
|
|
2660
|
+
};
|
|
2661
|
+
for (const ch of piece.text) {
|
|
2662
|
+
const eastAsian = EAST_ASIAN_CHAR.test(ch);
|
|
2663
|
+
if (eastAsian !== currentEastAsian) flush();
|
|
2664
|
+
currentEastAsian = eastAsian;
|
|
2665
|
+
current += ch;
|
|
2666
|
+
}
|
|
2667
|
+
flush();
|
|
2668
|
+
return parts;
|
|
2669
|
+
};
|
|
2610
2670
|
const buildSvgTextInput = (a) => {
|
|
2611
2671
|
const scale = a.autoFitScale;
|
|
2612
2672
|
const paragraphs = a.paraData.map((para, pi) => {
|
|
@@ -2696,10 +2756,10 @@ const buildSvgTextInput = (a) => {
|
|
|
2696
2756
|
const segs = run.text.split("\n");
|
|
2697
2757
|
segs.forEach((seg, i) => {
|
|
2698
2758
|
const segText = caps ? seg.toUpperCase() : seg;
|
|
2699
|
-
const segPieces = cap === "small" ? smallCapsParts(seg, base) : [{
|
|
2759
|
+
const segPieces = (cap === "small" ? smallCapsParts(seg, base) : [{
|
|
2700
2760
|
...base,
|
|
2701
2761
|
text: segText
|
|
2702
|
-
}];
|
|
2762
|
+
}]).flatMap((piece) => eastAsianParts(piece, a.eastAsianFamily));
|
|
2703
2763
|
if (a.vert === "upright") for (const segPiece of segPieces) for (const g of graphemesOf(segPiece.text)) {
|
|
2704
2764
|
const last = pieces[pieces.length - 1];
|
|
2705
2765
|
if (last !== void 0 && !last.isBreak) pieces.push(breakPiece());
|
|
@@ -2799,7 +2859,7 @@ const bulletFillOf = (para, theme, defaultColor) => {
|
|
|
2799
2859
|
const runColor = para.runs.find((r) => r.text !== "\n" && r.text !== "")?.fmt?.color;
|
|
2800
2860
|
return runColor === void 0 || runColor === null ? defaultColor : resolveColor(runColor, theme, defaultColor);
|
|
2801
2861
|
};
|
|
2802
|
-
const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defaultColor, useSvgTextRect = false, resolveFamily = substituteFamily) => {
|
|
2862
|
+
const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defaultColor, useSvgTextRect = false, resolveFamily = substituteFamily, eastAsianFamily) => {
|
|
2803
2863
|
let paragraphCount;
|
|
2804
2864
|
try {
|
|
2805
2865
|
paragraphCount = getShapeParagraphCount(shape);
|
|
@@ -3019,7 +3079,8 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
|
|
|
3019
3079
|
measure,
|
|
3020
3080
|
vert: fitVert,
|
|
3021
3081
|
columns: fitColumns,
|
|
3022
|
-
resolveFamily
|
|
3082
|
+
resolveFamily,
|
|
3083
|
+
...eastAsianFamily === void 0 ? {} : { eastAsianFamily }
|
|
3023
3084
|
};
|
|
3024
3085
|
let s = 1;
|
|
3025
3086
|
while (s > AUTOFIT_FLOOR) {
|
|
@@ -3060,6 +3121,7 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
|
|
|
3060
3121
|
vert,
|
|
3061
3122
|
autoFitScale,
|
|
3062
3123
|
resolveFamily,
|
|
3124
|
+
...eastAsianFamily === void 0 ? {} : { eastAsianFamily },
|
|
3063
3125
|
columns: vert === "none" && cols && cols.count >= 2 ? {
|
|
3064
3126
|
count: cols.count,
|
|
3065
3127
|
gapPx: cols.gapEmu !== void 0 ? cols.gapEmu / EMU_PER_PX : 0
|
|
@@ -3178,7 +3240,7 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
|
|
|
3178
3240
|
return paragraphs;
|
|
3179
3241
|
};
|
|
3180
3242
|
const renderTextBody = (pres, shape, bounds, theme, phType, ctx, output = "body") => {
|
|
3181
|
-
const model = resolveTextBodyModel(pres, shape, bounds, theme, phType, ctx.measure, activeDeckTextColor, ctx.mode === "svg" && output === "body", ctx.resolveFamily);
|
|
3243
|
+
const model = resolveTextBodyModel(pres, shape, bounds, theme, phType, ctx.measure, activeDeckTextColor, ctx.mode === "svg" && output === "body", ctx.resolveFamily, ctx.eastAsianFamily);
|
|
3182
3244
|
if (model === null) return "";
|
|
3183
3245
|
const bevel = textBevelOf(getShapeText3D(shape));
|
|
3184
3246
|
const { paraData, numberLabels, authoredAutofit, autoFitScale, defaultPt, themeFace, effectiveDefaultFont, effectiveBody, anchor, innerX, innerY, innerW, innerH, svgTextRect } = model;
|
|
@@ -3214,7 +3276,7 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx, output = "body"
|
|
|
3214
3276
|
innerW: vInnerW,
|
|
3215
3277
|
innerH: vInnerH,
|
|
3216
3278
|
measure: ctx.mode === "svg" ? ctx.measure : browserTextMeasurer() ?? ctx.measure,
|
|
3217
|
-
...ctx.mode === "foreignObject" ? BROWSER_FAMILIES :
|
|
3279
|
+
...ctx.mode === "foreignObject" ? BROWSER_FAMILIES : svgFamilies(ctx),
|
|
3218
3280
|
vert: svgVert,
|
|
3219
3281
|
columns: svgColumns,
|
|
3220
3282
|
bevel
|
|
@@ -3363,6 +3425,7 @@ const renderTextEffectsSvg = (pres, slide, shape, bounds, opts = {}) => {
|
|
|
3363
3425
|
mode: "foreignObject",
|
|
3364
3426
|
measure: opts.measureText ?? browserTextMeasurer() ?? defaultMeasurer,
|
|
3365
3427
|
resolveFamily: substituteFamily,
|
|
3428
|
+
eastAsianFamily: void 0,
|
|
3366
3429
|
ownShapes: true,
|
|
3367
3430
|
background: {
|
|
3368
3431
|
id: "",
|
|
@@ -3400,6 +3463,7 @@ const renderTableCellEffectsSvgInternal = (pres, slide, shape, cellPosition, bou
|
|
|
3400
3463
|
mode: "foreignObject",
|
|
3401
3464
|
measure: opts.measureText ?? browserTextMeasurer() ?? defaultMeasurer,
|
|
3402
3465
|
resolveFamily: substituteFamily,
|
|
3466
|
+
eastAsianFamily: void 0,
|
|
3403
3467
|
ownShapes: true,
|
|
3404
3468
|
background: {
|
|
3405
3469
|
id: "",
|
|
@@ -4994,7 +5058,7 @@ const renderTableCellText = (cell, paragraphs, cx, cy, cw, ch, color, pres, shap
|
|
|
4994
5058
|
innerW: innerW * EMU_PER_PX,
|
|
4995
5059
|
innerH: innerH * EMU_PER_PX,
|
|
4996
5060
|
measure: ctx.mode === "svg" ? ctx.measure : browserTextMeasurer() ?? ctx.measure,
|
|
4997
|
-
...ctx.mode === "foreignObject" ? BROWSER_FAMILIES :
|
|
5061
|
+
...ctx.mode === "foreignObject" ? BROWSER_FAMILIES : svgFamilies(ctx),
|
|
4998
5062
|
vert: verticalLayoutOf(getTableCellTextDirection(cell)),
|
|
4999
5063
|
columns: null,
|
|
5000
5064
|
...effectsOnly ? { effectsOnly: true } : {},
|
|
@@ -5874,6 +5938,7 @@ const renderSurface = (pres, surface, opts) => {
|
|
|
5874
5938
|
mode: opts.textLayout ?? "foreignObject",
|
|
5875
5939
|
measure: opts.measureText ?? defaultMeasurer,
|
|
5876
5940
|
resolveFamily: opts.textLayout === "svg" ? opts.resolveFamily ?? substituteFamily : substituteFamily,
|
|
5941
|
+
eastAsianFamily: opts.textLayout === "svg" ? opts.eastAsianFamily : void 0,
|
|
5877
5942
|
ownShapes: true,
|
|
5878
5943
|
background: {
|
|
5879
5944
|
id: "",
|
|
@@ -6316,4 +6381,4 @@ function shapeTextAnchorOffset(pres, shape, options = {}) {
|
|
|
6316
6381
|
//#endregion
|
|
6317
6382
|
export { shapeTextRect as S, paragraphNumberLabels as _, renderSlideSvg as a, verticalTextStyle as b, AVG_GLYPH_W_RATIO as c, SERIF as d, TIMES as f, decimalSeparatorOf as g, substituteFamily as h, renderSlideLayoutSvg as i, MONO as l, isCjk as m, shapeTextAnchorOffset as n, renderTextEffectsSvg as o, defaultMeasurer as p, auditTextLayout as r, ARIAL as s, shapeAutoFitScale as t, SANS as u, textUnderlineStyle as v, resolveTextBodyRect as x, textColumnsStyle as y };
|
|
6318
6383
|
|
|
6319
|
-
//# sourceMappingURL=src-
|
|
6384
|
+
//# sourceMappingURL=src-BObFGqe2.js.map
|