@pixldocs/canvas-renderer 0.5.56 → 0.5.58

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
@@ -25,6 +25,19 @@ export declare function applyThemeToConfig(config: TemplateConfig, themeOverride
25
25
  */
26
26
  export declare function assemblePdfFromSvgs(svgResults: SvgRenderResult[], options?: PdfAssemblyOptions): Promise<PdfRenderResult>;
27
27
 
28
+ /**
29
+ * Block until the webfonts referenced by `config` have actually loaded
30
+ * (or `maxWaitMs` elapses). Stronger than `ensureFontsForResolvedConfig`
31
+ * which only fire-and-forget queues `document.fonts.load()`.
32
+ *
33
+ * Use this BEFORE mounting `PreviewCanvas` so the synchronous
34
+ * `createText` auto-shrink loop measures against real font metrics
35
+ * instead of system fallback ones (which are typically narrower → loop
36
+ * decides "fits, no shrink needed" → text overflows once the real font
37
+ * swaps in).
38
+ */
39
+ export declare function awaitFontsForConfig(config: TemplateConfig, maxWaitMs: number): Promise<void>;
40
+
28
41
  export declare interface CanvasNode {
29
42
  id: string;
30
43
  name?: string;
@@ -64,6 +77,16 @@ export declare function collectFontsFromConfig(config: TemplateConfig): Set<stri
64
77
  */
65
78
  export declare function collectImageUrls(config: TemplateConfig): string[];
66
79
 
80
+ /**
81
+ * Returns true when any text node in `config` uses
82
+ * `overflowPolicy: 'auto-shrink'`. The synchronous shrink loop in
83
+ * `createText` measures with whatever font Fabric can resolve at mount
84
+ * time, so for these configs the consumer MUST block on real font load
85
+ * before mounting (otherwise it shrinks against fallback metrics and
86
+ * overflows once the real font swaps in).
87
+ */
88
+ export declare function configHasAutoShrinkText(config: TemplateConfig | null | undefined): boolean;
89
+
67
90
  export declare interface DynamicField {
68
91
  id: string;
69
92
  label: string;
@@ -231,7 +254,7 @@ export declare function normalizeFontFamily(fontStack: string): string;
231
254
  * Package version banner. Bump alongside package.json so we can confirm
232
255
  * (via browser:log) that the deployed bundle matches the expected build.
233
256
  */
234
- export declare const PACKAGE_VERSION = "0.5.54";
257
+ export declare const PACKAGE_VERSION = "0.5.57";
235
258
 
236
259
  export declare interface PageSettings {
237
260
  backgroundColor?: string;
@@ -391,6 +414,18 @@ export declare class PixldocsRenderer {
391
414
  private waitForCanvasImages;
392
415
  private waitForCanvasScene;
393
416
  private waitForRelevantFonts;
417
+ /**
418
+ * Block until the webfonts referenced by `config` have actually loaded
419
+ * (or `maxWaitMs` elapses). Used by the headless capture path BEFORE
420
+ * mounting `PreviewCanvas`, so the synchronous `createText` auto-shrink
421
+ * loop measures against final font metrics instead of fallback ones.
422
+ *
423
+ * Stronger than `ensureFontsForResolvedConfig` (which is fire-and-forget)
424
+ * — this awaits each `document.fonts.load(spec)` AND `document.fonts.ready`,
425
+ * racing the whole thing against `maxWaitMs` so a slow CDN can't hang the
426
+ * renderer.
427
+ */
428
+ private awaitFontsForConfig;
394
429
  private getNormalizedGradientStops;
395
430
  private paintPageBackground;
396
431
  private renderPageViaPreviewCanvas;
@@ -447,6 +482,21 @@ export declare interface RenderOptions {
447
482
  scale?: number;
448
483
  /** Custom pixel ratio override */
449
484
  pixelRatio?: number;
485
+ /**
486
+ * If true, skip the blocking font-ready wait before mounting the headless
487
+ * PreviewCanvas. Default: `false`. Setting this to `true` makes capture
488
+ * faster but can cause `overflowPolicy: 'auto-shrink'` text to overflow
489
+ * when the real webfont loads after auto-shrink has already measured
490
+ * against fallback metrics.
491
+ */
492
+ skipFontReadyWait?: boolean;
493
+ /**
494
+ * Maximum time (ms) to wait for `document.fonts.load()` per descriptor
495
+ * before mounting PreviewCanvas. Default: `4000` for configs that contain
496
+ * any `auto-shrink` text (correctness matters), `1800` otherwise. Only
497
+ * applies when `skipFontReadyWait` is false.
498
+ */
499
+ waitForFontsMs?: number;
450
500
  }
451
501
 
452
502
  export declare interface RenderResult {
package/dist/index.js CHANGED
@@ -12244,6 +12244,46 @@ async function ensureFontsForResolvedConfig(config) {
12244
12244
  });
12245
12245
  }
12246
12246
  }
12247
+ function configHasAutoShrinkText$1(config) {
12248
+ var _a;
12249
+ if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return false;
12250
+ const walk = (nodes) => {
12251
+ for (const node of nodes || []) {
12252
+ if (!node) continue;
12253
+ if (node.type === "text" && node.overflowPolicy === "auto-shrink") return true;
12254
+ if (Array.isArray(node.children) && node.children.length && walk(node.children)) return true;
12255
+ }
12256
+ return false;
12257
+ };
12258
+ for (const page of config.pages) {
12259
+ if (walk(page.children || [])) return true;
12260
+ }
12261
+ return false;
12262
+ }
12263
+ async function awaitFontsForConfig(config, maxWaitMs) {
12264
+ if (typeof document === "undefined" || !document.fonts) return;
12265
+ void ensureFontsForResolvedConfig(config);
12266
+ const descriptors = collectFontDescriptorsFromConfig(config);
12267
+ if (descriptors.length === 0) return;
12268
+ const loads = Promise.all(
12269
+ descriptors.map((d) => {
12270
+ const stylePrefix = d.style === "italic" ? "italic " : "";
12271
+ const spec = `${stylePrefix}${d.weight} 16px "${d.family}"`;
12272
+ return document.fonts.load(spec).catch(() => []);
12273
+ })
12274
+ ).then(() => void 0);
12275
+ await Promise.race([
12276
+ loads,
12277
+ new Promise((resolve) => setTimeout(resolve, maxWaitMs))
12278
+ ]);
12279
+ await Promise.race([
12280
+ document.fonts.ready.catch(() => void 0).then(() => void 0),
12281
+ new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
12282
+ ]);
12283
+ await new Promise(
12284
+ (resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
12285
+ );
12286
+ }
12247
12287
  const PREVIEW_DEBUG_PREFIX = "[canvas-renderer][preview-debug]";
12248
12288
  function countUnderlinedNodes(config) {
12249
12289
  var _a;
@@ -12329,15 +12369,17 @@ function PixldocsPreview(props) {
12329
12369
  underlinedNodes: countUnderlinedNodes(resolved.config)
12330
12370
  });
12331
12371
  setResolvedConfig(resolved.config);
12332
- ensureFontsForResolvedConfig(resolved.config).then(() => {
12372
+ const hasAutoShrink = configHasAutoShrinkText$1(resolved.config);
12373
+ const waitMs = hasAutoShrink ? 4e3 : 1800;
12374
+ awaitFontsForConfig(resolved.config, waitMs).then(() => {
12333
12375
  if (!cancelled) {
12334
- console.log(PREVIEW_DEBUG_PREFIX, "resolve-mode fonts queued");
12376
+ console.log(PREVIEW_DEBUG_PREFIX, "resolve-mode fonts settled", { hasAutoShrink, waitMs });
12335
12377
  setFontsReady(true);
12336
12378
  setIsLoading(false);
12337
12379
  }
12338
12380
  }).catch((err) => {
12339
12381
  if (!cancelled) {
12340
- console.warn(PREVIEW_DEBUG_PREFIX, "resolve-mode fonts queue failed", err);
12382
+ console.warn(PREVIEW_DEBUG_PREFIX, "resolve-mode font wait failed", err);
12341
12383
  setFontsReady(true);
12342
12384
  setIsLoading(false);
12343
12385
  }
@@ -12406,16 +12448,26 @@ function PixldocsPreview(props) {
12406
12448
  setFontsReady(false);
12407
12449
  setCanvasSettled(false);
12408
12450
  setStabilizationPass(0);
12409
- ensureFontsForResolvedConfig(config).then(() => {
12410
- console.log(PREVIEW_DEBUG_PREFIX, "config-mode fonts queued", {
12451
+ let cancelled = false;
12452
+ const hasAutoShrink = configHasAutoShrinkText$1(config);
12453
+ const waitMs = hasAutoShrink ? 4e3 : 1800;
12454
+ awaitFontsForConfig(config, waitMs).then(() => {
12455
+ if (cancelled) return;
12456
+ console.log(PREVIEW_DEBUG_PREFIX, "config-mode fonts settled", {
12411
12457
  pageIndex,
12458
+ hasAutoShrink,
12459
+ waitMs,
12412
12460
  underlinedNodes: countUnderlinedNodes(config)
12413
12461
  });
12414
12462
  setFontsReady(true);
12415
12463
  }).catch((err) => {
12416
- console.warn(PREVIEW_DEBUG_PREFIX, "config-mode fonts queue failed", err);
12464
+ if (cancelled) return;
12465
+ console.warn(PREVIEW_DEBUG_PREFIX, "config-mode font wait failed", err);
12417
12466
  setFontsReady(true);
12418
12467
  });
12468
+ return () => {
12469
+ cancelled = true;
12470
+ };
12419
12471
  }, [isResolveMode, config]);
12420
12472
  const handleCanvasReady = useCallback(() => {
12421
12473
  if (stabilizationPass === 0) {
@@ -12452,7 +12504,7 @@ function PixldocsPreview(props) {
12452
12504
  !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..." }) })
12453
12505
  ] });
12454
12506
  }
12455
- const PACKAGE_VERSION = "0.5.54";
12507
+ const PACKAGE_VERSION = "0.5.57";
12456
12508
  let __underlineFixInstalled = false;
12457
12509
  function installUnderlineFix(fab) {
12458
12510
  var _a;
@@ -12549,6 +12601,22 @@ function installUnderlineFix(fab) {
12549
12601
  __underlineFixInstalled = true;
12550
12602
  console.log(`[canvas-renderer] underline-fix monkey patch installed (v${PACKAGE_VERSION})`);
12551
12603
  }
12604
+ function configHasAutoShrinkText(config) {
12605
+ var _a;
12606
+ if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return false;
12607
+ const walk = (nodes) => {
12608
+ for (const node of nodes || []) {
12609
+ if (!node) continue;
12610
+ if (node.type === "text" && node.overflowPolicy === "auto-shrink") return true;
12611
+ if (Array.isArray(node.children) && node.children.length && walk(node.children)) return true;
12612
+ }
12613
+ return false;
12614
+ };
12615
+ for (const page of config.pages) {
12616
+ if (walk(page.children || [])) return true;
12617
+ }
12618
+ return false;
12619
+ }
12552
12620
  class PixldocsRenderer {
12553
12621
  constructor(config) {
12554
12622
  __publicField(this, "config");
@@ -12575,6 +12643,11 @@ class PixldocsRenderer {
12575
12643
  throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12576
12644
  }
12577
12645
  await ensureFontsForResolvedConfig(templateConfig);
12646
+ if (!options.skipFontReadyWait) {
12647
+ const hasAutoShrink = configHasAutoShrinkText(templateConfig);
12648
+ const defaultWait = hasAutoShrink ? 4e3 : 1800;
12649
+ await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
12650
+ }
12578
12651
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12579
12652
  setPackageApiUrl2(this.config.imageProxyUrl);
12580
12653
  const dataUrl = await this.renderPageViaPreviewCanvas(
@@ -12582,7 +12655,8 @@ class PixldocsRenderer {
12582
12655
  pageIndex,
12583
12656
  pixelRatio,
12584
12657
  format,
12585
- quality
12658
+ quality,
12659
+ { skipFontReadyWait: options.skipFontReadyWait, waitForFontsMs: options.waitForFontsMs }
12586
12660
  );
12587
12661
  return {
12588
12662
  dataUrl,
@@ -12596,9 +12670,14 @@ class PixldocsRenderer {
12596
12670
  * Render all pages and return array of results.
12597
12671
  */
12598
12672
  async renderAllPages(templateConfig, options = {}) {
12673
+ if (!options.skipFontReadyWait) {
12674
+ const hasAutoShrink = configHasAutoShrinkText(templateConfig);
12675
+ const defaultWait = hasAutoShrink ? 4e3 : 1800;
12676
+ await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
12677
+ }
12599
12678
  const results = [];
12600
12679
  for (let i = 0; i < templateConfig.pages.length; i++) {
12601
- results.push(await this.render(templateConfig, { ...options, pageIndex: i }));
12680
+ results.push(await this.render(templateConfig, { ...options, pageIndex: i, skipFontReadyWait: true }));
12602
12681
  }
12603
12682
  return results;
12604
12683
  }
@@ -12635,6 +12714,8 @@ class PixldocsRenderer {
12635
12714
  throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12636
12715
  }
12637
12716
  await ensureFontsForResolvedConfig(templateConfig);
12717
+ const hasAutoShrinkSvg = configHasAutoShrinkText(templateConfig);
12718
+ await this.awaitFontsForConfig(templateConfig, hasAutoShrinkSvg ? 4e3 : 1800);
12638
12719
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12639
12720
  setPackageApiUrl2(this.config.imageProxyUrl);
12640
12721
  const canvasWidth = templateConfig.canvas.width;
@@ -12646,6 +12727,8 @@ class PixldocsRenderer {
12646
12727
  */
12647
12728
  async renderAllPageSvgs(templateConfig) {
12648
12729
  await ensureFontsForResolvedConfig(templateConfig);
12730
+ const hasAutoShrinkSvg = configHasAutoShrinkText(templateConfig);
12731
+ await this.awaitFontsForConfig(templateConfig, hasAutoShrinkSvg ? 4e3 : 1800);
12649
12732
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12650
12733
  setPackageApiUrl2(this.config.imageProxyUrl);
12651
12734
  const results = [];
@@ -12898,6 +12981,26 @@ class PixldocsRenderer {
12898
12981
  ]);
12899
12982
  await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
12900
12983
  }
12984
+ /**
12985
+ * Block until the webfonts referenced by `config` have actually loaded
12986
+ * (or `maxWaitMs` elapses). Used by the headless capture path BEFORE
12987
+ * mounting `PreviewCanvas`, so the synchronous `createText` auto-shrink
12988
+ * loop measures against final font metrics instead of fallback ones.
12989
+ *
12990
+ * Stronger than `ensureFontsForResolvedConfig` (which is fire-and-forget)
12991
+ * — this awaits each `document.fonts.load(spec)` AND `document.fonts.ready`,
12992
+ * racing the whole thing against `maxWaitMs` so a slow CDN can't hang the
12993
+ * renderer.
12994
+ */
12995
+ async awaitFontsForConfig(config, maxWaitMs) {
12996
+ if (typeof document === "undefined" || !document.fonts) return;
12997
+ void ensureFontsForResolvedConfig(config);
12998
+ await this.waitForRelevantFonts(config, maxWaitMs);
12999
+ await Promise.race([
13000
+ document.fonts.ready.catch(() => void 0).then(() => void 0),
13001
+ new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
13002
+ ]);
13003
+ }
12901
13004
  getNormalizedGradientStops(gradient) {
12902
13005
  const stops = Array.isArray(gradient == null ? void 0 : gradient.stops) ? gradient.stops.map((stop) => ({
12903
13006
  offset: Math.max(0, Math.min(1, Number((stop == null ? void 0 : stop.offset) ?? 0))),
@@ -12971,10 +13074,19 @@ class PixldocsRenderer {
12971
13074
  } catch {
12972
13075
  }
12973
13076
  }
12974
- async renderPageViaPreviewCanvas(config, pageIndex, pixelRatio, format, quality) {
13077
+ async renderPageViaPreviewCanvas(config, pageIndex, pixelRatio, format, quality, options = {}) {
12975
13078
  const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
12976
13079
  const canvasWidth = config.canvas.width;
12977
13080
  const canvasHeight = config.canvas.height;
13081
+ const hasAutoShrink = configHasAutoShrinkText(config);
13082
+ let firstMountSettled = false;
13083
+ let lateFontSettleDetected = false;
13084
+ if (typeof document !== "undefined" && document.fonts && hasAutoShrink) {
13085
+ document.fonts.ready.then(() => {
13086
+ if (firstMountSettled) lateFontSettleDetected = true;
13087
+ }).catch(() => {
13088
+ });
13089
+ }
12978
13090
  return new Promise((resolve, reject) => {
12979
13091
  const container = document.createElement("div");
12980
13092
  container.style.cssText = `
@@ -12987,6 +13099,8 @@ class PixldocsRenderer {
12987
13099
  cleanup();
12988
13100
  reject(new Error("Render timeout (30s)"));
12989
13101
  }, 3e4);
13102
+ let root;
13103
+ let mountKey = 0;
12990
13104
  const cleanup = () => {
12991
13105
  clearTimeout(timeout);
12992
13106
  try {
@@ -12995,6 +13109,46 @@ class PixldocsRenderer {
12995
13109
  }
12996
13110
  container.remove();
12997
13111
  };
13112
+ const remountWithFreshKey = async () => {
13113
+ mountKey += 1;
13114
+ try {
13115
+ clearMeasurementCache();
13116
+ } catch {
13117
+ }
13118
+ try {
13119
+ clearFabricCharCache();
13120
+ } catch {
13121
+ }
13122
+ try {
13123
+ root.unmount();
13124
+ } catch {
13125
+ }
13126
+ root = createRoot(container);
13127
+ await new Promise((settle) => {
13128
+ const onReadyOnce = () => {
13129
+ this.waitForCanvasScene(container, config, pageIndex).then(async () => {
13130
+ const fabricInstance = this.getFabricCanvasFromContainer(container);
13131
+ const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
13132
+ await this.waitForCanvasImages(container, expectedImageCount);
13133
+ await this.waitForStableTextMetrics(container, config);
13134
+ await this.waitForCanvasScene(container, config, pageIndex);
13135
+ if (!fabricInstance) return settle();
13136
+ settle();
13137
+ }).catch(() => settle());
13138
+ };
13139
+ root.render(
13140
+ createElement(PreviewCanvas2, {
13141
+ key: `remount-${mountKey}`,
13142
+ config,
13143
+ pageIndex,
13144
+ zoom: pixelRatio,
13145
+ absoluteZoom: true,
13146
+ skipFontReadyWait: false,
13147
+ onReady: onReadyOnce
13148
+ })
13149
+ );
13150
+ });
13151
+ };
12998
13152
  const onReady = () => {
12999
13153
  this.waitForCanvasScene(container, config, pageIndex).then(async () => {
13000
13154
  try {
@@ -13003,16 +13157,23 @@ class PixldocsRenderer {
13003
13157
  await this.waitForCanvasImages(container, expectedImageCount);
13004
13158
  await this.waitForStableTextMetrics(container, config);
13005
13159
  await this.waitForCanvasScene(container, config, pageIndex);
13160
+ firstMountSettled = true;
13161
+ if (hasAutoShrink && lateFontSettleDetected) {
13162
+ console.log("[canvas-renderer][parity] late font-settle detected — remounting for auto-shrink reflow");
13163
+ await remountWithFreshKey();
13164
+ }
13006
13165
  const fabricCanvas = container.querySelector("canvas.upper-canvas, canvas");
13007
13166
  const sourceCanvas = (fabricInstance == null ? void 0 : fabricInstance.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || fabricCanvas;
13167
+ const fabricInstanceAfter = this.getFabricCanvasFromContainer(container) || fabricInstance;
13168
+ const sourceCanvasAfter = (fabricInstanceAfter == null ? void 0 : fabricInstanceAfter.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || sourceCanvas;
13008
13169
  if (!sourceCanvas) {
13009
13170
  cleanup();
13010
13171
  reject(new Error("No canvas element found after render"));
13011
13172
  return;
13012
13173
  }
13013
13174
  const exportCanvas = document.createElement("canvas");
13014
- exportCanvas.width = sourceCanvas.width;
13015
- exportCanvas.height = sourceCanvas.height;
13175
+ exportCanvas.width = sourceCanvasAfter.width;
13176
+ exportCanvas.height = sourceCanvasAfter.height;
13016
13177
  const exportCtx = exportCanvas.getContext("2d");
13017
13178
  if (!exportCtx) {
13018
13179
  cleanup();
@@ -13020,10 +13181,10 @@ class PixldocsRenderer {
13020
13181
  return;
13021
13182
  }
13022
13183
  exportCtx.save();
13023
- exportCtx.scale(sourceCanvas.width / canvasWidth, sourceCanvas.height / canvasHeight);
13184
+ exportCtx.scale(sourceCanvasAfter.width / canvasWidth, sourceCanvasAfter.height / canvasHeight);
13024
13185
  this.paintPageBackground(exportCtx, config.pages[pageIndex], canvasWidth, canvasHeight);
13025
13186
  exportCtx.restore();
13026
- exportCtx.drawImage(sourceCanvas, 0, 0);
13187
+ exportCtx.drawImage(sourceCanvasAfter, 0, 0);
13027
13188
  const mimeType = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
13028
13189
  const dataUrl = exportCanvas.toDataURL(mimeType, quality);
13029
13190
  cleanup();
@@ -13034,7 +13195,7 @@ class PixldocsRenderer {
13034
13195
  }
13035
13196
  });
13036
13197
  };
13037
- const root = createRoot(container);
13198
+ root = createRoot(container);
13038
13199
  root.render(
13039
13200
  createElement(PreviewCanvas2, {
13040
13201
  config,
@@ -15515,9 +15676,11 @@ export {
15515
15676
  PixldocsRenderer,
15516
15677
  applyThemeToConfig,
15517
15678
  assemblePdfFromSvgs,
15679
+ awaitFontsForConfig,
15518
15680
  collectFontDescriptorsFromConfig,
15519
15681
  collectFontsFromConfig,
15520
15682
  collectImageUrls,
15683
+ configHasAutoShrinkText$1 as configHasAutoShrinkText,
15521
15684
  embedFont,
15522
15685
  embedFontsForConfig,
15523
15686
  embedFontsInPdf,