@pixldocs/canvas-renderer 0.5.57 → 0.5.59

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.57";
257
+ export declare const PACKAGE_VERSION = "0.5.59";
235
258
 
236
259
  export declare interface PageSettings {
237
260
  backgroundColor?: string;
package/dist/index.js CHANGED
@@ -245,6 +245,57 @@ const WIDTH_BUCKET_PX = 8;
245
245
  function bucketWidth(w) {
246
246
  return Math.round(w / WIDTH_BUCKET_PX) * WIDTH_BUCKET_PX;
247
247
  }
248
+ let measureCanvas = null;
249
+ function getMeasureContext() {
250
+ if (typeof document === "undefined") return null;
251
+ measureCanvas ?? (measureCanvas = document.createElement("canvas"));
252
+ return measureCanvas.getContext("2d");
253
+ }
254
+ function lineToString(line) {
255
+ return Array.isArray(line) ? line.join("") : String(line ?? "");
256
+ }
257
+ function measureTextLineWithCanvas(textbox, lineText, lineIndex) {
258
+ var _a, _b, _c, _d, _e;
259
+ if (!lineText) return 0;
260
+ const ctx = getMeasureContext();
261
+ if (!ctx) return null;
262
+ const tb = textbox;
263
+ const fontSize = Number(((_a = tb.getValueOfPropertyAt) == null ? void 0 : _a.call(tb, lineIndex, 0, "fontSize")) ?? textbox.fontSize ?? 16);
264
+ const fontStyle = String(((_b = tb.getValueOfPropertyAt) == null ? void 0 : _b.call(tb, lineIndex, 0, "fontStyle")) ?? textbox.fontStyle ?? "normal");
265
+ const fontWeight = String(((_c = tb.getValueOfPropertyAt) == null ? void 0 : _c.call(tb, lineIndex, 0, "fontWeight")) ?? textbox.fontWeight ?? "400");
266
+ const fontFamily = String(((_d = tb.getValueOfPropertyAt) == null ? void 0 : _d.call(tb, lineIndex, 0, "fontFamily")) ?? textbox.fontFamily ?? "Open Sans");
267
+ const charSpacing = Number(((_e = tb.getValueOfPropertyAt) == null ? void 0 : _e.call(tb, lineIndex, 0, "charSpacing")) ?? textbox.charSpacing ?? 0);
268
+ ctx.save();
269
+ ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px ${fontFamily}`;
270
+ const measured = ctx.measureText(lineText).width;
271
+ ctx.restore();
272
+ const graphemeCount = Array.from(lineText).length;
273
+ const spacingWidth = graphemeCount > 1 ? charSpacing / 1e3 * fontSize * (graphemeCount - 1) : 0;
274
+ return Math.max(0, measured + spacingWidth);
275
+ }
276
+ function getCanvasMeasuredTextboxLineWidths(textbox) {
277
+ const rawLines = textbox._textLines ?? textbox.textLines ?? [];
278
+ const fallback = textbox.__lineWidths ?? [];
279
+ if (!rawLines.length) return fallback;
280
+ const measured = rawLines.map((line, index) => measureTextLineWithCanvas(textbox, lineToString(line), index));
281
+ if (measured.some((w) => w == null || Number.isNaN(w))) return fallback;
282
+ return measured;
283
+ }
284
+ function getTextboxWidthFitMetrics(textbox, targetWidth) {
285
+ const actualTextboxWidth = Number(textbox.width ?? targetWidth);
286
+ const dynamicMinWidth = Number(textbox.dynamicMinWidth ?? 0);
287
+ const lineWidths = getCanvasMeasuredTextboxLineWidths(textbox);
288
+ const maxLineWidth = lineWidths.length > 0 ? Math.max(...lineWidths) : 0;
289
+ const grownWidth = Math.max(actualTextboxWidth, dynamicMinWidth);
290
+ const widthDidGrow = grownWidth > targetWidth + 0.5;
291
+ return {
292
+ actualTextboxWidth,
293
+ dynamicMinWidth,
294
+ maxLineWidth,
295
+ widthDidGrow,
296
+ fitsWidth: !widthDidGrow && maxLineWidth <= targetWidth + 1
297
+ };
298
+ }
248
299
  function getCacheKey(element) {
249
300
  const widthPx = typeof element.width === "number" ? element.width : 200;
250
301
  return JSON.stringify({
@@ -301,11 +352,7 @@ function measureTextHeight(element) {
301
352
  const renderedLineCount = ((_a = testTb.textLines) == null ? void 0 : _a.length) || 1;
302
353
  const hasNoImplicitWrap = renderedLineCount <= explicitLineCount;
303
354
  const fitsHeight = !baseHeight || textHeight <= baseHeight;
304
- const actualTextboxWidth = testTb.width ?? measureWidth;
305
- const widthDidGrow = actualTextboxWidth > measureWidth + 0.5;
306
- const lineWidths = testTb.__lineWidths;
307
- const maxLineWidth = lineWidths && lineWidths.length > 0 ? Math.max(...lineWidths) : 0;
308
- const fitsWidth = !widthDidGrow && maxLineWidth <= measureWidth + 1;
355
+ const { fitsWidth } = getTextboxWidthFitMetrics(testTb, measureWidth);
309
356
  if (hasNoImplicitWrap && fitsHeight && fitsWidth) break;
310
357
  fontSize--;
311
358
  }
@@ -5290,20 +5337,16 @@ function createText(element) {
5290
5337
  const renderedLineCount = ((_a = testTextbox.textLines) == null ? void 0 : _a.length) || 1;
5291
5338
  const hasNoImplicitWrap = renderedLineCount <= explicitLineCount;
5292
5339
  const fitsHeight = !baseHeight || textHeight <= baseHeight;
5293
- const actualTextboxWidth = testTextbox.width ?? fixedWidth;
5294
- const widthDidGrow = actualTextboxWidth > fixedWidth + 0.5;
5295
- const lineWidths = testTextbox.__lineWidths;
5296
- const maxLineWidth = lineWidths && lineWidths.length > 0 ? Math.max(...lineWidths) : 0;
5297
- const fitsWidth = !widthDidGrow && maxLineWidth <= fixedWidth + 1;
5340
+ const widthMetrics = getTextboxWidthFitMetrics(testTextbox, fixedWidth);
5341
+ const { fitsWidth } = widthMetrics;
5298
5342
  if (debugAutoShrink) {
5299
5343
  lastIter = {
5300
5344
  fontSize,
5301
5345
  renderedLineCount,
5302
5346
  explicitLineCount,
5303
5347
  textHeight,
5304
- maxLineWidth,
5348
+ ...widthMetrics,
5305
5349
  fixedWidth,
5306
- widthDidGrow,
5307
5350
  hasNoImplicitWrap,
5308
5351
  fitsHeight,
5309
5352
  fitsWidth
@@ -8854,11 +8897,7 @@ const PageCanvas = forwardRef(
8854
8897
  const renderedLineCount = ((_b = testTextbox.textLines) == null ? void 0 : _b.length) || 1;
8855
8898
  const hasNoImplicitWrap = renderedLineCount <= explicitLineCount;
8856
8899
  const fitsHeight = rH <= 0 || textHeight <= rH;
8857
- const actualTextboxWidth = testTextbox.width ?? fixedWidth;
8858
- const widthDidGrow = actualTextboxWidth > fixedWidth + 0.5;
8859
- const lineWidths = testTextbox.__lineWidths;
8860
- const maxLineWidth = lineWidths && lineWidths.length > 0 ? Math.max(...lineWidths) : 0;
8861
- const fitsWidth = !widthDidGrow && maxLineWidth <= fixedWidth + 1;
8900
+ const { fitsWidth } = getTextboxWidthFitMetrics(testTextbox, fixedWidth);
8862
8901
  if (hasNoImplicitWrap && fitsHeight && fitsWidth) {
8863
8902
  break;
8864
8903
  }
@@ -12244,6 +12283,46 @@ async function ensureFontsForResolvedConfig(config) {
12244
12283
  });
12245
12284
  }
12246
12285
  }
12286
+ function configHasAutoShrinkText$1(config) {
12287
+ var _a;
12288
+ if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return false;
12289
+ const walk = (nodes) => {
12290
+ for (const node of nodes || []) {
12291
+ if (!node) continue;
12292
+ if (node.type === "text" && node.overflowPolicy === "auto-shrink") return true;
12293
+ if (Array.isArray(node.children) && node.children.length && walk(node.children)) return true;
12294
+ }
12295
+ return false;
12296
+ };
12297
+ for (const page of config.pages) {
12298
+ if (walk(page.children || [])) return true;
12299
+ }
12300
+ return false;
12301
+ }
12302
+ async function awaitFontsForConfig(config, maxWaitMs) {
12303
+ if (typeof document === "undefined" || !document.fonts) return;
12304
+ void ensureFontsForResolvedConfig(config);
12305
+ const descriptors = collectFontDescriptorsFromConfig(config);
12306
+ if (descriptors.length === 0) return;
12307
+ const loads = Promise.all(
12308
+ descriptors.map((d) => {
12309
+ const stylePrefix = d.style === "italic" ? "italic " : "";
12310
+ const spec = `${stylePrefix}${d.weight} 16px "${d.family}"`;
12311
+ return document.fonts.load(spec).catch(() => []);
12312
+ })
12313
+ ).then(() => void 0);
12314
+ await Promise.race([
12315
+ loads,
12316
+ new Promise((resolve) => setTimeout(resolve, maxWaitMs))
12317
+ ]);
12318
+ await Promise.race([
12319
+ document.fonts.ready.catch(() => void 0).then(() => void 0),
12320
+ new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
12321
+ ]);
12322
+ await new Promise(
12323
+ (resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
12324
+ );
12325
+ }
12247
12326
  const PREVIEW_DEBUG_PREFIX = "[canvas-renderer][preview-debug]";
12248
12327
  function countUnderlinedNodes(config) {
12249
12328
  var _a;
@@ -12329,15 +12408,17 @@ function PixldocsPreview(props) {
12329
12408
  underlinedNodes: countUnderlinedNodes(resolved.config)
12330
12409
  });
12331
12410
  setResolvedConfig(resolved.config);
12332
- ensureFontsForResolvedConfig(resolved.config).then(() => {
12411
+ const hasAutoShrink = configHasAutoShrinkText$1(resolved.config);
12412
+ const waitMs = hasAutoShrink ? 4e3 : 1800;
12413
+ awaitFontsForConfig(resolved.config, waitMs).then(() => {
12333
12414
  if (!cancelled) {
12334
- console.log(PREVIEW_DEBUG_PREFIX, "resolve-mode fonts queued");
12415
+ console.log(PREVIEW_DEBUG_PREFIX, "resolve-mode fonts settled", { hasAutoShrink, waitMs });
12335
12416
  setFontsReady(true);
12336
12417
  setIsLoading(false);
12337
12418
  }
12338
12419
  }).catch((err) => {
12339
12420
  if (!cancelled) {
12340
- console.warn(PREVIEW_DEBUG_PREFIX, "resolve-mode fonts queue failed", err);
12421
+ console.warn(PREVIEW_DEBUG_PREFIX, "resolve-mode font wait failed", err);
12341
12422
  setFontsReady(true);
12342
12423
  setIsLoading(false);
12343
12424
  }
@@ -12406,16 +12487,26 @@ function PixldocsPreview(props) {
12406
12487
  setFontsReady(false);
12407
12488
  setCanvasSettled(false);
12408
12489
  setStabilizationPass(0);
12409
- ensureFontsForResolvedConfig(config).then(() => {
12410
- console.log(PREVIEW_DEBUG_PREFIX, "config-mode fonts queued", {
12490
+ let cancelled = false;
12491
+ const hasAutoShrink = configHasAutoShrinkText$1(config);
12492
+ const waitMs = hasAutoShrink ? 4e3 : 1800;
12493
+ awaitFontsForConfig(config, waitMs).then(() => {
12494
+ if (cancelled) return;
12495
+ console.log(PREVIEW_DEBUG_PREFIX, "config-mode fonts settled", {
12411
12496
  pageIndex,
12497
+ hasAutoShrink,
12498
+ waitMs,
12412
12499
  underlinedNodes: countUnderlinedNodes(config)
12413
12500
  });
12414
12501
  setFontsReady(true);
12415
12502
  }).catch((err) => {
12416
- console.warn(PREVIEW_DEBUG_PREFIX, "config-mode fonts queue failed", err);
12503
+ if (cancelled) return;
12504
+ console.warn(PREVIEW_DEBUG_PREFIX, "config-mode font wait failed", err);
12417
12505
  setFontsReady(true);
12418
12506
  });
12507
+ return () => {
12508
+ cancelled = true;
12509
+ };
12419
12510
  }, [isResolveMode, config]);
12420
12511
  const handleCanvasReady = useCallback(() => {
12421
12512
  if (stabilizationPass === 0) {
@@ -12452,7 +12543,7 @@ function PixldocsPreview(props) {
12452
12543
  !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
12544
  ] });
12454
12545
  }
12455
- const PACKAGE_VERSION = "0.5.57";
12546
+ const PACKAGE_VERSION = "0.5.59";
12456
12547
  let __underlineFixInstalled = false;
12457
12548
  function installUnderlineFix(fab) {
12458
12549
  var _a;
@@ -15131,8 +15222,8 @@ async function convertTextDecorationsToLines(svg) {
15131
15222
  let ctx = null;
15132
15223
  try {
15133
15224
  const realDoc = typeof document !== "undefined" ? document : doc;
15134
- const measureCanvas = realDoc.createElement("canvas");
15135
- ctx = measureCanvas.getContext("2d");
15225
+ const measureCanvas2 = realDoc.createElement("canvas");
15226
+ ctx = measureCanvas2.getContext("2d");
15136
15227
  } catch {
15137
15228
  }
15138
15229
  const textEls = Array.from(svg.querySelectorAll("text"));
@@ -15624,9 +15715,11 @@ export {
15624
15715
  PixldocsRenderer,
15625
15716
  applyThemeToConfig,
15626
15717
  assemblePdfFromSvgs,
15718
+ awaitFontsForConfig,
15627
15719
  collectFontDescriptorsFromConfig,
15628
15720
  collectFontsFromConfig,
15629
15721
  collectImageUrls,
15722
+ configHasAutoShrinkText$1 as configHasAutoShrinkText,
15630
15723
  embedFont,
15631
15724
  embedFontsForConfig,
15632
15725
  embedFontsInPdf,