@pixldocs/canvas-renderer 0.5.191 → 0.5.193

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/README.md CHANGED
@@ -273,9 +273,20 @@ const renderer = new PixldocsRenderer({
273
273
  supabaseAnonKey: string,
274
274
  imageProxyUrl?: string, // CORS proxy for external images
275
275
  pixelRatio?: number, // default: 2
276
+ assetWaitTimeoutMs?: number, // default: 15000; use ~30000 for multi-photo PDF exports
277
+ assetWaitEarlyExitMs?: number, // default: 1500; ignored for strict PDF image-ID waits
278
+ maxImageEdgePx?: number, // recommended: 2048 for user-uploaded PDF photos
279
+ debug?: boolean, // logs image wait/SVG completeness diagnostics
276
280
  });
277
281
  ```
278
282
 
283
+ For BioMaker-style PDFs with user-uploaded profile/gallery photos, use the live
284
+ SVG path with `maxImageEdgePx: 2048` and **do not** set
285
+ `forcePerElementPdf: true`. Since `0.5.193`, PDF export waits for every expected
286
+ bound image ID, stamps those IDs into the captured SVG, preloads/decodes them
287
+ before `svg2pdf`, and throws if any expected photo is missing instead of
288
+ silently exporting an incomplete PDF.
289
+
279
290
  #### `renderer.renderFromForm(options)`
280
291
 
281
292
  Renders all pages from a V2 sectionState payload (same format as the server `/render-from-form` API).
@@ -16782,6 +16782,23 @@ function stampFabricLineMetricsOnTextSvg(svg, obj) {
16782
16782
  return `<tspan${cleaned} data-pd-line-width="${Number(lineWidth.toFixed(3))}" data-pd-line-start="${Number(lineStart.toFixed(3))}">`;
16783
16783
  });
16784
16784
  }
16785
+ function escapeSvgDataAttr(value) {
16786
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
16787
+ }
16788
+ function hasRenderableRasterCandidate(obj) {
16789
+ if (!obj || typeof obj !== "object") return false;
16790
+ const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
16791
+ for (const candidate of candidates) {
16792
+ if (candidate instanceof HTMLImageElement || candidate instanceof HTMLCanvasElement) return true;
16793
+ }
16794
+ const children = Array.isArray(obj == null ? void 0 : obj._objects) ? obj._objects : [];
16795
+ return children.some((child) => hasRenderableRasterCandidate(child));
16796
+ }
16797
+ function stampPixldocsImageIdOnSvg(svg, id) {
16798
+ if (!id || !/<image\b/i.test(svg) || /data-pixldocs-image-id=/i.test(svg)) return svg;
16799
+ const attr = ` data-pixldocs-image-id="${escapeSvgDataAttr(id)}"`;
16800
+ return svg.replace(/<image\b/i, `<image${attr}`);
16801
+ }
16785
16802
  function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight) {
16786
16803
  const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
16787
16804
  const prevSvgVPT = fabricInstance.svgViewportTransformation;
@@ -16798,14 +16815,20 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
16798
16815
  );
16799
16816
  } catch {
16800
16817
  }
16801
- const textSvgPatchRecords = [];
16818
+ const svgPatchRecords = [];
16802
16819
  try {
16803
16820
  const visit = (obj) => {
16804
16821
  if (!obj) return;
16805
- if (isTextboxLike(obj) && typeof obj.toSVG === "function") {
16822
+ const imageId = typeof obj.__docuforgeId === "string" && hasRenderableRasterCandidate(obj) ? obj.__docuforgeId : "";
16823
+ if ((isTextboxLike(obj) || imageId) && typeof obj.toSVG === "function") {
16806
16824
  const originalToSVG = obj.toSVG.bind(obj);
16807
- obj.toSVG = (reviver) => stampFabricLineMetricsOnTextSvg(originalToSVG(reviver), obj);
16808
- textSvgPatchRecords.push({ obj, originalToSVG });
16825
+ obj.toSVG = (reviver) => {
16826
+ let svg = originalToSVG(reviver);
16827
+ if (isTextboxLike(obj)) svg = stampFabricLineMetricsOnTextSvg(svg, obj);
16828
+ if (imageId) svg = stampPixldocsImageIdOnSvg(svg, imageId);
16829
+ return svg;
16830
+ };
16831
+ svgPatchRecords.push({ obj, originalToSVG });
16809
16832
  }
16810
16833
  const children = Array.isArray(obj == null ? void 0 : obj._objects) ? obj._objects : [];
16811
16834
  for (const child of children) visit(child);
@@ -16838,6 +16861,13 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
16838
16861
  evented: false,
16839
16862
  objectCaching: false
16840
16863
  });
16864
+ if (typeof obj.__docuforgeId === "string") {
16865
+ replacement.__docuforgeId = obj.__docuforgeId;
16866
+ if (typeof replacement.toSVG === "function") {
16867
+ const originalReplacementToSVG = replacement.toSVG.bind(replacement);
16868
+ replacement.toSVG = (reviver) => stampPixldocsImageIdOnSvg(originalReplacementToSVG(reviver), obj.__docuforgeId);
16869
+ }
16870
+ }
16841
16871
  const insertIndex = fabricInstance._objects.indexOf(obj);
16842
16872
  const prevExclude = obj.excludeFromExport;
16843
16873
  obj.excludeFromExport = true;
@@ -16870,7 +16900,7 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
16870
16900
  } catch {
16871
16901
  }
16872
16902
  }
16873
- for (const rec of textSvgPatchRecords) {
16903
+ for (const rec of svgPatchRecords) {
16874
16904
  try {
16875
16905
  rec.obj.toSVG = rec.originalToSVG;
16876
16906
  } catch {
@@ -16889,9 +16919,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
16889
16919
  }
16890
16920
  return svgString;
16891
16921
  }
16892
- const resolvedPackageVersion = "0.5.191";
16922
+ const resolvedPackageVersion = "0.5.193";
16893
16923
  const PACKAGE_VERSION = resolvedPackageVersion;
16894
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.191";
16924
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.193";
16895
16925
  const roundParityValue = (value) => {
16896
16926
  if (typeof value !== "number") return value;
16897
16927
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -17512,13 +17542,13 @@ class PixldocsRenderer {
17512
17542
  );
17513
17543
  });
17514
17544
  await this.waitForCanvasScene(container, cloned, i);
17515
- const expected = this.getExpectedImageCount(cloned, i);
17545
+ const expected = this.getExpectedImageIds(cloned, i);
17516
17546
  await this.waitForCanvasImages(container, expected);
17517
17547
  await this.waitForStableTextMetrics(container, cloned, { clearGlobalCharCache: false });
17518
17548
  await this.waitForCanvasScene(container, cloned, i);
17519
17549
  }
17520
17550
  console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
17521
- const { exportMultiPagePdf, preparePagesForExport } = await import("./vectorPdfExport-CrTGqxx6.js");
17551
+ const { exportMultiPagePdf, preparePagesForExport } = await import("./vectorPdfExport-Cd--tBky.js");
17522
17552
  const prepared = preparePagesForExport(
17523
17553
  cloned.pages,
17524
17554
  canvasWidth,
@@ -17529,7 +17559,27 @@ class PixldocsRenderer {
17529
17559
  watermark: !!options.watermark,
17530
17560
  returnBlob: true,
17531
17561
  pdfTextMode: options.textMode ?? (cloned == null ? void 0 : cloned.pdfTextMode) ?? ((_a = cloned.canvas) == null ? void 0 : _a.n) ?? "auto",
17532
- skipLiveCanvasSvgFastPath: shouldForcePerElement
17562
+ // IMPORTANT: We intentionally do NOT skip the live-canvas SVG fast path
17563
+ // here, even when `shouldForcePerElement` is true via auto-detection.
17564
+ //
17565
+ // Reason: skipping the fast path forces the config-space export, which
17566
+ // computes positions from stored values rather than the live Fabric
17567
+ // layout (post auto-shrink, post text-init, post crop-group bake).
17568
+ // That drift causes severe text overlap / mis-positioning in the PDF
17569
+ // while the on-screen preview (which uses the live canvas) still looks
17570
+ // correct. v0.5.191 shipped with this skip enabled and produced exactly
17571
+ // that regression.
17572
+ //
17573
+ // The Safari/iOS "missing photo" issue is solved end-to-end by the
17574
+ // `downscaleConfigRasterImages()` pre-pass above, which shrinks large
17575
+ // `data:image/*` JPEGs to a safe edge length before the canvas mounts.
17576
+ // That alone keeps Safari's SVG capture reliable without altering the
17577
+ // layout pipeline.
17578
+ //
17579
+ // We still honor an explicit `forcePerElementPdf: true` from the host
17580
+ // app (advanced opt-in for niche cases), but the auto path no longer
17581
+ // toggles this flag.
17582
+ skipLiveCanvasSvgFastPath: forceMode === true
17533
17583
  });
17534
17584
  if (!result || typeof result === "undefined") {
17535
17585
  throw new Error("exportMultiPagePdf returned no blob (returnBlob path failed)");
@@ -17570,17 +17620,21 @@ class PixldocsRenderer {
17570
17620
  walk(page.children);
17571
17621
  return ids;
17572
17622
  }
17573
- waitForCanvasImages(container, expectedImageCount, maxWaitMs, pollMs = 120) {
17623
+ waitForCanvasImages(container, expectedImages, maxWaitMs, pollMs = 120) {
17574
17624
  const timeout = Math.max(500, maxWaitMs ?? this.config.assetWaitTimeoutMs ?? 15e3);
17575
17625
  const earlyExitMs = Math.max(250, this.config.assetWaitEarlyExitMs ?? 1500);
17576
17626
  const debug = !!this.config.debug;
17577
- return new Promise((resolve) => {
17627
+ const expectedImageIds = Array.isArray(expectedImages) ? [...new Set(expectedImages)] : [];
17628
+ const expectedImageCount = Array.isArray(expectedImages) ? expectedImageIds.length : expectedImages;
17629
+ const strictById = expectedImageIds.length > 0;
17630
+ return new Promise((resolve, reject) => {
17578
17631
  const start = Date.now();
17579
17632
  let stableFrames = 0;
17580
17633
  let lastSummary = "";
17581
- let lastActual = -1;
17634
+ let lastProgressKey = "";
17582
17635
  let lastProgressAt = Date.now();
17583
17636
  const isRenderableImage = (value) => value instanceof HTMLImageElement && value.complete && value.naturalWidth > 0 && value.naturalHeight > 0;
17637
+ const isRenderableCanvas = (value) => value instanceof HTMLCanvasElement && value.width > 0 && value.height > 0;
17584
17638
  const collectRenderableImages = (obj, seen) => {
17585
17639
  if (!obj || typeof obj !== "object") return;
17586
17640
  const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
@@ -17597,6 +17651,25 @@ class PixldocsRenderer {
17597
17651
  }
17598
17652
  return true;
17599
17653
  };
17654
+ const collectRenderableImageIds = (obj, readyIds) => {
17655
+ if (!obj || typeof obj !== "object") return { loaded: false, pending: false };
17656
+ let loaded = false;
17657
+ let pending = false;
17658
+ const candidates = [obj._element, obj._originalElement, obj._filteredEl, obj._cacheCanvasEl];
17659
+ for (const candidate of candidates) {
17660
+ if (isRenderableImage(candidate) || isRenderableCanvas(candidate)) loaded = true;
17661
+ else if (candidate instanceof HTMLImageElement) pending = true;
17662
+ }
17663
+ const nested = Array.isArray(obj._objects) ? obj._objects : Array.isArray(obj.objects) ? obj.objects : [];
17664
+ for (const child of nested) {
17665
+ const childState = collectRenderableImageIds(child, readyIds);
17666
+ loaded = loaded || childState.loaded;
17667
+ pending = pending || childState.pending;
17668
+ }
17669
+ const id = typeof obj.__docuforgeId === "string" ? obj.__docuforgeId : "";
17670
+ if (id && loaded && !pending) readyIds.add(id);
17671
+ return { loaded, pending };
17672
+ };
17600
17673
  const getFabricCanvas = () => {
17601
17674
  const registry2 = window.__fabricCanvasRegistry;
17602
17675
  if (registry2 instanceof Map) {
@@ -17636,17 +17709,21 @@ class PixldocsRenderer {
17636
17709
  const fabricObjects = fabricCanvas && typeof fabricCanvas.getObjects === "function" ? fabricCanvas.getObjects() : [];
17637
17710
  const renderableImages = /* @__PURE__ */ new Set();
