@pixldocs/canvas-renderer 0.3.12 → 0.3.14

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
@@ -36,6 +36,8 @@ export declare interface CanvasSize {
36
36
  height: number;
37
37
  }
38
38
 
39
+ export declare function collectFontDescriptorsFromConfig(config: TemplateConfig): FontDescriptor[];
40
+
39
41
  /**
40
42
  * Collect all font families used in a template config.
41
43
  */
@@ -52,6 +54,35 @@ export declare interface DynamicField {
52
54
  [key: string]: any;
53
55
  }
54
56
 
57
+ /**
58
+ * Ensure all fonts required by a fully-resolved TemplateConfig are loaded
59
+ * and available to Fabric/Canvas before rendering.
60
+ *
61
+ * This is the **single API** consumers (and the renderer internally) should
62
+ * call to guarantee font parity with EC2 `/render-from-form`.
63
+ *
64
+ * It:
65
+ * 1. Walks ALL text nodes (including clones/repeatables) collecting
66
+ * fontFamily + fontWeight + fontStyle.
67
+ * 2. Loads each unique family via Google Fonts CSS v1 (idempotent).
68
+ * 3. Explicitly loads each weight+style combo via `document.fonts.load()`.
69
+ * 4. Awaits `document.fonts.ready` so Fabric never paints with fallback faces.
70
+ *
71
+ * Idempotent — safe to call multiple times for the same config.
72
+ */
73
+ export declare function ensureFontsForResolvedConfig(config: TemplateConfig): Promise<void>;
74
+
75
+ /**
76
+ * Walk a fully-resolved TemplateConfig and collect every unique
77
+ * { fontFamily, fontWeight, fontStyle } tuple from all text nodes
78
+ * (including clones, repeatable children, and per-character Fabric styles).
79
+ */
80
+ export declare interface FontDescriptor {
81
+ family: string;
82
+ weight: number | string;
83
+ style: string;
84
+ }
85
+
55
86
  export { InferredSection }
56
87
 
