@pixldocs/canvas-renderer 0.5.189 → 0.5.191

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
@@ -483,7 +483,7 @@ const blurFieldIds = ['text-reference-name', 'text-phone', 'text-address'];
483
483
  the resolved config before handing it to the imperative renderer, so the
484
484
  blur is baked into the exported pixels.
485
485
 
486
- ### Biodata teaser previews — blur by flat form key (v0.5.187+, hardened in v0.5.189)
486
+ ### Biodata teaser previews — blur by flat form key (v0.5.187+, hardened in v0.5.189, pagination-safe in v0.5.190)
487
487
 
488
488
  Form-driven apps (BioMaker, the pixldocs.com Use page, etc.) already
489
489
  speak the **flat form-key** language produced by `applyFormDataToConfig`
@@ -530,11 +530,17 @@ Notes:
530
530
  with or without the wrapper `field_` prefix). **As of v0.5.189**
531
531
  every resolved id is then verified against the actual rendered page
532
532
  tree (`config.pages[*]`) — stale or alias map entries that don't
533
- match a real element are dropped silently. This eliminates the
534
- "phantom frost rectangle near a section title" class of bug that
535
- showed up in templates with nested repeatables, where
536
- `__cloneIdMap` returned ids like `text-…__cgroup-…_e4` but the
537
- rendered tree used `text-…__cgrp-…_pN_grp-…_eN`.
533
+ match a real element are dropped silently.
534
+ - **As of v0.5.190** `applyContentBoundsPagination` rewrites
535
+ `__cloneIdMap` after pagination so flat keys for overflow rows that
536
+ moved to continuation pages still resolve to ids that exist on the
537
+ tree (clones inherit `__sourceId` from the original element). If the
538
+ map is still stale for any reason, the resolver also falls back to
539
+ walking the page tree and matching `__sourceId` / `__baseNodeId`
540
+ against the raw resolved ids — the same strategy the theming pipeline
541
+ uses to track paginated clones. End result: frosted blur for row N
542
+ appears on whichever page row N actually renders on, even when
543
+ content-bounds pagination moved it.
538
544
  - Pass `pageIndex` in `blurFlatFormKeyOptions` to scope verification
539
545
  to a single page — clones that landed on a different page will not
540
546
  contribute overlays to this page. This is the recommended pattern
@@ -606,3 +612,43 @@ function — there is now exactly one resolver shared across the entire
606
612
  stack. Anything that blurs correctly in one place blurs identically in
607
613
  the other; visual/UX bugs introduced in one pipeline cannot diverge
608
614
  silently from the other.
