@pixldocs/canvas-renderer 0.5.190 → 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 +40 -0
- package/dist/{index-BUm3wqlB.cjs → index-BJR7zaam.cjs} +129 -9
- package/dist/{index-BUm3wqlB.cjs.map → index-BJR7zaam.cjs.map} +1 -1
- package/dist/{index-Dt4aPZtQ.js → index-C3kuDISv.js} +129 -9
- package/dist/{index-Dt4aPZtQ.js.map → index-C3kuDISv.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +38 -0
- package/dist/index.js +1 -1
- package/dist/{vectorPdfExport-DFGqTzvI.js → vectorPdfExport-CrTGqxx6.js} +7 -7
- package/dist/{vectorPdfExport-DFGqTzvI.js.map → vectorPdfExport-CrTGqxx6.js.map} +1 -1
- package/dist/{vectorPdfExport-Cgw8dB-L.cjs → vectorPdfExport-D_2mdcwO.cjs} +7 -7
- package/dist/{vectorPdfExport-Cgw8dB-L.cjs.map → vectorPdfExport-D_2mdcwO.cjs.map} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -612,3 +612,43 @@ function — there is now exactly one resolver shared across the entire
|
|
|
612
612
|
stack. Anything that blurs correctly in one place blurs identically in
|
|
613
613
|
the other; visual/UX bugs introduced in one pipeline cannot diverge
|
|
614
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.
|
|
@@ -16907,9 +16907,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
|
|
|
16907
16907
|
}
|
|
16908
16908
|
return svgString;
|
|
16909
16909
|
}
|
|
16910
|
-
const resolvedPackageVersion = "0.5.
|
|
16910
|
+
const resolvedPackageVersion = "0.5.191";
|
|
16911
16911
|
const PACKAGE_VERSION = resolvedPackageVersion;
|
|
16912
|
-
const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.
|
|
16912
|
+
const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.191";
|
|
16913
16913
|
const roundParityValue = (value) => {
|
|
16914
16914
|
if (typeof value !== "number") return value;
|
|
16915
16915
|
return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
|
|
@@ -16936,6 +16936,96 @@ function isFabricTextboxLike(obj) {
|
|
|
16936
16936
|
function isFabricGroupLike(obj) {
|
|
16937
16937
|
return !!obj && (obj instanceof fabric__namespace.Group || obj.type === "group" || Array.isArray(obj._objects) && typeof obj.getObjects === "function");
|
|
16938
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
|
+
}
|
|
16939
17029
|
let __underlineFixInstalled = false;
|
|
16940
17030
|
function installUnderlineFix(fab) {
|
|
16941
17031
|
var _a;
|
|
@@ -17288,7 +17378,9 @@ class PixldocsRenderer {
|
|
|
17288
17378
|
async renderPdf(templateConfig, options) {
|
|
17289
17379
|
return this.renderPdfViaClientExport(templateConfig, {
|
|
17290
17380
|
title: options == null ? void 0 : options.title,
|
|
17291
|
-
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
|
|
17292
17384
|
});
|
|
17293
17385
|
}
|
|
17294
17386
|
/**
|
|
@@ -17296,7 +17388,7 @@ class PixldocsRenderer {
|
|
|
17296
17388
|
* This is the primary PDF export API — mirrors renderFromForm() but returns a PDF.
|
|
17297
17389
|
*/
|
|
17298
17390
|
async renderPdfFromForm(options) {
|
|
17299
|
-
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;
|
|
17300
17392
|
const resolved = await resolveFromForm({
|
|
17301
17393
|
templateId,
|
|
17302
17394
|
formSchemaId,
|
|
@@ -17317,7 +17409,9 @@ class PixldocsRenderer {
|
|
|
17317
17409
|
return this.renderPdfViaClientExport(configToRender, {
|
|
17318
17410
|
title: title ?? resolved.config.name,
|
|
17319
17411
|
watermark: shouldWatermark,
|
|
17320
|
-
textMode
|
|
17412
|
+
textMode,
|
|
17413
|
+
forcePerElementPdf,
|
|
17414
|
+
maxImageEdgePx
|
|
17321
17415
|
});
|
|
17322
17416
|
}
|
|
17323
17417
|
async renderById(templateId, formData, options) {
|
|
@@ -17358,6 +17452,31 @@ class PixldocsRenderer {
|
|
|
17358
17452
|
const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
|
|
17359
17453
|
setPackageApiUrl2(this.config.imageProxyUrl);
|
|
17360
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
|
+
});
|
|
17361
17480
|
const stampPrefix = `__pixldocs_pdf_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
17362
17481
|
const pageIds = cloned.pages.map((p, i) => {
|
|
17363
17482
|
const id = `${stampPrefix}_p${i}`;
|
|
@@ -17417,7 +17536,7 @@ class PixldocsRenderer {
|
|
|
17417
17536
|
await this.waitForCanvasScene(container, cloned, i);
|
|
17418
17537
|
}
|
|
17419
17538
|
console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
|
|
17420
|
-
const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-
|
|
17539
|
+
const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-D_2mdcwO.cjs"));
|
|
17421
17540
|
const prepared = preparePagesForExport(
|
|
17422
17541
|
cloned.pages,
|
|
17423
17542
|
canvasWidth,
|
|
@@ -17427,7 +17546,8 @@ class PixldocsRenderer {
|
|
|
17427
17546
|
title: options.title,
|
|
17428
17547
|
watermark: !!options.watermark,
|
|
17429
17548
|
returnBlob: true,
|
|
17430
|
-
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
|
|
17431
17551
|
});
|
|
17432
17552
|
if (!result || typeof result === "undefined") {
|
|
17433
17553
|
throw new Error("exportMultiPagePdf returned no blob (returnBlob path failed)");
|
|
@@ -19562,7 +19682,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
|
|
|
19562
19682
|
if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
|
|
19563
19683
|
sanitizeSvgTreeForPdf(svgToDraw);
|
|
19564
19684
|
try {
|
|
19565
|
-
const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-
|
|
19685
|
+
const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-D_2mdcwO.cjs"));
|
|
19566
19686
|
try {
|
|
19567
19687
|
await logTextMeasurementDiagnostic(svgToDraw);
|
|
19568
19688
|
} catch {
|
|
@@ -19959,4 +20079,4 @@ exports.setAutoShrinkDebug = setAutoShrinkDebug;
|
|
|
19959
20079
|
exports.setBundledAssetPrefixes = setBundledAssetPrefixes;
|
|
19960
20080
|
exports.warmResolvedTemplateForPreview = warmResolvedTemplateForPreview;
|
|
19961
20081
|
exports.warmTemplateFromForm = warmTemplateFromForm;
|
|
19962
|
-
//# sourceMappingURL=index-
|
|
20082
|
+
//# sourceMappingURL=index-BJR7zaam.cjs.map
|