17638
17711
  const fabricReady = fabricObjects.every((obj) => collectRenderableImages(obj, renderableImages) !== false);
17712
+ const renderableImageIds = /* @__PURE__ */ new Set();
17713
+ for (const obj of fabricObjects) collectRenderableImageIds(obj, renderableImageIds);
17714
+ const missingImageIds = strictById ? expectedImageIds.filter((id) => !renderableImageIds.has(id)) : [];
17639
17715
  const actualImageCount = Math.max(domImages.length, renderableImages.size);
17640
17716
  const canvasReady = !!fabricCanvas && !!(fabricCanvas.lowerCanvasEl || fabricCanvas.upperCanvasEl);
17641
- const hasExpectedAssets = expectedImageCount === 0 ? true : actualImageCount >= expectedImageCount;
17717
+ const hasExpectedAssets = strictById ? missingImageIds.length === 0 : expectedImageCount === 0 ? true : actualImageCount >= expectedImageCount;
17642
17718
  const ready = allDomLoaded && fabricReady && hasExpectedAssets;
17643
- const summary = `expected=${expectedImageCount} actual=${actualImageCount} dom=${domImages.length} fabricReady=${fabricReady} domReady=${allDomLoaded} canvasReady=${canvasReady}`;
17719
+ const summary = `expected=${expectedImageCount} actual=${actualImageCount} fabricIds=${renderableImageIds.size} dom=${domImages.length} fabricReady=${fabricReady} domReady=${allDomLoaded} canvasReady=${canvasReady}${missingImageIds.length ? ` missing=${missingImageIds.join(",")}` : ""}`;
17644
17720
  if (summary !== lastSummary) {
17645
17721
  lastSummary = summary;
17646
17722
  if (debug) console.log(`[canvas-renderer][asset-wait] ${summary}`);
17647
17723
  }
