@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.js CHANGED
@@ -5670,6 +5670,7 @@ const PageCanvas = forwardRef(
5670
5670
  onDynamicFieldClick,
5671
5671
  canvasUpdateVersion = 0,
5672
5672
  pageChildren,
5673
+ skipFontReadyWait = false,
5673
5674
  onReady
5674
5675
  }, ref) => {
5675
5676
  const isEditorMode = mode === "editor";
@@ -5955,9 +5956,11 @@ const PageCanvas = forwardRef(
5955
5956
  await Promise.all(fontFamilies.map((f) => ensureFontLoaded(f)));
5956
5957
  }
5957
5958
  }
5958
- await waitForFontsReady();
5959
- await waitUntilFontsAvailable(fontFamilies, { timeoutMs: 3500, pollIntervalMs: 60 });
5960
- await new Promise((r) => requestAnimationFrame(() => r()));
5959
+ if (!skipFontReadyWait) {
5960
+ await waitForFontsReady();
5961
+ await waitUntilFontsAvailable(fontFamilies, { timeoutMs: 3500, pollIntervalMs: 60 });
5962
+ await new Promise((r) => requestAnimationFrame(() => r()));
5963
+ }
5961
5964
  clearFabricCharCache();
5962
5965
  clearMeasurementCache();
5963
5966
  setReady(true);
