@pixldocs/canvas-renderer 0.5.67 → 0.5.70

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 CHANGED
@@ -12123,1384 +12123,19 @@ function paintRepeatableSections(config, repeatableSections) {
12123
12123
  }
12124
12124
  }
12125
12125
  }
12126
- function normalizeFontFamily(fontStack) {
12127
- const first = fontStack.split(",")[0].trim();
12128
- return first.replace(/^['"]|['"]$/g, "");
12129
- }
12130
- const loadedFonts = /* @__PURE__ */ new Set();
12131
- const loadingPromises = /* @__PURE__ */ new Map();
12132
- function withTimeout(promise, timeoutMs = 4e3) {
12133
- let timeoutId;
12134
- return Promise.race([
12135
- promise,
12136
- new Promise((resolve) => {
12137
- timeoutId = setTimeout(resolve, timeoutMs);
12138
- })
12139
- ]).finally(() => {
12140
- if (timeoutId) clearTimeout(timeoutId);
12141
- });
12142
- }
12143
- async function loadGoogleFontCSS(rawFontFamily) {
12144
- if (!rawFontFamily || typeof document === "undefined") return;
12145
- const fontFamily = normalizeFontFamily(rawFontFamily);
12146
- if (!fontFamily) return;
12147
- if (loadedFonts.has(fontFamily)) return;
12148
- const existing = loadingPromises.get(fontFamily);
12149
- if (existing) return existing;
12150
- const promise = (async () => {
12151
- try {
12152
- const encoded = encodeURIComponent(fontFamily);
12153
- const url = `https://fonts.googleapis.com/css?family=${encoded}:300,400,500,600,700&display=swap`;
12154
- const link = document.createElement("link");
12155
- link.rel = "stylesheet";
12156
- link.href = url;
12157
- link.crossOrigin = "anonymous";
12158
- await new Promise((resolve, reject) => {
12159
- link.onload = () => resolve();
12160
- link.onerror = () => reject(new Error(`Failed to load font: ${fontFamily}`));
12161
- document.head.appendChild(link);
12162
- });
12163
- loadedFonts.add(fontFamily);
12164
- } catch (e) {
12165
- console.warn(`[@pixldocs/canvas-renderer] Font load failed: ${fontFamily}`, e);
12166
- }
12167
- })();
12168
- loadingPromises.set(fontFamily, promise);
12169
- await promise;
12170
- loadingPromises.delete(fontFamily);
12171
- }
12172
- function collectFontsFromConfig(config) {
12173
- var _a;
12174
- const fonts = /* @__PURE__ */ new Set();
12175
- fonts.add("Open Sans");
12176
- fonts.add("Hind");
12177
- function walk(nodes) {
12178
- var _a2;
12179
- if (!nodes) return;
12180
- for (const node of nodes) {
12181
- if (node.fontFamily) fonts.add(normalizeFontFamily(node.fontFamily));
12182
- if ((_a2 = node.smartProps) == null ? void 0 : _a2.fontFamily) fonts.add(normalizeFontFamily(node.smartProps.fontFamily));
12183
- if (node.styles && Array.isArray(node.styles)) {
12184
- for (const lineStyle of node.styles) {
12185
- if (lineStyle && typeof lineStyle === "object") {
12186
- for (const charStyle of Object.values(lineStyle)) {
12187
- if (charStyle == null ? void 0 : charStyle.fontFamily) fonts.add(normalizeFontFamily(charStyle.fontFamily));
12188
- }
12189
- }
12190
- }
12191
- }
12192
- if (node.children) walk(node.children);
12193
- }
12194
- }
12195
- for (const page of config.pages || []) {
12196
- walk(page.children || []);
12197
- }
12198
- if ((_a = config.themeConfig) == null ? void 0 : _a.variables) {
12199
- for (const def of Object.values(config.themeConfig.variables)) {
12200
- if (def.value && typeof def.value === "string" && !def.value.startsWith("#") && !def.value.startsWith("rgb")) {
12201
- if (def.label && /font/i.test(def.label)) {
12202
- fonts.add(normalizeFontFamily(def.value));
12203
- }
12204
- }
12205
- }
12206
- }
12207
- return fonts;
12208
- }
12209
- function collectFontDescriptorsFromConfig(config) {
12210
- var _a;
12211
- const seen = /* @__PURE__ */ new Set();
12212
- const descriptors = [];
12213
- function add(family, weight, style) {
12214
- const f = normalizeFontFamily(family);
12215
- if (!f) return;
12216
- const w = weight ?? 400;
12217
- const s = style ?? "normal";
12218
- const key = `${f}|${w}|${s}`;
12219
- if (seen.has(key)) return;
12220
- seen.add(key);
12221
- descriptors.push({ family: f, weight: w, style: s });
12222
- }
12223
- function walk(nodes) {
12224
- var _a2;
12225
- if (!nodes) return;
12226
- for (const node of nodes) {
12227
- if (node.fontFamily) {
12228
- add(node.fontFamily, node.fontWeight, node.fontStyle);
12229
- if (node.type === "text") {
12230
- for (const w of [300, 400, 500, 600, 700]) {
12231
- add(node.fontFamily, w, node.fontStyle);
12232
- }
12233
- }
12234
- }
12235
- if ((_a2 = node.smartProps) == null ? void 0 : _a2.fontFamily) {
12236
- add(node.smartProps.fontFamily, node.smartProps.fontWeight, node.smartProps.fontStyle);
12237
- }
12238
- if (node.styles) {
12239
- const styleEntries = Array.isArray(node.styles) ? node.styles : Object.values(node.styles);
12240
- for (const lineStyle of styleEntries) {
12241
- if (lineStyle && typeof lineStyle === "object") {
12242
- for (const charStyle of Object.values(lineStyle)) {
12243
- if (charStyle == null ? void 0 : charStyle.fontFamily) {
12244
- add(charStyle.fontFamily, charStyle.fontWeight, charStyle.fontStyle);
12245
- }
12246
- }
12247
- }
12248
- }
12249
- }
12250
- if (node.children) walk(node.children);
12251
- }
12252
- }
12253
- add("Open Sans", 400, "normal");
12254
- add("Hind", 400, "normal");
12255
- add("Hind", 700, "normal");
12256
- for (const page of config.pages || []) {
12257
- walk(page.children || []);
12258
- }
12259
- if ((_a = config.themeConfig) == null ? void 0 : _a.variables) {
12260
- for (const def of Object.values(config.themeConfig.variables)) {
12261
- if (def.value && typeof def.value === "string" && !def.value.startsWith("#") && !def.value.startsWith("rgb")) {
12262
- if (def.label && /font/i.test(def.label)) {
12263
- add(def.value);
12264
- }
12265
- }
12266
- }
12267
- }
12268
- return descriptors;
12269
- }
12270
- async function ensureFontsForResolvedConfig(config) {
12271
- if (typeof document === "undefined") return;
12272
- const descriptors = collectFontDescriptorsFromConfig(config);
12273
- const families = new Set(descriptors.map((d) => d.family));
12274
- void withTimeout(Promise.all([...families].map((f) => loadGoogleFontCSS(f))), 2500);
12275
- if (document.fonts) {
12276
- descriptors.forEach((d) => {
12277
- const stylePrefix = d.style === "italic" ? "italic " : "";
12278
- const weightStr = String(d.weight);
12279
- const spec = `${stylePrefix}${weightStr} 16px "${d.family}"`;
12280
- document.fonts.load(spec).catch(() => {
12281
- });
12282
- });
12283
- }
12284
- }
12285
- function configHasAutoShrinkText$1(config) {
12286
- var _a;
12287
- if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return false;
12288
- const walk = (nodes) => {
12289
- for (const node of nodes || []) {
12290
- if (!node) continue;
12291
- if (node.type === "text" && node.overflowPolicy === "auto-shrink") return true;
12292
- if (Array.isArray(node.children) && node.children.length && walk(node.children)) return true;
12293
- }
12294
- return false;
12295
- };
12296
- for (const page of config.pages) {
12297
- if (walk(page.children || [])) return true;
12298
- }
12299
- return false;
12300
- }
12301
- async function awaitFontsForConfig(config, maxWaitMs) {
12302
- if (typeof document === "undefined" || !document.fonts) return;
12303
- void ensureFontsForResolvedConfig(config);
12304
- const descriptors = collectFontDescriptorsFromConfig(config);
12305
- if (descriptors.length === 0) return;
12306
- const loads = Promise.all(
12307
- descriptors.map((d) => {
12308
- const stylePrefix = d.style === "italic" ? "italic " : "";
12309
- const spec = `${stylePrefix}${d.weight} 16px "${d.family}"`;
12310
- return document.fonts.load(spec).catch(() => []);
12311
- })
12312
- ).then(() => void 0);
12313
- await Promise.race([
12314
- loads,
12315
- new Promise((resolve) => setTimeout(resolve, maxWaitMs))
12316
- ]);
12317
- await Promise.race([
12318
- document.fonts.ready.catch(() => void 0).then(() => void 0),
12319
- new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
12320
- ]);
12321
- await new Promise(
12322
- (resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
12323
- );
12324
- }
12325
- const PREVIEW_DEBUG_PREFIX = "[canvas-renderer][preview-debug]";
12326
- function countUnderlinedNodes(config) {
12327
- var _a;
12328
- if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return 0;
12329
- let count = 0;
12330
- const walk = (nodes) => {
12331
- var _a2;
12332
- for (const node of nodes || []) {
12333
- if (node == null ? void 0 : node.underline) count += 1;
12334
- if ((_a2 = node == null ? void 0 : node.children) == null ? void 0 : _a2.length) walk(node.children);
12335
- }
12336
- };
12337
- for (const page of config.pages) walk(page.children || []);
12338
- return count;
12339
- }
12340
- function PixldocsPreview(props) {
12341
- const {
12342
- pageIndex = 0,
12343
- zoom = 1,
12344
- absoluteZoom = false,
12345
- imageProxyUrl,
12346
- className,
12347
- style,
12348
- onDynamicFieldClick,
12349
- onReady,
12350
- onError,
12351
- // Default `false` so PageCanvas blocks textbox creation until the host
12352
- // browser actually has the @font-face rules registered. This matters for
12353
- // `overflowPolicy: 'auto-shrink'` text — `createText` runs the shrink
12354
- // loop synchronously at mount time using whatever font metrics Fabric
12355
- // can measure right then. If the real font hasn't loaded yet, Fabric
12356
- // falls back to the system font (typically narrower), the shrink loop
12357
- // decides "fits, no shrink needed", and when the real font finally
12358
- // loads the text overflows the box.
12359
- //
12360
- // The renderer's imperative PNG/PDF paths (`renderPageViaPreviewCanvas`,
12361
- // `captureSvgViaPreviewCanvas`) already pass `skipFontReadyWait: false`
12362
- // for this exact reason — that's why the downloaded PDF was correct
12363
- // while the on-screen preview wasn't.
12364
- skipFontReadyWait = false
12365
- } = props;
12366
- useEffect(() => {
12367
- setPackageApiUrl(imageProxyUrl);
12368
- }, [imageProxyUrl]);
12369
- const [resolvedConfig, setResolvedConfig] = useState(null);
12370
- const [isLoading, setIsLoading] = useState(false);
12371
- const [fontsReady, setFontsReady] = useState(false);
12372
- const [fontsReadyVersion, setFontsReadyVersion] = useState(0);
12373
- const [canvasSettled, setCanvasSettled] = useState(false);
12374
- const [stabilizationPass, setStabilizationPass] = useState(0);
12375
- const isResolveMode = !("config" in props && props.config);
12376
- useEffect(() => {
12377
- if (!isResolveMode) {
12378
- setResolvedConfig(null);
12379
- setCanvasSettled(false);
12380
- console.log(PREVIEW_DEBUG_PREFIX, "config-mode active");
12381
- return;
12382
- }
12383
- const p = props;
12384
- if (!p.templateId || !p.formSchemaId || !p.supabaseUrl || !p.supabaseAnonKey) return;
12385
- let cancelled = false;
12386
- setIsLoading(true);
12387
- setFontsReady(false);
12388
- setCanvasSettled(false);
12389
- console.log(PREVIEW_DEBUG_PREFIX, "resolve-start", {
12390
- templateId: p.templateId,
12391
- formSchemaId: p.formSchemaId,
12392
- themeId: p.themeId ?? null,
12393
- pageIndex
12394
- });
12395
- resolveFromForm({
12396
- templateId: p.templateId,
12397
- formSchemaId: p.formSchemaId,
12398
- sectionState: p.sectionState,
12399
- themeId: p.themeId,
12400
- supabaseUrl: p.supabaseUrl,
12401
- supabaseAnonKey: p.supabaseAnonKey
12402
- }).then((resolved) => {
12403
- var _a, _b;
12404
- if (!cancelled) {
12405
- console.log(PREVIEW_DEBUG_PREFIX, "resolve-done", {
12406
- pages: ((_b = (_a = resolved.config) == null ? void 0 : _a.pages) == null ? void 0 : _b.length) ?? 0,
12407
- underlinedNodes: countUnderlinedNodes(resolved.config)
12408
- });
12409
- setResolvedConfig(resolved.config);
12410
- const hasAutoShrink = configHasAutoShrinkText$1(resolved.config);
12411
- const waitMs = hasAutoShrink ? 4e3 : 1800;
12412
- awaitFontsForConfig(resolved.config, waitMs).then(() => {
12413
- if (!cancelled) {
12414
- console.log(PREVIEW_DEBUG_PREFIX, "resolve-mode fonts settled", { hasAutoShrink, waitMs });
12415
- setFontsReady(true);
12416
- setIsLoading(false);
12417
- }
12418
- }).catch((err) => {
12419
- if (!cancelled) {
12420
- console.warn(PREVIEW_DEBUG_PREFIX, "resolve-mode font wait failed", err);
12421
- setFontsReady(true);
12422
- setIsLoading(false);
12423
- }
12424
- });
12425
- }
12426
- }).catch((err) => {
12427
- if (!cancelled) {
12428
- setIsLoading(false);
12429
- console.warn(PREVIEW_DEBUG_PREFIX, "resolve-error", err);
12430
- onError == null ? void 0 : onError(err instanceof Error ? err : new Error(String(err)));
12431
- }
12432
- });
12433
- return () => {
12434
- cancelled = true;
12435
- };
12436
- }, [
12437
- isResolveMode,
12438
- // For resolve mode, re-resolve when these change
12439
- isResolveMode ? props.templateId : void 0,
12440
- isResolveMode ? props.formSchemaId : void 0,
12441
- isResolveMode ? JSON.stringify(props.sectionState) : void 0,
12442
- isResolveMode ? props.themeId : void 0
12443
- ]);
12444
- const config = isResolveMode ? resolvedConfig : props.config;
12445
- useEffect(() => {
12446
- var _a, _b, _c;
12447
- if (!config) return;
12448
- let cancelled = false;
12449
- setCanvasSettled(false);
12450
- setStabilizationPass(0);
12451
- console.log(PREVIEW_DEBUG_PREFIX, "config-changed", {
12452
- pageIndex,
12453
- pages: ((_a = config.pages) == null ? void 0 : _a.length) ?? 0,
12454
- underlinedNodes: countUnderlinedNodes(config),
12455
- isResolveMode
12456
- });
12457
- const bump = () => {
12458
- if (cancelled) return;
12459
- clearMeasurementCache();
12460
- clearFabricCharCache();
12461
- setFontsReadyVersion((v) => {
12462
- const next = v + 1;
12463
- console.log(PREVIEW_DEBUG_PREFIX, "font-bump", { pageIndex, next, stabilizationPass });
12464
- return next;
12465
- });
12466
- };
12467
- (_c = (_b = document.fonts) == null ? void 0 : _b.ready) == null ? void 0 : _c.then(bump);
12468
- const timeoutId = window.setTimeout(bump, 350);
12469
- return () => {
12470
- cancelled = true;
12471
- window.clearTimeout(timeoutId);
12472
- };
12473
- }, [config]);
12474
- const previewKey = useMemo(
12475
- () => `${pageIndex}-${fontsReadyVersion}-${stabilizationPass}`,
12476
- [pageIndex, fontsReadyVersion, stabilizationPass]
12477
- );
12478
- useEffect(() => {
12479
- if (isResolveMode) return;
12480
- if (!config) {
12481
- setFontsReady(false);
12482
- setCanvasSettled(false);
12483
- setStabilizationPass(0);
12484
- return;
12485
- }
12486
- setFontsReady(false);
12487
- setCanvasSettled(false);
12488
- setStabilizationPass(0);
12489
- let cancelled = false;
12490
- const hasAutoShrink = configHasAutoShrinkText$1(config);
12491
- const waitMs = hasAutoShrink ? 4e3 : 1800;
12492
- awaitFontsForConfig(config, waitMs).then(() => {
12493
- if (cancelled) return;
12494
- console.log(PREVIEW_DEBUG_PREFIX, "config-mode fonts settled", {
12495
- pageIndex,
12496
- hasAutoShrink,
12497
- waitMs,
12498
- underlinedNodes: countUnderlinedNodes(config)
12499
- });
12500
- setFontsReady(true);
12501
- }).catch((err) => {
12502
- if (cancelled) return;
12503
- console.warn(PREVIEW_DEBUG_PREFIX, "config-mode font wait failed", err);
12504
- setFontsReady(true);
12505
- });
12506
- return () => {
12507
- cancelled = true;
12508
- };
12509
- }, [isResolveMode, config]);
12510
- const handleCanvasReady = useCallback(() => {
12511
- if (stabilizationPass === 0) {
12512
- console.log(PREVIEW_DEBUG_PREFIX, "canvas-ready-pass", { pageIndex, stabilizationPass, action: "stabilize-again" });
12513
- setCanvasSettled(false);
12514
- setStabilizationPass(1);
12515
- return;
12516
- }
12517
- console.log(PREVIEW_DEBUG_PREFIX, "canvas-ready-pass", { pageIndex, stabilizationPass, action: "settled" });
12518
- setCanvasSettled(true);
12519
- onReady == null ? void 0 : onReady();
12520
- }, [onReady, pageIndex, stabilizationPass]);
12521
- if (isLoading) {
12522
- return /* @__PURE__ */ jsx("div", { className, style: { ...style, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) });
12523
- }
12524
- if (!config) return null;
12525
- if (!fontsReady) {
12526
- return /* @__PURE__ */ jsx("div", { className, style: { ...style, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) });
12527
- }
12528
- return /* @__PURE__ */ jsxs("div", { className, style: { ...style, position: "relative" }, children: [
12529
- /* @__PURE__ */ jsx("div", { style: { visibility: canvasSettled ? "visible" : "hidden" }, children: /* @__PURE__ */ jsx(
12530
- PreviewCanvas,
12531
- {
12532
- config,
12533
- pageIndex,
12534
- zoom,
12535
- absoluteZoom,
12536
- skipFontReadyWait,
12537
- onDynamicFieldClick,
12538
- onReady: handleCanvasReady
12539
- },
12540
- previewKey
12541
- ) }),
12542
- !canvasSettled && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) })
12543
- ] });
12544
- }
12545
- const PACKAGE_VERSION = "0.5.67";
12546
- let __underlineFixInstalled = false;
12547
- function installUnderlineFix(fab) {
12548
- var _a;
12549
- if (__underlineFixInstalled) return;
12550
- const TextProto = (_a = fab.Text) == null ? void 0 : _a.prototype;
12551
- if (!TextProto || typeof TextProto._renderTextDecoration !== "function") return;
12552
- const original = TextProto._renderTextDecoration;
12553
- const measureLineTextWidth = (obj, ctx, lineIndex) => {
12554
- var _a2, _b, _c, _d, _e, _f;
12555
- const rawLine = (_a2 = obj._textLines) == null ? void 0 : _a2[lineIndex];
12556
- const lineText = Array.isArray(rawLine) ? rawLine.join("") : String(rawLine ?? "");
12557
- if (!lineText) return 0;
12558
- const fontSize = Number(((_b = obj.getValueOfPropertyAt) == null ? void 0 : _b.call(obj, lineIndex, 0, "fontSize")) ?? obj.fontSize ?? 0);
12559
- const fontStyle = String(((_c = obj.getValueOfPropertyAt) == null ? void 0 : _c.call(obj, lineIndex, 0, "fontStyle")) ?? obj.fontStyle ?? "normal");
12560
- const fontWeight = String(((_d = obj.getValueOfPropertyAt) == null ? void 0 : _d.call(obj, lineIndex, 0, "fontWeight")) ?? obj.fontWeight ?? "400");
12561
- const fontFamily = String(((_e = obj.getValueOfPropertyAt) == null ? void 0 : _e.call(obj, lineIndex, 0, "fontFamily")) ?? obj.fontFamily ?? "sans-serif");
12562
- const charSpacing = Number(((_f = obj.getValueOfPropertyAt) == null ? void 0 : _f.call(obj, lineIndex, 0, "charSpacing")) ?? obj.charSpacing ?? 0);
12563
- ctx.save();
12564
- ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px ${fontFamily}`;
12565
- const measured = ctx.measureText(lineText).width;
12566
- ctx.restore();
12567
- const graphemeCount = Array.from(lineText).length;
12568
- const spacingWidth = graphemeCount > 1 ? charSpacing / 1e3 * fontSize * (graphemeCount - 1) : 0;
12569
- return Math.max(0, measured + spacingWidth);
12570
- };
12571
- TextProto._renderTextDecoration = function patchedRenderTextDecoration(ctx, type) {
12572
- try {
12573
- const hasOwn = !!this[type];
12574
- const hasStyled = typeof this.styleHas === "function" && this.styleHas(type);
12575
- if (!hasOwn && !hasStyled) return;
12576
- const lines = this._textLines;
12577
- const offsets = this.offsets;
12578
- if (!Array.isArray(lines) || !offsets) {
12579
- return original.call(this, ctx, type);
12580
- }
12581
- const offsetY = offsets[type];
12582
- const offsetAligner = type === "linethrough" ? 0.5 : type === "overline" ? 1 : 0;
12583
- const leftOffset = this._getLeftOffset();
12584
- let topOffset = this._getTopOffset();
12585
- for (let i = 0, len = lines.length; i < len; i++) {
12586
- const heightOfLine = this.getHeightOfLine(i);
12587
- const lineHas = !!this[type] || typeof this.styleHas === "function" && this.styleHas(type, i);
12588
- if (!lineHas) {
12589
- topOffset += heightOfLine;
12590
- continue;
12591
- }
12592
- const fillStyle = this.getValueOfPropertyAt(i, 0, "fill");
12593
- const thickness = this.getValueOfPropertyAt(i, 0, "textDecorationThickness");
12594
- const charSize = this.getHeightOfChar(i, 0);
12595
- const dy = this.getValueOfPropertyAt(i, 0, "deltaY") || 0;
12596
- const finalThickness = this.fontSize * (thickness || 0) / 1e3;
12597
- if (!fillStyle || !finalThickness) {
12598
- topOffset += heightOfLine;
12599
- continue;
12600
- }
12601
- const lineWidth = measureLineTextWidth(this, ctx, i);
12602
- if (!lineWidth) {
12603
- topOffset += heightOfLine;
12604
- continue;
12605
- }
12606
- const availableWidth = Number(this.width ?? lineWidth);
12607
- let lineLeftOffset = 0;
12608
- const align = String(this.textAlign ?? "left");
12609
- if (align === "center") lineLeftOffset = (availableWidth - lineWidth) / 2;
12610
- else if (align === "right" || align === "end") lineLeftOffset = availableWidth - lineWidth;
12611
- let drawStart = leftOffset + lineLeftOffset;
12612
- if (this.direction === "rtl") {
12613
- drawStart = this.width - drawStart - lineWidth;
12614
- }
12615
- const maxHeight = heightOfLine / this.lineHeight;
12616
- const top = topOffset + maxHeight * (1 - this._fontSizeFraction);
12617
- ctx.fillStyle = fillStyle;
12618
- ctx.fillRect(
12619
- drawStart,
12620
- top + offsetY * charSize + dy - offsetAligner * finalThickness,
12621
- lineWidth,
12622
- finalThickness
12623
- );
12624
- topOffset += heightOfLine;
12625
- }
12626
- if (typeof this._removeShadow === "function") {
12627
- try {
12628
- this._removeShadow(ctx);
12629
- } catch {
12630
- }
12631
- }
12632
- } catch {
12633
- try {
12634
- return original.call(this, ctx, type);
12635
- } catch {
12636
- }
12637
- }
12638
- };
12639
- __underlineFixInstalled = true;
12640
- console.log(`[canvas-renderer] underline-fix monkey patch installed (v${PACKAGE_VERSION})`);
12641
- }
12642
- function configHasAutoShrinkText(config) {
12643
- var _a;
12644
- if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return false;
12645
- const walk = (nodes) => {
12646
- for (const node of nodes || []) {
12647
- if (!node) continue;
12648
- if (node.type === "text" && node.overflowPolicy === "auto-shrink") return true;
12649
- if (Array.isArray(node.children) && node.children.length && walk(node.children)) return true;
12650
- }
12651
- return false;
12652
- };
12653
- for (const page of config.pages) {
12654
- if (walk(page.children || [])) return true;
12655
- }
12656
- return false;
12657
- }
12658
- class PixldocsRenderer {
12659
- constructor(config) {
12660
- __publicField(this, "config");
12661
- this.config = config;
12662
- installUnderlineFix(fabric);
12663
- try {
12664
- console.log(`[canvas-renderer] PixldocsRenderer v${PACKAGE_VERSION} initialized`);
12665
- } catch {
12666
- }
12667
- }
12668
- /**
12669
- * Render a pre-resolved template config to an image using the full PageCanvas engine.
12670
- * Mounts a hidden PreviewCanvas component and captures the Fabric canvas output.
12671
- */
12672
- async render(templateConfig, options = {}) {
12673
- const pageIndex = options.pageIndex ?? 0;
12674
- const format = options.format ?? "png";
12675
- const quality = options.quality ?? 0.92;
12676
- const pixelRatio = options.pixelRatio ?? this.config.pixelRatio ?? 2;
12677
- const canvasWidth = templateConfig.canvas.width;
12678
- const canvasHeight = templateConfig.canvas.height;
12679
- const page = templateConfig.pages[pageIndex];
12680
- if (!page) {
12681
- throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12682
- }
12683
- await ensureFontsForResolvedConfig(templateConfig);
12684
- if (!options.skipFontReadyWait) {
12685
- const hasAutoShrink = configHasAutoShrinkText(templateConfig);
12686
- const defaultWait = hasAutoShrink ? 4e3 : 1800;
12687
- await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
12688
- }
12689
- const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12690
- setPackageApiUrl2(this.config.imageProxyUrl);
12691
- const dataUrl = await this.renderPageViaPreviewCanvas(
12692
- templateConfig,
12693
- pageIndex,
12694
- pixelRatio,
12695
- format,
12696
- quality,
12697
- { skipFontReadyWait: options.skipFontReadyWait, waitForFontsMs: options.waitForFontsMs }
12698
- );
12699
- return {
12700
- dataUrl,
12701
- width: canvasWidth,
12702
- height: canvasHeight,
12703
- pixelWidth: canvasWidth * pixelRatio,
12704
- pixelHeight: canvasHeight * pixelRatio
12705
- };
12706
- }
12707
- /**
12708
- * Render all pages and return array of results.
12709
- */
12710
- async renderAllPages(templateConfig, options = {}) {
12711
- if (!options.skipFontReadyWait) {
12712
- const hasAutoShrink = configHasAutoShrinkText(templateConfig);
12713
- const defaultWait = hasAutoShrink ? 4e3 : 1800;
12714
- await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
12715
- }
12716
- const results = [];
12717
- for (let i = 0; i < templateConfig.pages.length; i++) {
12718
- results.push(await this.render(templateConfig, { ...options, pageIndex: i, skipFontReadyWait: true }));
12719
- }
12720
- return results;
12721
- }
12722
- /**
12723
- * Resolve from V2 sectionState (like the server API) and render all pages.
12724
- * This is the primary external API for the package.
12725
- */
12726
- async renderFromForm(options) {
12727
- const { templateId, formSchemaId, sectionState, themeId, watermark, watermarkOptions, prefetched, ...renderOpts } = options;
12728
- const resolved = await resolveFromForm({
12729
- templateId,
12730
- formSchemaId,
12731
- sectionState,
12732
- themeId,
12733
- supabaseUrl: this.config.supabaseUrl,
12734
- supabaseAnonKey: this.config.supabaseAnonKey,
12735
- prefetched
12736
- });
12737
- const shouldWatermark = watermark ?? resolved.price > 0;
12738
- let configToRender = resolved.config;
12739
- if (shouldWatermark) {
12740
- const { injectWatermark } = await import("./canvasWatermark-pkhacGge.js");
12741
- configToRender = injectWatermark(configToRender, watermarkOptions);
12742
- }
12743
- return this.renderAllPages(configToRender, renderOpts);
12744
- }
12745
- /**
12746
- * Render a page and capture the Fabric canvas SVG output (vector, not raster).
12747
- * This is the key building block for client-side vector PDF export.
12748
- */
12749
- async renderPageSvg(templateConfig, pageIndex = 0) {
12750
- const page = templateConfig.pages[pageIndex];
12751
- if (!page) {
12752
- throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12753
- }
12754
- await ensureFontsForResolvedConfig(templateConfig);
12755
- const hasAutoShrinkSvg = configHasAutoShrinkText(templateConfig);
12756
- await this.awaitFontsForConfig(templateConfig, hasAutoShrinkSvg ? 4e3 : 1800);
12757
- const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12758
- setPackageApiUrl2(this.config.imageProxyUrl);
12759
- const canvasWidth = templateConfig.canvas.width;
12760
- const canvasHeight = templateConfig.canvas.height;
12761
- return this.captureSvgViaPreviewCanvas(templateConfig, pageIndex, canvasWidth, canvasHeight);
12762
- }
12763
- /**
12764
- * Render all pages and return SVG strings for each.
12765
- */
12766
- async renderAllPageSvgs(templateConfig) {
12767
- await ensureFontsForResolvedConfig(templateConfig);
12768
- const hasAutoShrinkSvg = configHasAutoShrinkText(templateConfig);
12769
- await this.awaitFontsForConfig(templateConfig, hasAutoShrinkSvg ? 4e3 : 1800);
12770
- const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12771
- setPackageApiUrl2(this.config.imageProxyUrl);
12772
- const results = [];
12773
- for (let i = 0; i < templateConfig.pages.length; i++) {
12774
- const canvasWidth = templateConfig.canvas.width;
12775
- const canvasHeight = templateConfig.canvas.height;
12776
- results.push(await this.captureSvgViaPreviewCanvas(templateConfig, i, canvasWidth, canvasHeight));
12777
- }
12778
- return results;
12779
- }
12780
- /**
12781
- * Resolve from V2 sectionState and return SVGs for all pages (for server vector PDF).
12782
- */
12783
- async renderSvgsFromForm(options) {
12784
- const { templateId, formSchemaId, sectionState, themeId, watermark, watermarkOptions, prefetched } = options;
12785
- const resolved = await resolveFromForm({
12786
- templateId,
12787
- formSchemaId,
12788
- sectionState,
12789
- themeId,
12790
- supabaseUrl: this.config.supabaseUrl,
12791
- supabaseAnonKey: this.config.supabaseAnonKey,
12792
- prefetched
12793
- });
12794
- const shouldWatermark = watermark ?? resolved.price > 0;
12795
- let configToRender = resolved.config;
12796
- if (shouldWatermark) {
12797
- const { injectWatermark } = await import("./canvasWatermark-pkhacGge.js");
12798
- configToRender = injectWatermark(configToRender, watermarkOptions);
12799
- }
12800
- return this.renderAllPageSvgs(configToRender);
12801
- }
12802
- /**
12803
- * Render a pre-resolved template config to a vector PDF.
12804
- * Returns a Blob and ArrayBuffer.
12805
- */
12806
- async renderPdf(templateConfig, options) {
12807
- const svgs = await this.renderAllPageSvgs(templateConfig);
12808
- const { assemblePdfFromSvgs: assemblePdfFromSvgs2 } = await Promise.resolve().then(() => pdfExport);
12809
- return assemblePdfFromSvgs2(svgs, { title: options == null ? void 0 : options.title, fontBaseUrl: options == null ? void 0 : options.fontBaseUrl });
12810
- }
12811
- /**
12812
- * Resolve from V2 sectionState and render a vector PDF.
12813
- * This is the primary PDF export API — mirrors renderFromForm() but returns a PDF.
12814
- */
12815
- async renderPdfFromForm(options) {
12816
- const { templateId, formSchemaId, sectionState, themeId, watermark, watermarkOptions, prefetched, title, fontBaseUrl } = options;
12817
- const resolved = await resolveFromForm({
12818
- templateId,
12819
- formSchemaId,
12820
- sectionState,
12821
- themeId,
12822
- supabaseUrl: this.config.supabaseUrl,
12823
- supabaseAnonKey: this.config.supabaseAnonKey,
12824
- prefetched
12825
- });
12826
- const shouldWatermark = watermark ?? resolved.price > 0;
12827
- let configToRender = resolved.config;
12828
- if (shouldWatermark) {
12829
- const { injectWatermark } = await import("./canvasWatermark-pkhacGge.js");
12830
- configToRender = injectWatermark(configToRender, watermarkOptions);
12831
- }
12832
- const svgs = await this.renderAllPageSvgs(configToRender);
12833
- const { assemblePdfFromSvgs: assemblePdfFromSvgs2 } = await Promise.resolve().then(() => pdfExport);
12834
- return assemblePdfFromSvgs2(svgs, { title: title ?? resolved.config.name, fontBaseUrl });
12835
- }
12836
- async renderById(templateId, formData, options) {
12837
- const resolved = await resolveTemplateData({
12838
- templateId,
12839
- formData,
12840
- supabaseUrl: this.config.supabaseUrl,
12841
- supabaseAnonKey: this.config.supabaseAnonKey
12842
- });
12843
- return this.render(resolved.config, options);
12844
- }
12845
- /**
12846
- * Convenience: fetch by ID with flat data and render ALL pages.
12847
- */
12848
- async renderAllById(templateId, formData, options) {
12849
- const resolved = await resolveTemplateData({
12850
- templateId,
12851
- formData,
12852
- supabaseUrl: this.config.supabaseUrl,
12853
- supabaseAnonKey: this.config.supabaseAnonKey
12854
- });
12855
- return this.renderAllPages(resolved.config, options);
12856
- }
12857
- // ─── Internal: render a page using the full PreviewCanvas engine ───
12858
- getExpectedImageCount(config, pageIndex) {
12859
- const page = config.pages[pageIndex];
12860
- if (!(page == null ? void 0 : page.children)) return 0;
12861
- let count = 0;
12862
- const walk = (nodes) => {
12863
- for (const node of nodes) {
12864
- if (!node || node.visible === false) continue;
12865
- const src = typeof node.src === "string" ? node.src.trim() : "";
12866
- const imageUrl = typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
12867
- if (node.type === "image" && (src || imageUrl)) count += 1;
12868
- if (Array.isArray(node.children) && node.children.length > 0) {
12869
- walk(node.children);
12870
- }
12871
- }
12872
- };
12873
- walk(page.children);
12874
- return count;
12875
- }
12876
- waitForCanvasImages(container, expectedImageCount, maxWaitMs = 15e3, pollMs = 120) {
12877
- return new Promise((resolve) => {
12878
- const start = Date.now();
12879
- let stableFrames = 0;
12880
- let lastSummary = "";
12881
- const isRenderableImage = (value) => value instanceof HTMLImageElement && value.complete && value.naturalWidth > 0 && value.naturalHeight > 0;
12882
- const collectRenderableImages = (obj, seen) => {
12883
- if (!obj || typeof obj !== "object") return;
12884
- const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
12885
- for (const candidate of candidates) {
12886
- if (isRenderableImage(candidate)) {
12887
- seen.add(candidate);
12888
- } else if (candidate instanceof HTMLImageElement) {
12889
- return false;
12890
- }
12891
- }
12892
- const nested = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
12893
- for (const child of nested) {
12894
- if (collectRenderableImages(child, seen) === false) return false;
12895
- }
12896
- return true;
12897
- };
12898
- const getFabricCanvas = () => {
12899
- const registry2 = window.__fabricCanvasRegistry;
12900
- if (registry2 instanceof Map) {
12901
- for (const entry of registry2.values()) {
12902
- const canvas = entry == null ? void 0 : entry.canvas;
12903
- const el = (canvas == null ? void 0 : canvas.lowerCanvasEl) || (canvas == null ? void 0 : canvas.upperCanvasEl);
12904
- if (el && container.contains(el)) return canvas;
12905
- }
12906
- }
12907
- return null;
12908
- };
12909
- const settle = () => requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
12910
- const getImageDebugInfo = (obj, bucket) => {
12911
- if (!obj || typeof obj !== "object") return;
12912
- const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
12913
- for (const candidate of candidates) {
12914
- if (candidate instanceof HTMLImageElement) {
12915
- bucket.push({
12916
- id: obj.__docuforgeId || obj.id || "unknown",
12917
- src: (candidate.currentSrc || candidate.src || "").slice(0, 240),
12918
- complete: candidate.complete,
12919
- naturalWidth: candidate.naturalWidth,
12920
- naturalHeight: candidate.naturalHeight
12921
- });
12922
- }
12923
- }
12924
- const nested = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
12925
- for (const child of nested) {
12926
- getImageDebugInfo(child, bucket);
12927
- }
12928
- };
12929
- const check = () => {
12930
- const elapsed = Date.now() - start;
12931
- const domImages = Array.from(container.querySelectorAll("img"));
12932
- const allDomLoaded = domImages.every((img) => img.complete && img.naturalWidth > 0 && img.naturalHeight > 0);
12933
- const fabricCanvas = getFabricCanvas();
12934
- const fabricObjects = fabricCanvas && typeof fabricCanvas.getObjects === "function" ? fabricCanvas.getObjects() : [];
12935
- const renderableImages = /* @__PURE__ */ new Set();
12936
- const fabricReady = fabricObjects.every((obj) => collectRenderableImages(obj, renderableImages) !== false);
12937
- const actualImageCount = Math.max(domImages.length, renderableImages.size);
12938
- const canvasReady = !!fabricCanvas && !!(fabricCanvas.lowerCanvasEl || fabricCanvas.upperCanvasEl);
12939
- const hasExpectedAssets = expectedImageCount === 0 ? true : actualImageCount >= expectedImageCount;
12940
- const ready = allDomLoaded && fabricReady && hasExpectedAssets;
12941
- const summary = `expected=${expectedImageCount} actual=${actualImageCount} dom=${domImages.length} fabricReady=${fabricReady} domReady=${allDomLoaded} canvasReady=${canvasReady}`;
12942
- if (summary !== lastSummary) {
12943
- lastSummary = summary;
12944
- console.log(`[canvas-renderer][asset-wait] ${summary}`);
12945
- }
12946
- if (ready) {
12947
- stableFrames += 1;
12948
- if (stableFrames >= 2) {
12949
- console.log(`[canvas-renderer][asset-wait] ready after ${elapsed}ms (${summary})`);
12950
- settle();
12951
- return;
12952
- }
12953
- } else {
12954
- stableFrames = 0;
12955
- }
12956
- if (elapsed >= maxWaitMs) {
12957
- const fabricImageDebug = [];
12958
- for (const obj of fabricObjects) {
12959
- getImageDebugInfo(obj, fabricImageDebug);
12960
- }
12961
- const domImageDebug = domImages.map((img, index) => ({
12962
- index,
12963
- src: (img.currentSrc || img.src || "").slice(0, 240),
12964
- complete: img.complete,
12965
- naturalWidth: img.naturalWidth,
12966
- naturalHeight: img.naturalHeight
12967
- }));
12968
- console.warn(`[canvas-renderer][asset-wait-timeout] elapsed=${elapsed}ms ${summary}`);
12969
- console.warn("[canvas-renderer][asset-wait-timeout][dom-images]", domImageDebug);
12970
- console.warn("[canvas-renderer][asset-wait-timeout][fabric-images]", fabricImageDebug);
12971
- settle();
12972
- return;
12973
- }
12974
- setTimeout(check, pollMs);
12975
- };
12976
- setTimeout(check, 0);
12977
- });
12978
- }
12979
- waitForCanvasScene(container, config, pageIndex, maxWaitMs = 8e3, pollMs = 50) {
12980
- return new Promise((resolve) => {
12981
- var _a, _b;
12982
- const start = Date.now();
12983
- const pageHasContent = (((_b = (_a = config.pages[pageIndex]) == null ? void 0 : _a.children) == null ? void 0 : _b.length) ?? 0) > 0;
12984
- const settle = () => requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
12985
- const check = () => {
12986
- const fabricCanvas = this.getFabricCanvasFromContainer(container);
12987
- const lowerCanvas = (fabricCanvas == null ? void 0 : fabricCanvas.lowerCanvasEl) || container.querySelector("canvas.lower-canvas, canvas");
12988
- const objectCount = typeof (fabricCanvas == null ? void 0 : fabricCanvas.getObjects) === "function" ? fabricCanvas.getObjects().length : 0;
12989
- const ready = !!lowerCanvas && (!pageHasContent || objectCount > 0);
12990
- if (ready) {
12991
- console.log(`[canvas-renderer][scene-wait] ready after ${Date.now() - start}ms (objects=${objectCount})`);
12992
- settle();
12993
- return;
12994
- }
12995
- if (Date.now() - start >= maxWaitMs) {
12996
- console.warn(`[canvas-renderer][scene-wait-timeout] elapsed=${Date.now() - start}ms objects=${objectCount} pageHasContent=${pageHasContent}`);
12997
- settle();
12998
- return;
12999
- }
13000
- setTimeout(check, pollMs);
13001
- };
13002
- setTimeout(check, 0);
13003
- });
13004
- }
13005
- async waitForRelevantFonts(config, maxWaitMs = 1800) {
13006
- if (typeof document === "undefined" || !document.fonts) return;
13007
- const descriptors = collectFontDescriptorsFromConfig(config);
13008
- if (descriptors.length === 0) return;
13009
- const loads = Promise.all(
13010
- descriptors.map((descriptor) => {
13011
- const stylePrefix = descriptor.style === "italic" ? "italic " : "";
13012
- const spec = `${stylePrefix}${descriptor.weight} 16px "${descriptor.family}"`;
13013
- return document.fonts.load(spec).catch(() => []);
13014
- })
13015
- ).then(() => void 0);
13016
- await Promise.race([
13017
- loads,
13018
- new Promise((resolve) => setTimeout(resolve, maxWaitMs))
13019
- ]);
13020
- await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
13021
- }
13022
- /**
13023
- * Block until the webfonts referenced by `config` have actually loaded
13024
- * (or `maxWaitMs` elapses). Used by the headless capture path BEFORE
13025
- * mounting `PreviewCanvas`, so the synchronous `createText` auto-shrink
13026
- * loop measures against final font metrics instead of fallback ones.
13027
- *
13028
- * Stronger than `ensureFontsForResolvedConfig` (which is fire-and-forget)
13029
- * — this awaits each `document.fonts.load(spec)` AND `document.fonts.ready`,
13030
- * racing the whole thing against `maxWaitMs` so a slow CDN can't hang the
13031
- * renderer.
13032
- */
13033
- async awaitFontsForConfig(config, maxWaitMs) {
13034
- if (typeof document === "undefined" || !document.fonts) return;
13035
- void ensureFontsForResolvedConfig(config);
13036
- await this.waitForRelevantFonts(config, maxWaitMs);
13037
- await Promise.race([
13038
- document.fonts.ready.catch(() => void 0).then(() => void 0),
13039
- new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
13040
- ]);
13041
- }
13042
- getNormalizedGradientStops(gradient) {
13043
- const stops = Array.isArray(gradient == null ? void 0 : gradient.stops) ? gradient.stops.map((stop) => ({
13044
- offset: Math.max(0, Math.min(1, Number((stop == null ? void 0 : stop.offset) ?? 0))),
13045
- color: String((stop == null ? void 0 : stop.color) ?? "#ffffff")
13046
- })).filter((stop) => Number.isFinite(stop.offset)).sort((a, b) => a.offset - b.offset) : [];
13047
- if (stops.length === 0) return [];
13048
- const normalized = [...stops];
13049
- if (normalized[0].offset > 0) {
13050
- normalized.unshift({ offset: 0, color: normalized[0].color });
13051
- }
13052
- if (normalized[normalized.length - 1].offset < 1) {
13053
- normalized.push({ offset: 1, color: normalized[normalized.length - 1].color });
13054
- }
13055
- return normalized;
13056
- }
13057
- paintPageBackground(ctx, page, width, height) {
13058
- var _a, _b;
13059
- const backgroundColor = ((_a = page == null ? void 0 : page.settings) == null ? void 0 : _a.backgroundColor) || "#ffffff";
13060
- const gradient = (_b = page == null ? void 0 : page.settings) == null ? void 0 : _b.backgroundGradient;
13061
- ctx.clearRect(0, 0, width, height);
13062
- ctx.fillStyle = backgroundColor;
13063
- ctx.fillRect(0, 0, width, height);
13064
- const stops = this.getNormalizedGradientStops(gradient);
13065
- if (stops.length < 2) return;
13066
- try {
13067
- let canvasGradient = null;
13068
- if ((gradient == null ? void 0 : gradient.type) === "radial") {
13069
- const cx = Number.isFinite(gradient == null ? void 0 : gradient.cx) ? gradient.cx : 0.5;
13070
- const cy = Number.isFinite(gradient == null ? void 0 : gradient.cy) ? gradient.cy : 0.5;
13071
- const centerX = width * cx;
13072
- const centerY = height * cy;
13073
- const radius = Math.max(
13074
- Math.hypot(centerX, centerY),
13075
- Math.hypot(width - centerX, centerY),
13076
- Math.hypot(centerX, height - centerY),
13077
- Math.hypot(width - centerX, height - centerY)
13078
- );
13079
- canvasGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
13080
- } else if ((gradient == null ? void 0 : gradient.type) === "conic" && typeof ctx.createConicGradient === "function") {
13081
- const cx = Number.isFinite(gradient == null ? void 0 : gradient.cx) ? gradient.cx : 0.5;
13082
- const cy = Number.isFinite(gradient == null ? void 0 : gradient.cy) ? gradient.cy : 0.5;
13083
- const startAngle = (((gradient == null ? void 0 : gradient.angle) ?? 0) - 90) * Math.PI / 180;
13084
- canvasGradient = ctx.createConicGradient(startAngle, width * cx, height * cy);
13085
- } else {
13086
- const angleDeg = (gradient == null ? void 0 : gradient.angle) ?? 90;
13087
- const angleRad = angleDeg * Math.PI / 180;
13088
- const sinA = Math.sin(angleRad);
13089
- const cosA = Math.cos(angleRad);
13090
- const midX = width / 2;
13091
- const midY = height / 2;
13092
- const corners = [
13093
- [0, 0],
13094
- [width, 0],
13095
- [width, height],
13096
- [0, height]
13097
- ];
13098
- const projections = corners.map(([x, y]) => x * sinA - y * cosA);
13099
- const minProjection = Math.min(...projections);
13100
- const maxProjection = Math.max(...projections);
13101
- canvasGradient = ctx.createLinearGradient(
13102
- midX + minProjection * sinA,
13103
- midY - minProjection * cosA,
13104
- midX + maxProjection * sinA,
13105
- midY - maxProjection * cosA
13106
- );
13107
- }
13108
- if (!canvasGradient) return;
13109
- stops.forEach((stop) => canvasGradient.addColorStop(stop.offset, stop.color));
13110
- ctx.fillStyle = canvasGradient;
13111
- ctx.fillRect(0, 0, width, height);
13112
- } catch {
13113
- }
13114
- }
13115
- async renderPageViaPreviewCanvas(config, pageIndex, pixelRatio, format, quality, options = {}) {
13116
- const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
13117
- const canvasWidth = config.canvas.width;
13118
- const canvasHeight = config.canvas.height;
13119
- const hasAutoShrink = configHasAutoShrinkText(config);
13120
- let firstMountSettled = false;
13121
- let lateFontSettleDetected = false;
13122
- if (typeof document !== "undefined" && document.fonts && hasAutoShrink) {
13123
- document.fonts.ready.then(() => {
13124
- if (firstMountSettled) lateFontSettleDetected = true;
13125
- }).catch(() => {
13126
- });
13127
- }
13128
- return new Promise((resolve, reject) => {
13129
- const container = document.createElement("div");
13130
- container.style.cssText = `
13131
- position: fixed; left: -99999px; top: -99999px;
13132
- width: ${canvasWidth}px; height: ${canvasHeight}px;
13133
- overflow: hidden; pointer-events: none; opacity: 0;
13134
- `;
13135
- document.body.appendChild(container);
13136
- const timeout = setTimeout(() => {
13137
- cleanup();
13138
- reject(new Error("Render timeout (30s)"));
13139
- }, 3e4);
13140
- let root;
13141
- let mountKey = 0;
13142
- const cleanup = () => {
13143
- clearTimeout(timeout);
13144
- try {
13145
- root.unmount();
13146
- } catch {
13147
- }
13148
- container.remove();
13149
- };
13150
- const remountWithFreshKey = async () => {
13151
- mountKey += 1;
13152
- try {
13153
- clearMeasurementCache();
13154
- } catch {
13155
- }
13156
- try {
13157
- clearFabricCharCache();
13158
- } catch {
13159
- }
13160
- try {
13161
- root.unmount();
13162
- } catch {
13163
- }
13164
- root = createRoot(container);
13165
- await new Promise((settle) => {
13166
- const onReadyOnce = () => {
13167
- this.waitForCanvasScene(container, config, pageIndex).then(async () => {
13168
- const fabricInstance = this.getFabricCanvasFromContainer(container);
13169
- const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
13170
- await this.waitForCanvasImages(container, expectedImageCount);
13171
- await this.waitForStableTextMetrics(container, config);
13172
- await this.waitForCanvasScene(container, config, pageIndex);
13173
- if (!fabricInstance) return settle();
13174
- settle();
13175
- }).catch(() => settle());
13176
- };
13177
- root.render(
13178
- createElement(PreviewCanvas2, {
13179
- key: `remount-${mountKey}`,
13180
- config,
13181
- pageIndex,
13182
- zoom: pixelRatio,
13183
- absoluteZoom: true,
13184
- skipFontReadyWait: false,
13185
- onReady: onReadyOnce
13186
- })
13187
- );
13188
- });
13189
- };
13190
- const onReady = () => {
13191
- this.waitForCanvasScene(container, config, pageIndex).then(async () => {
13192
- try {
13193
- const fabricInstance = this.getFabricCanvasFromContainer(container);
13194
- const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
13195
- await this.waitForCanvasImages(container, expectedImageCount);
13196
- await this.waitForStableTextMetrics(container, config);
13197
- await this.waitForCanvasScene(container, config, pageIndex);
13198
- firstMountSettled = true;
13199
- if (hasAutoShrink && lateFontSettleDetected) {
13200
- console.log("[canvas-renderer][parity] late font-settle detected — remounting for auto-shrink reflow");
13201
- await remountWithFreshKey();
13202
- }
13203
- const fabricCanvas = container.querySelector("canvas.upper-canvas, canvas");
13204
- const sourceCanvas = (fabricInstance == null ? void 0 : fabricInstance.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || fabricCanvas;
13205
- const fabricInstanceAfter = this.getFabricCanvasFromContainer(container) || fabricInstance;
13206
- const sourceCanvasAfter = (fabricInstanceAfter == null ? void 0 : fabricInstanceAfter.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || sourceCanvas;
13207
- if (!sourceCanvas) {
13208
- cleanup();
13209
- reject(new Error("No canvas element found after render"));
13210
- return;
13211
- }
13212
- const exportCanvas = document.createElement("canvas");
13213
- exportCanvas.width = sourceCanvasAfter.width;
13214
- exportCanvas.height = sourceCanvasAfter.height;
13215
- const exportCtx = exportCanvas.getContext("2d");
13216
- if (!exportCtx) {
13217
- cleanup();
13218
- reject(new Error("Failed to create export canvas"));
13219
- return;
13220
- }
13221
- exportCtx.save();
13222
- exportCtx.scale(sourceCanvasAfter.width / canvasWidth, sourceCanvasAfter.height / canvasHeight);
13223
- this.paintPageBackground(exportCtx, config.pages[pageIndex], canvasWidth, canvasHeight);
13224
- exportCtx.restore();
13225
- exportCtx.drawImage(sourceCanvasAfter, 0, 0);
13226
- const mimeType = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
13227
- const dataUrl = exportCanvas.toDataURL(mimeType, quality);
13228
- cleanup();
13229
- resolve(dataUrl);
13230
- } catch (err) {
13231
- cleanup();
13232
- reject(err);
13233
- }
13234
- });
13235
- };
13236
- root = createRoot(container);
13237
- root.render(
13238
- createElement(PreviewCanvas2, {
13239
- config,
13240
- pageIndex,
13241
- zoom: pixelRatio,
13242
- absoluteZoom: true,
13243
- skipFontReadyWait: false,
13244
- onReady
13245
- })
13246
- );
13247
- });
13248
- }
13249
- // ─── Internal: capture SVG from a rendered Fabric canvas ───
13250
- //
13251
- // APPROACH: Use the SAME PreviewCanvas that renders perfect PNGs, then call
13252
- // Fabric's toSVG() on that canvas. This guarantees 100% layout parity —
13253
- // the SVG is a vector snapshot of exactly what's on screen.
13254
- //
13255
- // The trick: before calling toSVG(), we temporarily neutralize the viewport
13256
- // transform and retina scaling so Fabric emits coordinates in logical
13257
- // document space (e.g. 612x792) instead of inflated pixel space.
13258
- captureSvgViaPreviewCanvas(config, pageIndex, canvasWidth, canvasHeight) {
13259
- return new Promise(async (resolve, reject) => {
13260
- const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
13261
- const hasAutoShrink = configHasAutoShrinkText(config);
13262
- const container = document.createElement("div");
13263
- container.style.cssText = `
13264
- position: fixed; left: -99999px; top: -99999px;
13265
- width: ${canvasWidth}px; height: ${canvasHeight}px;
13266
- overflow: hidden; pointer-events: none; opacity: 0;
13267
- `;
13268
- document.body.appendChild(container);
13269
- const timeout = setTimeout(() => {
13270
- cleanup();
13271
- reject(new Error("SVG render timeout (30s)"));
13272
- }, 3e4);
13273
- let root = null;
13274
- let mountKey = 0;
13275
- let didPreviewParityRemount = false;
13276
- const cleanup = () => {
13277
- clearTimeout(timeout);
13278
- try {
13279
- root == null ? void 0 : root.unmount();
13280
- } catch {
13281
- }
13282
- container.remove();
13283
- };
13284
- const mountPreview = (readyHandler) => {
13285
- root = createRoot(container);
13286
- root.render(
13287
- createElement(PreviewCanvas2, {
13288
- key: `svg-capture-${mountKey}`,
13289
- config,
13290
- pageIndex,
13291
- zoom: 1,
13292
- absoluteZoom: true,
13293
- skipFontReadyWait: false,
13294
- onReady: readyHandler
13295
- })
13296
- );
13297
- };
13298
- const remountForPreviewParity = async () => {
13299
- if (didPreviewParityRemount) return;
13300
- didPreviewParityRemount = true;
13301
- mountKey += 1;
13302
- try {
13303
- clearMeasurementCache();
13304
- } catch {
13305
- }
13306
- try {
13307
- clearFabricCharCache();
13308
- } catch {
13309
- }
13310
- try {
13311
- root == null ? void 0 : root.unmount();
13312
- } catch {
13313
- }
13314
- await new Promise((settle) => {
13315
- mountPreview(() => {
13316
- this.waitForCanvasScene(container, config, pageIndex).then(async () => {
13317
- const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
13318
- await this.waitForCanvasImages(container, expectedImageCount);
13319
- await this.waitForStableTextMetrics(container, config);
13320
- await this.waitForCanvasScene(container, config, pageIndex);
13321
- settle();
13322
- }).catch(() => settle());
13323
- });
13324
- });
13325
- };
13326
- const onReady = () => {
13327
- this.waitForCanvasScene(container, config, pageIndex).then(async () => {
13328
- var _a, _b;
13329
- try {
13330
- const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
13331
- await this.waitForCanvasImages(container, expectedImageCount);
13332
- await this.waitForStableTextMetrics(container, config);
13333
- await this.waitForCanvasScene(container, config, pageIndex);
13334
- if (hasAutoShrink && !didPreviewParityRemount) {
13335
- console.log("[canvas-renderer][svg-parity] remounting once to match PixldocsPreview auto-shrink stabilization");
13336
- await remountForPreviewParity();
13337
- }
13338
- const fabricInstance = this.getFabricCanvasFromContainer(container);
13339
- if (!fabricInstance) {
13340
- cleanup();
13341
- reject(new Error("No Fabric canvas instance found for SVG capture"));
13342
- return;
13343
- }
13344
- const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
13345
- const prevSvgVPT = fabricInstance.svgViewportTransformation;
13346
- const prevRetina = fabricInstance.enableRetinaScaling;
13347
- const prevWidth = fabricInstance.width;
13348
- const prevHeight = fabricInstance.height;
13349
- fabricInstance.viewportTransform = [1, 0, 0, 1, 0, 0];
13350
- fabricInstance.svgViewportTransformation = false;
13351
- fabricInstance.enableRetinaScaling = false;
13352
- fabricInstance.setDimensions(
13353
- { width: canvasWidth, height: canvasHeight },
13354
- { cssOnly: false, backstoreOnly: false }
13355
- );
13356
- const rawSvgString = fabricInstance.toSVG();
13357
- const svgString = this.normalizeSvgDimensions(
13358
- rawSvgString,
13359
- canvasWidth,
13360
- canvasHeight
13361
- );
13362
- fabricInstance.enableRetinaScaling = prevRetina;
13363
- fabricInstance.setDimensions(
13364
- { width: prevWidth, height: prevHeight },
13365
- { cssOnly: false, backstoreOnly: false }
13366
- );
13367
- if (prevVPT) fabricInstance.viewportTransform = prevVPT;
13368
- fabricInstance.svgViewportTransformation = prevSvgVPT;
13369
- const page = config.pages[pageIndex];
13370
- const backgroundColor = ((_a = page == null ? void 0 : page.settings) == null ? void 0 : _a.backgroundColor) || "#ffffff";
13371
- const backgroundGradient = (_b = page == null ? void 0 : page.settings) == null ? void 0 : _b.backgroundGradient;
13372
- cleanup();
13373
- resolve({
13374
- svg: svgString,
13375
- width: canvasWidth,
13376
- height: canvasHeight,
13377
- backgroundColor,
13378
- backgroundGradient
13379
- });
13380
- } catch (err) {
13381
- cleanup();
13382
- reject(err);
13383
- }
13384
- });
13385
- };
13386
- mountPreview(onReady);
13387
- });
13388
- }
13389
- /**
13390
- * Normalize the SVG's width/height/viewBox to match the logical page dimensions.
13391
- * Fabric's toSVG() may output dimensions scaled by the canvas element's actual
13392
- * pixel size (e.g., 2x due to devicePixelRatio), causing svg2pdf to render
13393
- * content at the wrong scale. This rewrites the root <svg> attributes to ensure
13394
- * the SVG coordinate system matches the intended page size exactly.
13395
- */
13396
- normalizeSvgDimensions(svg, targetWidth, targetHeight) {
13397
- const widthMatch = svg.match(/<svg[^>]*\bwidth="([^"]+)"/i);
13398
- const heightMatch = svg.match(/<svg[^>]*\bheight="([^"]+)"/i);
13399
- const svgWidth = widthMatch ? parseFloat(widthMatch[1]) : targetWidth;
13400
- const svgHeight = heightMatch ? parseFloat(heightMatch[1]) : targetHeight;
13401
- console.log(
13402
- `[canvas-renderer][svg-normalize] root ${svgWidth}x${svgHeight} → page ${targetWidth}x${targetHeight}`
13403
- );
13404
- let normalized = svg;
13405
- if (/\bwidth="[^"]*"/i.test(normalized)) {
13406
- normalized = normalized.replace(/(<svg[^>]*\b)width="[^"]*"/i, `$1width="${targetWidth}"`);
13407
- } else {
13408
- normalized = normalized.replace(/<svg\b/i, `<svg width="${targetWidth}"`);
13409
- }
13410
- if (/\bheight="[^"]*"/i.test(normalized)) {
13411
- normalized = normalized.replace(/(<svg[^>]*\b)height="[^"]*"/i, `$1height="${targetHeight}"`);
13412
- } else {
13413
- normalized = normalized.replace(/<svg\b/i, `<svg height="${targetHeight}"`);
13414
- }
13415
- const viewBox = `0 0 ${targetWidth} ${targetHeight}`;
13416
- if (/\bviewBox="[^"]*"/i.test(normalized)) {
13417
- normalized = normalized.replace(/viewBox="[^"]*"/i, `viewBox="${viewBox}"`);
13418
- } else {
13419
- normalized = normalized.replace(/<svg\b/i, `<svg viewBox="${viewBox}"`);
13420
- }
13421
- normalized = normalized.replace(/="undefined"/g, '="0"');
13422
- normalized = normalized.replace(/="NaN"/g, '="0"');
13423
- if (/\bx="[^"]*"/i.test(normalized)) {
13424
- normalized = normalized.replace(/(<svg[^>]*\b)x="[^"]*"/i, '$1x="0"');
13425
- } else {
13426
- normalized = normalized.replace(/<svg\b/i, '<svg x="0"');
13427
- }
13428
- if (/\by="[^"]*"/i.test(normalized)) {
13429
- normalized = normalized.replace(/(<svg[^>]*\b)y="[^"]*"/i, '$1y="0"');
13430
- } else {
13431
- normalized = normalized.replace(/<svg\b/i, '<svg y="0"');
13432
- }
13433
- normalized = normalized.replace(/\bpreserveAspectRatio="[^"]*"/i, 'preserveAspectRatio="none"');
13434
- if (!/\bpreserveAspectRatio="[^"]*"/i.test(normalized)) {
13435
- normalized = normalized.replace(/<svg\b/i, '<svg preserveAspectRatio="none"');
13436
- }
13437
- return normalized;
13438
- }
13439
- /**
13440
- * Find the Fabric.Canvas instance that belongs to a given container element,
13441
- * using the global __fabricCanvasRegistry (set by PageCanvas).
13442
- */
13443
- getFabricCanvasFromContainer(container) {
13444
- const registry2 = window.__fabricCanvasRegistry;
13445
- if (registry2 instanceof Map) {
13446
- for (const entry of registry2.values()) {
13447
- const canvas = (entry == null ? void 0 : entry.canvas) || entry;
13448
- if (!canvas || typeof canvas.toSVG !== "function") continue;
13449
- const el = canvas.lowerCanvasEl || canvas.upperCanvasEl;
13450
- if (el && container.contains(el)) return canvas;
13451
- }
13452
- }
13453
- return null;
13454
- }
13455
- async waitForStableTextMetrics(container, config) {
13456
- var _a, _b, _c;
13457
- if (typeof document !== "undefined") {
13458
- void ensureFontsForResolvedConfig(config);
13459
- await this.waitForRelevantFonts(config);
13460
- }
13461
- const fabricInstance = this.getFabricCanvasFromContainer(container);
13462
- if (!(fabricInstance == null ? void 0 : fabricInstance.getObjects)) return;
13463
- const waitForPaint = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r())));
13464
- const primeCharBounds = (obj) => {
13465
- if (obj instanceof fabric.Textbox) {
13466
- const lines = obj._textLines;
13467
- if (Array.isArray(lines)) {
13468
- for (let i = 0; i < lines.length; i++) {
13469
- try {
13470
- obj.getLineWidth(i);
13471
- } catch {
13472
- }
13473
- }
13474
- }
13475
- obj.dirty = true;
13476
- return;
13477
- }
13478
- if (obj instanceof fabric.Group) {
13479
- obj.getObjects().forEach(primeCharBounds);
13480
- obj.dirty = true;
13481
- }
13482
- };
13483
- fabricInstance.getObjects().forEach(primeCharBounds);
13484
- (_a = fabricInstance.calcOffset) == null ? void 0 : _a.call(fabricInstance);
13485
- (_b = fabricInstance.renderAll) == null ? void 0 : _b.call(fabricInstance);
13486
- await waitForPaint();
13487
- (_c = fabricInstance.renderAll) == null ? void 0 : _c.call(fabricInstance);
13488
- await waitForPaint();
13489
- }
13490
- }
13491
- const FONT_WEIGHT_LABELS = {
13492
- 300: "Light",
13493
- 400: "Regular",
13494
- 500: "Medium",
13495
- 600: "SemiBold",
13496
- 700: "Bold"
13497
- };
13498
- function resolveFontWeight(weight) {
13499
- if (weight <= 350) return 300;
13500
- if (weight <= 450) return 400;
13501
- if (weight <= 550) return 500;
13502
- if (weight <= 650) return 600;
13503
- return 700;
12126
+ const FONT_WEIGHT_LABELS = {
12127
+ 300: "Light",
12128
+ 400: "Regular",
12129
+ 500: "Medium",
12130
+ 600: "SemiBold",
12131
+ 700: "Bold"
12132
+ };
12133
+ function resolveFontWeight(weight) {
12134
+ if (weight <= 350) return 300;
12135
+ if (weight <= 450) return 400;
12136
+ if (weight <= 550) return 500;
12137
+ if (weight <= 650) return 600;
12138
+ return 700;
13504
12139
  }
13505
12140
  const FONT_FALLBACK_SYMBOLS = "Noto Sans";
13506
12141
  const FONT_FALLBACK_DEVANAGARI = "Hind";
@@ -13985,327 +12620,1803 @@ const FONT_FILES = {
13985
12620
  italic: "CormorantGaramond-Italic.ttf",
13986
12621
  boldItalic: "CormorantGaramond-BoldItalic.ttf"
13987
12622
  }
13988
- };
13989
- const WEIGHT_TO_KEYS = {
13990
- 300: ["light", "regular"],
13991
- 400: ["regular"],
13992
- 500: ["medium", "regular"],
13993
- 600: ["semibold", "bold", "regular"],
13994
- 700: ["bold", "regular"]
13995
- };
13996
- const WEIGHT_TO_ITALIC_KEYS = {
13997
- 300: ["lightItalic", "italic", "light", "regular"],
13998
- 400: ["italic", "regular"],
13999
- 500: ["mediumItalic", "italic", "medium", "regular"],
14000
- 600: ["semiboldItalic", "boldItalic", "italic", "semibold", "bold", "regular"],
14001
- 700: ["boldItalic", "italic", "bold", "regular"]
14002
- };
14003
- function getFontPathForWeight(files, weight, isItalic = false) {
14004
- const keys = isItalic ? WEIGHT_TO_ITALIC_KEYS[weight] : WEIGHT_TO_KEYS[weight];
14005
- if (!keys) return files.regular;
14006
- for (const k of keys) {
14007
- const path = files[k];
14008
- if (path) return path;
12623
+ };
12624
+ const WEIGHT_TO_KEYS = {
12625
+ 300: ["light", "regular"],
12626
+ 400: ["regular"],
12627
+ 500: ["medium", "regular"],
12628
+ 600: ["semibold", "bold", "regular"],
12629
+ 700: ["bold", "regular"]
12630
+ };
12631
+ const WEIGHT_TO_ITALIC_KEYS = {
12632
+ 300: ["lightItalic", "italic", "light", "regular"],
12633
+ 400: ["italic", "regular"],
12634
+ 500: ["mediumItalic", "italic", "medium", "regular"],
12635
+ 600: ["semiboldItalic", "boldItalic", "italic", "semibold", "bold", "regular"],
12636
+ 700: ["boldItalic", "italic", "bold", "regular"]
12637
+ };
12638
+ function getFontPathForWeight(files, weight, isItalic = false) {
12639
+ const keys = isItalic ? WEIGHT_TO_ITALIC_KEYS[weight] : WEIGHT_TO_KEYS[weight];
12640
+ if (!keys) return files.regular;
12641
+ for (const k of keys) {
12642
+ const path = files[k];
12643
+ if (path) return path;
12644
+ }
12645
+ return files.regular;
12646
+ }
12647
+ function isItalicPath(files, path) {
12648
+ return path === files.italic || path === files.boldItalic || path === files.lightItalic || path === files.mediumItalic || path === files.semiboldItalic;
12649
+ }
12650
+ function getJsPDFFontName(fontName) {
12651
+ return fontName.replace(/\s+/g, "");
12652
+ }
12653
+ function getEmbeddedJsPDFFontName(fontName, weight, isItalic = false) {
12654
+ const resolved = resolveFontWeight(weight);
12655
+ const label = FONT_WEIGHT_LABELS[resolved];
12656
+ const italicSuffix = isItalic ? "Italic" : "";
12657
+ return `${getJsPDFFontName(fontName)}-${label}${italicSuffix}`;
12658
+ }
12659
+ function isFontAvailable(fontName) {
12660
+ return fontName in FONT_FILES;
12661
+ }
12662
+ const ttfCache = /* @__PURE__ */ new Map();
12663
+ async function fetchTTFAsBase64(url) {
12664
+ const cached = ttfCache.get(url);
12665
+ if (cached) return cached;
12666
+ try {
12667
+ const res = await fetch(url);
12668
+ if (!res.ok) return null;
12669
+ const buf = await res.arrayBuffer();
12670
+ const bytes = new Uint8Array(buf);
12671
+ let binary = "";
12672
+ for (let i = 0; i < bytes.length; i++) {
12673
+ binary += String.fromCharCode(bytes[i]);
12674
+ }
12675
+ const b64 = btoa(binary);
12676
+ ttfCache.set(url, b64);
12677
+ return b64;
12678
+ } catch {
12679
+ return null;
12680
+ }
12681
+ }
12682
+ async function embedFont(pdf, fontName, weight, fontBaseUrl, isItalic = false) {
12683
+ const fontFiles = FONT_FILES[fontName];
12684
+ if (!fontFiles) return false;
12685
+ const baseUrl = fontBaseUrl.endsWith("/") ? fontBaseUrl : fontBaseUrl + "/";
12686
+ const resolvedWeight = resolveFontWeight(weight);
12687
+ const fontPath = getFontPathForWeight(fontFiles, resolvedWeight, isItalic);
12688
+ if (!fontPath) return false;
12689
+ const hasItalicFile = isItalic && isItalicPath(fontFiles, fontPath);
12690
+ const jsPdfFontName = getEmbeddedJsPDFFontName(fontName, weight, hasItalicFile);
12691
+ const label = FONT_WEIGHT_LABELS[resolvedWeight];
12692
+ const italicSuffix = hasItalicFile ? "Italic" : "";
12693
+ const fileName = `${getJsPDFFontName(fontName)}-${label}${italicSuffix}.ttf`;
12694
+ const url = baseUrl + fontPath;
12695
+ try {
12696
+ const b64 = await fetchTTFAsBase64(url);
12697
+ if (!b64) return false;
12698
+ pdf.addFileToVFS(fileName, b64);
12699
+ pdf.addFont(fileName, jsPdfFontName, "normal");
12700
+ if (fontName !== jsPdfFontName) {
12701
+ try {
12702
+ pdf.addFont(fileName, fontName, "normal");
12703
+ } catch {
12704
+ }
12705
+ }
12706
+ return true;
12707
+ } catch (e) {
12708
+ console.warn(`[pdf-fonts] Failed to embed ${fontName} w${weight}:`, e);
12709
+ return false;
12710
+ }
12711
+ }
12712
+ async function embedFontsForConfig(pdf, config, fontBaseUrl) {
12713
+ const fontKeys = /* @__PURE__ */ new Set();
12714
+ const SEP = "";
12715
+ const walkElements = (elements) => {
12716
+ for (const el of elements) {
12717
+ if (el.fontFamily) {
12718
+ const w = resolveFontWeight(el.fontWeight ?? 400);
12719
+ fontKeys.add(`${el.fontFamily}${SEP}${w}`);
12720
+ }
12721
+ if (el.styles && typeof el.styles === "object") {
12722
+ for (const lineKey of Object.keys(el.styles)) {
12723
+ const lineStyles = el.styles[lineKey];
12724
+ if (lineStyles && typeof lineStyles === "object") {
12725
+ for (const charKey of Object.keys(lineStyles)) {
12726
+ const s = lineStyles[charKey];
12727
+ if (s == null ? void 0 : s.fontFamily) {
12728
+ const w = resolveFontWeight(s.fontWeight ?? 400);
12729
+ fontKeys.add(`${s.fontFamily}${SEP}${w}`);
12730
+ }
12731
+ }
12732
+ }
12733
+ }
12734
+ }
12735
+ if (el.children) walkElements(el.children);
12736
+ if (el.objects) walkElements(el.objects);
12737
+ }
12738
+ };
12739
+ for (const page of (config == null ? void 0 : config.pages) || []) {
12740
+ if (page.children) walkElements(page.children);
12741
+ if (page.elements) walkElements(page.elements);
12742
+ }
12743
+ fontKeys.add(`${FONT_FALLBACK_SYMBOLS}${SEP}400`);
12744
+ for (const w of [300, 400, 500, 600, 700]) {
12745
+ fontKeys.add(`${FONT_FALLBACK_DEVANAGARI}${SEP}${w}`);
12746
+ }
12747
+ const embedded = /* @__PURE__ */ new Set();
12748
+ const tasks = [];
12749
+ for (const key of fontKeys) {
12750
+ const sep = key.indexOf(SEP);
12751
+ const fontName = key.slice(0, sep);
12752
+ const weight = parseInt(key.slice(sep + 1), 10);
12753
+ if (!isFontAvailable(fontName)) continue;
12754
+ tasks.push(
12755
+ embedFont(pdf, fontName, weight, fontBaseUrl).then((ok) => {
12756
+ if (ok) embedded.add(key);
12757
+ })
12758
+ );
12759
+ }
12760
+ await Promise.all(tasks);
12761
+ console.log(`[pdf-fonts] Embedded ${embedded.size} font variants from config`);
12762
+ return embedded;
12763
+ }
12764
+ function isDevanagari(char) {
12765
+ const c = char.codePointAt(0) ?? 0;
12766
+ return c >= 2304 && c <= 2431 || c >= 43232 && c <= 43263 || c >= 7376 && c <= 7423;
12767
+ }
12768
+ function containsDevanagari(text) {
12769
+ if (!text) return false;
12770
+ for (const char of text) {
12771
+ if (isDevanagari(char)) return true;
12772
+ }
12773
+ return false;
12774
+ }
12775
+ function isBasicLatinOrLatin1(char) {
12776
+ const c = char.codePointAt(0) ?? 0;
12777
+ return c <= 591;
12778
+ }
12779
+ function classifyChar(char) {
12780
+ if (isBasicLatinOrLatin1(char)) return "main";
12781
+ if (isDevanagari(char)) return "devanagari";
12782
+ return "symbol";
12783
+ }
12784
+ function splitIntoRuns(text) {
12785
+ if (!text) return [];
12786
+ const runs = [];
12787
+ let currentType = null;
12788
+ let currentText = "";
12789
+ for (const char of text) {
12790
+ const type = classifyChar(char);
12791
+ if (type !== currentType && currentText) {
12792
+ runs.push({ text: currentText, runType: currentType });
12793
+ currentText = "";
12794
+ }
12795
+ currentType = type;
12796
+ currentText += char;
12797
+ }
12798
+ if (currentText && currentType) {
12799
+ runs.push({ text: currentText, runType: currentType });
12800
+ }
12801
+ return runs;
12802
+ }
12803
+ function rewriteSvgFontsForJsPDF(svgStr) {
12804
+ var _a, _b;
12805
+ const parser = new DOMParser();
12806
+ const doc = parser.parseFromString(svgStr, "image/svg+xml");
12807
+ const textEls = doc.querySelectorAll("text, tspan, textPath");
12808
+ const readStyleToken = (style, prop) => {
12809
+ var _a2;
12810
+ const match = style.match(new RegExp(`${prop}\\s*:\\s*([^;]+)`, "i"));
12811
+ return ((_a2 = match == null ? void 0 : match[1]) == null ? void 0 : _a2.trim()) || null;
12812
+ };
12813
+ const resolveInheritedValue = (el, attr, styleProp = attr) => {
12814
+ var _a2;
12815
+ let current = el;
12816
+ while (current) {
12817
+ const attrVal = (_a2 = current.getAttribute(attr)) == null ? void 0 : _a2.trim();
12818
+ if (attrVal) return attrVal;
12819
+ const styleVal = readStyleToken(current.getAttribute("style") || "", styleProp);
12820
+ if (styleVal) return styleVal;
12821
+ current = current.parentElement;
12822
+ }
12823
+ return null;
12824
+ };
12825
+ const resolveWeightNum = (weightRaw) => {
12826
+ const parsedWeight = Number.parseInt(weightRaw, 10);
12827
+ return Number.isFinite(parsedWeight) ? parsedWeight : /bold/i.test(weightRaw) ? 700 : /medium/i.test(weightRaw) ? 500 : /semi/i.test(weightRaw) ? 600 : /light/i.test(weightRaw) ? 300 : 400;
12828
+ };
12829
+ const buildStyleString = (existingStyle, fontName) => {
12830
+ const stylePairs = existingStyle.split(";").map((part) => part.trim()).filter(Boolean).filter((part) => !/^font-family\s*:/i.test(part) && !/^font-weight\s*:/i.test(part) && !/^font-style\s*:/i.test(part));
12831
+ stylePairs.push(`font-family: ${fontName}`);
12832
+ stylePairs.push(`font-weight: normal`);
12833
+ stylePairs.push(`font-style: normal`);
12834
+ return stylePairs.join("; ");
12835
+ };
12836
+ for (const el of textEls) {
12837
+ const inlineStyle = el.getAttribute("style") || "";
12838
+ const rawFf = resolveInheritedValue(el, "font-family");
12839
+ if (!rawFf) continue;
12840
+ const clean = (_a = rawFf.split(",")[0]) == null ? void 0 : _a.replace(/['"]/g, "").trim();
12841
+ if (!isFontAvailable(clean)) continue;
12842
+ const weightRaw = resolveInheritedValue(el, "font-weight") || "400";
12843
+ const styleRaw = resolveInheritedValue(el, "font-style") || "normal";
12844
+ const weight = resolveWeightNum(weightRaw);
12845
+ const resolved = resolveFontWeight(weight);
12846
+ const isItalic = /italic|oblique/i.test(styleRaw);
12847
+ const jsPdfName = getEmbeddedJsPDFFontName(clean, resolved, isItalic);
12848
+ el.setAttribute("data-source-font-family", clean);
12849
+ el.setAttribute("data-source-font-weight", String(resolved));
12850
+ el.setAttribute("data-source-font-style", isItalic ? "italic" : "normal");
12851
+ const directText = Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => n.textContent || "").join("");
12852
+ const hasDevanagari = containsDevanagari(directText);
12853
+ if (hasDevanagari && directText.length > 0) {
12854
+ const devanagariWeight = resolveFontWeight(weight);
12855
+ const devanagariJsPdfName = getEmbeddedJsPDFFontName(FONT_FALLBACK_DEVANAGARI, devanagariWeight);
12856
+ const symbolJsPdfName = isFontAvailable(FONT_FALLBACK_SYMBOLS) ? getEmbeddedJsPDFFontName(FONT_FALLBACK_SYMBOLS, 400) : jsPdfName;
12857
+ const childNodes = Array.from(el.childNodes);
12858
+ for (const node of childNodes) {
12859
+ if (node.nodeType !== 3 || !node.textContent) continue;
12860
+ const runs = splitIntoRuns(node.textContent);
12861
+ if (runs.length <= 1 && ((_b = runs[0]) == null ? void 0 : _b.runType) !== "devanagari") continue;
12862
+ const fragment = doc.createDocumentFragment();
12863
+ for (const run of runs) {
12864
+ const tspan = doc.createElementNS("http://www.w3.org/2000/svg", "tspan");
12865
+ let runFont;
12866
+ if (run.runType === "devanagari") {
12867
+ runFont = devanagariJsPdfName;
12868
+ } else if (run.runType === "symbol") {
12869
+ runFont = symbolJsPdfName;
12870
+ } else {
12871
+ runFont = jsPdfName;
12872
+ }
12873
+ tspan.setAttribute("font-family", runFont);
12874
+ tspan.setAttribute("font-weight", "normal");
12875
+ tspan.setAttribute("font-style", "normal");
12876
+ tspan.textContent = run.text;
12877
+ fragment.appendChild(tspan);
12878
+ }
12879
+ el.replaceChild(fragment, node);
12880
+ }
12881
+ el.setAttribute("font-family", jsPdfName);
12882
+ el.setAttribute("font-weight", "normal");
12883
+ el.setAttribute("font-style", "normal");
12884
+ el.setAttribute("style", buildStyleString(inlineStyle, jsPdfName));
12885
+ } else {
12886
+ el.setAttribute("font-family", jsPdfName);
12887
+ el.setAttribute("font-weight", "normal");
12888
+ el.setAttribute("font-style", "normal");
12889
+ el.setAttribute("style", buildStyleString(inlineStyle, jsPdfName));
12890
+ }
12891
+ }
12892
+ return new XMLSerializer().serializeToString(doc.documentElement);
12893
+ }
12894
+ function extractFontFamiliesFromSvgs(svgs) {
12895
+ const families = /* @__PURE__ */ new Set();
12896
+ const regex = /font-family[=:]\s*["']?([^"';},]+)/gi;
12897
+ for (const svg of svgs) {
12898
+ let m;
12899
+ while ((m = regex.exec(svg)) !== null) {
12900
+ const raw = m[1].trim().split(",")[0].trim().replace(/^['"]|['"]$/g, "");
12901
+ if (raw && raw !== "serif" && raw !== "sans-serif" && raw !== "monospace") {
12902
+ families.add(raw);
12903
+ }
12904
+ }
12905
+ }
12906
+ return families;
12907
+ }
12908
+ async function embedFontsInPdf(pdf, fontFamilies, fontBaseUrl) {
12909
+ const embedded = /* @__PURE__ */ new Set();
12910
+ const weights = [300, 400, 500, 600, 700];
12911
+ const tasks = [];
12912
+ for (const family of fontFamilies) {
12913
+ if (!isFontAvailable(family)) {
12914
+ console.warn(`[pdf-fonts] No TTF mapping for "${family}" — will use Helvetica fallback`);
12915
+ continue;
12916
+ }
12917
+ for (const w of weights) {
12918
+ tasks.push(
12919
+ embedFont(pdf, family, w, fontBaseUrl).then((ok) => {
12920
+ if (ok) embedded.add(`${family}${w}`);
12921
+ })
12922
+ );
12923
+ }
12924
+ }
12925
+ await Promise.all(tasks);
12926
+ console.log(`[pdf-fonts] Embedded ${embedded.size} font variants for ${fontFamilies.size} families`);
12927
+ return embedded;
12928
+ }
12929
+ const pdfFonts = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
12930
+ __proto__: null,
12931
+ FONT_FALLBACK_DEVANAGARI,
12932
+ FONT_FALLBACK_SYMBOLS,
12933
+ FONT_FILES,
12934
+ FONT_WEIGHT_LABELS,
12935
+ embedFont,
12936
+ embedFontsForConfig,
12937
+ embedFontsInPdf,
12938
+ extractFontFamiliesFromSvgs,
12939
+ getEmbeddedJsPDFFontName,
12940
+ getFontPathForWeight,
12941
+ isFontAvailable,
12942
+ resolveFontWeight,
12943
+ rewriteSvgFontsForJsPDF
12944
+ }, Symbol.toStringTag, { value: "Module" }));
12945
+ function normalizeFontFamily(fontStack) {
12946
+ const first = fontStack.split(",")[0].trim();
12947
+ return first.replace(/^['"]|['"]$/g, "");
12948
+ }
12949
+ const loadedFonts = /* @__PURE__ */ new Set();
12950
+ const loadingPromises = /* @__PURE__ */ new Map();
12951
+ function getFontBaseUrl() {
12952
+ if (typeof window === "undefined") return "/fonts/";
12953
+ return `${window.location.origin}/fonts/`;
12954
+ }
12955
+ function injectLocalFontFaces(rawFontFamily) {
12956
+ if (typeof document === "undefined") return false;
12957
+ const fontFamily = normalizeFontFamily(rawFontFamily);
12958
+ const files = FONT_FILES[fontFamily];
12959
+ if (!files) return false;
12960
+ const id = `pixldocs-local-font-${fontFamily.replace(/[^a-z0-9_-]+/gi, "-")}`;
12961
+ if (document.getElementById(id)) return true;
12962
+ const baseUrl = getFontBaseUrl();
12963
+ const rules = [];
12964
+ const seen = /* @__PURE__ */ new Set();
12965
+ for (const weight of [300, 400, 500, 600, 700]) {
12966
+ for (const italic of [false, true]) {
12967
+ const resolvedWeight = resolveFontWeight(weight);
12968
+ const file = getFontPathForWeight(files, resolvedWeight, italic);
12969
+ if (!file) continue;
12970
+ const key = `${file}|${weight}|${italic ? "italic" : "normal"}`;
12971
+ if (seen.has(key)) continue;
12972
+ seen.add(key);
12973
+ rules.push(
12974
+ `@font-face{font-family:${JSON.stringify(fontFamily)};src:url(${JSON.stringify(baseUrl + file)}) format("truetype");font-weight:${weight};font-style:${italic ? "italic" : "normal"};font-display:block;}`
12975
+ );
12976
+ }
12977
+ }
12978
+ if (rules.length === 0) return false;
12979
+ const style = document.createElement("style");
12980
+ style.id = id;
12981
+ style.textContent = rules.join("\n");
12982
+ document.head.appendChild(style);
12983
+ return true;
12984
+ }
12985
+ async function awaitLocalFontFace(rawFontFamily, timeoutMs = 1600) {
12986
+ if (typeof document === "undefined" || !document.fonts) return false;
12987
+ const fontFamily = normalizeFontFamily(rawFontFamily);
12988
+ if (!injectLocalFontFaces(fontFamily)) return false;
12989
+ const weights = [400, 700];
12990
+ const loads = Promise.all(
12991
+ weights.map((weight) => document.fonts.load(`${weight} 16px "${fontFamily}"`))
12992
+ ).then(() => document.fonts.check(`400 16px "${fontFamily}"`));
12993
+ return await Promise.race([
12994
+ loads.catch(() => false),
12995
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
12996
+ ]);
12997
+ }
12998
+ function withTimeout(promise, timeoutMs = 4e3) {
12999
+ let timeoutId;
13000
+ return Promise.race([
13001
+ promise,
13002
+ new Promise((resolve) => {
13003
+ timeoutId = setTimeout(resolve, timeoutMs);
13004
+ })
13005
+ ]).finally(() => {
13006
+ if (timeoutId) clearTimeout(timeoutId);
13007
+ });
13008
+ }
13009
+ async function loadGoogleFontCSS(rawFontFamily) {
13010
+ if (!rawFontFamily || typeof document === "undefined") return;
13011
+ const fontFamily = normalizeFontFamily(rawFontFamily);
13012
+ if (!fontFamily) return;
13013
+ if (loadedFonts.has(fontFamily)) return;
13014
+ const existing = loadingPromises.get(fontFamily);
13015
+ if (existing) return existing;
13016
+ const promise = (async () => {
13017
+ try {
13018
+ if (await awaitLocalFontFace(fontFamily)) {
13019
+ loadedFonts.add(fontFamily);
13020
+ return;
13021
+ }
13022
+ const encoded = encodeURIComponent(fontFamily);
13023
+ const url = `https://fonts.googleapis.com/css?family=${encoded}:300,400,500,600,700&display=swap`;
13024
+ const link = document.createElement("link");
13025
+ link.rel = "stylesheet";
13026
+ link.href = url;
13027
+ link.crossOrigin = "anonymous";
13028
+ await new Promise((resolve, reject) => {
13029
+ link.onload = () => resolve();
13030
+ link.onerror = () => reject(new Error(`Failed to load font: ${fontFamily}`));
13031
+ document.head.appendChild(link);
13032
+ });
13033
+ loadedFonts.add(fontFamily);
13034
+ } catch (e) {
13035
+ console.warn(`[@pixldocs/canvas-renderer] Font load failed: ${fontFamily}`, e);
13036
+ }
13037
+ })();
13038
+ loadingPromises.set(fontFamily, promise);
13039
+ await promise;
13040
+ loadingPromises.delete(fontFamily);
13041
+ }
13042
+ function collectFontsFromConfig(config) {
13043
+ var _a;
13044
+ const fonts = /* @__PURE__ */ new Set();
13045
+ fonts.add("Open Sans");
13046
+ fonts.add("Hind");
13047
+ function walk(nodes) {
13048
+ var _a2;
13049
+ if (!nodes) return;
13050
+ for (const node of nodes) {
13051
+ if (node.fontFamily) fonts.add(normalizeFontFamily(node.fontFamily));
13052
+ if ((_a2 = node.smartProps) == null ? void 0 : _a2.fontFamily) fonts.add(normalizeFontFamily(node.smartProps.fontFamily));
13053
+ if (node.styles && Array.isArray(node.styles)) {
13054
+ for (const lineStyle of node.styles) {
13055
+ if (lineStyle && typeof lineStyle === "object") {
13056
+ for (const charStyle of Object.values(lineStyle)) {
13057
+ if (charStyle == null ? void 0 : charStyle.fontFamily) fonts.add(normalizeFontFamily(charStyle.fontFamily));
13058
+ }
13059
+ }
13060
+ }
13061
+ }
13062
+ if (node.children) walk(node.children);
13063
+ }
13064
+ }
13065
+ for (const page of config.pages || []) {
13066
+ walk(page.children || []);
13067
+ }
13068
+ if ((_a = config.themeConfig) == null ? void 0 : _a.variables) {
13069
+ for (const def of Object.values(config.themeConfig.variables)) {
13070
+ if (def.value && typeof def.value === "string" && !def.value.startsWith("#") && !def.value.startsWith("rgb")) {
13071
+ if (def.label && /font/i.test(def.label)) {
13072
+ fonts.add(normalizeFontFamily(def.value));
13073
+ }
13074
+ }
13075
+ }
13076
+ }
13077
+ return fonts;
13078
+ }
13079
+ function collectFontDescriptorsFromConfig(config) {
13080
+ var _a;
13081
+ const seen = /* @__PURE__ */ new Set();
13082
+ const descriptors = [];
13083
+ function add(family, weight, style) {
13084
+ const f = normalizeFontFamily(family);
13085
+ if (!f) return;
13086
+ const w = weight ?? 400;
13087
+ const s = style ?? "normal";
13088
+ const key = `${f}|${w}|${s}`;
13089
+ if (seen.has(key)) return;
13090
+ seen.add(key);
13091
+ descriptors.push({ family: f, weight: w, style: s });
13092
+ }
13093
+ function walk(nodes) {
13094
+ var _a2;
13095
+ if (!nodes) return;
13096
+ for (const node of nodes) {
13097
+ if (node.fontFamily) {
13098
+ add(node.fontFamily, node.fontWeight, node.fontStyle);
13099
+ if (node.type === "text") {
13100
+ for (const w of [300, 400, 500, 600, 700]) {
13101
+ add(node.fontFamily, w, node.fontStyle);
13102
+ }
13103
+ }
13104
+ }
13105
+ if ((_a2 = node.smartProps) == null ? void 0 : _a2.fontFamily) {
13106
+ add(node.smartProps.fontFamily, node.smartProps.fontWeight, node.smartProps.fontStyle);
13107
+ }
13108
+ if (node.styles) {
13109
+ const styleEntries = Array.isArray(node.styles) ? node.styles : Object.values(node.styles);
13110
+ for (const lineStyle of styleEntries) {
13111
+ if (lineStyle && typeof lineStyle === "object") {
13112
+ for (const charStyle of Object.values(lineStyle)) {
13113
+ if (charStyle == null ? void 0 : charStyle.fontFamily) {
13114
+ add(charStyle.fontFamily, charStyle.fontWeight, charStyle.fontStyle);
13115
+ }
13116
+ }
13117
+ }
13118
+ }
13119
+ }
13120
+ if (node.children) walk(node.children);
13121
+ }
13122
+ }
13123
+ add("Open Sans", 400, "normal");
13124
+ add("Hind", 400, "normal");
13125
+ add("Hind", 700, "normal");
13126
+ for (const page of config.pages || []) {
13127
+ walk(page.children || []);
13128
+ }
13129
+ if ((_a = config.themeConfig) == null ? void 0 : _a.variables) {
13130
+ for (const def of Object.values(config.themeConfig.variables)) {
13131
+ if (def.value && typeof def.value === "string" && !def.value.startsWith("#") && !def.value.startsWith("rgb")) {
13132
+ if (def.label && /font/i.test(def.label)) {
13133
+ add(def.value);
13134
+ }
13135
+ }
13136
+ }
13137
+ }
13138
+ return descriptors;
13139
+ }
13140
+ async function ensureFontsForResolvedConfig(config) {
13141
+ if (typeof document === "undefined") return;
13142
+ const descriptors = collectFontDescriptorsFromConfig(config);
13143
+ const families = new Set(descriptors.map((d) => d.family));
13144
+ await withTimeout(Promise.all([...families].map((f) => loadGoogleFontCSS(f))), 3500);
13145
+ if (document.fonts) {
13146
+ descriptors.forEach((d) => {
13147
+ const stylePrefix = d.style === "italic" ? "italic " : "";
13148
+ const weightStr = String(d.weight);
13149
+ const spec = `${stylePrefix}${weightStr} 16px "${d.family}"`;
13150
+ document.fonts.load(spec).catch(() => {
13151
+ });
13152
+ });
13153
+ }
13154
+ }
13155
+ function configHasAutoShrinkText$1(config) {
13156
+ var _a;
13157
+ if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return false;
13158
+ const walk = (nodes) => {
13159
+ for (const node of nodes || []) {
13160
+ if (!node) continue;
13161
+ if (node.type === "text" && node.overflowPolicy === "auto-shrink") return true;
13162
+ if (Array.isArray(node.children) && node.children.length && walk(node.children)) return true;
13163
+ }
13164
+ return false;
13165
+ };
13166
+ for (const page of config.pages) {
13167
+ if (walk(page.children || [])) return true;
13168
+ }
13169
+ return false;
13170
+ }
13171
+ async function awaitFontsForConfig(config, maxWaitMs) {
13172
+ if (typeof document === "undefined" || !document.fonts) return;
13173
+ await ensureFontsForResolvedConfig(config);
13174
+ const descriptors = collectFontDescriptorsFromConfig(config);
13175
+ if (descriptors.length === 0) return;
13176
+ const loads = Promise.all(
13177
+ descriptors.map((d) => {
13178
+ const stylePrefix = d.style === "italic" ? "italic " : "";
13179
+ const spec = `${stylePrefix}${d.weight} 16px "${d.family}"`;
13180
+ return document.fonts.load(spec).catch(() => []);
13181
+ })
13182
+ ).then(() => void 0);
13183
+ await Promise.race([
13184
+ loads,
13185
+ new Promise((resolve) => setTimeout(resolve, maxWaitMs))
13186
+ ]);
13187
+ await Promise.race([
13188
+ document.fonts.ready.catch(() => void 0).then(() => void 0),
13189
+ new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
13190
+ ]);
13191
+ await new Promise(
13192
+ (resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
13193
+ );
13194
+ }
13195
+ const PREVIEW_DEBUG_PREFIX = "[canvas-renderer][preview-debug]";
13196
+ function countUnderlinedNodes(config) {
13197
+ var _a;
13198
+ if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return 0;
13199
+ let count = 0;
13200
+ const walk = (nodes) => {
13201
+ var _a2;
13202
+ for (const node of nodes || []) {
13203
+ if (node == null ? void 0 : node.underline) count += 1;
13204
+ if ((_a2 = node == null ? void 0 : node.children) == null ? void 0 : _a2.length) walk(node.children);
13205
+ }
13206
+ };
13207
+ for (const page of config.pages) walk(page.children || []);
13208
+ return count;
13209
+ }
13210
+ function PixldocsPreview(props) {
13211
+ const {
13212
+ pageIndex = 0,
13213
+ zoom = 1,
13214
+ absoluteZoom = false,
13215
+ imageProxyUrl,
13216
+ className,
13217
+ style,
13218
+ onDynamicFieldClick,
13219
+ onReady,
13220
+ onError,
13221
+ // Default `false` so PageCanvas blocks textbox creation until the host
13222
+ // browser actually has the @font-face rules registered. This matters for
13223
+ // `overflowPolicy: 'auto-shrink'` text — `createText` runs the shrink
13224
+ // loop synchronously at mount time using whatever font metrics Fabric
13225
+ // can measure right then. If the real font hasn't loaded yet, Fabric
13226
+ // falls back to the system font (typically narrower), the shrink loop
13227
+ // decides "fits, no shrink needed", and when the real font finally
13228
+ // loads the text overflows the box.
13229
+ //
13230
+ // The renderer's imperative PNG/PDF paths (`renderPageViaPreviewCanvas`,
13231
+ // `captureSvgViaPreviewCanvas`) already pass `skipFontReadyWait: false`
13232
+ // for this exact reason — that's why the downloaded PDF was correct
13233
+ // while the on-screen preview wasn't.
13234
+ skipFontReadyWait = false
13235
+ } = props;
13236
+ useEffect(() => {
13237
+ setPackageApiUrl(imageProxyUrl);
13238
+ }, [imageProxyUrl]);
13239
+ const [resolvedConfig, setResolvedConfig] = useState(null);
13240
+ const [isLoading, setIsLoading] = useState(false);
13241
+ const [fontsReady, setFontsReady] = useState(false);
13242
+ const [fontsReadyVersion, setFontsReadyVersion] = useState(0);
13243
+ const [canvasSettled, setCanvasSettled] = useState(false);
13244
+ const [stabilizationPass, setStabilizationPass] = useState(0);
13245
+ const isResolveMode = !("config" in props && props.config);
13246
+ useEffect(() => {
13247
+ if (!isResolveMode) {
13248
+ setResolvedConfig(null);
13249
+ setCanvasSettled(false);
13250
+ console.log(PREVIEW_DEBUG_PREFIX, "config-mode active");
13251
+ return;
13252
+ }
13253
+ const p = props;
13254
+ if (!p.templateId || !p.formSchemaId || !p.supabaseUrl || !p.supabaseAnonKey) return;
13255
+ let cancelled = false;
13256
+ setIsLoading(true);
13257
+ setFontsReady(false);
13258
+ setCanvasSettled(false);
13259
+ console.log(PREVIEW_DEBUG_PREFIX, "resolve-start", {
13260
+ templateId: p.templateId,
13261
+ formSchemaId: p.formSchemaId,
13262
+ themeId: p.themeId ?? null,
13263
+ pageIndex
13264
+ });
13265
+ resolveFromForm({
13266
+ templateId: p.templateId,
13267
+ formSchemaId: p.formSchemaId,
13268
+ sectionState: p.sectionState,
13269
+ themeId: p.themeId,
13270
+ supabaseUrl: p.supabaseUrl,
13271
+ supabaseAnonKey: p.supabaseAnonKey
13272
+ }).then((resolved) => {
13273
+ var _a, _b;
13274
+ if (!cancelled) {
13275
+ console.log(PREVIEW_DEBUG_PREFIX, "resolve-done", {
13276
+ pages: ((_b = (_a = resolved.config) == null ? void 0 : _a.pages) == null ? void 0 : _b.length) ?? 0,
13277
+ underlinedNodes: countUnderlinedNodes(resolved.config)
13278
+ });
13279
+ setResolvedConfig(resolved.config);
13280
+ const hasAutoShrink = configHasAutoShrinkText$1(resolved.config);
13281
+ const waitMs = hasAutoShrink ? 4e3 : 1800;
13282
+ awaitFontsForConfig(resolved.config, waitMs).then(() => {
13283
+ if (!cancelled) {
13284
+ console.log(PREVIEW_DEBUG_PREFIX, "resolve-mode fonts settled", { hasAutoShrink, waitMs });
13285
+ setFontsReady(true);
13286
+ setIsLoading(false);
13287
+ }
13288
+ }).catch((err) => {
13289
+ if (!cancelled) {
13290
+ console.warn(PREVIEW_DEBUG_PREFIX, "resolve-mode font wait failed", err);
13291
+ setFontsReady(true);
13292
+ setIsLoading(false);
13293
+ }
13294
+ });
13295
+ }
13296
+ }).catch((err) => {
13297
+ if (!cancelled) {
13298
+ setIsLoading(false);
13299
+ console.warn(PREVIEW_DEBUG_PREFIX, "resolve-error", err);
13300
+ onError == null ? void 0 : onError(err instanceof Error ? err : new Error(String(err)));
13301
+ }
13302
+ });
13303
+ return () => {
13304
+ cancelled = true;
13305
+ };
13306
+ }, [
13307
+ isResolveMode,
13308
+ // For resolve mode, re-resolve when these change
13309
+ isResolveMode ? props.templateId : void 0,
13310
+ isResolveMode ? props.formSchemaId : void 0,
13311
+ isResolveMode ? JSON.stringify(props.sectionState) : void 0,
13312
+ isResolveMode ? props.themeId : void 0
13313
+ ]);
13314
+ const config = isResolveMode ? resolvedConfig : props.config;
13315
+ useEffect(() => {
13316
+ var _a, _b, _c;
13317
+ if (!config) return;
13318
+ let cancelled = false;
13319
+ setCanvasSettled(false);
13320
+ setStabilizationPass(0);
13321
+ console.log(PREVIEW_DEBUG_PREFIX, "config-changed", {
13322
+ pageIndex,
13323
+ pages: ((_a = config.pages) == null ? void 0 : _a.length) ?? 0,
13324
+ underlinedNodes: countUnderlinedNodes(config),
13325
+ isResolveMode
13326
+ });
13327
+ const bump = () => {
13328
+ if (cancelled) return;
13329
+ clearMeasurementCache();
13330
+ clearFabricCharCache();
13331
+ setFontsReadyVersion((v) => {
13332
+ const next = v + 1;
13333
+ console.log(PREVIEW_DEBUG_PREFIX, "font-bump", { pageIndex, next, stabilizationPass });
13334
+ return next;
13335
+ });
13336
+ };
13337
+ (_c = (_b = document.fonts) == null ? void 0 : _b.ready) == null ? void 0 : _c.then(bump);
13338
+ const timeoutId = window.setTimeout(bump, 350);
13339
+ return () => {
13340
+ cancelled = true;
13341
+ window.clearTimeout(timeoutId);
13342
+ };
13343
+ }, [config]);
13344
+ const previewKey = useMemo(
13345
+ () => `${pageIndex}-${fontsReadyVersion}-${stabilizationPass}`,
13346
+ [pageIndex, fontsReadyVersion, stabilizationPass]
13347
+ );
13348
+ useEffect(() => {
13349
+ if (isResolveMode) return;
13350
+ if (!config) {
13351
+ setFontsReady(false);
13352
+ setCanvasSettled(false);
13353
+ setStabilizationPass(0);
13354
+ return;
13355
+ }
13356
+ setFontsReady(false);
13357
+ setCanvasSettled(false);
13358
+ setStabilizationPass(0);
13359
+ let cancelled = false;
13360
+ const hasAutoShrink = configHasAutoShrinkText$1(config);
13361
+ const waitMs = hasAutoShrink ? 4e3 : 1800;
13362
+ awaitFontsForConfig(config, waitMs).then(() => {
13363
+ if (cancelled) return;
13364
+ console.log(PREVIEW_DEBUG_PREFIX, "config-mode fonts settled", {
13365
+ pageIndex,
13366
+ hasAutoShrink,
13367
+ waitMs,
13368
+ underlinedNodes: countUnderlinedNodes(config)
13369
+ });
13370
+ setFontsReady(true);
13371
+ }).catch((err) => {
13372
+ if (cancelled) return;
13373
+ console.warn(PREVIEW_DEBUG_PREFIX, "config-mode font wait failed", err);
13374
+ setFontsReady(true);
13375
+ });
13376
+ return () => {
13377
+ cancelled = true;
13378
+ };
13379
+ }, [isResolveMode, config]);
13380
+ const handleCanvasReady = useCallback(() => {
13381
+ if (stabilizationPass === 0) {
13382
+ console.log(PREVIEW_DEBUG_PREFIX, "canvas-ready-pass", { pageIndex, stabilizationPass, action: "stabilize-again" });
13383
+ setCanvasSettled(false);
13384
+ setStabilizationPass(1);
13385
+ return;
13386
+ }
13387
+ console.log(PREVIEW_DEBUG_PREFIX, "canvas-ready-pass", { pageIndex, stabilizationPass, action: "settled" });
13388
+ setCanvasSettled(true);
13389
+ onReady == null ? void 0 : onReady();
13390
+ }, [onReady, pageIndex, stabilizationPass]);
13391
+ if (isLoading) {
13392
+ return /* @__PURE__ */ jsx("div", { className, style: { ...style, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) });
13393
+ }
13394
+ if (!config) return null;
13395
+ if (!fontsReady) {
13396
+ return /* @__PURE__ */ jsx("div", { className, style: { ...style, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) });
13397
+ }
13398
+ return /* @__PURE__ */ jsxs("div", { className, style: { ...style, position: "relative" }, children: [
13399
+ /* @__PURE__ */ jsx("div", { style: { visibility: canvasSettled ? "visible" : "hidden" }, children: /* @__PURE__ */ jsx(
13400
+ PreviewCanvas,
13401
+ {
13402
+ config,
13403
+ pageIndex,
13404
+ zoom,
13405
+ absoluteZoom,
13406
+ skipFontReadyWait,
13407
+ onDynamicFieldClick,
13408
+ onReady: handleCanvasReady
13409
+ },
13410
+ previewKey
13411
+ ) }),
13412
+ !canvasSettled && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) })
13413
+ ] });
13414
+ }
13415
+ const PACKAGE_VERSION = "0.5.70";
13416
+ let __underlineFixInstalled = false;
13417
+ function installUnderlineFix(fab) {
13418
+ var _a;
13419
+ if (__underlineFixInstalled) return;
13420
+ const TextProto = (_a = fab.Text) == null ? void 0 : _a.prototype;
13421
+ if (!TextProto || typeof TextProto._renderTextDecoration !== "function") return;
13422
+ const original = TextProto._renderTextDecoration;
13423
+ const measureLineTextWidth = (obj, ctx, lineIndex) => {
13424
+ var _a2, _b, _c, _d, _e, _f;
13425
+ const rawLine = (_a2 = obj._textLines) == null ? void 0 : _a2[lineIndex];
13426
+ const lineText = Array.isArray(rawLine) ? rawLine.join("") : String(rawLine ?? "");
13427
+ if (!lineText) return 0;
13428
+ const fontSize = Number(((_b = obj.getValueOfPropertyAt) == null ? void 0 : _b.call(obj, lineIndex, 0, "fontSize")) ?? obj.fontSize ?? 0);
13429
+ const fontStyle = String(((_c = obj.getValueOfPropertyAt) == null ? void 0 : _c.call(obj, lineIndex, 0, "fontStyle")) ?? obj.fontStyle ?? "normal");
13430
+ const fontWeight = String(((_d = obj.getValueOfPropertyAt) == null ? void 0 : _d.call(obj, lineIndex, 0, "fontWeight")) ?? obj.fontWeight ?? "400");
13431
+ const fontFamily = String(((_e = obj.getValueOfPropertyAt) == null ? void 0 : _e.call(obj, lineIndex, 0, "fontFamily")) ?? obj.fontFamily ?? "sans-serif");
13432
+ const charSpacing = Number(((_f = obj.getValueOfPropertyAt) == null ? void 0 : _f.call(obj, lineIndex, 0, "charSpacing")) ?? obj.charSpacing ?? 0);
13433
+ ctx.save();
13434
+ ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px ${fontFamily}`;
13435
+ const measured = ctx.measureText(lineText).width;
13436
+ ctx.restore();
13437
+ const graphemeCount = Array.from(lineText).length;
13438
+ const spacingWidth = graphemeCount > 1 ? charSpacing / 1e3 * fontSize * (graphemeCount - 1) : 0;
13439
+ return Math.max(0, measured + spacingWidth);
13440
+ };
13441
+ TextProto._renderTextDecoration = function patchedRenderTextDecoration(ctx, type) {
13442
+ try {
13443
+ const hasOwn = !!this[type];
13444
+ const hasStyled = typeof this.styleHas === "function" && this.styleHas(type);
13445
+ if (!hasOwn && !hasStyled) return;
13446
+ const lines = this._textLines;
13447
+ const offsets = this.offsets;
13448
+ if (!Array.isArray(lines) || !offsets) {
13449
+ return original.call(this, ctx, type);
13450
+ }
13451
+ const offsetY = offsets[type];
13452
+ const offsetAligner = type === "linethrough" ? 0.5 : type === "overline" ? 1 : 0;
13453
+ const leftOffset = this._getLeftOffset();
13454
+ let topOffset = this._getTopOffset();
13455
+ for (let i = 0, len = lines.length; i < len; i++) {
13456
+ const heightOfLine = this.getHeightOfLine(i);
13457
+ const lineHas = !!this[type] || typeof this.styleHas === "function" && this.styleHas(type, i);
13458
+ if (!lineHas) {
13459
+ topOffset += heightOfLine;
13460
+ continue;
13461
+ }
13462
+ const fillStyle = this.getValueOfPropertyAt(i, 0, "fill");
13463
+ const thickness = this.getValueOfPropertyAt(i, 0, "textDecorationThickness");
13464
+ const charSize = this.getHeightOfChar(i, 0);
13465
+ const dy = this.getValueOfPropertyAt(i, 0, "deltaY") || 0;
13466
+ const finalThickness = this.fontSize * (thickness || 0) / 1e3;
13467
+ if (!fillStyle || !finalThickness) {
13468
+ topOffset += heightOfLine;
13469
+ continue;
13470
+ }
13471
+ const lineWidth = measureLineTextWidth(this, ctx, i);
13472
+ if (!lineWidth) {
13473
+ topOffset += heightOfLine;
13474
+ continue;
13475
+ }
13476
+ const availableWidth = Number(this.width ?? lineWidth);
13477
+ let lineLeftOffset = 0;
13478
+ const align = String(this.textAlign ?? "left");
13479
+ if (align === "center") lineLeftOffset = (availableWidth - lineWidth) / 2;
13480
+ else if (align === "right" || align === "end") lineLeftOffset = availableWidth - lineWidth;
13481
+ let drawStart = leftOffset + lineLeftOffset;
13482
+ if (this.direction === "rtl") {
13483
+ drawStart = this.width - drawStart - lineWidth;
13484
+ }
13485
+ const maxHeight = heightOfLine / this.lineHeight;
13486
+ const top = topOffset + maxHeight * (1 - this._fontSizeFraction);
13487
+ ctx.fillStyle = fillStyle;
13488
+ ctx.fillRect(
13489
+ drawStart,
13490
+ top + offsetY * charSize + dy - offsetAligner * finalThickness,
13491
+ lineWidth,
13492
+ finalThickness
13493
+ );
13494
+ topOffset += heightOfLine;
13495
+ }
13496
+ if (typeof this._removeShadow === "function") {
13497
+ try {
13498
+ this._removeShadow(ctx);
13499
+ } catch {
13500
+ }
13501
+ }
13502
+ } catch {
13503
+ try {
13504
+ return original.call(this, ctx, type);
13505
+ } catch {
13506
+ }
13507
+ }
13508
+ };
13509
+ __underlineFixInstalled = true;
13510
+ console.log(`[canvas-renderer] underline-fix monkey patch installed (v${PACKAGE_VERSION})`);
13511
+ }
13512
+ function configHasAutoShrinkText(config) {
13513
+ var _a;
13514
+ if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return false;
13515
+ const walk = (nodes) => {
13516
+ for (const node of nodes || []) {
13517
+ if (!node) continue;
13518
+ if (node.type === "text" && node.overflowPolicy === "auto-shrink") return true;
13519
+ if (Array.isArray(node.children) && node.children.length && walk(node.children)) return true;
13520
+ }
13521
+ return false;
13522
+ };
13523
+ for (const page of config.pages) {
13524
+ if (walk(page.children || [])) return true;
13525
+ }
13526
+ return false;
13527
+ }
13528
+ class PixldocsRenderer {
13529
+ constructor(config) {
13530
+ __publicField(this, "config");
13531
+ this.config = config;
13532
+ installUnderlineFix(fabric);
13533
+ try {
13534
+ console.log(`[canvas-renderer] PixldocsRenderer v${PACKAGE_VERSION} initialized`);
13535
+ } catch {
13536
+ }
13537
+ }
13538
+ /**
13539
+ * Render a pre-resolved template config to an image using the full PageCanvas engine.
13540
+ * Mounts a hidden PreviewCanvas component and captures the Fabric canvas output.
13541
+ */
13542
+ async render(templateConfig, options = {}) {
13543
+ const pageIndex = options.pageIndex ?? 0;
13544
+ const format = options.format ?? "png";
13545
+ const quality = options.quality ?? 0.92;
13546
+ const pixelRatio = options.pixelRatio ?? this.config.pixelRatio ?? 2;
13547
+ const canvasWidth = templateConfig.canvas.width;
13548
+ const canvasHeight = templateConfig.canvas.height;
13549
+ const page = templateConfig.pages[pageIndex];
13550
+ if (!page) {
13551
+ throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
13552
+ }
13553
+ await ensureFontsForResolvedConfig(templateConfig);
13554
+ if (!options.skipFontReadyWait) {
13555
+ const hasAutoShrink = configHasAutoShrinkText(templateConfig);
13556
+ const defaultWait = hasAutoShrink ? 4e3 : 1800;
13557
+ await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
13558
+ }
13559
+ const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
13560
+ setPackageApiUrl2(this.config.imageProxyUrl);
13561
+ const dataUrl = await this.renderPageViaPreviewCanvas(
13562
+ templateConfig,
13563
+ pageIndex,
13564
+ pixelRatio,
13565
+ format,
13566
+ quality,
13567
+ { skipFontReadyWait: options.skipFontReadyWait, waitForFontsMs: options.waitForFontsMs }
13568
+ );
13569
+ return {
13570
+ dataUrl,
13571
+ width: canvasWidth,
13572
+ height: canvasHeight,
13573
+ pixelWidth: canvasWidth * pixelRatio,
13574
+ pixelHeight: canvasHeight * pixelRatio
13575
+ };
13576
+ }
13577
+ /**
13578
+ * Render all pages and return array of results.
13579
+ */
13580
+ async renderAllPages(templateConfig, options = {}) {
13581
+ if (!options.skipFontReadyWait) {
13582
+ const hasAutoShrink = configHasAutoShrinkText(templateConfig);
13583
+ const defaultWait = hasAutoShrink ? 4e3 : 1800;
13584
+ await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
13585
+ }
13586
+ const results = [];
13587
+ for (let i = 0; i < templateConfig.pages.length; i++) {
13588
+ results.push(await this.render(templateConfig, { ...options, pageIndex: i, skipFontReadyWait: true }));
13589
+ }
13590
+ return results;
13591
+ }
13592
+ /**
13593
+ * Resolve from V2 sectionState (like the server API) and render all pages.
13594
+ * This is the primary external API for the package.
13595
+ */
13596
+ async renderFromForm(options) {
13597
+ const { templateId, formSchemaId, sectionState, themeId, watermark, watermarkOptions, prefetched, ...renderOpts } = options;
13598
+ const resolved = await resolveFromForm({
13599
+ templateId,
13600
+ formSchemaId,
13601
+ sectionState,
13602
+ themeId,
13603
+ supabaseUrl: this.config.supabaseUrl,
13604
+ supabaseAnonKey: this.config.supabaseAnonKey,
13605
+ prefetched
13606
+ });
13607
+ const shouldWatermark = watermark ?? resolved.price > 0;
13608
+ let configToRender = resolved.config;
13609
+ if (shouldWatermark) {
13610
+ const { injectWatermark } = await import("./canvasWatermark-pkhacGge.js");
13611
+ configToRender = injectWatermark(configToRender, watermarkOptions);
13612
+ }
13613
+ return this.renderAllPages(configToRender, renderOpts);
13614
+ }
13615
+ /**
13616
+ * Render a page and capture the Fabric canvas SVG output (vector, not raster).
13617
+ * This is the key building block for client-side vector PDF export.
13618
+ */
13619
+ async renderPageSvg(templateConfig, pageIndex = 0) {
13620
+ const page = templateConfig.pages[pageIndex];
13621
+ if (!page) {
13622
+ throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
13623
+ }
13624
+ await ensureFontsForResolvedConfig(templateConfig);
13625
+ const hasAutoShrinkSvg = configHasAutoShrinkText(templateConfig);
13626
+ await this.awaitFontsForConfig(templateConfig, hasAutoShrinkSvg ? 4e3 : 1800);
13627
+ const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
13628
+ setPackageApiUrl2(this.config.imageProxyUrl);
13629
+ const canvasWidth = templateConfig.canvas.width;
13630
+ const canvasHeight = templateConfig.canvas.height;
13631
+ return this.captureSvgViaPreviewCanvas(templateConfig, pageIndex, canvasWidth, canvasHeight);
13632
+ }
13633
+ /**
13634
+ * Render all pages and return SVG strings for each.
13635
+ */
13636
+ async renderAllPageSvgs(templateConfig) {
13637
+ await ensureFontsForResolvedConfig(templateConfig);
13638
+ const hasAutoShrinkSvg = configHasAutoShrinkText(templateConfig);
13639
+ await this.awaitFontsForConfig(templateConfig, hasAutoShrinkSvg ? 4e3 : 1800);
13640
+ const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
13641
+ setPackageApiUrl2(this.config.imageProxyUrl);
13642
+ const results = [];
13643
+ for (let i = 0; i < templateConfig.pages.length; i++) {
13644
+ const canvasWidth = templateConfig.canvas.width;
13645
+ const canvasHeight = templateConfig.canvas.height;
13646
+ results.push(await this.captureSvgViaPreviewCanvas(templateConfig, i, canvasWidth, canvasHeight));
13647
+ }
13648
+ return results;
13649
+ }
13650
+ /**
13651
+ * Resolve from V2 sectionState and return SVGs for all pages (for server vector PDF).
13652
+ */
13653
+ async renderSvgsFromForm(options) {
13654
+ const { templateId, formSchemaId, sectionState, themeId, watermark, watermarkOptions, prefetched } = options;
13655
+ const resolved = await resolveFromForm({
13656
+ templateId,
13657
+ formSchemaId,
13658
+ sectionState,
13659
+ themeId,
13660
+ supabaseUrl: this.config.supabaseUrl,
13661
+ supabaseAnonKey: this.config.supabaseAnonKey,
13662
+ prefetched
13663
+ });
13664
+ const shouldWatermark = watermark ?? resolved.price > 0;
13665
+ let configToRender = resolved.config;
13666
+ if (shouldWatermark) {
13667
+ const { injectWatermark } = await import("./canvasWatermark-pkhacGge.js");
13668
+ configToRender = injectWatermark(configToRender, watermarkOptions);
13669
+ }
13670
+ return this.renderAllPageSvgs(configToRender);
13671
+ }
13672
+ /**
13673
+ * Render a pre-resolved template config to a vector PDF.
13674
+ * Returns a Blob and ArrayBuffer.
13675
+ */
13676
+ async renderPdf(templateConfig, options) {
13677
+ const svgs = await this.renderAllPageSvgs(templateConfig);
13678
+ const { assemblePdfFromSvgs: assemblePdfFromSvgs2 } = await Promise.resolve().then(() => pdfExport);
13679
+ return assemblePdfFromSvgs2(svgs, { title: options == null ? void 0 : options.title, fontBaseUrl: options == null ? void 0 : options.fontBaseUrl });
13680
+ }
13681
+ /**
13682
+ * Resolve from V2 sectionState and render a vector PDF.
13683
+ * This is the primary PDF export API — mirrors renderFromForm() but returns a PDF.
13684
+ */
13685
+ async renderPdfFromForm(options) {
13686
+ const { templateId, formSchemaId, sectionState, themeId, watermark, watermarkOptions, prefetched, title, fontBaseUrl } = options;
13687
+ const resolved = await resolveFromForm({
13688
+ templateId,
13689
+ formSchemaId,
13690
+ sectionState,
13691
+ themeId,
13692
+ supabaseUrl: this.config.supabaseUrl,
13693
+ supabaseAnonKey: this.config.supabaseAnonKey,
13694
+ prefetched
13695
+ });
13696
+ const shouldWatermark = watermark ?? resolved.price > 0;
13697
+ let configToRender = resolved.config;
13698
+ if (shouldWatermark) {
13699
+ const { injectWatermark } = await import("./canvasWatermark-pkhacGge.js");
13700
+ configToRender = injectWatermark(configToRender, watermarkOptions);
13701
+ }
13702
+ const svgs = await this.renderAllPageSvgs(configToRender);
13703
+ const { assemblePdfFromSvgs: assemblePdfFromSvgs2 } = await Promise.resolve().then(() => pdfExport);
13704
+ return assemblePdfFromSvgs2(svgs, { title: title ?? resolved.config.name, fontBaseUrl });
13705
+ }
13706
+ async renderById(templateId, formData, options) {
13707
+ const resolved = await resolveTemplateData({
13708
+ templateId,
13709
+ formData,
13710
+ supabaseUrl: this.config.supabaseUrl,
13711
+ supabaseAnonKey: this.config.supabaseAnonKey
13712
+ });
13713
+ return this.render(resolved.config, options);
13714
+ }
13715
+ /**
13716
+ * Convenience: fetch by ID with flat data and render ALL pages.
13717
+ */
13718
+ async renderAllById(templateId, formData, options) {
13719
+ const resolved = await resolveTemplateData({
13720
+ templateId,
13721
+ formData,
13722
+ supabaseUrl: this.config.supabaseUrl,
13723
+ supabaseAnonKey: this.config.supabaseAnonKey
13724
+ });
13725
+ return this.renderAllPages(resolved.config, options);
13726
+ }
13727
+ // ─── Internal: render a page using the full PreviewCanvas engine ───
13728
+ getExpectedImageCount(config, pageIndex) {
13729
+ const page = config.pages[pageIndex];
13730
+ if (!(page == null ? void 0 : page.children)) return 0;
13731
+ let count = 0;
13732
+ const walk = (nodes) => {
13733
+ for (const node of nodes) {
13734
+ if (!node || node.visible === false) continue;
13735
+ const src = typeof node.src === "string" ? node.src.trim() : "";
13736
+ const imageUrl = typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
13737
+ if (node.type === "image" && (src || imageUrl)) count += 1;
13738
+ if (Array.isArray(node.children) && node.children.length > 0) {
13739
+ walk(node.children);
13740
+ }
13741
+ }
13742
+ };
13743
+ walk(page.children);
13744
+ return count;
13745
+ }
13746
+ waitForCanvasImages(container, expectedImageCount, maxWaitMs = 15e3, pollMs = 120) {
13747
+ return new Promise((resolve) => {
13748
+ const start = Date.now();
13749
+ let stableFrames = 0;
13750
+ let lastSummary = "";
13751
+ const isRenderableImage = (value) => value instanceof HTMLImageElement && value.complete && value.naturalWidth > 0 && value.naturalHeight > 0;
13752
+ const collectRenderableImages = (obj, seen) => {
13753
+ if (!obj || typeof obj !== "object") return;
13754
+ const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
13755
+ for (const candidate of candidates) {
13756
+ if (isRenderableImage(candidate)) {
13757
+ seen.add(candidate);
13758
+ } else if (candidate instanceof HTMLImageElement) {
13759
+ return false;
13760
+ }
13761
+ }
13762
+ const nested = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
13763
+ for (const child of nested) {
13764
+ if (collectRenderableImages(child, seen) === false) return false;
13765
+ }
13766
+ return true;
13767
+ };
13768
+ const getFabricCanvas = () => {
13769
+ const registry2 = window.__fabricCanvasRegistry;
13770
+ if (registry2 instanceof Map) {
13771
+ for (const entry of registry2.values()) {
13772
+ const canvas = entry == null ? void 0 : entry.canvas;
13773
+ const el = (canvas == null ? void 0 : canvas.lowerCanvasEl) || (canvas == null ? void 0 : canvas.upperCanvasEl);
13774
+ if (el && container.contains(el)) return canvas;
13775
+ }
13776
+ }
13777
+ return null;
13778
+ };
13779
+ const settle = () => requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
13780
+ const getImageDebugInfo = (obj, bucket) => {
13781
+ if (!obj || typeof obj !== "object") return;
13782
+ const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
13783
+ for (const candidate of candidates) {
13784
+ if (candidate instanceof HTMLImageElement) {
13785
+ bucket.push({
13786
+ id: obj.__docuforgeId || obj.id || "unknown",
13787
+ src: (candidate.currentSrc || candidate.src || "").slice(0, 240),
13788
+ complete: candidate.complete,
13789
+ naturalWidth: candidate.naturalWidth,
13790
+ naturalHeight: candidate.naturalHeight
13791
+ });
13792
+ }
13793
+ }
13794
+ const nested = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
13795
+ for (const child of nested) {
13796
+ getImageDebugInfo(child, bucket);
13797
+ }
13798
+ };
13799
+ const check = () => {
13800
+ const elapsed = Date.now() - start;
13801
+ const domImages = Array.from(container.querySelectorAll("img"));
13802
+ const allDomLoaded = domImages.every((img) => img.complete && img.naturalWidth > 0 && img.naturalHeight > 0);
13803
+ const fabricCanvas = getFabricCanvas();
13804
+ const fabricObjects = fabricCanvas && typeof fabricCanvas.getObjects === "function" ? fabricCanvas.getObjects() : [];
13805
+ const renderableImages = /* @__PURE__ */ new Set();
13806
+ const fabricReady = fabricObjects.every((obj) => collectRenderableImages(obj, renderableImages) !== false);
13807
+ const actualImageCount = Math.max(domImages.length, renderableImages.size);
13808
+ const canvasReady = !!fabricCanvas && !!(fabricCanvas.lowerCanvasEl || fabricCanvas.upperCanvasEl);
13809
+ const hasExpectedAssets = expectedImageCount === 0 ? true : actualImageCount >= expectedImageCount;
13810
+ const ready = allDomLoaded && fabricReady && hasExpectedAssets;
13811
+ const summary = `expected=${expectedImageCount} actual=${actualImageCount} dom=${domImages.length} fabricReady=${fabricReady} domReady=${allDomLoaded} canvasReady=${canvasReady}`;
13812
+ if (summary !== lastSummary) {
13813
+ lastSummary = summary;
13814
+ console.log(`[canvas-renderer][asset-wait] ${summary}`);
13815
+ }
13816
+ if (ready) {
13817
+ stableFrames += 1;
13818
+ if (stableFrames >= 2) {
13819
+ console.log(`[canvas-renderer][asset-wait] ready after ${elapsed}ms (${summary})`);
13820
+ settle();
13821
+ return;
13822
+ }
13823
+ } else {
13824
+ stableFrames = 0;
13825
+ }
13826
+ if (elapsed >= maxWaitMs) {
13827
+ const fabricImageDebug = [];
13828
+ for (const obj of fabricObjects) {
13829
+ getImageDebugInfo(obj, fabricImageDebug);
13830
+ }
13831
+ const domImageDebug = domImages.map((img, index) => ({
13832
+ index,
13833
+ src: (img.currentSrc || img.src || "").slice(0, 240),
13834
+ complete: img.complete,
13835
+ naturalWidth: img.naturalWidth,
13836
+ naturalHeight: img.naturalHeight
13837
+ }));
13838
+ console.warn(`[canvas-renderer][asset-wait-timeout] elapsed=${elapsed}ms ${summary}`);
13839
+ console.warn("[canvas-renderer][asset-wait-timeout][dom-images]", domImageDebug);
13840
+ console.warn("[canvas-renderer][asset-wait-timeout][fabric-images]", fabricImageDebug);
13841
+ settle();
13842
+ return;
13843
+ }
13844
+ setTimeout(check, pollMs);
13845
+ };
13846
+ setTimeout(check, 0);
13847
+ });
13848
+ }
13849
+ waitForCanvasScene(container, config, pageIndex, maxWaitMs = 8e3, pollMs = 50) {
13850
+ return new Promise((resolve) => {
13851
+ var _a, _b;
13852
+ const start = Date.now();
13853
+ const pageHasContent = (((_b = (_a = config.pages[pageIndex]) == null ? void 0 : _a.children) == null ? void 0 : _b.length) ?? 0) > 0;
13854
+ const settle = () => requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
13855
+ const check = () => {
13856
+ const fabricCanvas = this.getFabricCanvasFromContainer(container);
13857
+ const lowerCanvas = (fabricCanvas == null ? void 0 : fabricCanvas.lowerCanvasEl) || container.querySelector("canvas.lower-canvas, canvas");
13858
+ const objectCount = typeof (fabricCanvas == null ? void 0 : fabricCanvas.getObjects) === "function" ? fabricCanvas.getObjects().length : 0;
13859
+ const ready = !!lowerCanvas && (!pageHasContent || objectCount > 0);
13860
+ if (ready) {
13861
+ console.log(`[canvas-renderer][scene-wait] ready after ${Date.now() - start}ms (objects=${objectCount})`);
13862
+ settle();
13863
+ return;
13864
+ }
13865
+ if (Date.now() - start >= maxWaitMs) {
13866
+ console.warn(`[canvas-renderer][scene-wait-timeout] elapsed=${Date.now() - start}ms objects=${objectCount} pageHasContent=${pageHasContent}`);
13867
+ settle();
13868
+ return;
13869
+ }
13870
+ setTimeout(check, pollMs);
13871
+ };
13872
+ setTimeout(check, 0);
13873
+ });
14009
13874
  }
14010
- return files.regular;
14011
- }
14012
- function isItalicPath(files, path) {
14013
- return path === files.italic || path === files.boldItalic || path === files.lightItalic || path === files.mediumItalic || path === files.semiboldItalic;
14014
- }
14015
- function getJsPDFFontName(fontName) {
14016
- return fontName.replace(/\s+/g, "");
14017
- }
14018
- function getEmbeddedJsPDFFontName(fontName, weight, isItalic = false) {
14019
- const resolved = resolveFontWeight(weight);
14020
- const label = FONT_WEIGHT_LABELS[resolved];
14021
- const italicSuffix = isItalic ? "Italic" : "";
14022
- return `${getJsPDFFontName(fontName)}-${label}${italicSuffix}`;
14023
- }
14024
- function isFontAvailable(fontName) {
14025
- return fontName in FONT_FILES;
14026
- }
14027
- const ttfCache = /* @__PURE__ */ new Map();
14028
- async function fetchTTFAsBase64(url) {
14029
- const cached = ttfCache.get(url);
14030
- if (cached) return cached;
14031
- try {
14032
- const res = await fetch(url);
14033
- if (!res.ok) return null;
14034
- const buf = await res.arrayBuffer();
14035
- const bytes = new Uint8Array(buf);
14036
- let binary = "";
14037
- for (let i = 0; i < bytes.length; i++) {
14038
- binary += String.fromCharCode(bytes[i]);
13875
+ async waitForRelevantFonts(config, maxWaitMs = 1800) {
13876
+ if (typeof document === "undefined" || !document.fonts) return;
13877
+ const descriptors = collectFontDescriptorsFromConfig(config);
13878
+ if (descriptors.length === 0) return;
13879
+ const loads = Promise.all(
13880
+ descriptors.map((descriptor) => {
13881
+ const stylePrefix = descriptor.style === "italic" ? "italic " : "";
13882
+ const spec = `${stylePrefix}${descriptor.weight} 16px "${descriptor.family}"`;
13883
+ return document.fonts.load(spec).catch(() => []);
13884
+ })
13885
+ ).then(() => void 0);
13886
+ await Promise.race([
13887
+ loads,
13888
+ new Promise((resolve) => setTimeout(resolve, maxWaitMs))
13889
+ ]);
13890
+ await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
13891
+ }
13892
+ /**
13893
+ * Block until the webfonts referenced by `config` have actually loaded
13894
+ * (or `maxWaitMs` elapses). Used by the headless capture path BEFORE
13895
+ * mounting `PreviewCanvas`, so the synchronous `createText` auto-shrink
13896
+ * loop measures against final font metrics instead of fallback ones.
13897
+ *
13898
+ * Stronger than `ensureFontsForResolvedConfig` (which is fire-and-forget)
13899
+ * this awaits each `document.fonts.load(spec)` AND `document.fonts.ready`,
13900
+ * racing the whole thing against `maxWaitMs` so a slow CDN can't hang the
13901
+ * renderer.
13902
+ */
13903
+ async awaitFontsForConfig(config, maxWaitMs) {
13904
+ if (typeof document === "undefined" || !document.fonts) return;
13905
+ await ensureFontsForResolvedConfig(config);
13906
+ await this.waitForRelevantFonts(config, maxWaitMs);
13907
+ await Promise.race([
13908
+ document.fonts.ready.catch(() => void 0).then(() => void 0),
13909
+ new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
13910
+ ]);
13911
+ }
13912
+ getNormalizedGradientStops(gradient) {
13913
+ const stops = Array.isArray(gradient == null ? void 0 : gradient.stops) ? gradient.stops.map((stop) => ({
13914
+ offset: Math.max(0, Math.min(1, Number((stop == null ? void 0 : stop.offset) ?? 0))),
13915
+ color: String((stop == null ? void 0 : stop.color) ?? "#ffffff")
13916
+ })).filter((stop) => Number.isFinite(stop.offset)).sort((a, b) => a.offset - b.offset) : [];
13917
+ if (stops.length === 0) return [];
13918
+ const normalized = [...stops];
13919
+ if (normalized[0].offset > 0) {
13920
+ normalized.unshift({ offset: 0, color: normalized[0].color });
14039
13921
  }
14040
- const b64 = btoa(binary);
14041
- ttfCache.set(url, b64);
14042
- return b64;
14043
- } catch {
14044
- return null;
13922
+ if (normalized[normalized.length - 1].offset < 1) {
13923
+ normalized.push({ offset: 1, color: normalized[normalized.length - 1].color });
13924
+ }
13925
+ return normalized;
14045
13926
  }
14046
- }
14047
- async function embedFont(pdf, fontName, weight, fontBaseUrl, isItalic = false) {
14048
- const fontFiles = FONT_FILES[fontName];
14049
- if (!fontFiles) return false;
14050
- const baseUrl = fontBaseUrl.endsWith("/") ? fontBaseUrl : fontBaseUrl + "/";
14051
- const resolvedWeight = resolveFontWeight(weight);
14052
- const fontPath = getFontPathForWeight(fontFiles, resolvedWeight, isItalic);
14053
- if (!fontPath) return false;
14054
- const hasItalicFile = isItalic && isItalicPath(fontFiles, fontPath);
14055
- const jsPdfFontName = getEmbeddedJsPDFFontName(fontName, weight, hasItalicFile);
14056
- const label = FONT_WEIGHT_LABELS[resolvedWeight];
14057
- const italicSuffix = hasItalicFile ? "Italic" : "";
14058
- const fileName = `${getJsPDFFontName(fontName)}-${label}${italicSuffix}.ttf`;
14059
- const url = baseUrl + fontPath;
14060
- try {
14061
- const b64 = await fetchTTFAsBase64(url);
14062
- if (!b64) return false;
14063
- pdf.addFileToVFS(fileName, b64);
14064
- pdf.addFont(fileName, jsPdfFontName, "normal");
14065
- if (fontName !== jsPdfFontName) {
14066
- try {
14067
- pdf.addFont(fileName, fontName, "normal");
14068
- } catch {
13927
+ paintPageBackground(ctx, page, width, height) {
13928
+ var _a, _b;
13929
+ const backgroundColor = ((_a = page == null ? void 0 : page.settings) == null ? void 0 : _a.backgroundColor) || "#ffffff";
13930
+ const gradient = (_b = page == null ? void 0 : page.settings) == null ? void 0 : _b.backgroundGradient;
13931
+ ctx.clearRect(0, 0, width, height);
13932
+ ctx.fillStyle = backgroundColor;
13933
+ ctx.fillRect(0, 0, width, height);
13934
+ const stops = this.getNormalizedGradientStops(gradient);
13935
+ if (stops.length < 2) return;
13936
+ try {
13937
+ let canvasGradient = null;
13938
+ if ((gradient == null ? void 0 : gradient.type) === "radial") {
13939
+ const cx = Number.isFinite(gradient == null ? void 0 : gradient.cx) ? gradient.cx : 0.5;
13940
+ const cy = Number.isFinite(gradient == null ? void 0 : gradient.cy) ? gradient.cy : 0.5;
13941
+ const centerX = width * cx;
13942
+ const centerY = height * cy;
13943
+ const radius = Math.max(
13944
+ Math.hypot(centerX, centerY),
13945
+ Math.hypot(width - centerX, centerY),
13946
+ Math.hypot(centerX, height - centerY),
13947
+ Math.hypot(width - centerX, height - centerY)
13948
+ );
13949
+ canvasGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
13950
+ } else if ((gradient == null ? void 0 : gradient.type) === "conic" && typeof ctx.createConicGradient === "function") {
13951
+ const cx = Number.isFinite(gradient == null ? void 0 : gradient.cx) ? gradient.cx : 0.5;
13952
+ const cy = Number.isFinite(gradient == null ? void 0 : gradient.cy) ? gradient.cy : 0.5;
13953
+ const startAngle = (((gradient == null ? void 0 : gradient.angle) ?? 0) - 90) * Math.PI / 180;
13954
+ canvasGradient = ctx.createConicGradient(startAngle, width * cx, height * cy);
13955
+ } else {
13956
+ const angleDeg = (gradient == null ? void 0 : gradient.angle) ?? 90;
13957
+ const angleRad = angleDeg * Math.PI / 180;
13958
+ const sinA = Math.sin(angleRad);
13959
+ const cosA = Math.cos(angleRad);
13960
+ const midX = width / 2;
13961
+ const midY = height / 2;
13962
+ const corners = [
13963
+ [0, 0],
13964
+ [width, 0],
13965
+ [width, height],
13966
+ [0, height]
13967
+ ];
13968
+ const projections = corners.map(([x, y]) => x * sinA - y * cosA);
13969
+ const minProjection = Math.min(...projections);
13970
+ const maxProjection = Math.max(...projections);
13971
+ canvasGradient = ctx.createLinearGradient(
13972
+ midX + minProjection * sinA,
13973
+ midY - minProjection * cosA,
13974
+ midX + maxProjection * sinA,
13975
+ midY - maxProjection * cosA
13976
+ );
14069
13977
  }
13978
+ if (!canvasGradient) return;
13979
+ stops.forEach((stop) => canvasGradient.addColorStop(stop.offset, stop.color));
13980
+ ctx.fillStyle = canvasGradient;
13981
+ ctx.fillRect(0, 0, width, height);
13982
+ } catch {
14070
13983
  }
14071
- return true;
14072
- } catch (e) {
14073
- console.warn(`[pdf-fonts] Failed to embed ${fontName} w${weight}:`, e);
14074
- return false;
14075
13984
  }
14076
- }
14077
- async function embedFontsForConfig(pdf, config, fontBaseUrl) {
14078
- const fontKeys = /* @__PURE__ */ new Set();
14079
- const SEP = "";
14080
- const walkElements = (elements) => {
14081
- for (const el of elements) {
14082
- if (el.fontFamily) {
14083
- const w = resolveFontWeight(el.fontWeight ?? 400);
14084
- fontKeys.add(`${el.fontFamily}${SEP}${w}`);
14085
- }
14086
- if (el.styles && typeof el.styles === "object") {
14087
- for (const lineKey of Object.keys(el.styles)) {
14088
- const lineStyles = el.styles[lineKey];
14089
- if (lineStyles && typeof lineStyles === "object") {
14090
- for (const charKey of Object.keys(lineStyles)) {
14091
- const s = lineStyles[charKey];
14092
- if (s == null ? void 0 : s.fontFamily) {
14093
- const w = resolveFontWeight(s.fontWeight ?? 400);
14094
- fontKeys.add(`${s.fontFamily}${SEP}${w}`);
14095
- }
13985
+ async renderPageViaPreviewCanvas(config, pageIndex, pixelRatio, format, quality, options = {}) {
13986
+ const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
13987
+ const canvasWidth = config.canvas.width;
13988
+ const canvasHeight = config.canvas.height;
13989
+ const hasAutoShrink = configHasAutoShrinkText(config);
13990
+ let firstMountSettled = false;
13991
+ let lateFontSettleDetected = false;
13992
+ if (typeof document !== "undefined" && document.fonts && hasAutoShrink) {
13993
+ document.fonts.ready.then(() => {
13994
+ if (firstMountSettled) lateFontSettleDetected = true;
13995
+ }).catch(() => {
13996
+ });
13997
+ }
13998
+ return new Promise((resolve, reject) => {
13999
+ const container = document.createElement("div");
14000
+ container.style.cssText = `
14001
+ position: fixed; left: -99999px; top: -99999px;
14002
+ width: ${canvasWidth}px; height: ${canvasHeight}px;
14003
+ overflow: hidden; pointer-events: none; opacity: 0;
14004
+ `;
14005
+ document.body.appendChild(container);
14006
+ const timeout = setTimeout(() => {
14007
+ cleanup();
14008
+ reject(new Error("Render timeout (30s)"));
14009
+ }, 3e4);
14010
+ let root;
14011
+ let mountKey = 0;
14012
+ const cleanup = () => {
14013
+ clearTimeout(timeout);
14014
+ try {
14015
+ root.unmount();
14016
+ } catch {
14017
+ }
14018
+ container.remove();
14019
+ };
14020
+ const remountWithFreshKey = async () => {
14021
+ mountKey += 1;
14022
+ try {
14023
+ clearMeasurementCache();
14024
+ } catch {
14025
+ }
14026
+ try {
14027
+ clearFabricCharCache();
14028
+ } catch {
14029
+ }
14030
+ try {
14031
+ root.unmount();
14032
+ } catch {
14033
+ }
14034
+ root = createRoot(container);
14035
+ await new Promise((settle) => {
14036
+ const onReadyOnce = () => {
14037
+ this.waitForCanvasScene(container, config, pageIndex).then(async () => {
14038
+ const fabricInstance = this.getFabricCanvasFromContainer(container);
14039
+ const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
14040
+ await this.waitForCanvasImages(container, expectedImageCount);
14041
+ await this.waitForStableTextMetrics(container, config);
14042
+ await this.waitForCanvasScene(container, config, pageIndex);
14043
+ if (!fabricInstance) return settle();
14044
+ settle();
14045
+ }).catch(() => settle());
14046
+ };
14047
+ root.render(
14048
+ createElement(PreviewCanvas2, {
14049
+ key: `remount-${mountKey}`,
14050
+ config,
14051
+ pageIndex,
14052
+ zoom: pixelRatio,
14053
+ absoluteZoom: true,
14054
+ skipFontReadyWait: false,
14055
+ onReady: onReadyOnce
14056
+ })
14057
+ );
14058
+ });
14059
+ };
14060
+ const onReady = () => {
14061
+ this.waitForCanvasScene(container, config, pageIndex).then(async () => {
14062
+ try {
14063
+ const fabricInstance = this.getFabricCanvasFromContainer(container);
14064
+ const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
14065
+ await this.waitForCanvasImages(container, expectedImageCount);
14066
+ await this.waitForStableTextMetrics(container, config);
14067
+ await this.waitForCanvasScene(container, config, pageIndex);
14068
+ firstMountSettled = true;
14069
+ if (hasAutoShrink && lateFontSettleDetected) {
14070
+ console.log("[canvas-renderer][parity] late font-settle detected — remounting for auto-shrink reflow");
14071
+ await remountWithFreshKey();
14072
+ }
14073
+ const fabricCanvas = container.querySelector("canvas.upper-canvas, canvas");
14074
+ const sourceCanvas = (fabricInstance == null ? void 0 : fabricInstance.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || fabricCanvas;
14075
+ const fabricInstanceAfter = this.getFabricCanvasFromContainer(container) || fabricInstance;
14076
+ const sourceCanvasAfter = (fabricInstanceAfter == null ? void 0 : fabricInstanceAfter.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || sourceCanvas;
14077
+ if (!sourceCanvas) {
14078
+ cleanup();
14079
+ reject(new Error("No canvas element found after render"));
14080
+ return;
14081
+ }
14082
+ const exportCanvas = document.createElement("canvas");
14083
+ exportCanvas.width = sourceCanvasAfter.width;
14084
+ exportCanvas.height = sourceCanvasAfter.height;
14085
+ const exportCtx = exportCanvas.getContext("2d");
14086
+ if (!exportCtx) {
14087
+ cleanup();
14088
+ reject(new Error("Failed to create export canvas"));
14089
+ return;
14096
14090
  }
14091
+ exportCtx.save();
14092
+ exportCtx.scale(sourceCanvasAfter.width / canvasWidth, sourceCanvasAfter.height / canvasHeight);
14093
+ this.paintPageBackground(exportCtx, config.pages[pageIndex], canvasWidth, canvasHeight);
14094
+ exportCtx.restore();
14095
+ exportCtx.drawImage(sourceCanvasAfter, 0, 0);
14096
+ const mimeType = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
14097
+ const dataUrl = exportCanvas.toDataURL(mimeType, quality);
14098
+ cleanup();
14099
+ resolve(dataUrl);
14100
+ } catch (err) {
14101
+ cleanup();
14102
+ reject(err);
14097
14103
  }
14098
- }
14099
- }
14100
- if (el.children) walkElements(el.children);
14101
- if (el.objects) walkElements(el.objects);
14102
- }
14103
- };
14104
- for (const page of (config == null ? void 0 : config.pages) || []) {
14105
- if (page.children) walkElements(page.children);
14106
- if (page.elements) walkElements(page.elements);
14107
- }
14108
- fontKeys.add(`${FONT_FALLBACK_SYMBOLS}${SEP}400`);
14109
- for (const w of [300, 400, 500, 600, 700]) {
14110
- fontKeys.add(`${FONT_FALLBACK_DEVANAGARI}${SEP}${w}`);
14111
- }
14112
- const embedded = /* @__PURE__ */ new Set();
14113
- const tasks = [];
14114
- for (const key of fontKeys) {
14115
- const sep = key.indexOf(SEP);
14116
- const fontName = key.slice(0, sep);
14117
- const weight = parseInt(key.slice(sep + 1), 10);
14118
- if (!isFontAvailable(fontName)) continue;
14119
- tasks.push(
14120
- embedFont(pdf, fontName, weight, fontBaseUrl).then((ok) => {
14121
- if (ok) embedded.add(key);
14122
- })
14123
- );
14124
- }
14125
- await Promise.all(tasks);
14126
- console.log(`[pdf-fonts] Embedded ${embedded.size} font variants from config`);
14127
- return embedded;
14128
- }
14129
- function isDevanagari(char) {
14130
- const c = char.codePointAt(0) ?? 0;
14131
- return c >= 2304 && c <= 2431 || c >= 43232 && c <= 43263 || c >= 7376 && c <= 7423;
14132
- }
14133
- function containsDevanagari(text) {
14134
- if (!text) return false;
14135
- for (const char of text) {
14136
- if (isDevanagari(char)) return true;
14104
+ });
14105
+ };
14106
+ root = createRoot(container);
14107
+ root.render(
14108
+ createElement(PreviewCanvas2, {
14109
+ config,
14110
+ pageIndex,
14111
+ zoom: pixelRatio,
14112
+ absoluteZoom: true,
14113
+ skipFontReadyWait: false,
14114
+ onReady
14115
+ })
14116
+ );
14117
+ });
14137
14118
  }
14138
- return false;
14139
- }
14140
- function isBasicLatinOrLatin1(char) {
14141
- const c = char.codePointAt(0) ?? 0;
14142
- return c <= 591;
14143
- }
14144
- function classifyChar(char) {
14145
- if (isBasicLatinOrLatin1(char)) return "main";
14146
- if (isDevanagari(char)) return "devanagari";
14147
- return "symbol";
14148
- }
14149
- function splitIntoRuns(text) {
14150
- if (!text) return [];
14151
- const runs = [];
14152
- let currentType = null;
14153
- let currentText = "";
14154
- for (const char of text) {
14155
- const type = classifyChar(char);
14156
- if (type !== currentType && currentText) {
14157
- runs.push({ text: currentText, runType: currentType });
14158
- currentText = "";
14159
- }
14160
- currentType = type;
14161
- currentText += char;
14119
+ // ─── Internal: capture SVG from a rendered Fabric canvas ───
14120
+ //
14121
+ // APPROACH: Use the SAME PreviewCanvas that renders perfect PNGs, then call
14122
+ // Fabric's toSVG() on that canvas. This guarantees 100% layout parity —
14123
+ // the SVG is a vector snapshot of exactly what's on screen.
14124
+ //
14125
+ // The trick: before calling toSVG(), we temporarily neutralize the viewport
14126
+ // transform and retina scaling so Fabric emits coordinates in logical
14127
+ // document space (e.g. 612x792) instead of inflated pixel space.
14128
+ captureSvgViaPreviewCanvas(config, pageIndex, canvasWidth, canvasHeight) {
14129
+ return new Promise(async (resolve, reject) => {
14130
+ const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
14131
+ const hasAutoShrink = configHasAutoShrinkText(config);
14132
+ const container = document.createElement("div");
14133
+ container.style.cssText = `
14134
+ position: fixed; left: -99999px; top: -99999px;
14135
+ width: ${canvasWidth}px; height: ${canvasHeight}px;
14136
+ overflow: hidden; pointer-events: none; opacity: 0;
14137
+ `;
14138
+ document.body.appendChild(container);
14139
+ const timeout = setTimeout(() => {
14140
+ cleanup();
14141
+ reject(new Error("SVG render timeout (30s)"));
14142
+ }, 3e4);
14143
+ let root = null;
14144
+ let mountKey = 0;
14145
+ let didPreviewParityRemount = false;
14146
+ const cleanup = () => {
14147
+ clearTimeout(timeout);
14148
+ try {
14149
+ root == null ? void 0 : root.unmount();
14150
+ } catch {
14151
+ }
14152
+ container.remove();
14153
+ };
14154
+ const mountPreview = (readyHandler) => {
14155
+ root = createRoot(container);
14156
+ root.render(
14157
+ createElement(PreviewCanvas2, {
14158
+ key: `svg-capture-${mountKey}`,
14159
+ config,
14160
+ pageIndex,
14161
+ zoom: 1,
14162
+ absoluteZoom: true,
14163
+ skipFontReadyWait: false,
14164
+ onReady: readyHandler
14165
+ })
14166
+ );
14167
+ };
14168
+ const remountForPreviewParity = async () => {
14169
+ if (didPreviewParityRemount) return;
14170
+ didPreviewParityRemount = true;
14171
+ mountKey += 1;
14172
+ try {
14173
+ clearMeasurementCache();
14174
+ } catch {
14175
+ }
14176
+ try {
14177
+ clearFabricCharCache();
14178
+ } catch {
14179
+ }
14180
+ try {
14181
+ root == null ? void 0 : root.unmount();
14182
+ } catch {
14183
+ }
14184
+ await new Promise((settle) => {
14185
+ mountPreview(() => {
14186
+ this.waitForCanvasScene(container, config, pageIndex).then(async () => {
14187
+ const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
14188
+ await this.waitForCanvasImages(container, expectedImageCount);
14189
+ await this.waitForStableTextMetrics(container, config);
14190
+ await this.waitForCanvasScene(container, config, pageIndex);
14191
+ settle();
14192
+ }).catch(() => settle());
14193
+ });
14194
+ });
14195
+ };
14196
+ const onReady = () => {
14197
+ this.waitForCanvasScene(container, config, pageIndex).then(async () => {
14198
+ var _a, _b;
14199
+ try {
14200
+ const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
14201
+ await this.waitForCanvasImages(container, expectedImageCount);
14202
+ await this.waitForStableTextMetrics(container, config);
14203
+ await this.waitForCanvasScene(container, config, pageIndex);
14204
+ if (hasAutoShrink && !didPreviewParityRemount) {
14205
+ console.log("[canvas-renderer][svg-parity] remounting once to match PixldocsPreview auto-shrink stabilization");
14206
+ await remountForPreviewParity();
14207
+ }
14208
+ const fabricInstance = this.getFabricCanvasFromContainer(container);
14209
+ if (!fabricInstance) {
14210
+ cleanup();
14211
+ reject(new Error("No Fabric canvas instance found for SVG capture"));
14212
+ return;
14213
+ }
14214
+ const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
14215
+ const prevSvgVPT = fabricInstance.svgViewportTransformation;
14216
+ const prevRetina = fabricInstance.enableRetinaScaling;
14217
+ const prevWidth = fabricInstance.width;
14218
+ const prevHeight = fabricInstance.height;
14219
+ fabricInstance.viewportTransform = [1, 0, 0, 1, 0, 0];
14220
+ fabricInstance.svgViewportTransformation = false;
14221
+ fabricInstance.enableRetinaScaling = false;
14222
+ fabricInstance.setDimensions(
14223
+ { width: canvasWidth, height: canvasHeight },
14224
+ { cssOnly: false, backstoreOnly: false }
14225
+ );
14226
+ const rawSvgString = fabricInstance.toSVG();
14227
+ const svgString = this.normalizeSvgDimensions(
14228
+ rawSvgString,
14229
+ canvasWidth,
14230
+ canvasHeight
14231
+ );
14232
+ fabricInstance.enableRetinaScaling = prevRetina;
14233
+ fabricInstance.setDimensions(
14234
+ { width: prevWidth, height: prevHeight },
14235
+ { cssOnly: false, backstoreOnly: false }
14236
+ );
14237
+ if (prevVPT) fabricInstance.viewportTransform = prevVPT;
14238
+ fabricInstance.svgViewportTransformation = prevSvgVPT;
14239
+ const page = config.pages[pageIndex];
14240
+ const backgroundColor = ((_a = page == null ? void 0 : page.settings) == null ? void 0 : _a.backgroundColor) || "#ffffff";
14241
+ const backgroundGradient = (_b = page == null ? void 0 : page.settings) == null ? void 0 : _b.backgroundGradient;
14242
+ cleanup();
14243
+ resolve({
14244
+ svg: svgString,
14245
+ width: canvasWidth,
14246
+ height: canvasHeight,
14247
+ backgroundColor,
14248
+ backgroundGradient
14249
+ });
14250
+ } catch (err) {
14251
+ cleanup();
14252
+ reject(err);
14253
+ }
14254
+ });
14255
+ };
14256
+ mountPreview(onReady);
14257
+ });
14162
14258
  }
14163
- if (currentText && currentType) {
14164
- runs.push({ text: currentText, runType: currentType });
14259
+ /**
14260
+ * Normalize the SVG's width/height/viewBox to match the logical page dimensions.
14261
+ * Fabric's toSVG() may output dimensions scaled by the canvas element's actual
14262
+ * pixel size (e.g., 2x due to devicePixelRatio), causing svg2pdf to render
14263
+ * content at the wrong scale. This rewrites the root <svg> attributes to ensure
14264
+ * the SVG coordinate system matches the intended page size exactly.
14265
+ */
14266
+ normalizeSvgDimensions(svg, targetWidth, targetHeight) {
14267
+ const widthMatch = svg.match(/<svg[^>]*\bwidth="([^"]+)"/i);
14268
+ const heightMatch = svg.match(/<svg[^>]*\bheight="([^"]+)"/i);
14269
+ const svgWidth = widthMatch ? parseFloat(widthMatch[1]) : targetWidth;
14270
+ const svgHeight = heightMatch ? parseFloat(heightMatch[1]) : targetHeight;
14271
+ console.log(
14272
+ `[canvas-renderer][svg-normalize] root ${svgWidth}x${svgHeight} → page ${targetWidth}x${targetHeight}`
14273
+ );
14274
+ let normalized = svg;
14275
+ if (/\bwidth="[^"]*"/i.test(normalized)) {
14276
+ normalized = normalized.replace(/(<svg[^>]*\b)width="[^"]*"/i, `$1width="${targetWidth}"`);
14277
+ } else {
14278
+ normalized = normalized.replace(/<svg\b/i, `<svg width="${targetWidth}"`);
14279
+ }
14280
+ if (/\bheight="[^"]*"/i.test(normalized)) {
14281
+ normalized = normalized.replace(/(<svg[^>]*\b)height="[^"]*"/i, `$1height="${targetHeight}"`);
14282
+ } else {
14283
+ normalized = normalized.replace(/<svg\b/i, `<svg height="${targetHeight}"`);
14284
+ }
14285
+ const viewBox = `0 0 ${targetWidth} ${targetHeight}`;
14286
+ if (/\bviewBox="[^"]*"/i.test(normalized)) {
14287
+ normalized = normalized.replace(/viewBox="[^"]*"/i, `viewBox="${viewBox}"`);
14288
+ } else {
14289
+ normalized = normalized.replace(/<svg\b/i, `<svg viewBox="${viewBox}"`);
14290
+ }
14291
+ normalized = normalized.replace(/="undefined"/g, '="0"');
14292
+ normalized = normalized.replace(/="NaN"/g, '="0"');
14293
+ if (/\bx="[^"]*"/i.test(normalized)) {
14294
+ normalized = normalized.replace(/(<svg[^>]*\b)x="[^"]*"/i, '$1x="0"');
14295
+ } else {
14296
+ normalized = normalized.replace(/<svg\b/i, '<svg x="0"');
14297
+ }
14298
+ if (/\by="[^"]*"/i.test(normalized)) {
14299
+ normalized = normalized.replace(/(<svg[^>]*\b)y="[^"]*"/i, '$1y="0"');
14300
+ } else {
14301
+ normalized = normalized.replace(/<svg\b/i, '<svg y="0"');
14302
+ }
14303
+ normalized = normalized.replace(/\bpreserveAspectRatio="[^"]*"/i, 'preserveAspectRatio="none"');
14304
+ if (!/\bpreserveAspectRatio="[^"]*"/i.test(normalized)) {
14305
+ normalized = normalized.replace(/<svg\b/i, '<svg preserveAspectRatio="none"');
14306
+ }
14307
+ return normalized;
14165
14308
  }
14166
- return runs;
14167
- }
14168
- function rewriteSvgFontsForJsPDF(svgStr) {
14169
- var _a, _b;
14170
- const parser = new DOMParser();
14171
- const doc = parser.parseFromString(svgStr, "image/svg+xml");
14172
- const textEls = doc.querySelectorAll("text, tspan, textPath");
14173
- const readStyleToken = (style, prop) => {
14174
- var _a2;
14175
- const match = style.match(new RegExp(`${prop}\\s*:\\s*([^;]+)`, "i"));
14176
- return ((_a2 = match == null ? void 0 : match[1]) == null ? void 0 : _a2.trim()) || null;
14177
- };
14178
- const resolveInheritedValue = (el, attr, styleProp = attr) => {
14179
- var _a2;
14180
- let current = el;
14181
- while (current) {
14182
- const attrVal = (_a2 = current.getAttribute(attr)) == null ? void 0 : _a2.trim();
14183
- if (attrVal) return attrVal;
14184
- const styleVal = readStyleToken(current.getAttribute("style") || "", styleProp);
14185
- if (styleVal) return styleVal;
14186
- current = current.parentElement;
14309
+ /**
14310
+ * Find the Fabric.Canvas instance that belongs to a given container element,
14311
+ * using the global __fabricCanvasRegistry (set by PageCanvas).
14312
+ */
14313
+ getFabricCanvasFromContainer(container) {
14314
+ const registry2 = window.__fabricCanvasRegistry;
14315
+ if (registry2 instanceof Map) {
14316
+ for (const entry of registry2.values()) {
14317
+ const canvas = (entry == null ? void 0 : entry.canvas) || entry;
14318
+ if (!canvas || typeof canvas.toSVG !== "function") continue;
14319
+ const el = canvas.lowerCanvasEl || canvas.upperCanvasEl;
14320
+ if (el && container.contains(el)) return canvas;
14321
+ }
14187
14322
  }
14188
14323
  return null;
14189
- };
14190
- const resolveWeightNum = (weightRaw) => {
14191
- const parsedWeight = Number.parseInt(weightRaw, 10);
14192
- return Number.isFinite(parsedWeight) ? parsedWeight : /bold/i.test(weightRaw) ? 700 : /medium/i.test(weightRaw) ? 500 : /semi/i.test(weightRaw) ? 600 : /light/i.test(weightRaw) ? 300 : 400;
14193
- };
14194
- const buildStyleString = (existingStyle, fontName) => {
14195
- const stylePairs = existingStyle.split(";").map((part) => part.trim()).filter(Boolean).filter((part) => !/^font-family\s*:/i.test(part) && !/^font-weight\s*:/i.test(part) && !/^font-style\s*:/i.test(part));
14196
- stylePairs.push(`font-family: ${fontName}`);
14197
- stylePairs.push(`font-weight: normal`);
14198
- stylePairs.push(`font-style: normal`);
14199
- return stylePairs.join("; ");
14200
- };
14201
- for (const el of textEls) {
14202
- const inlineStyle = el.getAttribute("style") || "";
14203
- const rawFf = resolveInheritedValue(el, "font-family");
14204
- if (!rawFf) continue;
14205
- const clean = (_a = rawFf.split(",")[0]) == null ? void 0 : _a.replace(/['"]/g, "").trim();
14206
- if (!isFontAvailable(clean)) continue;
14207
- const weightRaw = resolveInheritedValue(el, "font-weight") || "400";
14208
- const styleRaw = resolveInheritedValue(el, "font-style") || "normal";
14209
- const weight = resolveWeightNum(weightRaw);
14210
- const resolved = resolveFontWeight(weight);
14211
- const isItalic = /italic|oblique/i.test(styleRaw);
14212
- const jsPdfName = getEmbeddedJsPDFFontName(clean, resolved, isItalic);
14213
- el.setAttribute("data-source-font-family", clean);
14214
- el.setAttribute("data-source-font-weight", String(resolved));
14215
- el.setAttribute("data-source-font-style", isItalic ? "italic" : "normal");
14216
- const directText = Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => n.textContent || "").join("");
14217
- const hasDevanagari = containsDevanagari(directText);
14218
- if (hasDevanagari && directText.length > 0) {
14219
- const devanagariWeight = resolveFontWeight(weight);
14220
- const devanagariJsPdfName = getEmbeddedJsPDFFontName(FONT_FALLBACK_DEVANAGARI, devanagariWeight);
14221
- const symbolJsPdfName = isFontAvailable(FONT_FALLBACK_SYMBOLS) ? getEmbeddedJsPDFFontName(FONT_FALLBACK_SYMBOLS, 400) : jsPdfName;
14222
- const childNodes = Array.from(el.childNodes);
14223
- for (const node of childNodes) {
14224
- if (node.nodeType !== 3 || !node.textContent) continue;
14225
- const runs = splitIntoRuns(node.textContent);
14226
- if (runs.length <= 1 && ((_b = runs[0]) == null ? void 0 : _b.runType) !== "devanagari") continue;
14227
- const fragment = doc.createDocumentFragment();
14228
- for (const run of runs) {
14229
- const tspan = doc.createElementNS("http://www.w3.org/2000/svg", "tspan");
14230
- let runFont;
14231
- if (run.runType === "devanagari") {
14232
- runFont = devanagariJsPdfName;
14233
- } else if (run.runType === "symbol") {
14234
- runFont = symbolJsPdfName;
14235
- } else {
14236
- runFont = jsPdfName;
14324
+ }
14325
+ async waitForStableTextMetrics(container, config) {
14326
+ var _a, _b, _c;
14327
+ if (typeof document !== "undefined") {
14328
+ void ensureFontsForResolvedConfig(config);
14329
+ await this.waitForRelevantFonts(config);
14330
+ }
14331
+ const fabricInstance = this.getFabricCanvasFromContainer(container);
14332
+ if (!(fabricInstance == null ? void 0 : fabricInstance.getObjects)) return;
14333
+ const waitForPaint = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r())));
14334
+ const primeCharBounds = (obj) => {
14335
+ if (obj instanceof fabric.Textbox) {
14336
+ const lines = obj._textLines;
14337
+ if (Array.isArray(lines)) {
14338
+ for (let i = 0; i < lines.length; i++) {
14339
+ try {
14340
+ obj.getLineWidth(i);
14341
+ } catch {
14342
+ }
14237
14343
  }
14238
- tspan.setAttribute("font-family", runFont);
14239
- tspan.setAttribute("font-weight", "normal");
14240
- tspan.setAttribute("font-style", "normal");
14241
- tspan.textContent = run.text;
14242
- fragment.appendChild(tspan);
14243
14344
  }
14244
- el.replaceChild(fragment, node);
14345
+ obj.dirty = true;
14346
+ return;
14245
14347
  }
14246
- el.setAttribute("font-family", jsPdfName);
14247
- el.setAttribute("font-weight", "normal");
14248
- el.setAttribute("font-style", "normal");
14249
- el.setAttribute("style", buildStyleString(inlineStyle, jsPdfName));
14250
- } else {
14251
- el.setAttribute("font-family", jsPdfName);
14252
- el.setAttribute("font-weight", "normal");
14253
- el.setAttribute("font-style", "normal");
14254
- el.setAttribute("style", buildStyleString(inlineStyle, jsPdfName));
14255
- }
14256
- }
14257
- return new XMLSerializer().serializeToString(doc.documentElement);
14258
- }
14259
- function extractFontFamiliesFromSvgs(svgs) {
14260
- const families = /* @__PURE__ */ new Set();
14261
- const regex = /font-family[=:]\s*["']?([^"';},]+)/gi;
14262
- for (const svg of svgs) {
14263
- let m;
14264
- while ((m = regex.exec(svg)) !== null) {
14265
- const raw = m[1].trim().split(",")[0].trim().replace(/^['"]|['"]$/g, "");
14266
- if (raw && raw !== "serif" && raw !== "sans-serif" && raw !== "monospace") {
14267
- families.add(raw);
14348
+ if (obj instanceof fabric.Group) {
14349
+ obj.getObjects().forEach(primeCharBounds);
14350
+ obj.dirty = true;
14268
14351
  }
14269
- }
14352
+ };
14353
+ fabricInstance.getObjects().forEach(primeCharBounds);
14354
+ (_a = fabricInstance.calcOffset) == null ? void 0 : _a.call(fabricInstance);
14355
+ (_b = fabricInstance.renderAll) == null ? void 0 : _b.call(fabricInstance);
14356
+ await waitForPaint();
14357
+ (_c = fabricInstance.renderAll) == null ? void 0 : _c.call(fabricInstance);
14358
+ await waitForPaint();
14270
14359
  }
14271
- return families;
14272
14360
  }
14273
- async function embedFontsInPdf(pdf, fontFamilies, fontBaseUrl) {
14274
- const embedded = /* @__PURE__ */ new Set();
14275
- const weights = [300, 400, 500, 600, 700];
14276
- const tasks = [];
14277
- for (const family of fontFamilies) {
14278
- if (!isFontAvailable(family)) {
14279
- console.warn(`[pdf-fonts] No TTF mapping for "${family}" — will use Helvetica fallback`);
14280
- continue;
14281
- }
14282
- for (const w of weights) {
14283
- tasks.push(
14284
- embedFont(pdf, family, w, fontBaseUrl).then((ok) => {
14285
- if (ok) embedded.add(`${family}${w}`);
14286
- })
14287
- );
14361
+ function dumpSvgTextDiagnostics(svgStr, pageIndex, tag, stage, maxItems = 30) {
14362
+ try {
14363
+ if (typeof DOMParser === "undefined") return;
14364
+ const doc = new DOMParser().parseFromString(svgStr, "image/svg+xml");
14365
+ if (doc.querySelector("parsererror")) {
14366
+ console.warn(`${tag} page=${pageIndex} stage=${stage} parse-error`);
14367
+ return;
14288
14368
  }
14369
+ const root = doc.documentElement;
14370
+ const svgWidth = root == null ? void 0 : root.getAttribute("width");
14371
+ const svgHeight = root == null ? void 0 : root.getAttribute("height");
14372
+ const svgViewBox = root == null ? void 0 : root.getAttribute("viewBox");
14373
+ const texts = Array.from(doc.querySelectorAll("text"));
14374
+ const summary = {
14375
+ page: pageIndex,
14376
+ stage,
14377
+ svgLen: svgStr.length,
14378
+ svgWidth,
14379
+ svgHeight,
14380
+ svgViewBox,
14381
+ textCount: texts.length
14382
+ };
14383
+ console.log(`${tag} ${stage} page=${pageIndex} summary`, summary);
14384
+ const sample = texts.slice(0, maxItems).map((t, idx) => {
14385
+ var _a, _b;
14386
+ const tspans = Array.from(t.querySelectorAll("tspan"));
14387
+ const tspanInfo = tspans.slice(0, 4).map((s) => ({
14388
+ x: s.getAttribute("x"),
14389
+ y: s.getAttribute("y"),
14390
+ text: (s.textContent || "").slice(0, 40)
14391
+ }));
14392
+ let containerWidth = null;
14393
+ let cursor = t.parentElement;
14394
+ while (cursor && !containerWidth) {
14395
+ containerWidth = (_a = cursor.getAttribute) == null ? void 0 : _a.call(cursor, "width");
14396
+ cursor = cursor.parentElement;
14397
+ if (cursor && ((_b = cursor.tagName) == null ? void 0 : _b.toLowerCase()) === "svg") break;
14398
+ }
14399
+ return {
14400
+ idx,
14401
+ x: t.getAttribute("x"),
14402
+ y: t.getAttribute("y"),
14403
+ fontSize: t.getAttribute("font-size"),
14404
+ fontFamily: t.getAttribute("font-family"),
14405
+ fontWeight: t.getAttribute("font-weight"),
14406
+ textAnchor: t.getAttribute("text-anchor"),
14407
+ transform: t.getAttribute("transform"),
14408
+ style: (t.getAttribute("style") || "").slice(0, 120),
14409
+ ancestorWidth: containerWidth,
14410
+ textContent: (t.textContent || "").slice(0, 60),
14411
+ tspanCount: tspans.length,
14412
+ tspanSample: tspanInfo
14413
+ };
14414
+ });
14415
+ console.log(`${tag} ${stage} page=${pageIndex} text-sample (first ${sample.length}/${texts.length})`, sample);
14416
+ } catch (err) {
14417
+ console.warn(`${tag} ${stage} page=${pageIndex} dump threw`, err);
14289
14418
  }
14290
- await Promise.all(tasks);
14291
- console.log(`[pdf-fonts] Embedded ${embedded.size} font variants for ${fontFamilies.size} families`);
14292
- return embedded;
14293
14419
  }
14294
- const pdfFonts = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
14295
- __proto__: null,
14296
- FONT_FALLBACK_DEVANAGARI,
14297
- FONT_FALLBACK_SYMBOLS,
14298
- FONT_FILES,
14299
- FONT_WEIGHT_LABELS,
14300
- embedFont,
14301
- embedFontsForConfig,
14302
- embedFontsInPdf,
14303
- extractFontFamiliesFromSvgs,
14304
- getEmbeddedJsPDFFontName,
14305
- isFontAvailable,
14306
- resolveFontWeight,
14307
- rewriteSvgFontsForJsPDF
14308
- }, Symbol.toStringTag, { value: "Module" }));
14309
14420
  const SVG_DRAWABLE_TAGS = /* @__PURE__ */ new Set([
14310
14421
  "path",
14311
14422
  "rect",
@@ -15601,6 +15712,15 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
15601
15712
  const { title, stripPageBackground } = options;
15602
15713
  const firstPage = svgResults[0];
15603
15714
  const orientation = firstPage.width > firstPage.height ? "landscape" : "portrait";
15715
+ const PARITY_TAG = "[canvas-renderer][parity-diag][pkg-pdf]";
15716
+ console.log(`${PARITY_TAG} pkg-version=0.5.70 pages=${svgResults.length}`);
15717
+ try {
15718
+ for (let pi = 0; pi < svgResults.length; pi++) {
15719
+ dumpSvgTextDiagnostics(svgResults[pi].svg, pi, PARITY_TAG, "STAGE-1-raw-toSVG");
15720
+ }
15721
+ } catch (e) {
15722
+ console.warn(`${PARITY_TAG} dump failed`, e);
15723
+ }
15604
15724
  const pdf = new jsPDF({
15605
15725
  orientation,
15606
15726
  unit: "px",
@@ -15630,6 +15750,15 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
15630
15750
  stripPageBackground: shouldStripBg
15631
15751
  });
15632
15752
  if (processedSvg) {
15753
+ try {
15754
+ dumpSvgTextDiagnostics(
15755
+ new XMLSerializer().serializeToString(processedSvg),
15756
+ i,
15757
+ PARITY_TAG,
15758
+ "STAGE-2-after-prepareLiveCanvasSvgForPdf"
15759
+ );
15760
+ } catch {
15761
+ }
15633
15762
  await convertTextDecorationsToLines(processedSvg);
15634
15763
  if (shouldOutlineText) {
15635
15764
  try {
@@ -15651,6 +15780,15 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
15651
15780
  const reParser = new DOMParser();
15652
15781
  const reDoc = reParser.parseFromString(rewrittenSvg, "image/svg+xml");
15653
15782
  processedSvg = reDoc.documentElement;
15783
+ try {
15784
+ dumpSvgTextDiagnostics(
15785
+ rewrittenSvg,
15786
+ i,
15787
+ PARITY_TAG,
15788
+ "STAGE-3-after-rewriteSvgFontsForJsPDF"
15789
+ );
15790
+ } catch {
15791
+ }
15654
15792
  }
15655
15793
  if (processedSvg) {
15656
15794
  pdf.setFillColor(0, 0, 0);
@@ -15674,7 +15812,8 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
15674
15812
  }
15675
15813
  const pdfExport = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
15676
15814
  __proto__: null,
15677
- assemblePdfFromSvgs
15815
+ assemblePdfFromSvgs,
15816
+ dumpSvgTextDiagnostics
15678
15817
  }, Symbol.toStringTag, { value: "Module" }));
15679
15818
  function collectImageUrls(config) {
15680
15819
  const urls = [];
@@ -15778,6 +15917,7 @@ export {
15778
15917
  collectFontsFromConfig,
15779
15918
  collectImageUrls,
15780
15919
  configHasAutoShrinkText$1 as configHasAutoShrinkText,
15920
+ dumpSvgTextDiagnostics,
15781
15921
  embedFont,
15782
15922
  embedFontsForConfig,
15783
15923
  embedFontsInPdf,