@pixldocs/canvas-renderer 0.5.194 → 0.5.196

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
@@ -275,17 +275,16 @@ const renderer = new PixldocsRenderer({
275
275
  pixelRatio?: number, // default: 2
276
276
  assetWaitTimeoutMs?: number, // default: 15000; use ~30000 for multi-photo PDF exports
277
277
  assetWaitEarlyExitMs?: number, // default: 1500; ignored for strict PDF image-ID waits
278
- maxImageEdgePx?: number, // recommended: 2048 for user-uploaded PDF photos
278
+ maxImageEdgePx?: number, // auto: 2048 for user-uploaded PDF photos; 0 disables
279
279
  debug?: boolean, // logs image wait/SVG completeness diagnostics
280
280
  });
281
281
  ```
282
282
 
283
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.
284
+ SVG path and **do not** set `forcePerElementPdf: true`. Large `data:image/*`
285
+ photos are normalized before the hidden Fabric canvas mounts (default cap:
286
+ 2048px edge and ~2MB encoded data URL) so svg2pdf does not corrupt/drop
287
+ multi-megapixel uploads.
289
288
 
290
289
  #### `renderer.renderFromForm(options)`
291
290
 
@@ -638,13 +637,12 @@ v0.5.191 fixes this with two new options:
638
637
  ```ts
639
638
  const renderer = new PixldocsRenderer({
640
639
  supabaseUrl, supabaseAnonKey,
641
- // Force the per-element Fabric → jsPDF.addImage path on every export.
642
- // Recommended for biodata-style flows that always have user photos.
643
- forcePerElementPdf: true, // true | false | 'auto' (default)
644
- // Down-scale any data:image/* whose longest edge > 2048 to JPEG-0.85
645
- // before mounting into Fabric. Prevents Safari from silently dropping
646
- // multi-megapixel camera-roll JPEGs in jsPDF.addImage.
647
- maxImageEdgePx: 2048, // 0 to disable
640
+ // Keep the live SVG path. Use true only as a manual escape hatch because
641
+ // the per-element/config-space path can drift from live Fabric layout.
642
+ forcePerElementPdf: 'auto', // true | false | 'auto' (default)
643
+ // Normalize data:image/* photos before mounting into Fabric. The default
644
+ // auto value is 2048 and also recompresses oversized encoded data URLs.
645
+ maxImageEdgePx: 2048, // default auto; 0 disables
648
646
  });
649
647
 
650
648
  const pdf = await renderer.renderPdfFromForm({
@@ -655,10 +653,10 @@ const pdf = await renderer.renderPdfFromForm({
655
653
  });
656
654
  ```
657
655
 
658
- **Defaults:** `forcePerElementPdf: 'auto'` enables the safe path only when
659
- the resolved config contains a `data:image/(jpeg|png|webp)` source **and**
660
- the host is Safari / iOS (incl. iPadOS desktop spoof). Editors and
661
- non-Safari hosts keep the vector fast path unchanged.
656
+ **Defaults:** `forcePerElementPdf: 'auto'` keeps the live SVG path. Any resolved
657
+ config containing `data:image/(jpeg|png|webp)` sources is normalized before PDF
658
+ mounting so large camera/gallery uploads behave consistently across browsers
659
+ and server renderers.
662
660
 
663
661
  Hosts that previously monkey-patched `captureFabricCanvasSvgForPdf` to
664
662
  throw (the unofficial BioMaker workaround) can delete that patch after
@@ -16937,9 +16937,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
16937
16937
  }
16938
16938
  return svgString;
16939
16939
  }
16940
- const resolvedPackageVersion = "0.5.194";
16940
+ const resolvedPackageVersion = "0.5.196";
16941
16941
  const PACKAGE_VERSION = resolvedPackageVersion;
16942
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.194";
16942
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.196";
16943
16943
  const roundParityValue = (value) => {
16944
16944
  if (typeof value !== "number") return value;
16945
16945
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -16999,11 +16999,16 @@ function detectSafariOrIos() {
16999
16999
  return false;
17000
17000
  }
17001
17001
  }
17002
- async function downscaleConfigRasterImages(config, maxEdgePx) {
17002
+ async function downscaleConfigRasterImages(config, maxEdgePx, maxDataUrlBytes = 2e6) {
17003
17003
  if (!maxEdgePx || maxEdgePx <= 0) return 0;
17004
17004
  if (typeof document === "undefined") return 0;
17005
17005
  const targets = [];
17006
17006
  const isRasterDataUrl = (u) => typeof u === "string" && /^data:image\/(jpeg|jpg|png|webp)[;,]/i.test(u);
17007
+ const estimateDataUrlBytes = (u) => {
17008
+ const comma = u.indexOf(",");
17009
+ const payload = comma >= 0 ? u.slice(comma + 1) : u;
17010
+ return Math.floor(payload.length * 0.75);
17011
+ };
17007
17012
  const walk = (nodes) => {
17008
17013
  if (!Array.isArray(nodes)) return;
17009
17014
  for (const node of nodes) {
@@ -17029,28 +17034,43 @@ async function downscaleConfigRasterImages(config, maxEdgePx) {
17029
17034
  const w = img.naturalWidth, h = img.naturalHeight;
17030
17035
  if (!w || !h) return null;
17031
17036
  const longest = Math.max(w, h);
17032
- if (longest <= maxEdgePx) return null;
17033
- const scale = maxEdgePx / longest;
17034
- const tw = Math.max(1, Math.round(w * scale));
17035
- const th = Math.max(1, Math.round(h * scale));
17036
- const canvas = document.createElement("canvas");
17037
- canvas.width = tw;
17038
- canvas.height = th;
17039
- const ctx = canvas.getContext("2d");
17040
- if (!ctx) return null;
17041
- ctx.fillStyle = "#ffffff";
17042
- ctx.fillRect(0, 0, tw, th);
17043
- ctx.drawImage(img, 0, 0, tw, th);
17044
- return canvas.toDataURL("image/jpeg", 0.85);
17037
+ const tooLargeByEdge = longest > maxEdgePx;
17038
+ const tooLargeByBytes = estimateDataUrlBytes(dataUrl) > maxDataUrlBytes;
17039
+ if (!tooLargeByEdge && !tooLargeByBytes) return null;
17040
+ let scale = tooLargeByEdge ? maxEdgePx / longest : 1;
17041
+ let quality = 0.85;
17042
+ let best = null;
17043
+ for (let attempt = 0; attempt < 4; attempt++) {
17044
+ const tw = Math.max(1, Math.round(w * scale));
17045
+ const th = Math.max(1, Math.round(h * scale));
17046
+ const canvas = document.createElement("canvas");
17047
+ canvas.width = tw;
17048
+ canvas.height = th;
17049
+ const ctx = canvas.getContext("2d");
17050
+ if (!ctx) return best;
17051
+ ctx.fillStyle = "#ffffff";
17052
+ ctx.fillRect(0, 0, tw, th);
17053
+ ctx.drawImage(img, 0, 0, tw, th);
17054
+ best = canvas.toDataURL("image/jpeg", quality);
17055
+ const outBytes = estimateDataUrlBytes(best);
17056
+ if (outBytes <= maxDataUrlBytes || attempt === 3) return best;
17057
+ const byteScale = Math.sqrt(maxDataUrlBytes / Math.max(1, outBytes)) * 0.92;
17058
+ scale = Math.max(0.1, scale * Math.min(0.95, byteScale));
17059
+ quality = Math.max(0.68, quality - 0.06);
17060
+ }
17061
+ return best;
17045
17062
  } catch {
17046
17063
  return null;
17047
17064
  }
17048
17065
  };
17049
17066
  let shrunk = 0;
17050
17067
  for (const { node, field } of targets) {
17068
+ const before = String(node[field]);
17051
17069
  const next = await shrinkOne(String(node[field]));
17052
17070
  if (next) {
17053
17071
  node[field] = next;
17072
+ const twin = field === "src" ? "imageUrl" : "src";
17073
+ if (node[twin] === before) node[twin] = next;
17054
17074
  shrunk++;
17055
17075
  }
17056
17076
  }
@@ -17489,7 +17509,7 @@ class PixldocsRenderer {
17489
17509
  const isSafariLike = detectSafariOrIos();
17490
17510
  const shouldForcePerElement = forceMode === true ? true : forceMode === false ? false : hasUserDataImage && isSafariLike;
17491
17511
  const maxEdgeOpt = options.maxImageEdgePx ?? this.config.maxImageEdgePx;
17492
- const effectiveMaxEdge = typeof maxEdgeOpt === "number" ? Math.max(0, maxEdgeOpt | 0) : shouldForcePerElement ? 2048 : 0;
17512
+ const effectiveMaxEdge = typeof maxEdgeOpt === "number" ? Math.max(0, maxEdgeOpt | 0) : hasUserDataImage ? 2048 : 0;
17493
17513
  if (effectiveMaxEdge > 0) {
17494
17514
  try {
17495
17515
  const downscaled = await downscaleConfigRasterImages(cloned, effectiveMaxEdge);
@@ -17566,7 +17586,7 @@ class PixldocsRenderer {
17566
17586
  await this.waitForCanvasScene(container, cloned, i);
17567
17587
  }
17568
17588
  console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
17569
- const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-D5Xagjna.cjs"));
17589
+ const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-BGlG4zN9.cjs"));
17570
17590
  const prepared = preparePagesForExport(
17571
17591
  cloned.pages,
17572
17592
  canvasWidth,
@@ -17588,11 +17608,10 @@ class PixldocsRenderer {
17588
17608
  // correct. v0.5.191 shipped with this skip enabled and produced exactly
17589
17609
  // that regression.
17590
17610
  //
17591
- // The Safari/iOS "missing photo" issue is solved end-to-end by the
17592
- // `downscaleConfigRasterImages()` pre-pass above, which shrinks large
17593
- // `data:image/*` JPEGs to a safe edge length before the canvas mounts.
17594
- // That alone keeps Safari's SVG capture reliable without altering the
17595
- // layout pipeline.
17611
+ // The large-photo missing/corrupt image issue is solved by the
17612
+ // `downscaleConfigRasterImages()` pre-pass above, which shrinks and
17613
+ // recompresses oversized `data:image/*` sources before the canvas
17614
+ // mounts. That keeps SVG capture reliable without altering layout.
17596
17615
  //
17597
17616
  // We still honor an explicit `forcePerElementPdf: true` from the host
17598
17617
  // app (advanced opt-in for niche cases), but the auto path no longer
@@ -17781,8 +17800,9 @@ class PixldocsRenderer {
17781
17800
  console.warn("[canvas-renderer][asset-wait-timeout][dom-images]", domImageDebug);
17782
17801
  console.warn("[canvas-renderer][asset-wait-timeout][fabric-images]", fabricImageDebug);
17783
17802
  if (strictById && missingImageIds.length > 0) {
17784
- reject(new Error(`[canvas-renderer][asset-wait-timeout] Missing expected image(s): ${missingImageIds.join(", ")}`));
17785
- return;
17803
+ console.warn(
17804
+ `[canvas-renderer][asset-wait-timeout] Missing expected image(s) — continuing with partial set: ${missingImageIds.join(", ")}`
17805
+ );
17786
17806
  }
17787
17807
  settle();
17788
17808
  return;
@@ -19763,7 +19783,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
19763
19783
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
19764
19784
  sanitizeSvgTreeForPdf(svgToDraw);
19765
19785
  try {
19766
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-D5Xagjna.cjs"));
19786
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-BGlG4zN9.cjs"));
19767
19787
  try {
19768
19788
  await logTextMeasurementDiagnostic(svgToDraw);
19769
19789
  } catch {
@@ -20160,4 +20180,4 @@ exports.setAutoShrinkDebug = setAutoShrinkDebug;
20160
20180
  exports.setBundledAssetPrefixes = setBundledAssetPrefixes;
20161
20181
  exports.warmResolvedTemplateForPreview = warmResolvedTemplateForPreview;
20162
20182
  exports.warmTemplateFromForm = warmTemplateFromForm;
20163
- //# sourceMappingURL=index-pshD8xMc.cjs.map
20183
+ //# sourceMappingURL=index-DQnFS9GV.cjs.map