@pixldocs/canvas-renderer 0.5.55 → 0.5.57

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.d.ts CHANGED
@@ -231,7 +231,7 @@ export declare function normalizeFontFamily(fontStack: string): string;
231
231
  * Package version banner. Bump alongside package.json so we can confirm
232
232
  * (via browser:log) that the deployed bundle matches the expected build.
233
233
  */
234
- export declare const PACKAGE_VERSION = "0.5.54";
234
+ export declare const PACKAGE_VERSION = "0.5.57";
235
235
 
236
236
  export declare interface PageSettings {
237
237
  backgroundColor?: string;
@@ -391,6 +391,18 @@ export declare class PixldocsRenderer {
391
391
  private waitForCanvasImages;
392
392
  private waitForCanvasScene;
393
393
  private waitForRelevantFonts;
394
+ /**
395
+ * Block until the webfonts referenced by `config` have actually loaded
396
+ * (or `maxWaitMs` elapses). Used by the headless capture path BEFORE
397
+ * mounting `PreviewCanvas`, so the synchronous `createText` auto-shrink
398
+ * loop measures against final font metrics instead of fallback ones.
399
+ *
400
+ * Stronger than `ensureFontsForResolvedConfig` (which is fire-and-forget)
401
+ * — this awaits each `document.fonts.load(spec)` AND `document.fonts.ready`,
402
+ * racing the whole thing against `maxWaitMs` so a slow CDN can't hang the
403
+ * renderer.
404
+ */
405
+ private awaitFontsForConfig;
394
406
  private getNormalizedGradientStops;
395
407
  private paintPageBackground;
396
408
  private renderPageViaPreviewCanvas;
@@ -447,6 +459,21 @@ export declare interface RenderOptions {
447
459
  scale?: number;
448
460
  /** Custom pixel ratio override */
449
461
  pixelRatio?: number;
462
+ /**
463
+ * If true, skip the blocking font-ready wait before mounting the headless
464
+ * PreviewCanvas. Default: `false`. Setting this to `true` makes capture
465
+ * faster but can cause `overflowPolicy: 'auto-shrink'` text to overflow
466
+ * when the real webfont loads after auto-shrink has already measured
467
+ * against fallback metrics.
468
+ */
469
+ skipFontReadyWait?: boolean;
470
+ /**
471
+ * Maximum time (ms) to wait for `document.fonts.load()` per descriptor
472
+ * before mounting PreviewCanvas. Default: `4000` for configs that contain
473
+ * any `auto-shrink` text (correctness matters), `1800` otherwise. Only
474
+ * applies when `skipFontReadyWait` is false.
475
+ */
476
+ waitForFontsMs?: number;
450
477
  }
451
478
 
452
479
  export declare interface RenderResult {
@@ -578,6 +605,14 @@ export declare function rewriteSvgFontsForJsPDF(svgStr: string): string;
578
605
 
579
606
  export { SectionFormState }
580
607
 
608
+ /**
609
+ * Enable verbose console logs for the auto-shrink loop in the live preview.
610
+ * Logs target width, measured line widths, font availability, and the chosen
611
+ * fontSize for each text element with `overflowPolicy: 'auto-shrink'`.
612
+ * Disabled by default — call once from the host app when debugging.
613
+ */
614
+ export declare function setAutoShrinkDebug(enabled: boolean): void;
615
+
581
616
  export { setBundledAssetPrefixes }
582
617
 
583
618
  export { SmartElementProps }
package/dist/index.js CHANGED
@@ -5270,6 +5270,10 @@ function createText(element) {
5270
5270
  const splitByGrapheme = overflowPolicy === "auto-shrink" ? false : element.splitByGrapheme ?? element.wordWrap === "break-word";
5271
5271
  if (overflowPolicy === "auto-shrink") {
5272
5272
  const explicitLineCount = Math.max(1, text.split("\n").length);
5273
+ const debugAutoShrink = typeof window !== "undefined" && window.__pixldocsDebugAutoShrink === true;
5274
+ const startFontSize = fontSize;
5275
+ let breakReason = "min-font-size-reached";
5276
+ let lastIter = null;
5273
5277
  while (fontSize > 1) {
5274
5278
  const testTextbox = new fabric.Textbox(text, {
5275
5279
  width: fixedWidth,
@@ -5291,11 +5295,47 @@ function createText(element) {
5291
5295
  const lineWidths = testTextbox.__lineWidths;
5292
5296
  const maxLineWidth = lineWidths && lineWidths.length > 0 ? Math.max(...lineWidths) : 0;
5293
5297
  const fitsWidth = !widthDidGrow && maxLineWidth <= fixedWidth + 1;
5298
+ if (debugAutoShrink) {
5299
+ lastIter = {
5300
+ fontSize,
5301
+ renderedLineCount,
5302
+ explicitLineCount,
5303
+ textHeight,
5304
+ maxLineWidth,
5305
+ fixedWidth,
5306
+ widthDidGrow,
5307
+ hasNoImplicitWrap,
5308
+ fitsHeight,
5309
+ fitsWidth
5310
+ };
5311
+ }
5294
5312
  if (hasNoImplicitWrap && fitsHeight && fitsWidth) {
5313
+ breakReason = "fits";
5295
5314
  break;
5296
5315
  }
5297
5316
  fontSize--;
5298
5317
  }
5318
+ if (debugAutoShrink) {
5319
+ console.log("[auto-shrink][diag]", {
5320
+ id: element.id,
5321
+ name: element.name,
5322
+ text,
5323
+ fontFamily: element.fontFamily,
5324
+ fontWeight: element.fontWeight,
5325
+ elementWidth: element.width,
5326
+ elementHeight: element.height,
5327
+ scaleX: element.scaleX ?? 1,
5328
+ scaleY: element.scaleY ?? 1,
5329
+ fixedWidth,
5330
+ baseHeight,
5331
+ startFontSize,
5332
+ finalFontSize: fontSize,
5333
+ breakReason,
5334
+ lastIter,
5335
+ fontCheckRegular: typeof document !== "undefined" && document.fonts ? document.fonts.check(`16px "${element.fontFamily || "Open Sans"}"`) : null,
5336
+ fontCheckBold: typeof document !== "undefined" && document.fonts ? document.fonts.check(`bold 16px "${element.fontFamily || "Open Sans"}"`) : null
5337
+ });
5338
+ }
5299
5339
  }
5300
5340
  if (overflowPolicy === "max-lines-ellipsis") {
5301
5341
  const originalText = element.text || "Text";
@@ -12412,7 +12452,7 @@ function PixldocsPreview(props) {
12412
12452
  !canvasSettled && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) })
12413
12453
  ] });
12414
12454
  }
12415
- const PACKAGE_VERSION = "0.5.54";
12455
+ const PACKAGE_VERSION = "0.5.57";
12416
12456
  let __underlineFixInstalled = false;
12417
12457
  function installUnderlineFix(fab) {
12418
12458
  var _a;
@@ -12509,6 +12549,22 @@ function installUnderlineFix(fab) {
12509
12549
  __underlineFixInstalled = true;
12510
12550
  console.log(`[canvas-renderer] underline-fix monkey patch installed (v${PACKAGE_VERSION})`);
12511
12551
  }
12552
+ function configHasAutoShrinkText(config) {
12553
+ var _a;
12554
+ if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return false;
12555
+ const walk = (nodes) => {
12556
+ for (const node of nodes || []) {
12557
+ if (!node) continue;
12558
+ if (node.type === "text" && node.overflowPolicy === "auto-shrink") return true;
12559
+ if (Array.isArray(node.children) && node.children.length && walk(node.children)) return true;
12560
+ }
12561
+ return false;
12562
+ };
12563
+ for (const page of config.pages) {
12564
+ if (walk(page.children || [])) return true;
12565
+ }
12566
+ return false;
12567
+ }
12512
12568
  class PixldocsRenderer {
12513
12569
  constructor(config) {
12514
12570
  __publicField(this, "config");
@@ -12535,6 +12591,11 @@ class PixldocsRenderer {
12535
12591
  throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12536
12592
  }
12537
12593
  await ensureFontsForResolvedConfig(templateConfig);
12594
+ if (!options.skipFontReadyWait) {
12595
+ const hasAutoShrink = configHasAutoShrinkText(templateConfig);
12596
+ const defaultWait = hasAutoShrink ? 4e3 : 1800;
12597
+ await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
12598
+ }
12538
12599
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12539
12600
  setPackageApiUrl2(this.config.imageProxyUrl);
12540
12601
  const dataUrl = await this.renderPageViaPreviewCanvas(
@@ -12542,7 +12603,8 @@ class PixldocsRenderer {
12542
12603
  pageIndex,
12543
12604
  pixelRatio,
12544
12605
  format,
12545
- quality
12606
+ quality,
12607
+ { skipFontReadyWait: options.skipFontReadyWait, waitForFontsMs: options.waitForFontsMs }
12546
12608
  );
12547
12609
  return {
12548
12610
  dataUrl,
@@ -12556,9 +12618,14 @@ class PixldocsRenderer {
12556
12618
  * Render all pages and return array of results.
12557
12619
  */
12558
12620
  async renderAllPages(templateConfig, options = {}) {
12621
+ if (!options.skipFontReadyWait) {
12622
+ const hasAutoShrink = configHasAutoShrinkText(templateConfig);
12623
+ const defaultWait = hasAutoShrink ? 4e3 : 1800;
12624
+ await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
12625
+ }
12559
12626
  const results = [];
12560
12627
  for (let i = 0; i < templateConfig.pages.length; i++) {
12561
- results.push(await this.render(templateConfig, { ...options, pageIndex: i }));
12628
+ results.push(await this.render(templateConfig, { ...options, pageIndex: i, skipFontReadyWait: true }));
12562
12629
  }
12563
12630
  return results;
12564
12631
  }
@@ -12595,6 +12662,8 @@ class PixldocsRenderer {
12595
12662
  throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12596
12663
  }
12597
12664
  await ensureFontsForResolvedConfig(templateConfig);
12665
+ const hasAutoShrinkSvg = configHasAutoShrinkText(templateConfig);
12666
+ await this.awaitFontsForConfig(templateConfig, hasAutoShrinkSvg ? 4e3 : 1800);
12598
12667
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12599
12668
  setPackageApiUrl2(this.config.imageProxyUrl);
12600
12669
  const canvasWidth = templateConfig.canvas.width;
@@ -12606,6 +12675,8 @@ class PixldocsRenderer {
12606
12675
  */
12607
12676
  async renderAllPageSvgs(templateConfig) {
12608
12677
  await ensureFontsForResolvedConfig(templateConfig);
12678
+ const hasAutoShrinkSvg = configHasAutoShrinkText(templateConfig);
12679
+ await this.awaitFontsForConfig(templateConfig, hasAutoShrinkSvg ? 4e3 : 1800);
12609
12680
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12610
12681
  setPackageApiUrl2(this.config.imageProxyUrl);
12611
12682
  const results = [];
@@ -12858,6 +12929,26 @@ class PixldocsRenderer {
12858
12929
  ]);
12859
12930
  await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
12860
12931
  }
12932
+ /**
12933
+ * Block until the webfonts referenced by `config` have actually loaded
12934
+ * (or `maxWaitMs` elapses). Used by the headless capture path BEFORE
12935
+ * mounting `PreviewCanvas`, so the synchronous `createText` auto-shrink
12936
+ * loop measures against final font metrics instead of fallback ones.
12937
+ *
12938
+ * Stronger than `ensureFontsForResolvedConfig` (which is fire-and-forget)
12939
+ * — this awaits each `document.fonts.load(spec)` AND `document.fonts.ready`,
12940
+ * racing the whole thing against `maxWaitMs` so a slow CDN can't hang the
12941
+ * renderer.
12942
+ */
12943
+ async awaitFontsForConfig(config, maxWaitMs) {
12944
+ if (typeof document === "undefined" || !document.fonts) return;
12945
+ void ensureFontsForResolvedConfig(config);
12946
+ await this.waitForRelevantFonts(config, maxWaitMs);
12947
+ await Promise.race([
12948
+ document.fonts.ready.catch(() => void 0).then(() => void 0),
12949
+ new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
12950
+ ]);
12951
+ }
12861
12952
  getNormalizedGradientStops(gradient) {
12862
12953
  const stops = Array.isArray(gradient == null ? void 0 : gradient.stops) ? gradient.stops.map((stop) => ({
12863
12954
  offset: Math.max(0, Math.min(1, Number((stop == null ? void 0 : stop.offset) ?? 0))),
@@ -12931,10 +13022,19 @@ class PixldocsRenderer {
12931
13022
  } catch {
12932
13023
  }
12933
13024
  }
12934
- async renderPageViaPreviewCanvas(config, pageIndex, pixelRatio, format, quality) {
13025
+ async renderPageViaPreviewCanvas(config, pageIndex, pixelRatio, format, quality, options = {}) {
12935
13026
  const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
12936
13027
  const canvasWidth = config.canvas.width;
12937
13028
  const canvasHeight = config.canvas.height;
13029
+ const hasAutoShrink = configHasAutoShrinkText(config);
13030
+ let firstMountSettled = false;
13031
+ let lateFontSettleDetected = false;
13032
+ if (typeof document !== "undefined" && document.fonts && hasAutoShrink) {
13033
+ document.fonts.ready.then(() => {
13034
+ if (firstMountSettled) lateFontSettleDetected = true;
13035
+ }).catch(() => {
13036
+ });
13037
+ }
12938
13038
  return new Promise((resolve, reject) => {
12939
13039
  const container = document.createElement("div");
12940
13040
  container.style.cssText = `
@@ -12947,6 +13047,8 @@ class PixldocsRenderer {
12947
13047
  cleanup();
12948
13048
  reject(new Error("Render timeout (30s)"));
12949
13049
  }, 3e4);
13050
+ let root;
13051
+ let mountKey = 0;
12950
13052
  const cleanup = () => {
12951
13053
  clearTimeout(timeout);
12952
13054
  try {
@@ -12955,6 +13057,46 @@ class PixldocsRenderer {
12955
13057
  }
12956
13058
  container.remove();
12957
13059
  };
13060
+ const remountWithFreshKey = async () => {
13061
+ mountKey += 1;
13062
+ try {
13063
+ clearMeasurementCache();
13064
+ } catch {
13065
+ }
13066
+ try {
13067
+ clearFabricCharCache();
13068
+ } catch {
13069
+ }
13070
+ try {
13071
+ root.unmount();
13072
+ } catch {
13073
+ }
13074
+ root = createRoot(container);
13075
+ await new Promise((settle) => {
13076
+ const onReadyOnce = () => {
13077
+ this.waitForCanvasScene(container, config, pageIndex).then(async () => {
13078
+ const fabricInstance = this.getFabricCanvasFromContainer(container);
13079
+ const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
13080
+ await this.waitForCanvasImages(container, expectedImageCount);
13081
+ await this.waitForStableTextMetrics(container, config);
13082
+ await this.waitForCanvasScene(container, config, pageIndex);
13083
+ if (!fabricInstance) return settle();
13084
+ settle();
13085
+ }).catch(() => settle());
13086
+ };
13087
+ root.render(
13088
+ createElement(PreviewCanvas2, {
13089
+ key: `remount-${mountKey}`,
13090
+ config,
13091
+ pageIndex,
13092
+ zoom: pixelRatio,
13093
+ absoluteZoom: true,
13094
+ skipFontReadyWait: false,
13095
+ onReady: onReadyOnce
13096
+ })
13097
+ );
13098
+ });
13099
+ };
12958
13100
  const onReady = () => {
12959
13101
  this.waitForCanvasScene(container, config, pageIndex).then(async () => {
12960
13102
  try {
@@ -12963,16 +13105,23 @@ class PixldocsRenderer {
12963
13105
  await this.waitForCanvasImages(container, expectedImageCount);
12964
13106
  await this.waitForStableTextMetrics(container, config);
12965
13107
  await this.waitForCanvasScene(container, config, pageIndex);
13108
+ firstMountSettled = true;
13109
+ if (hasAutoShrink && lateFontSettleDetected) {
13110
+ console.log("[canvas-renderer][parity] late font-settle detected — remounting for auto-shrink reflow");
13111
+ await remountWithFreshKey();
13112
+ }
12966
13113
  const fabricCanvas = container.querySelector("canvas.upper-canvas, canvas");
12967
13114
  const sourceCanvas = (fabricInstance == null ? void 0 : fabricInstance.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || fabricCanvas;
13115
+ const fabricInstanceAfter = this.getFabricCanvasFromContainer(container) || fabricInstance;
13116
+ const sourceCanvasAfter = (fabricInstanceAfter == null ? void 0 : fabricInstanceAfter.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || sourceCanvas;
12968
13117
  if (!sourceCanvas) {
12969
13118
  cleanup();
12970
13119
  reject(new Error("No canvas element found after render"));
12971
13120
  return;
12972
13121
  }
12973
13122
  const exportCanvas = document.createElement("canvas");
12974
- exportCanvas.width = sourceCanvas.width;
12975
- exportCanvas.height = sourceCanvas.height;
13123
+ exportCanvas.width = sourceCanvasAfter.width;
13124
+ exportCanvas.height = sourceCanvasAfter.height;
12976
13125
  const exportCtx = exportCanvas.getContext("2d");
12977
13126
  if (!exportCtx) {
12978
13127
  cleanup();
@@ -12980,10 +13129,10 @@ class PixldocsRenderer {
12980
13129
  return;
12981
13130
  }
12982
13131
  exportCtx.save();
12983
- exportCtx.scale(sourceCanvas.width / canvasWidth, sourceCanvas.height / canvasHeight);
13132
+ exportCtx.scale(sourceCanvasAfter.width / canvasWidth, sourceCanvasAfter.height / canvasHeight);
12984
13133
  this.paintPageBackground(exportCtx, config.pages[pageIndex], canvasWidth, canvasHeight);
12985
13134
  exportCtx.restore();
12986
- exportCtx.drawImage(sourceCanvas, 0, 0);
13135
+ exportCtx.drawImage(sourceCanvasAfter, 0, 0);
12987
13136
  const mimeType = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
12988
13137
  const dataUrl = exportCanvas.toDataURL(mimeType, quality);
12989
13138
  cleanup();
@@ -12994,7 +13143,7 @@ class PixldocsRenderer {
12994
13143
  }
12995
13144
  });
12996
13145
  };
12997
- const root = createRoot(container);
13146
+ root = createRoot(container);
12998
13147
  root.render(
12999
13148
  createElement(PreviewCanvas2, {
13000
13149
  config,
@@ -15461,6 +15610,11 @@ async function warmTemplateFromForm(options) {
15461
15610
  if (signal == null ? void 0 : signal.aborted) return;
15462
15611
  await warmResolvedTemplateForPreview(resolved.config, { signal, imageProxyUrl });
15463
15612
  }
15613
+ function setAutoShrinkDebug(enabled) {
15614
+ if (typeof window !== "undefined") {
15615
+ window.__pixldocsDebugAutoShrink = !!enabled;
15616
+ }
15617
+ }
15464
15618
  export {
15465
15619
  FONT_FALLBACK_DEVANAGARI,
15466
15620
  FONT_FALLBACK_SYMBOLS,
@@ -15489,6 +15643,7 @@ export {
15489
15643
  resolveFromForm,
15490
15644
  resolveTemplateData,
15491
15645
  rewriteSvgFontsForJsPDF,
15646
+ setAutoShrinkDebug,
15492
15647
  setBundledAssetPrefixes,
15493
15648
  warmResolvedTemplateForPreview,
15494
15649
  warmTemplateFromForm