615
+
616
+ ## Reliable PDF photo embedding across browsers (v0.5.191+)
617
+
618
+ Form-driven apps that embed **user-uploaded photos** (biodatas, resumes,
619
+ ID cards…) historically hit a Safari/iOS bug where the downloaded PDF
620
+ rendered without the photos even though the on-screen preview was fine.
621
+ The svg2pdf fast path internally re-rasterises `data:image/*` sources
622
+ through a tainted offscreen canvas roundtrip, which Safari silently drops
623
+ for large camera-roll JPEGs.
624
+
625
+ v0.5.191 fixes this with two new options:
626
+
627
+ ```ts
628
+ const renderer = new PixldocsRenderer({
629
+ supabaseUrl, supabaseAnonKey,
630
+ // Force the per-element Fabric → jsPDF.addImage path on every export.
631
+ // Recommended for biodata-style flows that always have user photos.
632
+ forcePerElementPdf: true, // true | false | 'auto' (default)
633
+ // Down-scale any data:image/* whose longest edge > 2048 to JPEG-0.85
634
+ // before mounting into Fabric. Prevents Safari from silently dropping
635
+ // multi-megapixel camera-roll JPEGs in jsPDF.addImage.
636
+ maxImageEdgePx: 2048, // 0 to disable
637
+ });
638
+
639
+ const pdf = await renderer.renderPdfFromForm({
640
+ templateId, formSchemaId, sectionState, title,
641
+ // Per-call overrides also accepted:
642
+ // forcePerElementPdf: 'auto',
643
+ // maxImageEdgePx: 2400,
644
+ });
645
+ ```
646
+
647
+ **Defaults:** `forcePerElementPdf: 'auto'` enables the safe path only when
648
+ the resolved config contains a `data:image/(jpeg|png|webp)` source **and**
649
+ the host is Safari / iOS (incl. iPadOS desktop spoof). Editors and
650
+ non-Safari hosts keep the vector fast path unchanged.
651
+
652
+ Hosts that previously monkey-patched `captureFabricCanvasSvgForPdf` to
653
+ throw (the unofficial BioMaker workaround) can delete that patch after
654
+ upgrading.
@@ -13327,6 +13327,7 @@ function isStaticOnNewPage(node) {
13327
13327
  function cloneNodeWithNewIds(node) {
13328
13328
  const base = node.__baseNodeId;
13329
13329
  const source = node.__sourceId;
13330
+ const effectiveSource = source ?? node.id;
13330
13331
  if (isGroup(node)) {
13331
13332
  const g = node;
13332
13333
  const cloned2 = {
@@ -13335,14 +13336,14 @@ function cloneNodeWithNewIds(node) {
13335
13336
  children: (g.children ?? []).map(cloneNodeWithNewIds)
13336
13337
  };
13337
13338
  if (base != null) cloned2.__baseNodeId = base;
13338
- if (source != null) cloned2.__sourceId = source;
13339
+ cloned2.__sourceId = effectiveSource;
13339
13340
  return cloned2;
13340
13341
  }
13341
13342
  const el = node;
13342
13343
  const prefix = el.type === "text" ? "text" : el.type === "image" ? "img" : el.type === "shape" ? "shape" : "line";
13343
13344
  const cloned = { ...el, id: generateId(prefix) };
13344
13345
  if (base != null) cloned.__baseNodeId = base;
13345
- if (source != null) cloned.__sourceId = source;
13346
+ cloned.__sourceId = effectiveSource;
13346
13347
  return cloned;
13347
13348
  }
13348
13349
  function cloneNodeWithStableIds(node, baseId2, path) {
@@ -13648,7 +13649,51 @@ function applyContentBoundsPagination(config) {
13648
13649
  resultPages.push(...paginated);
13649
13650
  }
13650
13651
  if (!mutated) return config;
13651
- return { ...config, pages: resultPages };
13652
+ const next = { ...config, pages: resultPages };
13653
+ remapCloneIdMapAfterPagination(next);
13654
+ return next;
13655
+ }
13656
+ function remapCloneIdMapAfterPagination(config) {
13657
+ const cloneIdMap = config.__cloneIdMap;
13658
+ if (!cloneIdMap || typeof cloneIdMap !== "object") return;
13659
+ const allIds = /* @__PURE__ */ new Set();
13660
+ const sourceToNew = /* @__PURE__ */ new Map();
13661
+ const walk = (node) => {
13662
+ if (!node || typeof node !== "object") return;
13663
+ const n = node;
13664
+ if (typeof n.id === "string") {
13665
+ allIds.add(n.id);
13666
+ const src = n.__sourceId;
13667
+ if (typeof src === "string" && src !== n.id) {
13668
+ const arr = sourceToNew.get(src);
13669
+ if (arr) arr.push(n.id);
13670
+ else sourceToNew.set(src, [n.id]);
13671
+ }
13672
+ }
13673
+ const children = n.children;
13674
+ if (Array.isArray(children)) for (const c of children) walk(c);
13675
+ };
13676
+ for (const page of config.pages ?? []) {
13677
+ const ch = page.children;
13678
+ if (Array.isArray(ch)) for (const c of ch) walk(c);
13679
+ }
13680
+ const updated = {};
13681
+ for (const [key, val] of Object.entries(cloneIdMap)) {
13682
+ const oldIds = Array.isArray(val) ? val : [val];
13683
+ const newIds = [];
13684
+ for (const oid of oldIds) {
13685
+ if (typeof oid !== "string") continue;
13686
+ if (allIds.has(oid)) newIds.push(oid);
13687
+ const clones = sourceToNew.get(oid);
13688
+ if (clones) {
13689
+ for (const c of clones) if (allIds.has(c)) newIds.push(c);
13690
+ }
13691
+ }
13692
+ if (newIds.length === 0) continue;
13693
+ const uniq = Array.from(new Set(newIds));
13694
+ updated[key] = uniq.length === 1 ? uniq[0] : uniq;
13695
+ }
13696
+ config.__cloneIdMap = updated;
13652
13697
  }
13653
13698
  const __vite_import_meta_env__ = {};
13654
13699
  const FONT_WEIGHT_LABELS = {
@@ -16274,6 +16319,32 @@ function collectPageTreeElementIds(config, pageIndex = "all") {
16274
16319
  }
16275
16320
  return out;
16276
16321
  }
16322
+ function collectIdsBySourceMatch(config, targetIds, pageIndex = "all") {
16323
+ const targets = /* @__PURE__ */ new Set();
16324
+ for (const t of targetIds) if (typeof t === "string" && t) targets.add(t);
16325
+ if (targets.size === 0) return [];
16326
+ const pages = config == null ? void 0 : config.pages;
16327
+ if (!Array.isArray(pages)) return [];
16328
+ const visitPages = pageIndex === "all" ? pages : pages[pageIndex] != null ? [pages[pageIndex]] : [];
16329
+ const out = /* @__PURE__ */ new Set();
16330
+ const walk = (node) => {
16331
+ if (!node || typeof node !== "object") return;
16332
+ const src = typeof node.__sourceId === "string" ? node.__sourceId : void 0;
16333
+ const base = typeof node.__baseNodeId === "string" ? node.__baseNodeId : void 0;
16334
+ if (typeof node.id === "string") {
16335
+ if (targets.has(node.id)) out.add(node.id);
16336
+ else if (src && targets.has(src)) out.add(node.id);
16337
+ else if (base && targets.has(base)) out.add(node.id);
16338
+ }
16339
+ const children = node.children || node.elements;
16340
+ if (Array.isArray(children)) for (const c of children) walk(c);
16341
+ };
16342
+ for (const page of visitPages) {
16343
+ const children = (page == null ? void 0 : page.children) || (page == null ? void 0 : page.elements);
16344
+ if (Array.isArray(children)) for (const c of children) walk(c);
16345
+ }
16346
+ return Array.from(out);
16347
+ }
16277
16348
  function resolveBlurElementExactIdsFromFlatFormKeys(config, flatFormKeys, options) {
16278
16349
  const cloneIdMap = (config == null ? void 0 : config.__cloneIdMap) || {};
16279
16350
  if (!cloneIdMap || typeof cloneIdMap !== "object") return [];
@@ -16307,7 +16378,8 @@ function resolveBlurElementExactIdsFromFlatFormKeys(config, flatFormKeys, option
16307
16378
  if (treeIds.size === 0) return Array.from(out);
16308
16379
  const verified = [];
16309
16380
  for (const id of out) if (treeIds.has(id)) verified.push(id);
16310
- return verified;
16381
+ if (verified.length > 0) return verified;
16382
+ return collectIdsBySourceMatch(config, out, (options == null ? void 0 : options.pageIndex) ?? "all");
16311
16383
  }
16312
16384
  function buildTeaserBlurFlatKeys(sectionState, sections, options) {
16313
16385
  const afterRow = Math.max(0, options.afterRow | 0);
@@ -16835,9 +16907,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
16835
16907
  }
16836
16908
  return svgString;
16837
16909
  }
16838
- const resolvedPackageVersion = "0.5.189";
16910
+ const resolvedPackageVersion = "0.5.191";
16839
16911
  const PACKAGE_VERSION = resolvedPackageVersion;
16840
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.189";
16912
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.191";
16841
16913
  const roundParityValue = (value) => {
16842
16914
  if (typeof value !== "number") return value;
16843
16915
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -16864,6 +16936,96 @@ function isFabricTextboxLike(obj) {
16864
16936
  function isFabricGroupLike(obj) {
16865
16937
  return !!obj && (obj instanceof fabric__namespace.Group || obj.type === "group" || Array.isArray(obj._objects) && typeof obj.getObjects === "function");
16866
16938
  }
16939
+ function configHasUserDataImage(config) {
16940
+ let found = false;
16941
+ const isRasterDataUrl = (u) => typeof u === "string" && /^data:image\/(jpeg|jpg|png|webp)[;,]/i.test(u);
16942
+ const walk = (nodes) => {
16943
+ if (found || !Array.isArray(nodes)) return;
16944
+ for (const node of nodes) {
16945
+ if (!node || typeof node !== "object") continue;
16946
+ if (node.type === "image" && (isRasterDataUrl(node.src) || isRasterDataUrl(node.imageUrl))) {
16947
+ found = true;
16948
+ return;
16949
+ }
16950
+ if (Array.isArray(node.children)) walk(node.children);
16951
+ }
16952
+ };
16953
+ for (const page of config.pages ?? []) {
16954
+ walk((page == null ? void 0 : page.children) ?? []);
16955
+ if (found) break;
16956
+ }
16957
+ return found;
16958
+ }
16959
+ function detectSafariOrIos() {
16960
+ try {
16961
+ if (typeof navigator === "undefined") return false;
16962
+ const ua = navigator.userAgent || "";
16963
+ const vendor = navigator.vendor || "";
16964
+ const isIOS = /iPad|iPhone|iPod/.test(ua) || // iPadOS 13+ reports as Mac with touch
16965
+ /Macintosh/.test(ua) && typeof navigator.maxTouchPoints === "number" && navigator.maxTouchPoints > 1;
16966
+ const isSafariDesktop = /Safari/.test(ua) && /Apple/.test(vendor) && !/Chrome|CriOS|Chromium|Edg|FxiOS/.test(ua);
16967
+ return isIOS || isSafariDesktop;
16968
+ } catch {
16969
+ return false;
16970
+ }
16971
+ }
16972
+ async function downscaleConfigRasterImages(config, maxEdgePx) {
16973
+ if (!maxEdgePx || maxEdgePx <= 0) return 0;
16974
+ if (typeof document === "undefined") return 0;
16975
+ const targets = [];
16976
+ const isRasterDataUrl = (u) => typeof u === "string" && /^data:image\/(jpeg|jpg|png|webp)[;,]/i.test(u);
16977
+ const walk = (nodes) => {
16978
+ if (!Array.isArray(nodes)) return;
16979
+ for (const node of nodes) {
16980
+ if (!node || typeof node !== "object") continue;
16981
+ if (node.type === "image") {
16982
+ if (isRasterDataUrl(node.src)) targets.push({ node, field: "src" });
16983
+ else if (isRasterDataUrl(node.imageUrl)) targets.push({ node, field: "imageUrl" });
16984
+ }
16985
+ if (Array.isArray(node.children)) walk(node.children);
16986
+ }
16987
+ };
16988
+ for (const page of config.pages ?? []) walk((page == null ? void 0 : page.children) ?? []);
16989
+ if (targets.length === 0) return 0;
16990
+ const shrinkOne = async (dataUrl) => {
16991
+ try {
16992
+ const img = await new Promise((resolve, reject) => {
16993
+ const el = new Image();
16994
+ el.onload = () => resolve(el);
16995
+ el.onerror = (e) => reject(e);
16996
+ el.decoding = "sync";
16997
+ el.src = dataUrl;
16998
+ });
16999
+ const w = img.naturalWidth, h = img.naturalHeight;
17000
+ if (!w || !h) return null;
17001
+ const longest = Math.max(w, h);
17002
+ if (longest <= maxEdgePx) return null;
17003
+ const scale = maxEdgePx / longest;
17004
+ const tw = Math.max(1, Math.round(w * scale));
17005
+ const th = Math.max(1, Math.round(h * scale));
17006
+ const canvas = document.createElement("canvas");
17007
+ canvas.width = tw;
17008
+ canvas.height = th;
17009
+ const ctx = canvas.getContext("2d");
17010
+ if (!ctx) return null;
17011
+ ctx.fillStyle = "#ffffff";
17012
+ ctx.fillRect(0, 0, tw, th);
17013
+ ctx.drawImage(img, 0, 0, tw, th);
17014
+ return canvas.toDataURL("image/jpeg", 0.85);
17015
+ } catch {
17016
+ return null;
17017
+ }
17018
+ };
17019
+ let shrunk = 0;
17020
+ for (const { node, field } of targets) {
17021
+ const next = await shrinkOne(String(node[field]));
17022
+ if (next) {
17023
+ node[field] = next;
17024
+ shrunk++;
17025
+ }
17026
+ }
17027
+ return shrunk;
17028
+ }
16867
17029
  let __underlineFixInstalled = false;
16868
17030
  function installUnderlineFix(fab) {
16869
17031
  var _a;
@@ -17216,7 +17378,9 @@ class PixldocsRenderer {
17216
17378
  async renderPdf(templateConfig, options) {
17217
17379
  return this.renderPdfViaClientExport(templateConfig, {
17218
17380
  title: options == null ? void 0 : options.title,
17219
- textMode: options == null ? void 0 : options.textMode
17381
+ textMode: options == null ? void 0 : options.textMode,
17382
+ forcePerElementPdf: options == null ? void 0 : options.forcePerElementPdf,
17383
+ maxImageEdgePx: options == null ? void 0 : options.maxImageEdgePx
17220
17384
  });
17221
17385
  }
17222
17386
  /**
@@ -17224,7 +17388,7 @@ class PixldocsRenderer {
17224
17388
  * This is the primary PDF export API — mirrors renderFromForm() but returns a PDF.
17225
17389
  */
17226
17390
  async renderPdfFromForm(options) {
17227
- const { templateId, formSchemaId, sectionState, themeId, watermark, watermarkOptions, prefetched, title, fontBaseUrl, textMode } = options;
17391
+ const { templateId, formSchemaId, sectionState, themeId, watermark, watermarkOptions, prefetched, title, fontBaseUrl, textMode, forcePerElementPdf, maxImageEdgePx } = options;
17228
17392
  const resolved = await resolveFromForm({
17229
17393
  templateId,
17230
17394
  formSchemaId,
@@ -17245,7 +17409,9 @@ class PixldocsRenderer {
17245
17409
  return this.renderPdfViaClientExport(configToRender, {
17246
17410
  title: title ?? resolved.config.name,
17247
17411
  watermark: shouldWatermark,
17248
- textMode
17412
+ textMode,
17413
+ forcePerElementPdf,
17414
+ maxImageEdgePx
17249
17415
  });
17250
17416
  }
17251
17417
  async renderById(templateId, formData, options) {
@@ -17286,6 +17452,31 @@ class PixldocsRenderer {
17286
17452
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
17287
17453
  setPackageApiUrl2(this.config.imageProxyUrl);
17288
17454
  const cloned = JSON.parse(JSON.stringify(templateConfig));
17455
+ const callForce = options.forcePerElementPdf;
17456
+ const cfgForce = this.config.forcePerElementPdf;
17457
+ const forceMode = callForce !== void 0 ? callForce : cfgForce !== void 0 ? cfgForce : "auto";
17458
+ const hasUserDataImage = configHasUserDataImage(cloned);
17459
+ const isSafariLike = detectSafariOrIos();
17460
+ const shouldForcePerElement = forceMode === true ? true : forceMode === false ? false : hasUserDataImage && isSafariLike;
17461
+ const maxEdgeOpt = options.maxImageEdgePx ?? this.config.maxImageEdgePx;
17462
+ const effectiveMaxEdge = typeof maxEdgeOpt === "number" ? Math.max(0, maxEdgeOpt | 0) : shouldForcePerElement ? 2048 : 0;
17463
+ if (effectiveMaxEdge > 0) {
17464
+ try {
17465
+ const downscaled = await downscaleConfigRasterImages(cloned, effectiveMaxEdge);
17466
+ if (downscaled > 0) {
17467
+ console.log(`[canvas-renderer][pdf-unified] downscaled ${downscaled} raster image(s) to <=${effectiveMaxEdge}px edge`);
17468
+ }
17469
+ } catch (e) {
17470
+ console.warn("[canvas-renderer][pdf-unified] image downscale pass failed (continuing with originals):", e);
17471
+ }
17472
+ }
17473
+ console.log("[canvas-renderer][pdf-unified] export switches", {
17474
+ forceMode,
17475
+ hasUserDataImage,
17476
+ isSafariLike,
17477
+ shouldForcePerElement,
17478
+ effectiveMaxEdge
17479
+ });
17289
17480
  const stampPrefix = `__pixldocs_pdf_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
17290
17481
  const pageIds = cloned.pages.map((p, i) => {
17291
17482
  const id = `${stampPrefix}_p${i}`;
@@ -17345,7 +17536,7 @@ class PixldocsRenderer {
17345
17536
  await this.waitForCanvasScene(container, cloned, i);
17346
17537
  }
17347
17538
  console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
17348
- const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-BAAJU0RL.cjs"));
17539
+ const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-D_2mdcwO.cjs"));
17349
17540
  const prepared = preparePagesForExport(
17350
17541
  cloned.pages,
17351
17542
  canvasWidth,
@@ -17355,7 +17546,8 @@ class PixldocsRenderer {
17355
17546
  title: options.title,
17356
17547
  watermark: !!options.watermark,
17357
17548
  returnBlob: true,
17358
- pdfTextMode: options.textMode ?? (cloned == null ? void 0 : cloned.pdfTextMode) ?? ((_a = cloned.canvas) == null ? void 0 : _a.n) ?? "auto"
17549
+ pdfTextMode: options.textMode ?? (cloned == null ? void 0 : cloned.pdfTextMode) ?? ((_a = cloned.canvas) == null ? void 0 : _a.n) ?? "auto",
17550
+ skipLiveCanvasSvgFastPath: shouldForcePerElement
17359
17551
  });
17360
17552
  if (!result || typeof result === "undefined") {
17361
17553
  throw new Error("exportMultiPagePdf returned no blob (returnBlob path failed)");
@@ -19490,7 +19682,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
19490
19682
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
19491
19683
  sanitizeSvgTreeForPdf(svgToDraw);
19492
19684
  try {
19493
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-BAAJU0RL.cjs"));
19685
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-D_2mdcwO.cjs"));
19494
19686
  try {
19495
19687
  await logTextMeasurementDiagnostic(svgToDraw);
19496
19688
  } catch {
@@ -19887,4 +20079,4 @@ exports.setAutoShrinkDebug = setAutoShrinkDebug;
19887
20079
  exports.setBundledAssetPrefixes = setBundledAssetPrefixes;
19888
20080
  exports.warmResolvedTemplateForPreview = warmResolvedTemplateForPreview;
19889
20081
  exports.warmTemplateFromForm = warmTemplateFromForm;
19890
- //# sourceMappingURL=index-CmrxeQ_K.cjs.map
20082
+ //# sourceMappingURL=index-BJR7zaam.cjs.map