@pixldocs/canvas-renderer 0.5.17 → 0.5.19

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
@@ -5689,6 +5689,7 @@ const PageCanvas = react.forwardRef(
5689
5689
  onDynamicFieldClick,
5690
5690
  canvasUpdateVersion = 0,
5691
5691
  pageChildren,
5692
+ skipFontReadyWait = false,
5692
5693
  onReady
5693
5694
  }, ref) => {
5694
5695
  const isEditorMode = mode === "editor";
@@ -5974,9 +5975,11 @@ const PageCanvas = react.forwardRef(
5974
5975
  await Promise.all(fontFamilies.map((f) => ensureFontLoaded(f)));
5975
5976
  }
5976
5977
  }
5977
- await waitForFontsReady();
5978
- await waitUntilFontsAvailable(fontFamilies, { timeoutMs: 3500, pollIntervalMs: 60 });
5979
- await new Promise((r) => requestAnimationFrame(() => r()));
5978
+ if (!skipFontReadyWait) {
5979
+ await waitForFontsReady();
5980
+ await waitUntilFontsAvailable(fontFamilies, { timeoutMs: 3500, pollIntervalMs: 60 });
5981
+ await new Promise((r) => requestAnimationFrame(() => r()));
5982
+ }
5980
5983
  clearFabricCharCache();
5981
5984
  clearMeasurementCache();
5982
5985
  setReady(true);
