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