@pixldocs/canvas-renderer 0.5.495 → 0.5.497

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.
@@ -94,6 +94,15 @@ const generateId = (prefix = "el") => {
94
94
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}-${Math.random().toString(36).substr(2, 9)}`;
95
95
  };
96
96
  const createDefaultElement = (partial) => {
97
+ const loose = partial;
98
+ if (loose.type === "frame" || loose.type === "group") {
99
+ return createDefaultGroup({
100
+ layoutMode: loose.layout,
101
+ ...partial,
102
+ type: "group",
103
+ children: Array.isArray(loose.children) ? loose.children : []
104
+ });
105
+ }
97
106
  const isText = partial.type === "text";
98
107
  const base = {
99
108
  left: 48,
@@ -111,7 +120,7 @@ const createDefaultElement = (partial) => {
111
120
  originY: "top",
112
121
  opacity: 1,
113
122
  fill: "#e2e8f0",
114
- ...isText ? {} : { stroke: "#64748b", strokeWidth: 1 },
123
+ ...isText ? {} : { stroke: "transparent", strokeWidth: 0 },
115
124
  visible: true,
116
125
  selectable: true,
117
126
  evented: true,
@@ -957,49 +966,15 @@ function worldPointToParentLocal(worldX, worldY, parent, pageChildren, resolveGr
957
966
  );
958
967
  return { left: local.x, top: local.y };
959
968
  }
960
- function unbakeChildrenToGroupLocal(group) {
961
- const angle = getGroupAngleDeg(group);
962
- if (Math.abs(angle) < 0.01) return group;
963
- const { width, height } = getGroupFrameSize(group);
964
- const cx = width / 2;
965
- const cy = height / 2;
966
- const inv = rotateAroundAffDeg(-angle, cx, cy);
967
- const kids = (group.children ?? []).map((child) => {
968
- const p = applyAff(inv, child.left ?? 0, child.top ?? 0);
969
- const childAngle = typeof child.angle === "number" ? child.angle : 0;
970
- const localAngle = normalizeAngleDeg(childAngle - angle);
971
- if (isGroup(child)) {
972
- const relocated = {
973
- ...child,
974
- left: p.x,
975
- top: p.y,
976
- angle: localAngle
977
- };
978
- return Math.abs(getGroupAngleDeg(relocated)) > 0.01 ? unbakeChildrenToGroupLocal(relocated) : relocated;
979
- }
980
- return {
981
- ...child,
982
- left: p.x,
983
- top: p.y,
984
- angle: localAngle
985
- };
986
- });
987
- return { ...group, children: kids };
988
- }
989
969
  function migratePageTreeToGroupOwnedTransforms(nodes) {
990
970
  return nodes.map((node) => {
991
971
  if (!isGroup(node)) return node;
992
972
  const g = node;
993
- let next = {
973
+ const next = {
994
974
  ...g,
995
975
  children: migratePageTreeToGroupOwnedTransforms(g.children ?? [])
996
976
  };
997
- if (isRotatableGroup(next) && Math.abs(getGroupAngleDeg(next)) > 0.01) {
998
- if (g.groupTransformModel !== "owned-v1") {
999
- next = unbakeChildrenToGroupLocal(next);
1000
- next.groupTransformModel = "owned-v1";
1001
- }
1002
- } else if (isRotatableGroup(next)) {
977
+ if (isRotatableGroup(next)) {
1003
978
  next.groupTransformModel = "owned-v1";
1004
979
  }
1005
980
  return next;
@@ -2239,6 +2214,40 @@ function adaptLayoutGrid(grid, mode) {
2239
2214
  const cells = (grid.cells ?? []).map((c) => c && c.grid ? { ...c, grid: adaptLayoutGrid(c.grid, mode) } : c);
2240
2215
  return { ...grid, dir: grid.dir === from ? to : grid.dir, cells };
2241
2216
  }
2217
+ function isAspectLockedNode(node) {
2218
+ const e = node;
2219
+ if (!e) return false;
2220
+ if (e.lockAspect === true) return true;
2221
+ if (e.clipShape === "circle") return true;
2222
+ const kind = e.componentKind || e.smartType;
2223
+ return kind === "qrcode" || kind === "barcode";
2224
+ }
2225
+ function scaleNodeForReflow(node, sx, sy, fscale) {
2226
+ const n = node;
2227
+ const el = { ...n };
2228
+ const hasBox = typeof n.width === "number" && typeof n.height === "number";
2229
+ const f = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
2230
+ if (hasBox && isAspectLockedNode(n) && sx !== sy) {
2231
+ const s = Math.min(sx, sy);
2232
+ const cx = (f(n.left) + n.width / 2) * sx;
2233
+ const cy = (f(n.top) + n.height / 2) * sy;
2234
+ el.width = n.width * s;
2235
+ el.height = n.height * s;
2236
+ el.left = cx - el.width / 2;
2237
+ el.top = cy - el.height / 2;
2238
+ } else {
2239
+ if (typeof el.left === "number") el.left = el.left * sx;
2240
+ if (typeof el.top === "number") el.top = el.top * sy;
2241
+ if (typeof el.width === "number") el.width = el.width * sx;
2242
+ if (typeof el.height === "number") el.height = el.height * sy;
2243
+ }
2244
+ if (typeof el.fontSize === "number") el.fontSize = Math.max(4, el.fontSize * fscale);
2245
+ if (typeof el.minBoxHeight === "number") el.minBoxHeight = el.minBoxHeight * sy;
2246
+ if (Array.isArray(el.children)) {
2247
+ el.children = el.children.map((c) => scaleNodeForReflow(c, sx, sy, fscale));
2248
+ }
2249
+ return el;
2250
+ }
2242
2251
  function computeGridHandles(grid, box, gridPath = []) {
2243
2252
  const out = [];
2244
2253
  const n = grid.cells.length;
@@ -2641,16 +2650,7 @@ const reflowPagesFromBase = (base, W, H) => {
2641
2650
  const fscale = Math.sqrt(Math.max(0.01, sx * sy));
2642
2651
  const adapt = layoutAdaptationFor(base.width, base.height, W, H);
2643
2652
  const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
2644
- const scaleNode = (node) => {
2645
- const el = { ...node };
2646
- if (typeof el.left === "number") el.left = el.left * sx;
2647
- if (typeof el.top === "number") el.top = el.top * sy;
2648
- if (typeof el.width === "number") el.width = el.width * sx;
2649
- if (typeof el.height === "number") el.height = el.height * sy;
2650
- if (typeof el.fontSize === "number") el.fontSize = Math.max(4, el.fontSize * fscale);
2651
- if (Array.isArray(el.children)) el.children = el.children.map(scaleNode);
2652
- return el;
2653
- };
2653
+ const scaleNode = (node) => scaleNodeForReflow(node, sx, sy, fscale);
2654
2654
  return base.pages.map((p) => {
2655
2655
  const pg = p;
2656
2656
  const page = {
@@ -3239,6 +3239,13 @@ const useEditorStore = create((set, get) => ({
3239
3239
  return { canvas: nextCanvas, ...committed };
3240
3240
  });
3241
3241
  },
3242
+ getCanvasForSave: () => {
3243
+ const { canvas } = get();
3244
+ const base = sizeBaseSnapshot;
3245
+ if (!base) return canvas;
3246
+ if (canvas.width === base.width && canvas.height === base.height) return canvas;
3247
+ return { ...canvas, width: base.width, height: base.height, pages: base.pages };
3248
+ },
3242
3249
  addSizeVariant: (variant) => set((state) => {
3243
3250
  const sizes = state.canvas.sizes ?? [];
3244
3251
  if (sizes.some((s) => s.id === variant.id)) return {};
@@ -6336,8 +6343,13 @@ async function loadImageAsync(element, placeholder, fc, fabricRef, syncLockedRef
6336
6343
  });
6337
6344
  const imgLoadOptions = url.startsWith("data:") || url.startsWith("blob:") ? {} : { crossOrigin: "anonymous" };
6338
6345
  const img = await fabric.FabricImage.fromURL(url, imgLoadOptions);
6339
- if (!fabricRef.current) return;
6346
+ if (fabricRef && !fabricRef.current) return;
6340
6347
  await normalizeSvgImageDimensions(img, imageUrl, element.sourceFormat);
6348
+ const natW = img.width ?? 0;
6349
+ const natH = img.height ?? 0;
6350
+ if (natW <= 1 && natH <= 1 && Number(element.width) > 8 && Number(element.height) > 8) {
6351
+ throw new Error(`image decoded to ${natW}x${natH} — treating as failed load`);
6352
+ }
6341
6353
  const isHidden = !element.visible;
6342
6354
  img.set({
6343
6355
  originX: "left",
@@ -6492,8 +6504,14 @@ async function loadImageAsync(element, placeholder, fc, fabricRef, syncLockedRef
6492
6504
  shape,
6493
6505
  rx: rxRatio,
6494
6506
  // Pass ratio, not pixel value
6495
- stroke: element.stroke,
6496
- strokeWidth: element.strokeWidth,
6507
+ // Image outline is an ELEMENT property (imageBorder*), not the generic
6508
+ // shape stroke — this builder forwarded only stroke/strokeWidth, so
6509
+ // outline/gap never reached it.
6510
+ stroke: (element.imageBorderWidth ?? 0) > 0 ? element.imageBorderColor ?? "#FFFFFF" : element.stroke,
6511
+ strokeWidth: (element.imageBorderWidth ?? 0) > 0 ? element.imageBorderWidth : element.strokeWidth,
6512
+ borderPadding: element.imageBorderPadding ?? 0,
6513
+ borderPaddingColor: element.imageBorderPaddingColor ?? "transparent",
6514
+ borderPosition: element.imageBorderPosition ?? "outside",
6497
6515
  panX,
6498
6516
  panY,
6499
6517
  zoom
@@ -6529,9 +6547,111 @@ async function loadImageAsync(element, placeholder, fc, fabricRef, syncLockedRef
6529
6547
  proxiedUrl: getProxiedImageUrl(imageUrl).slice(0, 240),
6530
6548
  error: error instanceof Error ? error.message : String(error)
6531
6549
  });
6550
+ if (fc.__pixldocsEditMode === true) {
6551
+ try {
6552
+ markPlaceholderAsBrokenImage(placeholder);
6553
+ fc.requestRenderAll();
6554
+ } catch {
6555
+ }
6556
+ }
6532
6557
  return;
6533
6558
  }
6534
6559
  }
6560
+ function markPlaceholderAsBrokenImage(placeholder) {
6561
+ if (!(placeholder instanceof fabric.Group)) return;
6562
+ const group = placeholder;
6563
+ if (group.__pixldocsBrokenImage) return;
6564
+ group.__pixldocsBrokenImage = true;
6565
+ const frameChild = group.getObjects().find(
6566
+ (o) => o.__isPlaceholderFrame
6567
+ );
6568
+ const w = Math.max(1, (frameChild == null ? void 0 : frameChild.width) ?? group.width ?? 1);
6569
+ const h = Math.max(1, (frameChild == null ? void 0 : frameChild.height) ?? group.height ?? 1);
6570
+ const center = group.getCenterPoint();
6571
+ const cx = center.x;
6572
+ const cy = center.y;
6573
+ const prevPose = { left: group.left, top: group.top, width: group.width, height: group.height };
6574
+ const bg = new fabric.Rect({
6575
+ originX: "center",
6576
+ originY: "center",
6577
+ left: cx,
6578
+ top: cy,
6579
+ width: w,
6580
+ height: h,
6581
+ fill: "rgba(148, 163, 184, 0.12)",
6582
+ stroke: "#94A3B8",
6583
+ strokeWidth: 1.5,
6584
+ strokeDashArray: [6, 4]
6585
+ });
6586
+ const s = Math.max(16, Math.min(w, h) * 0.28);
6587
+ const glyphStroke = { stroke: "#64748B", strokeWidth: Math.max(1.5, s / 12), fill: "transparent" };
6588
+ const iconFrame = new fabric.Rect({
6589
+ originX: "center",
6590
+ originY: "center",
6591
+ left: cx,
6592
+ top: cy,
6593
+ width: s,
6594
+ height: s * 0.8,
6595
+ rx: s * 0.08,
6596
+ ry: s * 0.08,
6597
+ ...glyphStroke
6598
+ });
6599
+ const mountain = new fabric.Polyline(
6600
+ [
6601
+ { x: -s * 0.38, y: s * 0.24 },
6602
+ { x: -s * 0.1, y: -s * 0.08 },
6603
+ { x: s * 0.08, y: s * 0.1 },
6604
+ { x: s * 0.22, y: -s * 0.02 },
6605
+ { x: s * 0.38, y: s * 0.24 }
6606
+ ],
6607
+ { originX: "center", originY: "center", left: cx, top: cy + s * 0.08, ...glyphStroke }
6608
+ );
6609
+ const slash = new fabric.Line([cx - s * 0.55, cy - s * 0.5, cx + s * 0.55, cy + s * 0.5], {
6610
+ ...glyphStroke,
6611
+ originX: "center",
6612
+ originY: "center",
6613
+ left: cx,
6614
+ top: cy
6615
+ });
6616
+ for (const obj of [bg, iconFrame, mountain, slash]) {
6617
+ obj.set({ selectable: false, evented: false, excludeFromExport: true });
6618
+ obj.__pixldocsEditorChrome = true;
6619
+ group.add(obj);
6620
+ }
6621
+ group.set(prevPose);
6622
+ group.setCoords();
6623
+ group.dirty = true;
6624
+ }
6625
+ function hideEditorChromeForExport(fc) {
6626
+ const hidden = [];
6627
+ const walk = (objs) => {
6628
+ var _a2;
6629
+ for (const o of objs) {
6630
+ if (o.__pixldocsEditorChrome && o.visible !== false) {
6631
+ o.visible = false;
6632
+ hidden.push(o);
6633
+ }
6634
+ const kids = (_a2 = o.getObjects) == null ? void 0 : _a2.call(o);
6635
+ if (kids == null ? void 0 : kids.length) walk(kids);
6636
+ }
6637
+ };
6638
+ walk(fc.getObjects());
6639
+ if (hidden.length) {
6640
+ for (const o of hidden) {
6641
+ const g = o.group;
6642
+ if (g) g.dirty = true;
6643
+ }
6644
+ fc.renderAll();
6645
+ }
6646
+ return () => {
6647
+ for (const o of hidden) {
6648
+ o.visible = true;
6649
+ const g = o.group;
6650
+ if (g) g.dirty = true;
6651
+ }
6652
+ if (hidden.length) fc.requestRenderAll();
6653
+ };
6654
+ }
6535
6655
  const canvasImageLoader = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
6536
6656
  __proto__: null,
6537
6657
  SVG_DECODE_HARD_CAP,
@@ -6541,12 +6661,14 @@ const canvasImageLoader = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.d
6541
6661
  fetchSvgTextPublic,
6542
6662
  getNormalizedSvgUrl,
6543
6663
  getProxiedImageUrl,
6664
+ hideEditorChromeForExport,
6544
6665
  isBundledAssetUrl,
6545
6666
  isEmptyImagePlaceholderGroup,
6546
6667
  isPrivateUrl,
6547
6668
  isSvgImage,
6548
6669
  loadImageAsync,
6549
6670
  loadSvgDimensions,
6671
+ markPlaceholderAsBrokenImage,
6550
6672
  normalizeSvgImageDimensions,
6551
6673
  parseSvgDimensionsFromText,
6552
6674
  preloadImage,
@@ -7044,10 +7166,10 @@ function finalizeCropGroupCoords(g) {
7044
7166
  g.setCoords();
7045
7167
  }
7046
7168
  function updateCoverLayout(g) {
7047
- var _a2;
7169
+ var _a2, _b2;
7048
7170
  const ct = g.__cropData;
7049
7171
  if (!ct) return;
7050
- const { frameW, frameH, shape, rx: rxRatio, _img: img, _border: border } = ct;
7172
+ const { frameW, frameH, shape, rx: rxRatio, _img: img } = ct;
7051
7173
  const minDim = Math.min(frameW, frameH);
7052
7174
  let rx = rxRatio > 0.5 ? rxRatio : rxRatio * minDim;
7053
7175
  rx = Math.max(0, Math.min(rx, frameW / 2, frameH / 2));
@@ -7180,25 +7302,31 @@ function updateCoverLayout(g) {
7180
7302
  g.clipPath.excludeFromExport = true;
7181
7303
  }
7182
7304
  }
7305
+ const wantCircle = shape === "circle";
7306
+ const border = ensureRingShapeClass(g, ct, "_border", wantCircle);
7307
+ const mat = ensureRingShapeClass(g, ct, "_mat", wantCircle);
7183
7308
  const bw = border ? Number(border.strokeWidth) || 0 : 0;
7184
- const mat = ct._mat;
7185
7309
  const pad = Math.max(0, Number(ct.borderPadding) || 0);
7310
+ const outward = (ct.borderPosition ?? "outside") === "outside";
7186
7311
  const borderMinDim = Math.min(frameW, frameH);
7187
7312
  let borderRx = rxRatio > 0.5 ? rxRatio : rxRatio * borderMinDim;
7188
7313
  borderRx = Math.max(0, Math.min(borderRx, frameW / 2, frameH / 2));
7314
+ const geo = computeOutlineGeometry({ frameW, frameH, bw, pad, rx: borderRx, outward });
7189
7315
  if (border) {
7190
7316
  if (shape === "circle") {
7191
- border.set({ rx: (frameW - bw) / 2, ry: (frameH - bw) / 2 });
7317
+ border.set({ rx: geo.border.width / 2, ry: geo.border.height / 2 });
7192
7318
  } else {
7193
- border.set({ width: frameW - bw, height: frameH - bw, rx: borderRx, ry: borderRx });
7319
+ border.set({ width: geo.border.width, height: geo.border.height, rx: geo.border.rx, ry: geo.border.rx });
7194
7320
  }
7321
+ border.dirty = true;
7195
7322
  }
7196
7323
  if (mat) {
7197
7324
  if (shape === "circle") {
7198
- mat.set({ rx: (frameW - 2 * bw - pad) / 2, ry: (frameH - 2 * bw - pad) / 2 });
7325
+ mat.set({ rx: geo.mat.width / 2, ry: geo.mat.height / 2 });
7199
7326
  } else {
7200
- mat.set({ width: frameW - 2 * bw - pad, height: frameH - 2 * bw - pad, rx: Math.max(0, borderRx - bw), ry: Math.max(0, borderRx - bw) });
7327
+ mat.set({ width: geo.mat.width, height: geo.mat.height, rx: geo.mat.rx, ry: geo.mat.rx });
7201
7328
  }
7329
+ mat.dirty = true;
7202
7330
  }
7203
7331
  if (!img) {
7204
7332
  const placeholderFrame = ct._placeholderFrame ?? ((_a2 = g._objects) == null ? void 0 : _a2.find((obj) => obj.__isPlaceholderFrame));
@@ -7227,8 +7355,9 @@ function updateCoverLayout(g) {
7227
7355
  img._ct.panX = img.__panX ?? 0.5;
7228
7356
  img._ct.panY = img.__panY ?? 0.5;
7229
7357
  }
7230
- const iw = img.width || 1;
7231
- const ih = img.height || 1;
7358
+ const srcEl = img._originalElement ?? ((_b2 = img.getElement) == null ? void 0 : _b2.call(img));
7359
+ const iw = Number(srcEl == null ? void 0 : srcEl.naturalWidth) || Number(srcEl == null ? void 0 : srcEl.width) || img.width || 1;
7360
+ const ih = Number(srcEl == null ? void 0 : srcEl.naturalHeight) || Number(srcEl == null ? void 0 : srcEl.height) || img.height || 1;
7232
7361
  const fitContain = ct.fit === "contain";
7233
7362
  const baseScale = fitContain ? Math.min(frameW / iw, frameH / ih) : Math.max(frameW / iw, frameH / ih);
7234
7363
  const zoom = fitContain ? Math.max(1, Math.min(3, ct.containScale ?? 1)) : Math.max(1, img._ct.zoom ?? 1);
@@ -7265,6 +7394,42 @@ function updateCoverLayout(g) {
7265
7394
  offsetY = overflowY > 0 ? -overflowY * (panY - 0.5) : 0;
7266
7395
  }
7267
7396
  img.set({ left: offsetX, top: offsetY });
7397
+ const wantsOutline = bw > 0 || pad > 0;
7398
+ const shapedMask = shape === "circle" || shape === "roundRect";
7399
+ if (wantsOutline && shapedMask) {
7400
+ if (g.clipPath) {
7401
+ img.clipPath = g.clipPath;
7402
+ g.clipPath = void 0;
7403
+ }
7404
+ if (img.cropX || img.cropY || img.width !== iw) {
7405
+ img.set({ width: iw, height: ih, cropX: 0, cropY: 0 });
7406
+ }
7407
+ } else if (wantsOutline && !fitContain) {
7408
+ const regionW = Math.min(iw, frameW / finalScale);
7409
+ const regionH = Math.min(ih, frameH / finalScale);
7410
+ img.set({
7411
+ width: regionW,
7412
+ height: regionH,
7413
+ cropX: (iw - regionW) * panX,
7414
+ cropY: (ih - regionH) * panY,
7415
+ left: 0,
7416
+ top: 0
7417
+ });
7418
+ img.clipPath = void 0;
7419
+ g.clipPath = void 0;
7420
+ } else {
7421
+ if (img.clipPath) img.clipPath = void 0;
7422
+ if (img.cropX || img.cropY || img.width !== iw) {
7423
+ img.set({ width: iw, height: ih, cropX: 0, cropY: 0 });
7424
+ }
7425
+ }
7426
+ const imgClip = img.clipPath;
7427
+ if (imgClip && typeof imgClip.set === "function") {
7428
+ const csx = img.scaleX || 1;
7429
+ const csy = img.scaleY || 1;
7430
+ imgClip.set({ left: -offsetX / csx, top: -offsetY / csy, scaleX: 1 / csx, scaleY: 1 / csy });
7431
+ imgClip.dirty = true;
7432
+ }
7268
7433
  g.dirty = true;
7269
7434
  img.dirty = true;
7270
7435
  if (g.clipPath) {
@@ -7615,6 +7780,51 @@ function installCanvaMaskControls(g) {
7615
7780
  g.set(controlStyle);
7616
7781
  g.setCoords();
7617
7782
  }
7783
+ function ensureRingShapeClass(g, ct, key, wantCircle) {
7784
+ const cur = ct[key];
7785
+ if (!cur) return void 0;
7786
+ const isEllipse = cur instanceof fabric.Ellipse;
7787
+ if (isEllipse === wantCircle) return cur;
7788
+ const common = {
7789
+ left: 0,
7790
+ top: 0,
7791
+ originX: "center",
7792
+ originY: "center",
7793
+ fill: "transparent",
7794
+ stroke: cur.stroke,
7795
+ strokeWidth: cur.strokeWidth,
7796
+ selectable: false,
7797
+ evented: false
7798
+ };
7799
+ const next = wantCircle ? new fabric.Ellipse({ ...common, rx: 1, ry: 1, objectCaching: false }) : new fabric.Rect({ ...common, width: 1, height: 1, objectCaching: false });
7800
+ const objs = g._objects;
7801
+ const idx = objs ? objs.indexOf(cur) : -1;
7802
+ if (objs && idx >= 0) objs.splice(idx, 1, next);
7803
+ else return cur;
7804
+ next.group = cur.group ?? g;
7805
+ next.canvas = cur.canvas ?? g.canvas;
7806
+ cur.group = void 0;
7807
+ next.setCoords();
7808
+ ct[key] = next;
7809
+ g.dirty = true;
7810
+ return next;
7811
+ }
7812
+ function computeOutlineGeometry(opts) {
7813
+ const { frameW, frameH, bw, pad, rx, outward } = opts;
7814
+ if (outward) {
7815
+ const grow = (r, by) => r > 0 ? r + by : 0;
7816
+ return {
7817
+ // Ring centre-line is pad + bw/2 past the frame → inner edge exactly `pad` out.
7818
+ border: { width: frameW + 2 * pad + bw, height: frameH + 2 * pad + bw, rx: grow(rx, pad + bw / 2) },
7819
+ // The gap band is transparent; it exists only to keep the child list stable.
7820
+ mat: { width: frameW + pad, height: frameH + pad, rx: grow(rx, pad / 2) }
7821
+ };
7822
+ }
7823
+ return {
7824
+ border: { width: frameW - bw, height: frameH - bw, rx },
7825
+ mat: { width: frameW - 2 * bw - pad, height: frameH - 2 * bw - pad, rx: Math.max(0, rx - bw) }
7826
+ };
7827
+ }
7618
7828
  async function createMaskedImageElement({
7619
7829
  url,
7620
7830
  image,
@@ -7643,7 +7853,8 @@ async function createMaskedImageElement({
7643
7853
  containScale = 1,
7644
7854
  shadow = null,
7645
7855
  borderPadding = 0,
7646
- borderPaddingColor = "#FFFFFF"
7856
+ borderPaddingColor = "transparent",
7857
+ borderPosition = "outside"
7647
7858
  }) {
7648
7859
  const img = image || (url ? await fabric.FabricImage.fromURL(getProxiedImageUrl(url), { crossOrigin: "anonymous" }) : null);
7649
7860
  if (!img) {
@@ -7668,36 +7879,47 @@ async function createMaskedImageElement({
7668
7879
  });
7669
7880
  const bw = strokeWidth || 0;
7670
7881
  const pad = Math.max(0, borderPadding || 0);
7882
+ const outward = borderPosition === "outside";
7883
+ const geo = computeOutlineGeometry({ frameW, frameH, bw, pad, rx, outward });
7884
+ const matStroke = outward ? borderPaddingColor || "transparent" : borderPaddingColor;
7885
+ const matW = geo.mat.width;
7886
+ const matH = geo.mat.height;
7887
+ const matRx = geo.mat.rx;
7671
7888
  const mat = shape === "circle" ? new fabric.Ellipse({
7672
- rx: (frameW - 2 * bw - pad) / 2,
7673
- ry: (frameH - 2 * bw - pad) / 2,
7889
+ rx: matW / 2,
7890
+ ry: matH / 2,
7674
7891
  left: 0,
7675
7892
  top: 0,
7676
7893
  originX: "center",
7677
7894
  originY: "center",
7678
7895
  fill: "transparent",
7679
- stroke: borderPaddingColor,
7896
+ stroke: matStroke,
7680
7897
  strokeWidth: pad,
7681
7898
  selectable: false,
7682
- evented: false
7899
+ evented: false,
7900
+ objectCaching: false
7683
7901
  }) : new fabric.Rect({
7684
- width: frameW - 2 * bw - pad,
7685
- height: frameH - 2 * bw - pad,
7686
- rx: Math.max(0, rx - bw),
7687
- ry: Math.max(0, rx - bw),
7902
+ width: matW,
7903
+ height: matH,
7904
+ rx: matRx,
7905
+ ry: matRx,
7688
7906
  left: 0,
7689
7907
  top: 0,
7690
7908
  originX: "center",
7691
7909
  originY: "center",
7692
7910
  fill: "transparent",
7693
- stroke: borderPaddingColor,
7911
+ stroke: matStroke,
7694
7912
  strokeWidth: pad,
7695
7913
  selectable: false,
7696
- evented: false
7914
+ evented: false,
7915
+ objectCaching: false
7697
7916
  });
7917
+ const bordW = geo.border.width;
7918
+ const bordH = geo.border.height;
7919
+ const bordRx = geo.border.rx;
7698
7920
  const border = shape === "circle" ? new fabric.Ellipse({
7699
- rx: (frameW - bw) / 2,
7700
- ry: (frameH - bw) / 2,
7921
+ rx: bordW / 2,
7922
+ ry: bordH / 2,
7701
7923
  left: 0,
7702
7924
  top: 0,
7703
7925
  originX: "center",
@@ -7706,12 +7928,13 @@ async function createMaskedImageElement({
7706
7928
  stroke: stroke || "transparent",
7707
7929
  strokeWidth: bw,
7708
7930
  selectable: false,
7709
- evented: false
7931
+ evented: false,
7932
+ objectCaching: false
7710
7933
  }) : new fabric.Rect({
7711
- width: frameW - bw,
7712
- height: frameH - bw,
7713
- rx,
7714
- ry: rx,
7934
+ width: bordW,
7935
+ height: bordH,
7936
+ rx: bordRx,
7937
+ ry: bordRx,
7715
7938
  left: 0,
7716
7939
  top: 0,
7717
7940
  originX: "center",
@@ -7720,7 +7943,8 @@ async function createMaskedImageElement({
7720
7943
  stroke: stroke || "transparent",
7721
7944
  strokeWidth: bw,
7722
7945
  selectable: false,
7723
- evented: false
7946
+ evented: false,
7947
+ objectCaching: false
7724
7948
  });
7725
7949
  img.set({
7726
7950
  left: 0,
@@ -7814,7 +8038,9 @@ async function createMaskedImageElement({
7814
8038
  _border: border,
7815
8039
  _mat: mat,
7816
8040
  borderPadding: pad,
7817
- borderPaddingColor
8041
+ borderPaddingColor,
8042
+ // Read back by refreshMaskedImage so it recomputes with the same geometry.
8043
+ borderPosition
7818
8044
  };
7819
8045
  g._ct = g._ct || {};
7820
8046
  g._ct.isCropGroup = true;
@@ -7848,7 +8074,12 @@ async function createMaskedImageElement({
7848
8074
  });
7849
8075
  clip.absolutePositioned = false;
7850
8076
  clip.excludeFromExport = true;
7851
- g.clipPath = clip;
8077
+ if (outward) {
8078
+ img.clipPath = clip;
8079
+ g.clipPath = void 0;
8080
+ } else {
8081
+ g.clipPath = clip;
8082
+ }
7852
8083
  g.set({
7853
8084
  selectable: true,
7854
8085
  evented: true,
@@ -17949,6 +18180,7 @@ const PageCanvas = forwardRef(
17949
18180
  // Transparent so underlay (page bg + group bgs) shows through
17950
18181
  backgroundColor: "transparent"
17951
18182
  });
18183
+ fabricCanvas.__pixldocsEditMode = isEditorMode && !isPreviewMode;
17952
18184
  fabricCanvas.hoverCursor = "default";
17953
18185
  fabricCanvas.moveCursor = "move";
17954
18186
  const suppressTextEditForClick = (textbox) => {
@@ -23938,11 +24170,33 @@ const PageCanvas = forwardRef(
23938
24170
  const wantBW = Math.max(0, Number(element.imageBorderWidth) || 0);
23939
24171
  const wantBC = element.imageBorderColor ?? "#FFFFFF";
23940
24172
  const wantPad = Math.max(0, Number(element.imageBorderPadding) || 0);
23941
- const wantPC = element.imageBorderPaddingColor ?? "#FFFFFF";
24173
+ const wantPC = element.imageBorderPaddingColor ?? "transparent";
24174
+ const wantPos = element.imageBorderPosition ?? "outside";
23942
24175
  if (ct._border) ct._border.set({ stroke: wantBW > 0 ? wantBC : "transparent", strokeWidth: wantBW, dirty: true });
23943
24176
  if (ct._mat) ct._mat.set({ stroke: wantPC, strokeWidth: wantPad, dirty: true });
23944
24177
  ct.borderPadding = wantPad;
23945
24178
  ct.borderPaddingColor = wantPC;
24179
+ ct.borderPosition = wantPos;
24180
+ {
24181
+ const gMinDim = Math.min(ct.frameW, ct.frameH);
24182
+ const rxRaw = ct.rx || 0;
24183
+ const gRx = Math.max(0, Math.min(rxRaw > 0.5 ? rxRaw : rxRaw * gMinDim, ct.frameW / 2, ct.frameH / 2));
24184
+ const gGeo = computeOutlineGeometry({
24185
+ frameW: ct.frameW,
24186
+ frameH: ct.frameH,
24187
+ bw: wantBW,
24188
+ pad: wantPad,
24189
+ rx: gRx,
24190
+ outward: wantPos === "outside"
24191
+ });
24192
+ const isCircle = (ct.shape || "rect") === "circle";
24193
+ if (ct._border) {
24194
+ ct._border.set(isCircle ? { rx: gGeo.border.width / 2, ry: gGeo.border.height / 2 } : { width: gGeo.border.width, height: gGeo.border.height, rx: gGeo.border.rx, ry: gGeo.border.rx });
24195
+ }
24196
+ if (ct._mat) {
24197
+ ct._mat.set(isCircle ? { rx: gGeo.mat.width / 2, ry: gGeo.mat.height / 2 } : { width: gGeo.mat.width, height: gGeo.mat.height, rx: gGeo.mat.rx, ry: gGeo.mat.rx });
24198
+ }
24199
+ }
23946
24200
  const sBlur = Math.max(0, Number(element.imageShadowBlur) || 0);
23947
24201
  const sColor = element.imageShadowColor;
23948
24202
  if (sBlur > 0 || sColor) {
@@ -25252,6 +25506,7 @@ const PageCanvas = forwardRef(
25252
25506
  } else if (obj instanceof fabric.Textbox) {
25253
25507
  const overflowPolicy = element.overflowPolicy || "grow-and-push";
25254
25508
  let text = element.text != null && element.text !== "" ? element.text : " ";
25509
+ text = applyElementTextCase(text, element.textCase);
25255
25510
  let parsedStyles = null;
25256
25511
  if (element.formattingEnabled === true) {
25257
25512
  const parsed = parseTextMarkdown(text);
@@ -25896,6 +26151,13 @@ const PageCanvas = forwardRef(
25896
26151
  const img = await fabric.FabricImage.fromURL(url, { crossOrigin: "anonymous" });
25897
26152
  if (!fabricRef.current || !isLatestRequest()) return;
25898
26153
  await normalizeSvgImageDimensions(img, imageUrl, element.sourceFormat, svgDecodeTarget);
26154
+ {
26155
+ const natW = img.width ?? 0;
26156
+ const natH = img.height ?? 0;
26157
+ if (natW <= 1 && natH <= 1 && Number(element.width) > 8 && Number(element.height) > 8) {
26158
+ throw new Error(`image decoded to ${natW}x${natH} — treating as failed load`);
26159
+ }
26160
+ }
25899
26161
  if (!isLatestRequest()) return;
25900
26162
  const imageFitForFade = element.imageFit || ((_a2 = element.style) == null ? void 0 : _a2.imageFit) || "cover";
25901
26163
  const clipShapeForFade = element.clipShape ?? ((_b2 = element.style) == null ? void 0 : _b2.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
@@ -26103,7 +26365,9 @@ const PageCanvas = forwardRef(
26103
26365
  strokeWidth: element.imageBorderWidth ?? 0,
26104
26366
  // Gap between the image and the outline (the "matted photo" look).
26105
26367
  borderPadding: element.imageBorderPadding ?? 0,
26106
- borderPaddingColor: element.imageBorderPaddingColor ?? "#FFFFFF",
26368
+ // Gap is empty space by default, not a white mat.
26369
+ borderPaddingColor: element.imageBorderPaddingColor ?? "transparent",
26370
+ borderPosition: element.imageBorderPosition ?? "outside",
26107
26371
  // Optional image drop shadow (element property).
26108
26372
  shadow: (element.imageShadowBlur ?? 0) > 0 || element.imageShadowColor ? {
26109
26373
  color: element.imageShadowColor ?? "rgba(0,0,0,0.25)",
@@ -26260,6 +26524,19 @@ const PageCanvas = forwardRef(
26260
26524
  }
26261
26525
  fc.requestRenderAll();
26262
26526
  } catch (error) {
26527
+ console.error("[PageCanvas] image failed to load", {
26528
+ id: element.id,
26529
+ url: (element.src || element.imageUrl || "").slice(0, 200),
26530
+ error: error instanceof Error ? error.message : String(error)
26531
+ });
26532
+ const fcNow = fabricRef.current;
26533
+ if (fcNow && fcNow.__pixldocsEditMode === true) {
26534
+ try {
26535
+ markPlaceholderAsBrokenImage(placeholder);
26536
+ fcNow.requestRenderAll();
26537
+ } catch {
26538
+ }
26539
+ }
26263
26540
  }
26264
26541
  };
26265
26542
  const handleCanvasClick = useCallback(
@@ -28865,7 +29142,9 @@ function expandBoundTables(pages, formValues) {
28865
29142
  const t = node;
28866
29143
  if (t.type === "table" && ((_a2 = t.tableData) == null ? void 0 : _a2.repeat)) {
28867
29144
  const entries = tableEntriesFor(t.tableData.repeat.from, formValues);
28868
- t.tableData = expandTableRepeat(t.tableData, entries);
29145
+ if (entries.length > 0) {
29146
+ t.tableData = expandTableRepeat(t.tableData, entries);
29147
+ }
28869
29148
  for (const row of t.tableData.cells) {
28870
29149
  for (const cell of row) {
28871
29150
  if (!cell.field) continue;
@@ -30020,7 +30299,12 @@ function pagesHaveConstantTokens(pages) {
30020
30299
  return pages.some((p) => walk(p.children));
30021
30300
  }
30022
30301
  function substituteConstantTokens(pages, constants) {
30023
- const sub = (s) => s.replace(/\{\{\s*\$([a-zA-Z0-9_]+)\s*\}\}/g, (m, name) => Object.prototype.hasOwnProperty.call(constants, name) ? String(constants[name]) : m);
30302
+ const sub = (s) => s.replace(/\{\{\s*\$([a-zA-Z0-9_]+)\s*\}\}/g, (m, name) => (
30303
+ // Unresolved tokens are ERASED rather than left as literal braces: on a
30304
+ // money cell a missing symbol still reads as a number, whereas
30305
+ // "{{$currency}}9,500.00" is visibly broken output.
30306
+ Object.prototype.hasOwnProperty.call(constants, name) ? String(constants[name]) : ""
30307
+ ));
30024
30308
  const walk = (nodes) => {
30025
30309
  for (const n of nodes ?? []) {
30026
30310
  if (n.type === "text") {
@@ -30051,10 +30335,9 @@ function applyContentBoundsPagination(config) {
30051
30335
  }
30052
30336
  let mutated = continuationMasters.length > 0;
30053
30337
  const constants = config.constants;
30054
- const hasConstants = !!constants && typeof constants === "object" && Object.keys(constants).length > 0;
30055
- if (hasConstants && pagesHaveConstantTokens(pages)) {
30338
+ if (pagesHaveConstantTokens(pages)) {
30056
30339
  pages = pages.map((p) => JSON.parse(JSON.stringify(p)));
30057
- substituteConstantTokens(pages, constants);
30340
+ substituteConstantTokens(pages, constants && typeof constants === "object" ? constants : {});
30058
30341
  mutated = true;
30059
30342
  }
30060
30343
  let resultPages = [];
@@ -32224,8 +32507,9 @@ async function resolveTemplateData(options) {
32224
32507
  void 0,
32225
32508
  repeatablePagesInput.length > 0 ? repeatablePagesInput : void 0
32226
32509
  );
32510
+ const paginated = applyContentBoundsPagination(resolvedConfig);
32227
32511
  return {
32228
- config: applyThemeIfNeeded(resolvedConfig),
32512
+ config: applyThemeIfNeeded(paginated),
32229
32513
  templateName: template.name || "Untitled",
32230
32514
  templateId,
32231
32515
  price: template.price ?? 0
@@ -33866,6 +34150,43 @@ function stampPixldocsImageIdOnSvg(svg, id) {
33866
34150
  const attr = ` data-pixldocs-image-id="${escapeSvgDataAttr(id)}"`;
33867
34151
  return svg.replace(/<image\b/i, `<image${attr}`);
33868
34152
  }
34153
+ function colorHasAlpha(color) {
34154
+ if (typeof color !== "string") return false;
34155
+ const c = color.trim().toLowerCase();
34156
+ if (c === "transparent") return true;
34157
+ const rgba = c.match(/^rgba?\(([^)]+)\)$/);
34158
+ if (rgba) {
34159
+ const parts = rgba[1].split(",").map((x) => x.trim());
34160
+ return parts.length > 3 && Number(parts[3]) < 1;
34161
+ }
34162
+ if (/^#[0-9a-f]{8}$/.test(c)) return parseInt(c.slice(7, 9), 16) < 255;
34163
+ if (/^#[0-9a-f]{4}$/.test(c)) return parseInt(c[4] + c[4], 16) < 255;
34164
+ return false;
34165
+ }
34166
+ function paintIsAlphaGradient(paint) {
34167
+ const stops = paint == null ? void 0 : paint.colorStops;
34168
+ if (!Array.isArray(stops) || !stops.length) return false;
34169
+ return stops.some((st) => colorHasAlpha(st == null ? void 0 : st.color) || typeof (st == null ? void 0 : st.opacity) === "number" && st.opacity < 1);
34170
+ }
34171
+ function objectNeedsAlphaGradientRaster(obj) {
34172
+ if (!obj) return false;
34173
+ return paintIsAlphaGradient(obj.fill) || paintIsAlphaGradient(obj.stroke);
34174
+ }
34175
+ function rasterizeObjectToSvgImage(obj, multiplier) {
34176
+ try {
34177
+ if (typeof obj.toDataURL !== "function") return null;
34178
+ const rect = obj.getBoundingRect();
34179
+ if (!rect || !(rect.width > 0) || !(rect.height > 0)) return null;
34180
+ const url = obj.toDataURL({ multiplier, enableRetinaScaling: false });
34181
+ if (typeof url !== "string" || !url.startsWith("data:image")) return null;
34182
+ const n = (v) => Number(v.toFixed(4));
34183
+ return `<image x="${n(rect.left)}" y="${n(rect.top)}" width="${n(rect.width)}" height="${n(rect.height)}" preserveAspectRatio="none" xlink:href="${url}"></image>`;
34184
+ } catch (e) {
34185
+ console.warn("[canvas-svg-capture][alphaGradient] raster failed:", e);
34186
+ return null;
34187
+ }
34188
+ }
34189
+ const ALPHA_GRADIENT_RASTER_MULTIPLIER = 3;
33869
34190
  function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight) {
33870
34191
  const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
33871
34192
  const prevSvgVPT = fabricInstance.svgViewportTransformation;
@@ -33886,10 +34207,23 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
33886
34207
  try {
33887
34208
  const visit = (obj) => {
33888
34209
  if (!obj) return;
34210
+ if (obj.__pixldocsEditorChrome) {
34211
+ const originalToSVG = typeof obj.toSVG === "function" ? obj.toSVG.bind(obj) : null;
34212
+ if (originalToSVG) {
34213
+ obj.toSVG = () => "";
34214
+ svgPatchRecords.push({ obj, originalToSVG });
34215
+ }
34216
+ return;
34217
+ }
33889
34218
  const imageId = typeof obj.__docuforgeId === "string" && hasRenderableRasterCandidate(obj) ? obj.__docuforgeId : "";
33890
- if ((isTextboxLike(obj) || imageId) && typeof obj.toSVG === "function") {
34219
+ const alphaGradient = objectNeedsAlphaGradientRaster(obj);
34220
+ if ((isTextboxLike(obj) || imageId || alphaGradient) && typeof obj.toSVG === "function") {
33891
34221
  const originalToSVG = obj.toSVG.bind(obj);
33892
34222
  obj.toSVG = (reviver) => {
34223
+ if (alphaGradient) {
34224
+ const raster = rasterizeObjectToSvgImage(obj, ALPHA_GRADIENT_RASTER_MULTIPLIER);
34225
+ if (raster) return raster;
34226
+ }
33893
34227
  let svg = originalToSVG(reviver);
33894
34228
  if (isTextboxLike(obj)) svg = stampFabricLineMetricsOnTextSvg(svg, obj);
33895
34229
  if (isTextboxLike(obj)) svg = warpTextboxSvgAlongPath(svg, obj);
@@ -33988,9 +34322,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
33988
34322
  }
33989
34323
  return svgString;
33990
34324
  }
33991
- const resolvedPackageVersion = "0.5.495";
34325
+ const resolvedPackageVersion = "0.5.497";
33992
34326
  const PACKAGE_VERSION = resolvedPackageVersion;
33993
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.495";
34327
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.497";
33994
34328
  const roundParityValue = (value) => {
33995
34329
  if (typeof value !== "number") return value;
33996
34330
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -34429,6 +34763,11 @@ class PixldocsRenderer {
34429
34763
  * Mounts a hidden PreviewCanvas component and captures the Fabric canvas output.
34430
34764
  */
34431
34765
  async render(templateConfig, options = {}) {
34766
+ if (options.watermark === true && !templateConfig.__pixldocsWatermarked) {
34767
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34768
+ templateConfig = injectWatermark2(templateConfig, options.watermarkOptions);
34769
+ templateConfig.__pixldocsWatermarked = true;
34770
+ }
34432
34771
  const pageIndex = options.pageIndex ?? 0;
34433
34772
  const format = options.format ?? "png";
34434
34773
  const quality = options.quality ?? 0.92;
@@ -34444,6 +34783,7 @@ class PixldocsRenderer {
34444
34783
  const hasAutoShrink = configHasAutoShrinkText(templateConfig);
34445
34784
  const defaultWait = hasAutoShrink ? 4e3 : 1800;
34446
34785
  await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
34786
+ this.flushTextMeasurementCaches();
34447
34787
  }
34448
34788
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
34449
34789
  setPackageApiUrl2(this.config.imageProxyUrl);
@@ -34471,6 +34811,7 @@ class PixldocsRenderer {
34471
34811
  const hasAutoShrink = configHasAutoShrinkText(templateConfig);
34472
34812
  const defaultWait = hasAutoShrink ? 4e3 : 1800;
34473
34813
  await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
34814
+ this.flushTextMeasurementCaches();
34474
34815
  }
34475
34816
  const results = [];
34476
34817
  for (let i = 0; i < templateConfig.pages.length; i++) {
@@ -34497,8 +34838,8 @@ class PixldocsRenderer {
34497
34838
  const shouldWatermark = watermark ?? resolved.price > 0;
34498
34839
  let configToRender = resolved.config;
34499
34840
  if (shouldWatermark) {
34500
- const { injectWatermark } = await import("./canvasWatermark-pkhacGge.js");
34501
- configToRender = injectWatermark(configToRender, watermarkOptions);
34841
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34842
+ configToRender = injectWatermark2(configToRender, watermarkOptions);
34502
34843
  const { injectPreviewBlur: injectPreviewBlur2 } = await Promise.resolve().then(() => previewBlur);
34503
34844
  configToRender = injectPreviewBlur2(configToRender);
34504
34845
  }
@@ -34557,8 +34898,8 @@ class PixldocsRenderer {
34557
34898
  const shouldWatermark = watermark ?? resolved.price > 0;
34558
34899
  let configToRender = resolved.config;
34559
34900
  if (shouldWatermark) {
34560
- const { injectWatermark } = await import("./canvasWatermark-pkhacGge.js");
34561
- configToRender = injectWatermark(configToRender, watermarkOptions);
34901
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34902
+ configToRender = injectWatermark2(configToRender, watermarkOptions);
34562
34903
  const { injectPreviewBlur: injectPreviewBlur2 } = await Promise.resolve().then(() => previewBlur);
34563
34904
  configToRender = injectPreviewBlur2(configToRender);
34564
34905
  }
@@ -34578,6 +34919,10 @@ class PixldocsRenderer {
34578
34919
  * exporter, which is what `renderPdfViaClientExport` does below.
34579
34920
  */
34580
34921
  async renderPdf(templateConfig, options) {
34922
+ if ((options == null ? void 0 : options.watermark) === true) {
34923
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34924
+ templateConfig = injectWatermark2(templateConfig, options.watermarkOptions);
34925
+ }
34581
34926
  return this.renderPdfViaClientExport(templateConfig, {
34582
34927
  title: options == null ? void 0 : options.title,
34583
34928
  textMode: options == null ? void 0 : options.textMode,
@@ -34606,8 +34951,8 @@ class PixldocsRenderer {
34606
34951
  const shouldWatermark = watermark ?? resolved.price > 0;
34607
34952
  let configToRender = resolved.config;
34608
34953
  if (shouldWatermark) {
34609
- const { injectWatermark } = await import("./canvasWatermark-pkhacGge.js");
34610
- configToRender = injectWatermark(configToRender, watermarkOptions);
34954
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34955
+ configToRender = injectWatermark2(configToRender, watermarkOptions);
34611
34956
  const { injectPreviewBlur: injectPreviewBlur2 } = await Promise.resolve().then(() => previewBlur);
34612
34957
  configToRender = injectPreviewBlur2(configToRender);
34613
34958
  }
@@ -34804,7 +35149,7 @@ class PixldocsRenderer {
34804
35149
  await this.waitForCanvasScene(container, cloned, i);
34805
35150
  }
34806
35151
  console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
34807
- const { exportMultiPagePdf, preparePagesForExport } = await import("./vectorPdfExport-B6m8o09g.js");
35152
+ const { exportMultiPagePdf, preparePagesForExport } = await import("./vectorPdfExport-DV-TJwjP.js");
34808
35153
  const prepared = preparePagesForExport(
34809
35154
  cloned.pages,
34810
35155
  canvasWidth,
@@ -35082,6 +35427,37 @@ class PixldocsRenderer {
35082
35427
  new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
35083
35428
  ]);
35084
35429
  }
35430
+ /**
35431
+ * Flush BOTH process-global text-measurement caches after fonts are ready
35432
+ * and BEFORE the capture canvas mounts.
35433
+ *
35434
+ * Why: anything that measured text before the webfonts finished loading —
35435
+ * most notably `applyContentBoundsPagination` running inside
35436
+ * `resolveFromForm` on a cold page (the EC2/MCP flow: resolve + render in
35437
+ * one browser context) — populates the app measurement cache and Fabric's
35438
+ * global char-width cache with FALLBACK metrics. `createText` then places
35439
+ * center/right-aligned lines from those stale widths, so a right-aligned
35440
+ * grid cell on a continuation page drew shifted left by
35441
+ * (fallbackWidth − realWidth) px vs the editor (caught by
35442
+ * tests/parity `auto-paginate`, a 9px shift). The later
35443
+ * `waitForStableTextMetrics` pass re-primes glyph bounds but never moves
35444
+ * the already-positioned box, so the flush must happen pre-mount.
35445
+ *
35446
+ * Only called from the headless PNG paths (`render`/`renderAllPages`),
35447
+ * which already default to clearing the global char cache in
35448
+ * `waitForStableTextMetrics` — the live-preview-adjacent PDF path keeps
35449
+ * its `clearGlobalCharCache: false` behaviour untouched.
35450
+ */
35451
+ flushTextMeasurementCaches() {
35452
+ try {
35453
+ clearMeasurementCache();
35454
+ } catch {
35455
+ }
35456
+ try {
35457
+ clearFabricCharCache();
35458
+ } catch {
35459
+ }
35460
+ }
35085
35461
  getNormalizedGradientStops(gradient) {
35086
35462
  const stops = Array.isArray(gradient == null ? void 0 : gradient.stops) ? gradient.stops.map((stop) => ({
35087
35463
  offset: Math.max(0, Math.min(1, Number((stop == null ? void 0 : stop.offset) ?? 0))),
@@ -37124,7 +37500,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
37124
37500
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
37125
37501
  sanitizeSvgTreeForPdf(svgToDraw);
37126
37502
  try {
37127
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-B6m8o09g.js");
37503
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-DV-TJwjP.js");
37128
37504
  try {
37129
37505
  await logTextMeasurementDiagnostic(svgToDraw);
37130
37506
  } catch {
@@ -37372,13 +37748,136 @@ async function getPublishedTemplate(options) {
37372
37748
  const rows = await res.json();
37373
37749
  return rows[0] ?? null;
37374
37750
  }
37751
+ const WATERMARK_ID_PREFIX = "__wm_";
37752
+ const DEFAULT_WATERMARK_OPACITY = 0.14;
37753
+ const DEFAULT_WATERMARK_FONT_SIZE = 18;
37754
+ const DEFAULT_WATERMARK_ANGLE = -30;
37755
+ function getWatermarkText() {
37756
+ try {
37757
+ const host = window.location.hostname;
37758
+ if (host.includes("biomaker")) return "biomaker.app";
37759
+ } catch {
37760
+ }
37761
+ return "pixldocs.com";
37762
+ }
37763
+ function getLuminance(color) {
37764
+ if (!color || color === "transparent" || color === "none") return 1;
37765
+ let r = 255, g = 255, b = 255;
37766
+ if (color.startsWith("#")) {
37767
+ const hex = color.slice(1);
37768
+ const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
37769
+ const n = parseInt(full, 16);
37770
+ r = n >> 16 & 255;
37771
+ g = n >> 8 & 255;
37772
+ b = n & 255;
37773
+ } else {
37774
+ const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
37775
+ if (m) {
37776
+ r = +m[1];
37777
+ g = +m[2];
37778
+ b = +m[3];
37779
+ }
37780
+ }
37781
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
37782
+ }
37783
+ function getWatermarkColor(bgColor) {
37784
+ const lum = getLuminance(bgColor);
37785
+ return lum < 0.45 ? "#cccccc" : "#808080";
37786
+ }
37787
+ function getPageBgColor(page, canvasBg) {
37788
+ var _a2, _b2;
37789
+ const settingsBg = (_a2 = page.settings) == null ? void 0 : _a2.backgroundColor;
37790
+ if (settingsBg && settingsBg !== "transparent" && settingsBg !== "none") {
37791
+ return settingsBg;
37792
+ }
37793
+ const children = page.children || [];
37794
+ for (const child of children) {
37795
+ if (child.type === "shape" && child.fill && child.fill !== "transparent" && child.fill !== "none") {
37796
+ return child.fill;
37797
+ }
37798
+ if ((_b2 = child.children) == null ? void 0 : _b2.length) {
37799
+ for (const gc of child.children) {
37800
+ if (gc.type === "shape" && gc.fill && gc.fill !== "transparent" && gc.fill !== "none") {
37801
+ return gc.fill;
37802
+ }
37803
+ }
37804
+ }
37805
+ }
37806
+ return canvasBg;
37807
+ }
37808
+ function generateWatermarkElements(canvasWidth, canvasHeight, wmColor, options = {}) {
37809
+ const elements = [];
37810
+ const text = options.text ?? getWatermarkText();
37811
+ const fontSize = options.fontSize ?? DEFAULT_WATERMARK_FONT_SIZE;
37812
+ const angle = options.angle ?? DEFAULT_WATERMARK_ANGLE;
37813
+ const opacity = options.opacity ?? DEFAULT_WATERMARK_OPACITY;
37814
+ const estimatedTextWidth = text.length * fontSize * 0.55;
37815
+ const spacingX = Math.max(160, estimatedTextWidth + 30);
37816
+ const spacingY = 90;
37817
+ const startX = -canvasWidth * 0.3;
37818
+ const startY = -canvasHeight * 0.3;
37819
+ const endX = canvasWidth * 1.5;
37820
+ const endY = canvasHeight * 1.5;
37821
+ let idx = 0;
37822
+ for (let y = startY; y < endY; y += spacingY) {
37823
+ for (let x = startX; x < endX; x += spacingX) {
37824
+ elements.push({
37825
+ id: `${WATERMARK_ID_PREFIX}${idx++}`,
37826
+ type: "text",
37827
+ left: x,
37828
+ top: y,
37829
+ width: 200,
37830
+ height: 40,
37831
+ text,
37832
+ fontFamily: "Montserrat",
37833
+ fontSize,
37834
+ fontWeight: "600",
37835
+ fill: wmColor,
37836
+ angle,
37837
+ selectable: false,
37838
+ locked: true,
37839
+ visible: true,
37840
+ opacity,
37841
+ scaleX: 1,
37842
+ scaleY: 1,
37843
+ letterSpacing: 4
37844
+ });
37845
+ }
37846
+ }
37847
+ return elements;
37848
+ }
37849
+ function injectWatermark(config, options = {}) {
37850
+ var _a2, _b2, _c2;
37851
+ const canvasWidth = ((_a2 = config.canvas) == null ? void 0 : _a2.width) || 612;
37852
+ const canvasHeight = ((_b2 = config.canvas) == null ? void 0 : _b2.height) || 792;
37853
+ const canvasBg = (_c2 = config.canvas) == null ? void 0 : _c2.backgroundColor;
37854
+ return {
37855
+ ...config,
37856
+ pages: config.pages.map((page) => {
37857
+ const pageBg = getPageBgColor(page, canvasBg);
37858
+ const wmColor = getWatermarkColor(pageBg);
37859
+ const watermarkElements = generateWatermarkElements(canvasWidth, canvasHeight, wmColor, options);
37860
+ return {
37861
+ ...page,
37862
+ children: [
37863
+ ...page.children || [],
37864
+ ...watermarkElements
37865
+ ]
37866
+ };
37867
+ })
37868
+ };
37869
+ }
37870
+ const canvasWatermark = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
37871
+ __proto__: null,
37872
+ injectWatermark
37873
+ }, Symbol.toStringTag, { value: "Module" }));
37375
37874
  function setAutoShrinkDebug(enabled) {
37376
37875
  if (typeof window !== "undefined") {
37377
37876
  window.__pixldocsDebugAutoShrink = !!enabled;
37378
37877
  }
37379
37878
  }
37380
37879
  export {
37381
- resolveFontWeight as $,
37880
+ resolveBlurElementExactIdsFromFlatFormKeys as $,
37382
37881
  API_URL as A,
37383
37882
  collectFontDescriptorsFromConfig as B,
37384
37883
  collectFontsFromConfig as C,
@@ -37399,23 +37898,24 @@ export {
37399
37898
  hasAnyPreviewBlur as R,
37400
37899
  injectPreviewBlur as S,
37401
37900
  TRIANGLE_STROKE_MITER_LIMIT as T,
37402
- isBundledAssetUrl as U,
37403
- isFontAvailable as V,
37404
- isPrivateUrl as W,
37405
- listPublishedTemplates as X,
37406
- loadGoogleFontCSS as Y,
37407
- normalizeFontFamily as Z,
37408
- resolveBlurElementExactIdsFromFlatFormKeys as _,
37901
+ injectWatermark as U,
37902
+ isBundledAssetUrl as V,
37903
+ isFontAvailable as W,
37904
+ isPrivateUrl as X,
37905
+ listPublishedTemplates as Y,
37906
+ loadGoogleFontCSS as Z,
37907
+ normalizeFontFamily as _,
37409
37908
  getAbsoluteBounds as a,
37410
- resolveForRender as a0,
37411
- resolveFromForm as a1,
37412
- resolveTemplateData as a2,
37413
- rewriteSvgFontsForJsPDF as a3,
37414
- setAutoShrinkDebug as a4,
37415
- setBundledAssetPrefixes as a5,
37416
- warmResolvedTemplateForPreview as a6,
37417
- warmTemplateFromForm as a7,
37418
- canvasImageLoader as a8,
37909
+ resolveFontWeight as a0,
37910
+ resolveForRender as a1,
37911
+ resolveFromForm as a2,
37912
+ resolveTemplateData as a3,
37913
+ rewriteSvgFontsForJsPDF as a4,
37914
+ setAutoShrinkDebug as a5,
37915
+ setBundledAssetPrefixes as a6,
37916
+ warmResolvedTemplateForPreview as a7,
37917
+ warmTemplateFromForm as a8,
37918
+ canvasImageLoader as a9,
37419
37919
  getProxiedImageUrl as b,
37420
37920
  captureFabricCanvasSvgForPdf as c,
37421
37921
  getImageProxyFetchOptions as d,
@@ -37442,4 +37942,4 @@ export {
37442
37942
  awaitFontsForConfig as y,
37443
37943
  buildTeaserBlurFlatKeys as z
37444
37944
  };
37445
- //# sourceMappingURL=index-Cvi3YYOq.js.map
37945
+ //# sourceMappingURL=index-CKhnRi3S.js.map