@@ -9413,6 +9416,7 @@ function PreviewCanvas({
9413
9416
  pageIndex = 0,
9414
9417
  zoom = 1,
9415
9418
  absoluteZoom = false,
9419
+ skipFontReadyWait = false,
9416
9420
  className,
9417
9421
  onDynamicFieldClick,
9418
9422
  onReady
@@ -9567,6 +9571,7 @@ function PreviewCanvas({
9567
9571
  selectedIds: [],
9568
9572
  activeTool: "select",
9569
9573
  mode: "preview",
9574
+ skipFontReadyWait,
9570
9575
  dynamicFieldIds,
9571
9576
  onDynamicFieldClick: handleDynamicFieldClick,
9572
9577
  onReady
@@ -11999,6 +12004,66 @@ function PixldocsPreview(props) {
11999
12004
  !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..." }) })
12000
12005
  ] });
12001
12006
  }
12007
+ const inlinedAssetCache = /* @__PURE__ */ new Map();
12008
+ function shouldInlineImageUrl(url) {
12009
+ if (!url || url.startsWith("data:")) return false;
12010
+ if (url.startsWith("blob:")) return true;
12011
+ if (url.startsWith("/") && !url.startsWith("//")) return true;
12012
+ try {
12013
+ const parsed = new URL(url, window.location.href);
12014
+ const current = new URL(window.location.href);
12015
+ const host = parsed.hostname.toLowerCase();
12016
+ return parsed.origin === current.origin || host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host.endsWith(".local") || /^(10\.|192\.168\.|169\.254\.)/.test(host);
12017
+ } catch {
12018
+ return false;
12019
+ }
12020
+ }
12021
+ async function imageUrlToDataUrl(url) {
12022
+ if (inlinedAssetCache.has(url)) return inlinedAssetCache.get(url) ?? null;
12023
+ try {
12024
+ const response = await fetch(url);
12025
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
12026
+ const blob = await response.blob();
12027
+ if (blob.type.includes("text/html")) throw new Error("Expected image but got text/html");
12028
+ const dataUrl = await new Promise((resolve, reject) => {
12029
+ const reader = new FileReader();
12030
+ reader.onloadend = () => resolve(String(reader.result || ""));
12031
+ reader.onerror = reject;
12032
+ reader.readAsDataURL(blob);
12033
+ });
12034
+ inlinedAssetCache.set(url, dataUrl);
12035
+ return dataUrl;
12036
+ } catch (error) {
12037
+ console.warn("[@pixldocs/canvas-renderer] Failed to inline image asset:", url, error);
12038
+ inlinedAssetCache.set(url, null);
12039
+ return null;
12040
+ }
12041
+ }
12042
+ async function inlineNodeAssets(node) {
12043
+ if (node.type === "image") {
12044
+ const url = typeof node.src === "string" && node.src.trim() ? node.src.trim() : typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
12045
+ if (shouldInlineImageUrl(url)) {
12046
+ const dataUrl = await imageUrlToDataUrl(url);
12047
+ if (dataUrl) {
12048
+ node.src = dataUrl;
12049
+ node.imageUrl = dataUrl;
12050
+ }
12051
+ }
12052
+ }
12053
+ if (Array.isArray(node.children)) {
12054
+ await Promise.all(node.children.map(inlineNodeAssets));
12055
+ }
12056
+ }
12057
+ async function inlineBrowserReachableImageAssets(config) {
12058
+ if (typeof window === "undefined" || typeof fetch === "undefined" || typeof FileReader === "undefined") {
12059
+ return config;
12060
+ }
12061
+ const cloned = JSON.parse(JSON.stringify(config));
12062
+ await Promise.all(
12063
+ (cloned.pages || []).flatMap((page) => (page.children || []).map(inlineNodeAssets))
12064
+ );
12065
+ return cloned;
12066
+ }
12002
12067
  class PixldocsRenderer {
12003
12068
  constructor(config) {
12004
12069
  __publicField(this, "config");
@@ -12009,21 +12074,22 @@ class PixldocsRenderer {
12009
12074
  * Mounts a hidden PreviewCanvas component and captures the Fabric canvas output.
12010
12075
  */
12011
12076
  async render(templateConfig, options = {}) {
12077
+ const renderConfig = await inlineBrowserReachableImageAssets(templateConfig);
12012
12078
  const pageIndex = options.pageIndex ?? 0;
12013
12079
  const format = options.format ?? "png";
12014
12080
  const quality = options.quality ?? 0.92;
12015
12081
  const pixelRatio = options.pixelRatio ?? this.config.pixelRatio ?? 2;
12016
- const canvasWidth = templateConfig.canvas.width;
12017
- const canvasHeight = templateConfig.canvas.height;
12018
- const page = templateConfig.pages[pageIndex];
12082
+ const canvasWidth = renderConfig.canvas.width;
12083
+ const canvasHeight = renderConfig.canvas.height;
12084
+ const page = renderConfig.pages[pageIndex];
12019
12085
  if (!page) {
12020
- throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12086
+ throw new Error(`Page index ${pageIndex} not found (template has ${renderConfig.pages.length} pages)`);
12021
12087
  }
12022
- await ensureFontsForResolvedConfig(templateConfig);
12088
+ await ensureFontsForResolvedConfig(renderConfig);
12023
12089
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12024
12090
  setPackageApiUrl2(this.config.imageProxyUrl);
12025
12091
  const dataUrl = await this.renderPageViaPreviewCanvas(
12026
- templateConfig,
12092
+ renderConfig,
12027
12093
  pageIndex,
12028
12094
  pixelRatio,
12029
12095
  format,
@@ -12075,16 +12141,17 @@ class PixldocsRenderer {
12075
12141
  * This is the key building block for client-side vector PDF export.
12076
12142
  */
12077
12143
  async renderPageSvg(templateConfig, pageIndex = 0) {
12078
- const page = templateConfig.pages[pageIndex];
12144
+ const renderConfig = await inlineBrowserReachableImageAssets(templateConfig);
12145
+ const page = renderConfig.pages[pageIndex];
12079
12146
  if (!page) {
12080
- throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12147
+ throw new Error(`Page index ${pageIndex} not found (template has ${renderConfig.pages.length} pages)`);
12081
12148
  }
12082
- await ensureFontsForResolvedConfig(templateConfig);
12149
+ await ensureFontsForResolvedConfig(renderConfig);
12083
12150
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12084
12151
  setPackageApiUrl2(this.config.imageProxyUrl);
12085
- const canvasWidth = templateConfig.canvas.width;
12086
- const canvasHeight = templateConfig.canvas.height;
12087
- return this.captureSvgViaPreviewCanvas(templateConfig, pageIndex, canvasWidth, canvasHeight);
12152
+ const canvasWidth = renderConfig.canvas.width;
12153
+ const canvasHeight = renderConfig.canvas.height;
12154
+ return this.captureSvgViaPreviewCanvas(renderConfig, pageIndex, canvasWidth, canvasHeight);
12088
12155
  }
12089
12156
  /**
12090
12157
  * Render all pages and return SVG strings for each.
@@ -12432,7 +12499,7 @@ class PixldocsRenderer {
12432
12499
  cleanup();
12433
12500
  reject(new Error("Render timeout (30s)"));
12434
12501
  }, 3e4);
12435
- let stabilizationPass = 0;
12502
+ let finished = false;
12436
12503
  const cleanup = () => {
12437
12504
  clearTimeout(timeout);
12438
12505
  try {
@@ -12442,25 +12509,15 @@ class PixldocsRenderer {
12442
12509
  container.remove();
12443
12510
  };
12444
12511
  const onReady = () => {
12445
- if (stabilizationPass === 0) {
12446
- stabilizationPass = 1;
12447
- root.render(
12448
- react.createElement(PreviewCanvas2, {
12449
- config,
12450
- pageIndex,
12451
- zoom: pixelRatio,
12452
- absoluteZoom: true,
12453
- onReady
12454
- })
12455
- );
12456
- return;
12457
- }
12512
+ if (finished) return;
12513
+ finished = true;
12458
12514
  this.waitForCanvasScene(container, config, pageIndex).then(async () => {
12459
12515
  try {
12516
+ await this.waitForStableTextMetrics(container, config);
12517
+ await this.waitForCanvasScene(container, config, pageIndex, 2500, 50);
12460
12518
  const fabricInstance = this.getFabricCanvasFromContainer(container);
12461
12519
  const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
12462
12520
  await this.waitForCanvasImages(container, expectedImageCount);
12463
- await this.waitForStableTextMetrics(container, config);
12464
12521
  const fabricCanvas = container.querySelector("canvas.upper-canvas, canvas");
12465
12522
  const sourceCanvas = (fabricInstance == null ? void 0 : fabricInstance.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || fabricCanvas;
12466
12523
  if (!sourceCanvas) {
@@ -12499,6 +12556,7 @@ class PixldocsRenderer {
12499
12556
  pageIndex,
12500
12557
  zoom: pixelRatio,
12501
12558
  absoluteZoom: true,
12559
+ skipFontReadyWait: true,
12502
12560
  onReady
12503
12561
  })
12504
12562
  );
@@ -12527,7 +12585,7 @@ class PixldocsRenderer {
12527
12585
  cleanup();
12528
12586
  reject(new Error("SVG render timeout (30s)"));
12529
12587
  }, 3e4);
12530
- let stabilizationPass = 0;
12588
+ let finished = false;
12531
12589
  const cleanup = () => {
12532
12590
  clearTimeout(timeout);
12533
12591
  try {
@@ -12537,22 +12595,13 @@ class PixldocsRenderer {
12537
12595
  container.remove();
12538
12596
  };
12539
12597
  const onReady = () => {
12540
- if (stabilizationPass === 0) {
12541
- stabilizationPass = 1;
12542
- root.render(
12543
- react.createElement(PreviewCanvas2, {
12544
- config,
12545
- pageIndex,
12546
- zoom: 1,
12547
- absoluteZoom: true,
12548
- onReady
12549
- })
12550
- );
12551
- return;
12552
- }
12598
+ if (finished) return;
12599
+ finished = true;
12553
12600
  this.waitForCanvasScene(container, config, pageIndex).then(async () => {
12554
12601
  var _a, _b;
12555
12602
  try {
12603
+ await this.waitForStableTextMetrics(container, config);
12604
+ await this.waitForCanvasScene(container, config, pageIndex, 2500, 50);
12556
12605
  const fabricInstance = this.getFabricCanvasFromContainer(container);
12557
12606
  if (!fabricInstance) {
12558
12607
  cleanup();
@@ -12561,7 +12610,6 @@ class PixldocsRenderer {
12561
12610
  }
12562
12611
  const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
12563
12612
  await this.waitForCanvasImages(container, expectedImageCount);
12564
- await this.waitForStableTextMetrics(container, config);
12565
12613
  const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
12566
12614
  const prevSvgVPT = fabricInstance.svgViewportTransformation;
12567
12615
  const prevRetina = fabricInstance.enableRetinaScaling;
@@ -12612,6 +12660,7 @@ class PixldocsRenderer {
12612
12660
  zoom: 1,
12613
12661
  // 1:1 — no UI scaling for SVG capture
12614
12662
  absoluteZoom: true,
12663
+ skipFontReadyWait: true,
12615
12664
  onReady
12616
12665
  })
12617
12666
  );