@@ -9394,6 +9397,7 @@ function PreviewCanvas({
9394
9397
  pageIndex = 0,
9395
9398
  zoom = 1,
9396
9399
  absoluteZoom = false,
9400
+ skipFontReadyWait = false,
9397
9401
  className,
9398
9402
  onDynamicFieldClick,
9399
9403
  onReady
@@ -9548,6 +9552,7 @@ function PreviewCanvas({
9548
9552
  selectedIds: [],
9549
9553
  activeTool: "select",
9550
9554
  mode: "preview",
9555
+ skipFontReadyWait,
9551
9556
  dynamicFieldIds,
9552
9557
  onDynamicFieldClick: handleDynamicFieldClick,
9553
9558
  onReady
@@ -11980,6 +11985,66 @@ function PixldocsPreview(props) {
11980
11985
  !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..." }) })
11981
11986
  ] });
11982
11987
  }
11988
+ const inlinedAssetCache = /* @__PURE__ */ new Map();
11989
+ function shouldInlineImageUrl(url) {
11990
+ if (!url || url.startsWith("data:")) return false;
11991
+ if (url.startsWith("blob:")) return true;
11992
+ if (url.startsWith("/") && !url.startsWith("//")) return true;
11993
+ try {
11994
+ const parsed = new URL(url, window.location.href);
11995
+ const current = new URL(window.location.href);
11996
+ const host = parsed.hostname.toLowerCase();
11997
+ 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);
11998
+ } catch {
11999
+ return false;
12000
+ }
12001
+ }
12002
+ async function imageUrlToDataUrl(url) {
12003
+ if (inlinedAssetCache.has(url)) return inlinedAssetCache.get(url) ?? null;
12004
+ try {
12005
+ const response = await fetch(url);
12006
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
12007
+ const blob = await response.blob();
12008
+ if (blob.type.includes("text/html")) throw new Error("Expected image but got text/html");
12009
+ const dataUrl = await new Promise((resolve, reject) => {
12010
+ const reader = new FileReader();
12011
+ reader.onloadend = () => resolve(String(reader.result || ""));
12012
+ reader.onerror = reject;
12013
+ reader.readAsDataURL(blob);
12014
+ });
12015
+ inlinedAssetCache.set(url, dataUrl);
12016
+ return dataUrl;
12017
+ } catch (error) {
12018
+ console.warn("[@pixldocs/canvas-renderer] Failed to inline image asset:", url, error);
12019
+ inlinedAssetCache.set(url, null);
12020
+ return null;
12021
+ }
12022
+ }
12023
+ async function inlineNodeAssets(node) {
12024
+ if (node.type === "image") {
12025
+ const url = typeof node.src === "string" && node.src.trim() ? node.src.trim() : typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
12026
+ if (shouldInlineImageUrl(url)) {
12027
+ const dataUrl = await imageUrlToDataUrl(url);
12028
+ if (dataUrl) {
12029
+ node.src = dataUrl;
12030
+ node.imageUrl = dataUrl;
12031
+ }
12032
+ }
12033
+ }
12034
+ if (Array.isArray(node.children)) {
12035
+ await Promise.all(node.children.map(inlineNodeAssets));
12036
+ }
12037
+ }
12038
+ async function inlineBrowserReachableImageAssets(config) {
12039
+ if (typeof window === "undefined" || typeof fetch === "undefined" || typeof FileReader === "undefined") {
12040
+ return config;
12041
+ }
12042
+ const cloned = JSON.parse(JSON.stringify(config));
12043
+ await Promise.all(
12044
+ (cloned.pages || []).flatMap((page) => (page.children || []).map(inlineNodeAssets))
12045
+ );
12046
+ return cloned;
12047
+ }
11983
12048
  class PixldocsRenderer {
11984
12049
  constructor(config) {
11985
12050
  __publicField(this, "config");
@@ -11990,21 +12055,22 @@ class PixldocsRenderer {
11990
12055
  * Mounts a hidden PreviewCanvas component and captures the Fabric canvas output.
11991
12056
  */
11992
12057
  async render(templateConfig, options = {}) {
12058
+ const renderConfig = await inlineBrowserReachableImageAssets(templateConfig);
11993
12059
  const pageIndex = options.pageIndex ?? 0;
11994
12060
  const format = options.format ?? "png";
11995
12061
  const quality = options.quality ?? 0.92;
11996
12062
  const pixelRatio = options.pixelRatio ?? this.config.pixelRatio ?? 2;
11997
- const canvasWidth = templateConfig.canvas.width;
11998
- const canvasHeight = templateConfig.canvas.height;
11999
- const page = templateConfig.pages[pageIndex];
12063
+ const canvasWidth = renderConfig.canvas.width;
12064
+ const canvasHeight = renderConfig.canvas.height;
12065
+ const page = renderConfig.pages[pageIndex];
12000
12066
  if (!page) {
12001
- throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12067
+ throw new Error(`Page index ${pageIndex} not found (template has ${renderConfig.pages.length} pages)`);
12002
12068
  }
12003
- await ensureFontsForResolvedConfig(templateConfig);
12069
+ await ensureFontsForResolvedConfig(renderConfig);
12004
12070
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12005
12071
  setPackageApiUrl2(this.config.imageProxyUrl);
12006
12072
  const dataUrl = await this.renderPageViaPreviewCanvas(
12007
- templateConfig,
12073
+ renderConfig,
12008
12074
  pageIndex,
12009
12075
  pixelRatio,
12010
12076
  format,
@@ -12056,16 +12122,17 @@ class PixldocsRenderer {
12056
12122
  * This is the key building block for client-side vector PDF export.
12057
12123
  */
12058
12124
  async renderPageSvg(templateConfig, pageIndex = 0) {
12059
- const page = templateConfig.pages[pageIndex];
12125
+ const renderConfig = await inlineBrowserReachableImageAssets(templateConfig);
12126
+ const page = renderConfig.pages[pageIndex];
12060
12127
  if (!page) {
12061
- throw new Error(`Page index ${pageIndex} not found (template has ${templateConfig.pages.length} pages)`);
12128
+ throw new Error(`Page index ${pageIndex} not found (template has ${renderConfig.pages.length} pages)`);
12062
12129
  }
12063
- await ensureFontsForResolvedConfig(templateConfig);
12130
+ await ensureFontsForResolvedConfig(renderConfig);
12064
12131
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
12065
12132
  setPackageApiUrl2(this.config.imageProxyUrl);
12066
- const canvasWidth = templateConfig.canvas.width;
12067
- const canvasHeight = templateConfig.canvas.height;
12068
- return this.captureSvgViaPreviewCanvas(templateConfig, pageIndex, canvasWidth, canvasHeight);
12133
+ const canvasWidth = renderConfig.canvas.width;
12134
+ const canvasHeight = renderConfig.canvas.height;
12135
+ return this.captureSvgViaPreviewCanvas(renderConfig, pageIndex, canvasWidth, canvasHeight);
12069
12136
  }
12070
12137
  /**
12071
12138
  * Render all pages and return SVG strings for each.
@@ -12413,7 +12480,7 @@ class PixldocsRenderer {
12413
12480
  cleanup();
12414
12481
  reject(new Error("Render timeout (30s)"));
12415
12482
  }, 3e4);
12416
- let stabilizationPass = 0;
12483
+ let finished = false;
12417
12484
  const cleanup = () => {
12418
12485
  clearTimeout(timeout);
12419
12486
  try {
@@ -12423,25 +12490,15 @@ class PixldocsRenderer {
12423
12490
  container.remove();
12424
12491
  };
12425
12492
  const onReady = () => {
12426
- if (stabilizationPass === 0) {
12427
- stabilizationPass = 1;
12428
- root.render(
12429
- createElement(PreviewCanvas2, {
12430
- config,
12431
- pageIndex,
12432
- zoom: pixelRatio,
12433
- absoluteZoom: true,
12434
- onReady
12435
- })
12436
- );
12437
- return;
12438
- }
12493
+ if (finished) return;
12494
+ finished = true;
12439
12495
  this.waitForCanvasScene(container, config, pageIndex).then(async () => {
12440
12496
  try {
12497
+ await this.waitForStableTextMetrics(container, config);
12498
+ await this.waitForCanvasScene(container, config, pageIndex, 2500, 50);
12441
12499
  const fabricInstance = this.getFabricCanvasFromContainer(container);
12442
12500
  const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
12443
12501
  await this.waitForCanvasImages(container, expectedImageCount);
12444
- await this.waitForStableTextMetrics(container, config);
12445
12502
  const fabricCanvas = container.querySelector("canvas.upper-canvas, canvas");
12446
12503
  const sourceCanvas = (fabricInstance == null ? void 0 : fabricInstance.lowerCanvasEl) || container.querySelector("canvas.lower-canvas") || fabricCanvas;
12447
12504
  if (!sourceCanvas) {
@@ -12480,6 +12537,7 @@ class PixldocsRenderer {
12480
12537
  pageIndex,
12481
12538
  zoom: pixelRatio,
12482
12539
  absoluteZoom: true,
12540
+ skipFontReadyWait: true,
12483
12541
  onReady
12484
12542
  })
12485
12543
  );
@@ -12508,7 +12566,7 @@ class PixldocsRenderer {
12508
12566
  cleanup();
12509
12567
  reject(new Error("SVG render timeout (30s)"));
12510
12568
  }, 3e4);
12511
- let stabilizationPass = 0;
12569
+ let finished = false;
12512
12570
  const cleanup = () => {
12513
12571
  clearTimeout(timeout);
12514
12572
  try {
@@ -12518,22 +12576,13 @@ class PixldocsRenderer {
12518
12576
  container.remove();
12519
12577
  };
12520
12578
  const onReady = () => {
12521
- if (stabilizationPass === 0) {
12522
- stabilizationPass = 1;
12523
- root.render(
12524
- createElement(PreviewCanvas2, {
12525
- config,
12526
- pageIndex,
12527
- zoom: 1,
12528
- absoluteZoom: true,
12529
- onReady
12530
- })
12531
- );
12532
- return;
12533
- }
12579
+ if (finished) return;
12580
+ finished = true;
12534
12581
  this.waitForCanvasScene(container, config, pageIndex).then(async () => {
12535
12582
  var _a, _b;
12536
12583
  try {
12584
+ await this.waitForStableTextMetrics(container, config);
12585
+ await this.waitForCanvasScene(container, config, pageIndex, 2500, 50);
12537
12586
  const fabricInstance = this.getFabricCanvasFromContainer(container);
12538
12587
  if (!fabricInstance) {
12539
12588
  cleanup();
@@ -12542,7 +12591,6 @@ class PixldocsRenderer {
12542
12591
  }
12543
12592
  const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
12544
12593
  await this.waitForCanvasImages(container, expectedImageCount);
12545
- await this.waitForStableTextMetrics(container, config);
12546
12594
  const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
12547
12595
  const prevSvgVPT = fabricInstance.svgViewportTransformation;
12548
12596
  const prevRetina = fabricInstance.enableRetinaScaling;
@@ -12593,6 +12641,7 @@ class PixldocsRenderer {
12593
12641
  zoom: 1,
12594
12642
  // 1:1 — no UI scaling for SVG capture
12595
12643
  absoluteZoom: true,
12644
+ skipFontReadyWait: true,
12596
12645
  onReady
12597
12646
  })
12598
12647
  );