57
88
  /**
package/dist/index.js CHANGED
@@ -10369,6 +10369,165 @@ function paintRepeatableSections(config, repeatableSections) {
10369
10369
  }
10370
10370
  }
10371
10371
  }
10372
+ function normalizeFontFamily(fontStack) {
10373
+ const first = fontStack.split(",")[0].trim();
10374
+ return first.replace(/^['"]|['"]$/g, "");
10375
+ }
10376
+ const loadedFonts = /* @__PURE__ */ new Set();
10377
+ const loadingPromises = /* @__PURE__ */ new Map();
10378
+ async function loadGoogleFontCSS(rawFontFamily) {
10379
+ if (!rawFontFamily || typeof document === "undefined") return;
10380
+ const fontFamily = normalizeFontFamily(rawFontFamily);
10381
+ if (!fontFamily) return;
10382
+ if (loadedFonts.has(fontFamily)) return;
10383
+ const existing = loadingPromises.get(fontFamily);
10384
+ if (existing) return existing;
10385
+ const promise = (async () => {
10386
+ var _a;
10387
+ try {
10388
+ if ((_a = document.fonts) == null ? void 0 : _a.check(`16px "${fontFamily}"`)) {
10389
+ loadedFonts.add(fontFamily);
10390
+ return;
10391
+ }
10392
+ const encoded = encodeURIComponent(fontFamily);
10393
+ const url = `https://fonts.googleapis.com/css?family=${encoded}:300,400,500,600,700&display=swap`;
10394
+ const link = document.createElement("link");
10395
+ link.rel = "stylesheet";
10396
+ link.href = url;
10397
+ link.crossOrigin = "anonymous";
10398
+ await new Promise((resolve, reject) => {
10399
+ link.onload = () => resolve();
10400
+ link.onerror = () => reject(new Error(`Failed to load font: ${fontFamily}`));
10401
+ document.head.appendChild(link);
10402
+ });
10403
+ if (document.fonts) {
10404
+ await document.fonts.load(`16px "${fontFamily}"`);
10405
+ await document.fonts.ready;
10406
+ }
10407
+ loadedFonts.add(fontFamily);
10408
+ } catch (e) {
10409
+ console.warn(`[@pixldocs/canvas-renderer] Font load failed: ${fontFamily}`, e);
10410
+ }
10411
+ })();
10412
+ loadingPromises.set(fontFamily, promise);
10413
+ await promise;
10414
+ loadingPromises.delete(fontFamily);
10415
+ }
10416
+ function collectFontsFromConfig(config) {
10417
+ var _a;
10418
+ const fonts = /* @__PURE__ */ new Set();
10419
+ fonts.add("Open Sans");
10420
+ fonts.add("Hind");
10421
+ function walk(nodes) {
10422
+ var _a2;
10423
+ if (!nodes) return;
10424
+ for (const node of nodes) {
10425
+ if (node.fontFamily) fonts.add(normalizeFontFamily(node.fontFamily));
10426
+ if ((_a2 = node.smartProps) == null ? void 0 : _a2.fontFamily) fonts.add(normalizeFontFamily(node.smartProps.fontFamily));
10427
+ if (node.styles && Array.isArray(node.styles)) {
10428
+ for (const lineStyle of node.styles) {
10429
+ if (lineStyle && typeof lineStyle === "object") {
10430
+ for (const charStyle of Object.values(lineStyle)) {
10431
+ if (charStyle == null ? void 0 : charStyle.fontFamily) fonts.add(normalizeFontFamily(charStyle.fontFamily));
10432
+ }
10433
+ }
10434
+ }
10435
+ }
10436
+ if (node.children) walk(node.children);
10437
+ }
10438
+ }
10439
+ for (const page of config.pages || []) {
10440
+ walk(page.children || []);
10441
+ }
10442
+ if ((_a = config.themeConfig) == null ? void 0 : _a.variables) {
10443
+ for (const def of Object.values(config.themeConfig.variables)) {
10444
+ if (def.value && typeof def.value === "string" && !def.value.startsWith("#") && !def.value.startsWith("rgb")) {
10445
+ if (def.label && /font/i.test(def.label)) {
10446
+ fonts.add(normalizeFontFamily(def.value));
10447
+ }
10448
+ }
10449
+ }
10450
+ }
10451
+ return fonts;
10452
+ }
10453
+ function collectFontDescriptorsFromConfig(config) {
10454
+ var _a;
10455
+ const seen = /* @__PURE__ */ new Set();
10456
+ const descriptors = [];
10457
+ function add(family, weight, style) {
10458
+ const f = normalizeFontFamily(family);
10459
+ if (!f) return;
10460
+ const w = weight ?? 400;
10461
+ const s = style ?? "normal";
10462
+ const key = `${f}|${w}|${s}`;
10463
+ if (seen.has(key)) return;
10464
+ seen.add(key);
10465
+ descriptors.push({ family: f, weight: w, style: s });
10466
+ }
10467
+ function walk(nodes) {
10468
+ var _a2;
10469
+ if (!nodes) return;
10470
+ for (const node of nodes) {
10471
+ if (node.fontFamily) {
10472
+ add(node.fontFamily, node.fontWeight, node.fontStyle);
10473
+ if (node.type === "text") {
10474
+ for (const w of [300, 400, 500, 600, 700]) {
10475
+ add(node.fontFamily, w, node.fontStyle);
10476
+ }
10477
+ }
10478
+ }
10479
+ if ((_a2 = node.smartProps) == null ? void 0 : _a2.fontFamily) {
10480
+ add(node.smartProps.fontFamily, node.smartProps.fontWeight, node.smartProps.fontStyle);
10481
+ }
10482
+ if (node.styles) {
10483
+ const styleEntries = Array.isArray(node.styles) ? node.styles : Object.values(node.styles);
10484
+ for (const lineStyle of styleEntries) {
10485
+ if (lineStyle && typeof lineStyle === "object") {
10486
+ for (const charStyle of Object.values(lineStyle)) {
10487
+ if (charStyle == null ? void 0 : charStyle.fontFamily) {
10488
+ add(charStyle.fontFamily, charStyle.fontWeight, charStyle.fontStyle);
10489
+ }
10490
+ }
10491
+ }
10492
+ }
10493
+ }
10494
+ if (node.children) walk(node.children);
10495
+ }
10496
+ }
10497
+ add("Open Sans", 400, "normal");
10498
+ add("Hind", 400, "normal");
10499
+ add("Hind", 700, "normal");
10500
+ for (const page of config.pages || []) {
10501
+ walk(page.children || []);
10502
+ }
10503
+ if ((_a = config.themeConfig) == null ? void 0 : _a.variables) {
10504
+ for (const def of Object.values(config.themeConfig.variables)) {
10505
+ if (def.value && typeof def.value === "string" && !def.value.startsWith("#") && !def.value.startsWith("rgb")) {
10506
+ if (def.label && /font/i.test(def.label)) {
10507
+ add(def.value);
10508
+ }
10509
+ }
10510
+ }
10511
+ }
10512
+ return descriptors;
10513
+ }
10514
+ async function ensureFontsForResolvedConfig(config) {
10515
+ if (typeof document === "undefined") return;
10516
+ const descriptors = collectFontDescriptorsFromConfig(config);
10517
+ const families = new Set(descriptors.map((d) => d.family));
10518
+ await Promise.all([...families].map((f) => loadGoogleFontCSS(f)));
10519
+ if (document.fonts) {
10520
+ const loadPromises = descriptors.map((d) => {
10521
+ const stylePrefix = d.style === "italic" ? "italic " : "";
10522
+ const weightStr = String(d.weight);
10523
+ const spec = `${stylePrefix}${weightStr} 16px "${d.family}"`;
10524
+ return document.fonts.load(spec).catch(() => {
10525
+ });
10526
+ });
10527
+ await Promise.all(loadPromises);
10528
+ await document.fonts.ready;
10529
+ }
10530
+ }
10372
10531
  function PixldocsPreview(props) {
10373
10532
  const {
10374
10533
  pageIndex = 0,
@@ -10386,6 +10545,7 @@ function PixldocsPreview(props) {
10386
10545
  }, [imageProxyUrl]);
10387
10546
  const [resolvedConfig, setResolvedConfig] = useState(null);
10388
10547
  const [isLoading, setIsLoading] = useState(false);
10548
+ const [fontsReady, setFontsReady] = useState(false);
10389
10549
  const isResolveMode = !("config" in props && props.config);
10390
10550
  useEffect(() => {
10391
10551
  if (!isResolveMode) {
@@ -10406,7 +10566,17 @@ function PixldocsPreview(props) {
10406
10566
  }).then((resolved) => {
10407
10567
  if (!cancelled) {
10408
10568
  setResolvedConfig(resolved.config);
10409
- setIsLoading(false);
10569
+ ensureFontsForResolvedConfig(resolved.config).then(() => {
10570
+ if (!cancelled) {
10571
+ setFontsReady(true);
10572
+ setIsLoading(false);
10573
+ }
10574
+ }).catch(() => {
10575
+ if (!cancelled) {
10576
+ setFontsReady(true);
10577
+ setIsLoading(false);
10578
+ }
10579
+ });
10410
10580
  }
10411
10581
  }).catch((err) => {
10412
10582
  if (!cancelled) {
@@ -10426,6 +10596,11 @@ function PixldocsPreview(props) {
10426
10596
  isResolveMode ? props.themeId : void 0
10427
10597
  ]);
10428
10598
  const config = isResolveMode ? resolvedConfig : props.config;
10599
+ useEffect(() => {
10600
+ if (isResolveMode || !config) return;
10601
+ setFontsReady(false);
10602
+ ensureFontsForResolvedConfig(config).then(() => setFontsReady(true)).catch(() => setFontsReady(true));
10603
+ }, [isResolveMode, config]);
10429
10604
  if (isLoading) {
10430
10605
  return /* @__PURE__ */ jsx("div", { className, style: { ...style, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) });
10431
10606
  }
@@ -10442,77 +10617,6 @@ function PixldocsPreview(props) {
10442
10617
  }
10443
10618
  ) });
10444
10619
  }
10445
- function normalizeFontFamily(fontStack) {
10446
- const first = fontStack.split(",")[0].trim();
10447
- return first.replace(/^['"]|['"]$/g, "");
10448
- }
10449
- const loadedFonts = /* @__PURE__ */ new Set();
10450
- const loadingPromises = /* @__PURE__ */ new Map();
10451
- async function loadGoogleFontCSS(rawFontFamily) {
10452
- if (!rawFontFamily || typeof document === "undefined") return;
10453
- const fontFamily = normalizeFontFamily(rawFontFamily);
10454
- if (!fontFamily) return;
10455
- if (loadedFonts.has(fontFamily)) return;
10456
- const existing = loadingPromises.get(fontFamily);
10457
- if (existing) return existing;
10458
- const promise = (async () => {
10459
- var _a;
10460
- try {
10461
- if ((_a = document.fonts) == null ? void 0 : _a.check(`16px "${fontFamily}"`)) {
10462
- loadedFonts.add(fontFamily);
10463
- return;
10464
- }
10465
- const encoded = encodeURIComponent(fontFamily);
10466
- const url = `https://fonts.googleapis.com/css?family=${encoded}:300,400,500,600,700&display=swap`;
10467
- const link = document.createElement("link");
10468
- link.rel = "stylesheet";
10469
- link.href = url;
10470
- link.crossOrigin = "anonymous";
10471
- await new Promise((resolve, reject) => {
10472
- link.onload = () => resolve();
10473
- link.onerror = () => reject(new Error(`Failed to load font: ${fontFamily}`));
10474
- document.head.appendChild(link);
10475
- });
10476
- if (document.fonts) {
10477
- await document.fonts.load(`16px "${fontFamily}"`);
10478
- await document.fonts.ready;
10479
- }
10480
- loadedFonts.add(fontFamily);
10481
- } catch (e) {
10482
- console.warn(`[@pixldocs/canvas-renderer] Font load failed: ${fontFamily}`, e);
10483
- }
10484
- })();
10485
- loadingPromises.set(fontFamily, promise);
10486
- await promise;
10487
- loadingPromises.delete(fontFamily);
10488
- }
10489
- function collectFontsFromConfig(config) {
10490
- var _a;
10491
- const fonts = /* @__PURE__ */ new Set();
10492
- fonts.add("Open Sans");
10493
- function walk(nodes) {
10494
- var _a2;
10495
- if (!nodes) return;
10496
- for (const node of nodes) {
10497
- if (node.fontFamily) fonts.add(normalizeFontFamily(node.fontFamily));
10498
- if ((_a2 = node.smartProps) == null ? void 0 : _a2.fontFamily) fonts.add(normalizeFontFamily(node.smartProps.fontFamily));
10499
- if (node.children) walk(node.children);
10500
- }
10501
- }
10502
- for (const page of config.pages || []) {
10503
- walk(page.children || []);
10504
- }
10505
- if ((_a = config.themeConfig) == null ? void 0 : _a.variables) {
10506
- for (const def of Object.values(config.themeConfig.variables)) {
10507
- if (def.value && typeof def.value === "string" && !def.value.startsWith("#") && !def.value.startsWith("rgb")) {
10508
- if (def.label && /font/i.test(def.label)) {
10509
- fonts.add(normalizeFontFamily(def.value));
10510
- }
10511
- }
10512
- }
10513
- }
10514
- return fonts;
10515
- }
10516
10620
  class PixldocsRenderer {
10517
10621
  constructor(config) {
10518
10622
  __publicField(this, "config");
@@ -10533,8 +10637,7 @@ class PixldocsRenderer {
10533
10637
  if (!page) {
10534
10638
  throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
10535
10639
  }
10536
- const fonts = collectFontsFromConfig(templateConfig);
10537
- await Promise.all([...fonts].map((f) => loadGoogleFontCSS(f)));
10640
+ await ensureFontsForResolvedConfig(templateConfig);
10538
10641
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
10539
10642
  setPackageApiUrl2(this.config.imageProxyUrl);
10540
10643
  const dataUrl = await this.renderPageViaPreviewCanvas(
@@ -10762,7 +10865,9 @@ export {
10762
10865
  PixldocsPreview,
10763
10866
  PixldocsRenderer,
10764
10867
  applyThemeToConfig,
10868
+ collectFontDescriptorsFromConfig,
10765
10869
  collectFontsFromConfig,
10870
+ ensureFontsForResolvedConfig,
10766
10871
  loadGoogleFontCSS,
10767
10872
  normalizeFontFamily,
10768
10873
  resolveFromForm,