@pixldocs/canvas-renderer 0.5.195 → 0.5.197

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
@@ -16919,9 +16919,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
16919
16919
  }
16920
16920
  return svgString;
16921
16921
  }
16922
- const resolvedPackageVersion = "0.5.195";
16922
+ const resolvedPackageVersion = "0.5.197";
16923
16923
  const PACKAGE_VERSION = resolvedPackageVersion;
16924
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.195";
16924
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.197";
16925
16925
  const roundParityValue = (value) => {
16926
16926
  if (typeof value !== "number") return value;
16927
16927
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -16981,11 +16981,16 @@ function detectSafariOrIos() {
16981
16981
  return false;
16982
16982
  }
16983
16983
  }
16984
- async function downscaleConfigRasterImages(config, maxEdgePx) {
16984
+ async function downscaleConfigRasterImages(config, maxEdgePx, maxDataUrlBytes = 2e6) {
16985
16985
  if (!maxEdgePx || maxEdgePx <= 0) return 0;
16986
16986
  if (typeof document === "undefined") return 0;
16987
16987
  const targets = [];
16988
16988
  const isRasterDataUrl = (u) => typeof u === "string" && /^data:image\/(jpeg|jpg|png|webp)[;,]/i.test(u);
16989
+ const estimateDataUrlBytes = (u) => {
16990
+ const comma = u.indexOf(",");
16991
+ const payload = comma >= 0 ? u.slice(comma + 1) : u;
16992
+ return Math.floor(payload.length * 0.75);
16993
+ };
16989
16994
  const walk = (nodes) => {
16990
16995
  if (!Array.isArray(nodes)) return;
16991
16996
  for (const node of nodes) {
@@ -17001,38 +17006,82 @@ async function downscaleConfigRasterImages(config, maxEdgePx) {
17001
17006
  if (targets.length === 0) return 0;
17002
17007
  const shrinkOne = async (dataUrl) => {
17003
17008
  try {
17004
- const img = await new Promise((resolve, reject) => {
17009
+ const decode = async (src) => new Promise((resolve, reject) => {
17005
17010
  const el = new Image();
17006
17011
  el.onload = () => resolve(el);
17007
17012
  el.onerror = (e) => reject(e);
17008
17013
  el.decoding = "sync";
17009
- el.src = dataUrl;
17014
+ el.src = src;
17010
17015
  });
17016
+ let img;
17017
+ let blobUrl = null;
17018
+ try {
17019
+ img = await decode(dataUrl);
17020
+ } catch {
17021
+ try {
17022
+ const resp = await fetch(dataUrl);
17023
+ const blob = await resp.blob();
17024
+ blobUrl = URL.createObjectURL(blob);
17025
+ img = await decode(blobUrl);
17026
+ } catch (inner) {
17027
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
17028
+ console.warn("[canvas-renderer] shrinkOne: failed to decode oversized data URL on this browser, dropping shrink attempt", inner);
17029
+ return null;
17030
+ }
17031
+ }
17011
17032
  const w = img.naturalWidth, h = img.naturalHeight;
17012
- if (!w || !h) return null;
17033
+ if (!w || !h) {
17034
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
17035
+ return null;
17036
+ }
17013
17037
  const longest = Math.max(w, h);
17014
- if (longest <= maxEdgePx) return null;
17015
- const scale = maxEdgePx / longest;
17016
- const tw = Math.max(1, Math.round(w * scale));
17017
- const th = Math.max(1, Math.round(h * scale));
17018
- const canvas = document.createElement("canvas");
17019
- canvas.width = tw;
17020
- canvas.height = th;
17021
- const ctx = canvas.getContext("2d");
17022
- if (!ctx) return null;
17023
- ctx.fillStyle = "#ffffff";
17024
- ctx.fillRect(0, 0, tw, th);
17025
- ctx.drawImage(img, 0, 0, tw, th);
17026
- return canvas.toDataURL("image/jpeg", 0.85);
17038
+ const tooLargeByEdge = longest > maxEdgePx;
17039
+ const tooLargeByBytes = estimateDataUrlBytes(dataUrl) > maxDataUrlBytes;
17040
+ if (!tooLargeByEdge && !tooLargeByBytes) {
17041
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
17042
+ return null;
17043
+ }
17044
+ let scale = tooLargeByEdge ? maxEdgePx / longest : 1;
17045
+ let quality = 0.85;
17046
+ let best = null;
17047
+ for (let attempt = 0; attempt < 4; attempt++) {
17048
+ const tw = Math.max(1, Math.round(w * scale));
17049
+ const th = Math.max(1, Math.round(h * scale));
17050
+ const canvas = document.createElement("canvas");
17051
+ canvas.width = tw;
17052
+ canvas.height = th;
17053
+ const ctx = canvas.getContext("2d");
17054
+ if (!ctx) {
17055
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
17056
+ return best;
17057
+ }
17058
+ ctx.fillStyle = "#ffffff";
17059
+ ctx.fillRect(0, 0, tw, th);
17060
+ ctx.drawImage(img, 0, 0, tw, th);
17061
+ best = canvas.toDataURL("image/jpeg", quality);
17062
+ const outBytes = estimateDataUrlBytes(best);
17063
+ if (outBytes <= maxDataUrlBytes || attempt === 3) {
17064
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
17065
+ return best;
17066
+ }
17067
+ const byteScale = Math.sqrt(maxDataUrlBytes / Math.max(1, outBytes)) * 0.92;
17068
+ scale = Math.max(0.1, scale * Math.min(0.95, byteScale));
17069
+ quality = Math.max(0.68, quality - 0.06);
17070
+ }
17071
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
17072
+ return best;
17027
17073
  } catch {
17028
17074
  return null;
17029
17075
  }
17030
17076
  };
17031
17077
  let shrunk = 0;
17032
17078
  for (const { node, field } of targets) {
17079
+ const before = String(node[field]);
17033
17080
  const next = await shrinkOne(String(node[field]));
17034
17081
  if (next) {
17035
17082
  node[field] = next;
17083
+ const twin = field === "src" ? "imageUrl" : "src";
17084
+ if (node[twin] === before) node[twin] = next;
17036
17085
  shrunk++;
17037
17086
  }
17038
17087
  }
@@ -17471,7 +17520,7 @@ class PixldocsRenderer {
17471
17520
  const isSafariLike = detectSafariOrIos();
17472
17521
  const shouldForcePerElement = forceMode === true ? true : forceMode === false ? false : hasUserDataImage && isSafariLike;
17473
17522
  const maxEdgeOpt = options.maxImageEdgePx ?? this.config.maxImageEdgePx;
17474
- const effectiveMaxEdge = typeof maxEdgeOpt === "number" ? Math.max(0, maxEdgeOpt | 0) : shouldForcePerElement ? 2048 : 0;
17523
+ const effectiveMaxEdge = typeof maxEdgeOpt === "number" ? Math.max(0, maxEdgeOpt | 0) : hasUserDataImage ? 2048 : 0;
17475
17524
  if (effectiveMaxEdge > 0) {
17476
17525
  try {
17477
17526
  const downscaled = await downscaleConfigRasterImages(cloned, effectiveMaxEdge);
@@ -17548,7 +17597,7 @@ class PixldocsRenderer {
17548
17597
  await this.waitForCanvasScene(container, cloned, i);
17549
17598
  }
17550
17599
  console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
17551
- const { exportMultiPagePdf, preparePagesForExport } = await import("./vectorPdfExport-eV507wxX.js");
17600
+ const { exportMultiPagePdf, preparePagesForExport } = await import("./vectorPdfExport-D4pUlnXw.js");
17552
17601
  const prepared = preparePagesForExport(
17553
17602
  cloned.pages,
17554
17603
  canvasWidth,
@@ -17570,11 +17619,10 @@ class PixldocsRenderer {
17570
17619
  // correct. v0.5.191 shipped with this skip enabled and produced exactly
17571
17620
  // that regression.
17572
17621
  //
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.
17622
+ // The large-photo missing/corrupt image issue is solved by the
17623
+ // `downscaleConfigRasterImages()` pre-pass above, which shrinks and
17624
+ // recompresses oversized `data:image/*` sources before the canvas
17625
+ // mounts. That keeps SVG capture reliable without altering layout.
17578
17626
  //
17579
17627
  // We still honor an explicit `forcePerElementPdf: true` from the host
17580
17628
  // app (advanced opt-in for niche cases), but the auto path no longer
@@ -19746,7 +19794,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
19746
19794
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
19747
19795
  sanitizeSvgTreeForPdf(svgToDraw);
19748
19796
  try {
19749
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-eV507wxX.js");
19797
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-D4pUlnXw.js");
19750
19798
  try {
19751
19799
  await logTextMeasurementDiagnostic(svgToDraw);
19752
19800
  } catch {
@@ -20146,4 +20194,4 @@ export {
20146
20194
  buildTeaserBlurFlatKeys as y,
20147
20195
  collectFontDescriptorsFromConfig as z
20148
20196
  };
20149
- //# sourceMappingURL=index-BggTMIGH.js.map
20197
+ //# sourceMappingURL=index-6bqq7X_L.js.map