17648
- if (actualImageCount !== lastActual) {
17649
- lastActual = actualImageCount;
17724
+ const progressKey = strictById ? expectedImageIds.filter((id) => renderableImageIds.has(id)).join("|") : String(actualImageCount);
17725
+ if (progressKey !== lastProgressKey) {
17726
+ lastProgressKey = progressKey;
17650
17727
  lastProgressAt = Date.now();
17651
17728
  }
17652
17729
  if (ready) {
@@ -17661,7 +17738,7 @@ class PixldocsRenderer {
17661
17738
  }
17662
17739
  const sceneSettled = fabricReady && allDomLoaded && canvasReady && fabricObjects.length > 0;
17663
17740
  const idleFor = Date.now() - lastProgressAt;
17664
- if (sceneSettled && actualImageCount < expectedImageCount && idleFor >= earlyExitMs) {
17741
+ if (!strictById && sceneSettled && actualImageCount < expectedImageCount && idleFor >= earlyExitMs) {
17665
17742
  if (debug) {
17666
17743
  console.log(
17667
17744
  `[canvas-renderer][asset-wait] early-exit after ${elapsed}ms (idle=${idleFor}ms, ${summary})`
@@ -17685,6 +17762,10 @@ class PixldocsRenderer {
17685
17762
  console.warn(`[canvas-renderer][asset-wait-timeout] elapsed=${elapsed}ms ${summary}`);
17686
17763
  console.warn("[canvas-renderer][asset-wait-timeout][dom-images]", domImageDebug);
17687
17764
  console.warn("[canvas-renderer][asset-wait-timeout][fabric-images]", fabricImageDebug);
17765
+ if (strictById && missingImageIds.length > 0) {
17766
+ reject(new Error(`[canvas-renderer][asset-wait-timeout] Missing expected image(s): ${missingImageIds.join(", ")}`));
17767
+ return;
17768
+ }
17688
17769
  settle();
17689
17770
  return;
17690
17771
  }
@@ -17885,8 +17966,8 @@ class PixldocsRenderer {
17885
17966
  const onReadyOnce = () => {
17886
17967
  this.waitForCanvasScene(container, renderConfig, pageIndex).then(async () => {
17887
17968
  const fabricInstance = this.getFabricCanvasFromContainer(container);
17888
- const expectedImageCount = this.getExpectedImageCount(renderConfig, pageIndex);
17889
- await this.waitForCanvasImages(container, expectedImageCount);
17969
+ const expectedImageIds = this.getExpectedImageIds(renderConfig, pageIndex);
17970
+ await this.waitForCanvasImages(container, expectedImageIds);
17890
17971
  await this.waitForStableTextMetrics(container, renderConfig);
17891
17972
  await this.waitForCanvasScene(container, renderConfig, pageIndex);
17892
17973
  if (!fabricInstance) return settle();
@@ -17912,8 +17993,8 @@ class PixldocsRenderer {
17912
17993
  this.waitForCanvasScene(container, renderConfig, pageIndex).then(async () => {
17913
17994
  try {
17914
17995
  const fabricInstance = this.getFabricCanvasFromContainer(container);
17915
- const expectedImageCount = this.getExpectedImageCount(renderConfig, pageIndex);
17916
- await this.waitForCanvasImages(container, expectedImageCount);
17996
+ const expectedImageIds = this.getExpectedImageIds(renderConfig, pageIndex);
17997
+ await this.waitForCanvasImages(container, expectedImageIds);
17917
17998
  await this.waitForStableTextMetrics(container, renderConfig);
17918
17999
  await this.waitForCanvasScene(container, renderConfig, pageIndex);
17919
18000
  firstMountSettled = true;
@@ -18024,8 +18105,8 @@ class PixldocsRenderer {
18024
18105
  this.waitForCanvasScene(container, renderConfig, pageIndex).then(async () => {
18025
18106
  var _a, _b;
18026
18107
  try {
18027
- const expectedImageCount = this.getExpectedImageCount(renderConfig, pageIndex);
18028
- await this.waitForCanvasImages(container, expectedImageCount);
18108
+ const expectedImageIds = this.getExpectedImageIds(renderConfig, pageIndex);
18109
+ await this.waitForCanvasImages(container, expectedImageIds);
18029
18110
  await this.waitForStableTextMetrics(container, renderConfig);
18030
18111
  await this.waitForCanvasScene(container, renderConfig, pageIndex);
18031
18112
  const fabricInstance = this.getFabricCanvasFromContainer(container);
@@ -19664,7 +19745,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
19664
19745
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
19665
19746
  sanitizeSvgTreeForPdf(svgToDraw);
19666
19747
  try {
19667
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-CrTGqxx6.js");
19748
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-Cd--tBky.js");
19668
19749
  try {
19669
19750
  await logTextMeasurementDiagnostic(svgToDraw);
19670
19751
  } catch {
@@ -20064,4 +20145,4 @@ export {
20064
20145
  buildTeaserBlurFlatKeys as y,
20065
20146
  collectFontDescriptorsFromConfig as z
20066
20147
  };
20067
- //# sourceMappingURL=index-C3kuDISv.js.map
20148
+ //# sourceMappingURL=index-CDoyeKa8.js.map