@office-kit/pptx-preview 0.7.0 → 0.8.1
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/dist/index.js +1 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +3 -3
- package/dist/node.js.map +1 -1
- package/dist/{src-CKGlEIdk.js → src-BMb2-vud.js} +30 -37
- package/dist/src-BMb2-vud.js.map +1 -0
- package/package.json +1 -1
- package/dist/src-CKGlEIdk.js.map +0 -1
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-
|
|
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-BMb2-vud.js";
|
|
2
2
|
export { ARIAL, MONO, SANS, SERIF, TIMES, auditTextLayout, defaultMeasurer, renderSlideSvg as renderSlideToSvg, substituteFamily };
|
package/dist/node.d.ts
CHANGED
|
@@ -44,6 +44,22 @@ interface RenderImageOptions {
|
|
|
44
44
|
* result to control its lifetime, or a custom measurer to swap fonts.
|
|
45
45
|
*/
|
|
46
46
|
readonly measureText?: TextMeasurer;
|
|
47
|
+
/**
|
|
48
|
+
* Extra font files (ttf / otf / ttc paths) loaded into the rasterizer in
|
|
49
|
+
* addition to the bundled Latin faces. The bundled set has no CJK
|
|
50
|
+
* coverage, so decks with Japanese / Chinese / Korean text render as
|
|
51
|
+
* missing-glyph boxes unless the caller supplies a face here (pair it with
|
|
52
|
+
* a {@link buildFontkitMeasurer} `fonts` registration so wrap math uses the
|
|
53
|
+
* same metrics).
|
|
54
|
+
*/
|
|
55
|
+
readonly fontFiles?: ReadonlyArray<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Also let the rasterizer discover OS-installed fonts as a fallback for
|
|
58
|
+
* families the bundled + `fontFiles` faces don't cover. Defaults to
|
|
59
|
+
* `false`: system fonts vary per machine, so the default keeps output
|
|
60
|
+
* deterministic.
|
|
61
|
+
*/
|
|
62
|
+
readonly loadSystemFonts?: boolean;
|
|
47
63
|
}
|
|
48
64
|
/** Render one slide to PNG-encoded bytes. */
|
|
49
65
|
declare const renderSlideToImage: (pres: PresentationData, slide: SlideData, opts?: RenderImageOptions) => Uint8Array;
|
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-
|
|
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-BMb2-vud.js";
|
|
2
2
|
import { getSlideSize } from "@office-kit/pptx";
|
|
3
3
|
import { Resvg } from "@resvg/resvg-js";
|
|
4
4
|
import * as fontkit from "fontkit";
|
|
@@ -147,8 +147,8 @@ const rasterize = (pres, slide, opts) => {
|
|
|
147
147
|
value: width
|
|
148
148
|
},
|
|
149
149
|
font: {
|
|
150
|
-
loadSystemFonts: false,
|
|
151
|
-
fontFiles: FONT_FILES,
|
|
150
|
+
loadSystemFonts: opts.loadSystemFonts ?? false,
|
|
151
|
+
fontFiles: [...FONT_FILES, ...opts.fontFiles ?? []],
|
|
152
152
|
defaultFontFamily: SANS,
|
|
153
153
|
sansSerifFamily: SANS,
|
|
154
154
|
serifFamily: SERIF,
|
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\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: false, // deterministic: only the bundled fonts\n fontFiles: FONT_FILES,\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 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":";;;;;;;AAuBA,MAAa,WAAW,cAAc,IAAI,IAAI,aAAa,OAAO,KAAK,GAAG,CAAC;AAG3E,MAAM,mBAA2C;EAC9C,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,OAAO;AACV;AAEA,MAAM,SAAS;CAAC;CAAW;CAAQ;CAAU;AAAY;AAEzD,MAAM,YAAY,QAAgB,UAA0B,GAAG,WAAW,OAAO,GAAG,MAAM;;AAG1F,MAAa,aAAuB,OAAO,OAAO,gBAAgB,CAAC,CAAC,SAAS,WAC3E,OAAO,KAAK,MAAM,SAAS,QAAQ,CAAC,CAAC,CACvC;AAGA,MAAM,YAAY,SAA+B;CAC/C,MAAM,OAAO,QAAQ,OAAO,aAAa,IAAI,CAAC;CAC9C,IAAI,EAAE,YAAY,OAChB,MAAM,IAAI,MAAM,6BAA6B,KAAK,mBAAmB;CAEvE,OAAO;AACT;AAEA,MAAM,eAAe,SAAsE;CACzF,IAAI,KAAK,QAAQ,KAAK,QAAQ,OAAO;CACrC,IAAI,KAAK,MAAM,OAAO;CACtB,IAAI,KAAK,QAAQ,OAAO;CACxB,OAAO;AACT;AAqBA,MAAM,kBAAkB,WAA8C;CACpE,IAAI,OAAO,WAAW,UAAU,OAAO,SAAS,MAAM;CACtD,MAAM,OAAO,QAAQ,OAAO,OAAO,KAAK,MAAM,CAAC;CAC/C,IAAI,EAAE,YAAY,OAChB,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO;AACT;AAIA,MAAM,qBAAqB,IAAY,WACrC,UAAU,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,IAAI;AAEhD,MAAa,wBAAwB,UAAkC,CAAC,MAAoB;CAC1F,MAAM,wBAAQ,IAAI,IAA0B;CAI5C,KAAK,MAAM,CAAC,QAAQ,WAAW,OAAO,QAAQ,gBAAgB,GAC5D,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,OAAO,SAAS,QAAQ,KAAK;EACnC,IAAI,CAAC,WAAW,IAAI,GAAG,MAAM,IAAI,MAAM,yBAAyB,MAAM;EACtE,MAAM,OAAO,SAAS,IAAI;EAC1B,IAAI,UAAU,aAAa,KAAK,eAAe,QAC7C,MAAM,IAAI,MACR,yBAAyB,KAAK,YAAY,KAAK,WAAW,eAAe,OAAO,8DAElF;EAEF,MAAM,IAAI,GAAG,OAAO,GAAG,SAAS,IAAI;CACtC;CAMF,MAAM,6BAAa,IAAI,IAA0B;CACjD,MAAM,gBAAgC,CAAC;CACvC,KAAK,MAAM,OAAO,QAAQ,SAAS,CAAC,GAAG;EACrC,MAAM,OAAO,eAAe,IAAI,MAAM;EACtC,MAAM,QAAQ,YAAY;GAAE,MAAM,IAAI,QAAQ;GAAO,QAAQ,IAAI,UAAU;EAAM,CAAC;EAClF,WAAW,IAAI,GAAG,IAAI,OAAO,YAAY,EAAE,GAAG,SAAS,IAAI;EAC3D,cAAc,KAAK,IAAI;CACzB;CAEA,MAAM,QAAQ,SAAiC;EAC7C,MAAM,QAAQ,YAAY,IAAI;EAC9B,MAAM,SAAS,KAAK,OAAO,YAAY;EACvC,MAAM,MAAM,WAAW,IAAI,GAAG,OAAO,GAAG,OAAO,KAAK,WAAW,IAAI,GAAG,OAAO,SAAS;EACtF,IAAI,KAAK,OAAO;EAIhB,MAAM,SACJ,iBAAiB,KAAK,WAAW,iBAAiB,iBAAiB,KAAK,MAAM;EAChF,OACE,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,KAC9B,MAAM,IAAI,GAAG,OAAO,SAAS,KAC7B,MAAM,IAAI,iBAAiB;CAE/B;CAKA,MAAM,uBACJ,MACA,MACA,YACkB;EAClB,MAAM,QAAQ,CAAC,SAAS,GAAG,cAAc,QAAQ,MAAM,MAAM,OAAO,CAAC;EACrE,IAAI,UAAU;EACd,IAAI,cAAc;EAClB,IAAI,cAAmC;EACvC,IAAI,UAAU;EACd,IAAI,cAAmC;EACvC,MAAM,cAAoB;GACxB,IAAI,YAAY,MAAM,gBAAgB,MAAM;GAC5C,MAAM,QAAQ,KAAK,SAAS,YAAY;GACxC,WAAW,YAAY,OAAO,OAAO,CAAC,CAAC,eAAe;GACtD,gBAAgB;GAChB,UAAU;EACZ;EACA,KAAK,MAAM,MAAM,MAAM;GACrB,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK;GAChC,MAAM,OAAO,MAAM,MAAM,MAAM,EAAE,qBAAqB,EAAE,CAAC,KAAK;GAC9D,IAAI,SAAS,aAAa;IACxB,MAAM;IACN,cAAc;GAChB;GACA,IAAI,SAAS,MAAM;IACjB,WAAW,kBAAkB,IAAI,KAAK,MAAM;IAC5C,cAAc;GAChB,OACE,WAAW;EAEf;EACA,MAAM;EACN,MAAM,KAAK,gBAAgB,eAAe,OAAO;EACjD,MAAM,UAAU,KAAK,UAAU,eAAe,QAAA,CAAS;EACvD,MAAM,aAAa,CAAC,GAAG,IAAI,CAAC,CAAC;EAC7B,MAAM,WAAW,aAAa,IAAI,KAAK,mBAAmB,aAAa,KAAK;EAC5E,OAAO;GACL,SAAS,UAAU;GACnB,UAAU,GAAG,SAAS;GACtB,WAAW,GAAG,UAAU;GACxB,WAAW,GAAG,UAAU;GACxB,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;EACvC;CACF;CAEA,QAAQ,MAAM,SAAS,oBAAoB,MAAM,MAAM,KAAK,IAAI,CAAC;AACnE;AAeA,MAAM,mBAAmB,SAAiC;CACxD,MAAM,MAAgB,KAA8B;CACpD,IAAI,OAAO,OAAO,QAAQ,UAAU;EAClC,MAAM,IAAI;EAQV,IAAI,EAAE,aAAa,kBAAkB,EAAE,iBAAiB,KAAA,GACtD,OAAO;GACL,QAAQ,EAAE;GACV,SAAS,KAAK,IAAI,EAAE,iBAAiB,CAAC;GACtC,SAAS,EAAE,eAAe;EAC5B;EAEF,IAAI,EAAE,cAAc,KAAA,KAAa,EAAE,eAAe,KAAA,GAChD,OAAO;GAAE,QAAQ,EAAE;GAAW,SAAS,KAAK,IAAI,EAAE,UAAU;GAAG,SAAS;EAAE;CAE9E;CAEA,OAAO;EAAE,QAAQ,KAAK;EAAQ,SAAS,KAAK,IAAI,KAAK,OAAO;EAAG,SAAS,KAAK;CAAQ;AACvF;;;ACzKA,MAAM,gBAAgB;AAKtB,IAAI;AACJ,MAAM,0BAAyC,mBAAmB,qBAAqB;AAEvF,MAAM,aAAa,MAAwB,OAAkB,SAA6B;CAGxF,aAAa,IAAI;CACjB,MAAM,QAAQ,KAAK,SAAS;CAc5B,OAAO,IAXW,MADN,eAAe,MAAM,OAAO;EAAE,aADtB,KAAK,eAAe,kBAAkB;EACH,YAAY;CAAM,CAC/C,GAAG;EAC3B,OAAO;GAAE,MAAM;GAAS,OAAO;EAAM;EACrC,MAAM;GACJ,iBAAiB,KAAK,mBAAmB;GACzC,WAAW,CAAC,GAAG,YAAY,GAAI,KAAK,aAAa,CAAC,CAAE;GACpD,mBAAmB;GACnB,iBAAiB;GACjB,aAAa;GACb,iBAAiB;EACnB;CACF,CACW,CAAC,CAAC,OAAO;AACtB;;AAGA,MAAa,sBACX,MACA,OACA,OAA2B,CAAC,MACb,IAAI,WAAW,UAAU,MAAM,OAAO,IAAI,CAAC,CAAC,MAAM,CAAC;;;;;;AAOpE,MAAa,qBACX,MACA,OACA,OAA2B,CAAC,MACgC;CAC5D,MAAM,WAAW,UAAU,MAAM,OAAO,IAAI;CAC5C,OAAO;EACL,OAAO;GACL,OAAO,SAAS;GAChB,QAAQ,SAAS;GACjB,MAAM,IAAI,WAAW,SAAS,MAAM;EACtC;EACA,KAAK,IAAI,WAAW,SAAS,MAAM,CAAC;CACtC;AACF"}
|
|
@@ -16,11 +16,11 @@ const substituteFamily = (family) => {
|
|
|
16
16
|
return SANS;
|
|
17
17
|
};
|
|
18
18
|
const isCjk = (cp) => cp >= 12352 && cp <= 12447 || cp >= 12448 && cp <= 12543 || cp >= 19968 && cp <= 40959 || cp >= 44032 && cp <= 55215;
|
|
19
|
-
const AVG_GLYPH_W_RATIO
|
|
19
|
+
const AVG_GLYPH_W_RATIO = .55;
|
|
20
20
|
const defaultMeasurer = (text, spec) => {
|
|
21
21
|
let w = 0;
|
|
22
22
|
for (const ch of text) {
|
|
23
|
-
const ratio = isCjk(ch.codePointAt(0) ?? 0) ? 1 : AVG_GLYPH_W_RATIO
|
|
23
|
+
const ratio = isCjk(ch.codePointAt(0) ?? 0) ? 1 : AVG_GLYPH_W_RATIO;
|
|
24
24
|
w += spec.sizePx * ratio;
|
|
25
25
|
}
|
|
26
26
|
const n = [...text].length;
|
|
@@ -1876,7 +1876,6 @@ const renderRun = (text, format, theme, effectivePt, _wasDefault = false) => {
|
|
|
1876
1876
|
return `<span style="${styles.join(";")}">${content}</span>`;
|
|
1877
1877
|
};
|
|
1878
1878
|
const LINE_HEIGHT = 1.05;
|
|
1879
|
-
const AVG_GLYPH_W_RATIO = .55;
|
|
1880
1879
|
const AUTOFIT_FLOOR = .25;
|
|
1881
1880
|
const AUTOFIT_STEP = .05;
|
|
1882
1881
|
const underlineStyleOf = (fmt) => {
|
|
@@ -2254,33 +2253,7 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
|
|
|
2254
2253
|
if (!hasAnyText) return null;
|
|
2255
2254
|
const authoredAutofit = getShapeTextAutoFitParams(shape);
|
|
2256
2255
|
let autoFitScale = authoredAutofit?.fontScale ?? 1;
|
|
2257
|
-
|
|
2258
|
-
if (!authoredAutofit) {
|
|
2259
|
-
const innerWPx = innerW / EMU_PER_PX;
|
|
2260
|
-
const innerHPx = innerH / EMU_PER_PX;
|
|
2261
|
-
let totalH = 0;
|
|
2262
|
-
for (const para of paraData) {
|
|
2263
|
-
let maxSize = defaultPt;
|
|
2264
|
-
let totalChars = 0;
|
|
2265
|
-
let cjkChars = 0;
|
|
2266
|
-
for (const run of para.runs) {
|
|
2267
|
-
if (run.sizePt > maxSize) maxSize = run.sizePt;
|
|
2268
|
-
totalChars += run.text.length;
|
|
2269
|
-
for (let i = 0; i < run.text.length; i++) {
|
|
2270
|
-
const c = run.text.charCodeAt(i);
|
|
2271
|
-
if (c >= 12352 && c <= 12447 || c >= 12448 && c <= 12543 || c >= 19968 && c <= 40959 || c >= 44032 && c <= 55215) cjkChars++;
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
if (totalChars === 0) totalChars = 1;
|
|
2275
|
-
const cjkRatio = cjkChars / totalChars;
|
|
2276
|
-
const glyphRatio = cjkRatio * 1 + (1 - cjkRatio) * AVG_GLYPH_W_RATIO;
|
|
2277
|
-
const sizePx = maxSize * PX_PER_PT;
|
|
2278
|
-
const charsPerLine = Math.max(1, Math.floor(innerWPx / Math.max(1, sizePx * glyphRatio)));
|
|
2279
|
-
const lineCount = Math.max(1, Math.ceil(totalChars / charsPerLine));
|
|
2280
|
-
totalH += sizePx * LINE_HEIGHT * lineCount;
|
|
2281
|
-
}
|
|
2282
|
-
if (totalH > innerHPx) autoFitScale = Math.max(.4, innerHPx / totalH);
|
|
2283
|
-
}
|
|
2256
|
+
const lineHeightScale = 1 - (authoredAutofit?.lnSpcReduction ?? 0);
|
|
2284
2257
|
LINE_HEIGHT * lineHeightScale;
|
|
2285
2258
|
const numberLabels = Array.from({ length: paraData.length }, () => null);
|
|
2286
2259
|
{
|
|
@@ -4342,6 +4315,27 @@ const buildEffectsFilter = (pres, shape) => {
|
|
|
4342
4315
|
defs: `<defs><filter id="${id}" x="-25%" y="-25%" width="150%" height="150%">${primitives.join("")}</filter></defs>`
|
|
4343
4316
|
};
|
|
4344
4317
|
};
|
|
4318
|
+
const flattenedDescendantCount = (group, dropPlaceholders) => {
|
|
4319
|
+
let count = 0;
|
|
4320
|
+
for (const child of getGroupChildren(group)) {
|
|
4321
|
+
if (!dropPlaceholders || !isShapePlaceholder(child)) count += 1;
|
|
4322
|
+
if (getShapeKind(child) === "group") count += flattenedDescendantCount(child, dropPlaceholders);
|
|
4323
|
+
}
|
|
4324
|
+
return count;
|
|
4325
|
+
};
|
|
4326
|
+
const topLevelShapes = (shapes, opts) => {
|
|
4327
|
+
const out = [];
|
|
4328
|
+
let skip = 0;
|
|
4329
|
+
for (const shape of shapes) {
|
|
4330
|
+
if (skip > 0) {
|
|
4331
|
+
skip -= 1;
|
|
4332
|
+
continue;
|
|
4333
|
+
}
|
|
4334
|
+
out.push(shape);
|
|
4335
|
+
if (getShapeKind(shape) === "group") skip = flattenedDescendantCount(shape, opts.dropPlaceholders);
|
|
4336
|
+
}
|
|
4337
|
+
return out;
|
|
4338
|
+
};
|
|
4345
4339
|
const renderSlideSvg = (pres, slide, opts = {}) => {
|
|
4346
4340
|
const size = getSlideSize(pres) ?? DEFAULT_SIZE;
|
|
4347
4341
|
const W = size.width;
|
|
@@ -4417,13 +4411,13 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
|
|
|
4417
4411
|
let layoutBgShapes = "";
|
|
4418
4412
|
const layoutForBg = getSlideLayout(slide);
|
|
4419
4413
|
if (layoutForBg) try {
|
|
4420
|
-
const masterShapes = getSlideMasterShapes(pres, layoutForBg);
|
|
4421
|
-
const layoutShapes = getSlideLayoutShapes(pres, layoutForBg);
|
|
4414
|
+
const masterShapes = topLevelShapes(getSlideMasterShapes(pres, layoutForBg), { dropPlaceholders: true });
|
|
4415
|
+
const layoutShapes = topLevelShapes(getSlideLayoutShapes(pres, layoutForBg), { dropPlaceholders: true });
|
|
4422
4416
|
layoutBgShapes = [...masterShapes, ...layoutShapes].map((s) => renderShape(s, pres, theme, ctx)).join("");
|
|
4423
4417
|
} catch {
|
|
4424
4418
|
layoutBgShapes = "";
|
|
4425
4419
|
}
|
|
4426
|
-
const shapesSvg = getSlideShapes(slide).map((s) => renderShape(s, pres, theme, ctx)).join("");
|
|
4420
|
+
const shapesSvg = topLevelShapes(getSlideShapes(slide), { dropPlaceholders: false }).map((s) => renderShape(s, pres, theme, ctx)).join("");
|
|
4427
4421
|
return [
|
|
4428
4422
|
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${E(W)} ${E(H)}" preserveAspectRatio="xMidYMid meet">`,
|
|
4429
4423
|
bgGradientDefs,
|
|
@@ -4449,8 +4443,7 @@ const detectImageFormatLocal = (bytes) => {
|
|
|
4449
4443
|
const DEFAULT_TOLERANCE_PX = 1;
|
|
4450
4444
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
4451
4445
|
function* walkShapes(shapes) {
|
|
4452
|
-
for (const shape of shapes) if (getShapeKind(shape)
|
|
4453
|
-
else yield shape;
|
|
4446
|
+
for (const shape of shapes) if (getShapeKind(shape) !== "group") yield shape;
|
|
4454
4447
|
}
|
|
4455
4448
|
const passthroughFamily = (family) => family ?? "Carlito";
|
|
4456
4449
|
const auditShape = (pres, theme, shape, slideIndex, measure, tolerancePx, reportSoftWraps, issues) => {
|
|
@@ -4596,6 +4589,6 @@ const auditTextLayout = (pres, options = {}) => {
|
|
|
4596
4589
|
return issues;
|
|
4597
4590
|
};
|
|
4598
4591
|
//#endregion
|
|
4599
|
-
export { MONO as a, TIMES as c, substituteFamily as d, AVG_GLYPH_W_RATIO
|
|
4592
|
+
export { MONO as a, TIMES as c, substituteFamily as d, AVG_GLYPH_W_RATIO as i, defaultMeasurer as l, renderSlideSvg as n, SANS as o, ARIAL as r, SERIF as s, auditTextLayout as t, isCjk as u };
|
|
4600
4593
|
|
|
4601
|
-
//# sourceMappingURL=src-
|
|
4594
|
+
//# sourceMappingURL=src-BMb2-vud.js.map
|