@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.
@@ -112,6 +112,15 @@ const generateId = (prefix = "el") => {
112
112
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}-${Math.random().toString(36).substr(2, 9)}`;
113
113
  };
114
114
  const createDefaultElement = (partial) => {
115
+ const loose = partial;
116
+ if (loose.type === "frame" || loose.type === "group") {
117
+ return createDefaultGroup({
118
+ layoutMode: loose.layout,
119
+ ...partial,
120
+ type: "group",
121
+ children: Array.isArray(loose.children) ? loose.children : []
122
+ });
123
+ }
115
124
  const isText = partial.type === "text";
116
125
  const base = {
117
126
  left: 48,
@@ -129,7 +138,7 @@ const createDefaultElement = (partial) => {
129
138
  originY: "top",
130
139
  opacity: 1,
131
140
  fill: "#e2e8f0",
132
- ...isText ? {} : { stroke: "#64748b", strokeWidth: 1 },
141
+ ...isText ? {} : { stroke: "transparent", strokeWidth: 0 },
133
142
  visible: true,
134
143
  selectable: true,
135
144
  evented: true,
@@ -975,49 +984,15 @@ function worldPointToParentLocal(worldX, worldY, parent, pageChildren, resolveGr
975
984
  );
976
985
  return { left: local.x, top: local.y };
977
986
  }
978
- function unbakeChildrenToGroupLocal(group) {
979
- const angle = getGroupAngleDeg(group);
980
- if (Math.abs(angle) < 0.01) return group;
981
- const { width, height } = getGroupFrameSize(group);
982
- const cx = width / 2;
983
- const cy = height / 2;
984
- const inv = rotateAroundAffDeg(-angle, cx, cy);
985
- const kids = (group.children ?? []).map((child) => {
986
- const p = applyAff(inv, child.left ?? 0, child.top ?? 0);
987
- const childAngle = typeof child.angle === "number" ? child.angle : 0;
988
- const localAngle = normalizeAngleDeg(childAngle - angle);
989
- if (isGroup(child)) {
990
- const relocated = {
991
- ...child,
992
- left: p.x,
993
- top: p.y,
994
- angle: localAngle
995
- };
996
- return Math.abs(getGroupAngleDeg(relocated)) > 0.01 ? unbakeChildrenToGroupLocal(relocated) : relocated;
997
- }
998
- return {
999
- ...child,
1000
- left: p.x,
1001
- top: p.y,
1002
- angle: localAngle
1003
- };
1004
- });
1005
- return { ...group, children: kids };
1006
- }
1007
987
  function migratePageTreeToGroupOwnedTransforms(nodes) {
1008
988
  return nodes.map((node) => {
1009
989
  if (!isGroup(node)) return node;
1010
990
  const g = node;
1011
- let next = {
991
+ const next = {
1012
992
  ...g,
1013
993
  children: migratePageTreeToGroupOwnedTransforms(g.children ?? [])
1014
994
  };
1015
- if (isRotatableGroup(next) && Math.abs(getGroupAngleDeg(next)) > 0.01) {
1016
- if (g.groupTransformModel !== "owned-v1") {
1017
- next = unbakeChildrenToGroupLocal(next);
1018
- next.groupTransformModel = "owned-v1";
1019
- }
1020
- } else if (isRotatableGroup(next)) {
995
+ if (isRotatableGroup(next)) {
1021
996
  next.groupTransformModel = "owned-v1";
1022
997
  }
1023
998
  return next;
@@ -2257,6 +2232,40 @@ function adaptLayoutGrid(grid, mode) {
2257
2232
  const cells = (grid.cells ?? []).map((c) => c && c.grid ? { ...c, grid: adaptLayoutGrid(c.grid, mode) } : c);
2258
2233
  return { ...grid, dir: grid.dir === from ? to : grid.dir, cells };
2259
2234
  }
2235
+ function isAspectLockedNode(node) {
2236
+ const e = node;
2237
+ if (!e) return false;
2238
+ if (e.lockAspect === true) return true;
2239
+ if (e.clipShape === "circle") return true;
2240
+ const kind = e.componentKind || e.smartType;
2241
+ return kind === "qrcode" || kind === "barcode";
2242
+ }
2243
+ function scaleNodeForReflow(node, sx, sy, fscale) {
2244
+ const n = node;
2245
+ const el = { ...n };
2246
+ const hasBox = typeof n.width === "number" && typeof n.height === "number";
2247
+ const f = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
2248
+ if (hasBox && isAspectLockedNode(n) && sx !== sy) {
2249
+ const s = Math.min(sx, sy);
2250
+ const cx = (f(n.left) + n.width / 2) * sx;
2251
+ const cy = (f(n.top) + n.height / 2) * sy;
2252
+ el.width = n.width * s;
2253
+ el.height = n.height * s;
2254
+ el.left = cx - el.width / 2;
2255
+ el.top = cy - el.height / 2;
2256
+ } else {
2257
+ if (typeof el.left === "number") el.left = el.left * sx;
2258
+ if (typeof el.top === "number") el.top = el.top * sy;
2259
+ if (typeof el.width === "number") el.width = el.width * sx;
2260
+ if (typeof el.height === "number") el.height = el.height * sy;
2261
+ }
2262
+ if (typeof el.fontSize === "number") el.fontSize = Math.max(4, el.fontSize * fscale);
2263
+ if (typeof el.minBoxHeight === "number") el.minBoxHeight = el.minBoxHeight * sy;
2264
+ if (Array.isArray(el.children)) {
2265
+ el.children = el.children.map((c) => scaleNodeForReflow(c, sx, sy, fscale));
2266
+ }
2267
+ return el;
2268
+ }
2260
2269
  function computeGridHandles(grid, box, gridPath = []) {
2261
2270
  const out = [];
2262
2271
  const n = grid.cells.length;
@@ -2659,16 +2668,7 @@ const reflowPagesFromBase = (base, W, H) => {
2659
2668
  const fscale = Math.sqrt(Math.max(0.01, sx * sy));
2660
2669
  const adapt = layoutAdaptationFor(base.width, base.height, W, H);
2661
2670
  const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
2662
- const scaleNode = (node) => {
2663
- const el = { ...node };
2664
- if (typeof el.left === "number") el.left = el.left * sx;
2665
- if (typeof el.top === "number") el.top = el.top * sy;
2666
- if (typeof el.width === "number") el.width = el.width * sx;
2667
- if (typeof el.height === "number") el.height = el.height * sy;
2668
- if (typeof el.fontSize === "number") el.fontSize = Math.max(4, el.fontSize * fscale);
2669
- if (Array.isArray(el.children)) el.children = el.children.map(scaleNode);
2670
- return el;
2671
- };
2671
+ const scaleNode = (node) => scaleNodeForReflow(node, sx, sy, fscale);
2672
2672
  return base.pages.map((p) => {
2673
2673
  const pg = p;
2674
2674
  const page = {
@@ -3257,6 +3257,13 @@ const useEditorStore = zustand.create((set, get) => ({
3257
3257
  return { canvas: nextCanvas, ...committed };
3258
3258
  });
3259
3259
  },
3260
+ getCanvasForSave: () => {
3261
+ const { canvas } = get();
3262
+ const base = sizeBaseSnapshot;
3263
+ if (!base) return canvas;
3264
+ if (canvas.width === base.width && canvas.height === base.height) return canvas;
3265
+ return { ...canvas, width: base.width, height: base.height, pages: base.pages };
3266
+ },
3260
3267
  addSizeVariant: (variant) => set((state) => {
3261
3268
  const sizes = state.canvas.sizes ?? [];
3262
3269
  if (sizes.some((s) => s.id === variant.id)) return {};
@@ -6354,8 +6361,13 @@ async function loadImageAsync(element, placeholder, fc, fabricRef, syncLockedRef
6354
6361
  });
6355
6362
  const imgLoadOptions = url.startsWith("data:") || url.startsWith("blob:") ? {} : { crossOrigin: "anonymous" };
6356
6363
  const img = await fabric__namespace.FabricImage.fromURL(url, imgLoadOptions);
6357
- if (!fabricRef.current) return;
6364
+ if (fabricRef && !fabricRef.current) return;
6358
6365
  await normalizeSvgImageDimensions(img, imageUrl, element.sourceFormat);
6366
+ const natW = img.width ?? 0;
6367
+ const natH = img.height ?? 0;
6368
+ if (natW <= 1 && natH <= 1 && Number(element.width) > 8 && Number(element.height) > 8) {
6369
+ throw new Error(`image decoded to ${natW}x${natH} — treating as failed load`);
6370
+ }
6359
6371
  const isHidden = !element.visible;
6360
6372
  img.set({
6361
6373
  originX: "left",
@@ -6510,8 +6522,14 @@ async function loadImageAsync(element, placeholder, fc, fabricRef, syncLockedRef
6510
6522
  shape,
6511
6523
  rx: rxRatio,
6512
6524
  // Pass ratio, not pixel value
6513
- stroke: element.stroke,
6514
- strokeWidth: element.strokeWidth,
6525
+ // Image outline is an ELEMENT property (imageBorder*), not the generic
6526
+ // shape stroke — this builder forwarded only stroke/strokeWidth, so
6527
+ // outline/gap never reached it.
6528
+ stroke: (element.imageBorderWidth ?? 0) > 0 ? element.imageBorderColor ?? "#FFFFFF" : element.stroke,
6529
+ strokeWidth: (element.imageBorderWidth ?? 0) > 0 ? element.imageBorderWidth : element.strokeWidth,
6530
+ borderPadding: element.imageBorderPadding ?? 0,
6531
+ borderPaddingColor: element.imageBorderPaddingColor ?? "transparent",
6532
+ borderPosition: element.imageBorderPosition ?? "outside",
6515
6533
  panX,
6516
6534
  panY,
6517
6535
  zoom
@@ -6547,9 +6565,111 @@ async function loadImageAsync(element, placeholder, fc, fabricRef, syncLockedRef
6547
6565
  proxiedUrl: getProxiedImageUrl(imageUrl).slice(0, 240),
6548
6566
  error: error instanceof Error ? error.message : String(error)
6549
6567
  });
6568
+ if (fc.__pixldocsEditMode === true) {
6569
+ try {
6570
+ markPlaceholderAsBrokenImage(placeholder);
6571
+ fc.requestRenderAll();
6572
+ } catch {
6573
+ }
6574
+ }
6550
6575
  return;
6551
6576
  }
6552
6577
  }
6578
+ function markPlaceholderAsBrokenImage(placeholder) {
6579
+ if (!(placeholder instanceof fabric__namespace.Group)) return;
6580
+ const group = placeholder;
6581
+ if (group.__pixldocsBrokenImage) return;
6582
+ group.__pixldocsBrokenImage = true;
6583
+ const frameChild = group.getObjects().find(
6584
+ (o) => o.__isPlaceholderFrame
6585
+ );
6586
+ const w = Math.max(1, (frameChild == null ? void 0 : frameChild.width) ?? group.width ?? 1);
6587
+ const h = Math.max(1, (frameChild == null ? void 0 : frameChild.height) ?? group.height ?? 1);
6588
+ const center = group.getCenterPoint();
6589
+ const cx = center.x;
6590
+ const cy = center.y;
6591
+ const prevPose = { left: group.left, top: group.top, width: group.width, height: group.height };
6592
+ const bg = new fabric__namespace.Rect({
6593
+ originX: "center",
6594
+ originY: "center",
6595
+ left: cx,
6596
+ top: cy,
6597
+ width: w,
6598
+ height: h,
6599
+ fill: "rgba(148, 163, 184, 0.12)",
6600
+ stroke: "#94A3B8",
6601
+ strokeWidth: 1.5,
6602
+ strokeDashArray: [6, 4]
6603
+ });
6604
+ const s = Math.max(16, Math.min(w, h) * 0.28);
6605
+ const glyphStroke = { stroke: "#64748B", strokeWidth: Math.max(1.5, s / 12), fill: "transparent" };
6606
+ const iconFrame = new fabric__namespace.Rect({
6607
+ originX: "center",
6608
+ originY: "center",
6609
+ left: cx,
6610
+ top: cy,
6611
+ width: s,
6612
+ height: s * 0.8,
6613
+ rx: s * 0.08,
6614
+ ry: s * 0.08,
6615
+ ...glyphStroke
6616
+ });
6617
+ const mountain = new fabric__namespace.Polyline(
6618
+ [
6619
+ { x: -s * 0.38, y: s * 0.24 },
6620
+ { x: -s * 0.1, y: -s * 0.08 },
6621
+ { x: s * 0.08, y: s * 0.1 },
6622
+ { x: s * 0.22, y: -s * 0.02 },
6623
+ { x: s * 0.38, y: s * 0.24 }
6624
+ ],
6625
+ { originX: "center", originY: "center", left: cx, top: cy + s * 0.08, ...glyphStroke }
6626
+ );
6627
+ const slash = new fabric__namespace.Line([cx - s * 0.55, cy - s * 0.5, cx + s * 0.55, cy + s * 0.5], {
6628
+ ...glyphStroke,
6629
+ originX: "center",
6630
+ originY: "center",
6631
+ left: cx,
6632
+ top: cy
6633
+ });
6634
+ for (const obj of [bg, iconFrame, mountain, slash]) {
6635
+ obj.set({ selectable: false, evented: false, excludeFromExport: true });
6636
+ obj.__pixldocsEditorChrome = true;
6637
+ group.add(obj);
6638
+ }
6639
+ group.set(prevPose);
6640
+ group.setCoords();
6641
+ group.dirty = true;
6642
+ }
6643
+ function hideEditorChromeForExport(fc) {
6644
+ const hidden = [];
6645
+ const walk = (objs) => {
6646
+ var _a2;
6647
+ for (const o of objs) {
6648
+ if (o.__pixldocsEditorChrome && o.visible !== false) {
6649
+ o.visible = false;
6650
+ hidden.push(o);
6651
+ }
6652
+ const kids = (_a2 = o.getObjects) == null ? void 0 : _a2.call(o);
6653
+ if (kids == null ? void 0 : kids.length) walk(kids);
6654
+ }
6655
+ };
6656
+ walk(fc.getObjects());
6657
+ if (hidden.length) {
6658
+ for (const o of hidden) {
6659
+ const g = o.group;
6660
+ if (g) g.dirty = true;
6661
+ }
6662
+ fc.renderAll();
6663
+ }
6664
+ return () => {
6665
+ for (const o of hidden) {
6666
+ o.visible = true;
6667
+ const g = o.group;
6668
+ if (g) g.dirty = true;
6669
+ }
6670
+ if (hidden.length) fc.requestRenderAll();
6671
+ };
6672
+ }
6553
6673
  const canvasImageLoader = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
6554
6674
  __proto__: null,
6555
6675
  SVG_DECODE_HARD_CAP,
@@ -6559,12 +6679,14 @@ const canvasImageLoader = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.d
6559
6679
  fetchSvgTextPublic,
6560
6680
  getNormalizedSvgUrl,
6561
6681
  getProxiedImageUrl,
6682
+ hideEditorChromeForExport,
6562
6683
  isBundledAssetUrl,
6563
6684
  isEmptyImagePlaceholderGroup,
6564
6685
  isPrivateUrl,
6565
6686
  isSvgImage,
6566
6687
  loadImageAsync,
6567
6688
  loadSvgDimensions,
6689
+ markPlaceholderAsBrokenImage,
6568
6690
  normalizeSvgImageDimensions,
6569
6691
  parseSvgDimensionsFromText,
6570
6692
  preloadImage,
@@ -7062,10 +7184,10 @@ function finalizeCropGroupCoords(g) {
7062
7184
  g.setCoords();
7063
7185
  }
7064
7186
  function updateCoverLayout(g) {
7065
- var _a2;
7187
+ var _a2, _b2;
7066
7188
  const ct = g.__cropData;
7067
7189
  if (!ct) return;
7068
- const { frameW, frameH, shape, rx: rxRatio, _img: img, _border: border } = ct;
7190
+ const { frameW, frameH, shape, rx: rxRatio, _img: img } = ct;
7069
7191
  const minDim = Math.min(frameW, frameH);
7070
7192
  let rx = rxRatio > 0.5 ? rxRatio : rxRatio * minDim;
7071
7193
  rx = Math.max(0, Math.min(rx, frameW / 2, frameH / 2));
@@ -7198,25 +7320,31 @@ function updateCoverLayout(g) {
7198
7320
  g.clipPath.excludeFromExport = true;
7199
7321
  }
7200
7322
  }
7323
+ const wantCircle = shape === "circle";
7324
+ const border = ensureRingShapeClass(g, ct, "_border", wantCircle);
7325
+ const mat = ensureRingShapeClass(g, ct, "_mat", wantCircle);
7201
7326
  const bw = border ? Number(border.strokeWidth) || 0 : 0;
7202
- const mat = ct._mat;
7203
7327
  const pad = Math.max(0, Number(ct.borderPadding) || 0);
7328
+ const outward = (ct.borderPosition ?? "outside") === "outside";
7204
7329
  const borderMinDim = Math.min(frameW, frameH);
7205
7330
  let borderRx = rxRatio > 0.5 ? rxRatio : rxRatio * borderMinDim;
7206
7331
  borderRx = Math.max(0, Math.min(borderRx, frameW / 2, frameH / 2));
7332
+ const geo = computeOutlineGeometry({ frameW, frameH, bw, pad, rx: borderRx, outward });
7207
7333
  if (border) {
7208
7334
  if (shape === "circle") {
7209
- border.set({ rx: (frameW - bw) / 2, ry: (frameH - bw) / 2 });
7335
+ border.set({ rx: geo.border.width / 2, ry: geo.border.height / 2 });
7210
7336
  } else {
7211
- border.set({ width: frameW - bw, height: frameH - bw, rx: borderRx, ry: borderRx });
7337
+ border.set({ width: geo.border.width, height: geo.border.height, rx: geo.border.rx, ry: geo.border.rx });
7212
7338
  }
7339
+ border.dirty = true;
7213
7340
  }
7214
7341
  if (mat) {
7215
7342
  if (shape === "circle") {
7216
- mat.set({ rx: (frameW - 2 * bw - pad) / 2, ry: (frameH - 2 * bw - pad) / 2 });
7343
+ mat.set({ rx: geo.mat.width / 2, ry: geo.mat.height / 2 });
7217
7344
  } else {
7218
- mat.set({ width: frameW - 2 * bw - pad, height: frameH - 2 * bw - pad, rx: Math.max(0, borderRx - bw), ry: Math.max(0, borderRx - bw) });
7345
+ mat.set({ width: geo.mat.width, height: geo.mat.height, rx: geo.mat.rx, ry: geo.mat.rx });
7219
7346
  }
7347
+ mat.dirty = true;
7220
7348
  }
7221
7349
  if (!img) {
7222
7350
  const placeholderFrame = ct._placeholderFrame ?? ((_a2 = g._objects) == null ? void 0 : _a2.find((obj) => obj.__isPlaceholderFrame));
@@ -7245,8 +7373,9 @@ function updateCoverLayout(g) {
7245
7373
  img._ct.panX = img.__panX ?? 0.5;
7246
7374
  img._ct.panY = img.__panY ?? 0.5;
7247
7375
  }
7248
- const iw = img.width || 1;
7249
- const ih = img.height || 1;
7376
+ const srcEl = img._originalElement ?? ((_b2 = img.getElement) == null ? void 0 : _b2.call(img));
7377
+ const iw = Number(srcEl == null ? void 0 : srcEl.naturalWidth) || Number(srcEl == null ? void 0 : srcEl.width) || img.width || 1;
7378
+ const ih = Number(srcEl == null ? void 0 : srcEl.naturalHeight) || Number(srcEl == null ? void 0 : srcEl.height) || img.height || 1;
7250
7379
  const fitContain = ct.fit === "contain";
7251
7380
  const baseScale = fitContain ? Math.min(frameW / iw, frameH / ih) : Math.max(frameW / iw, frameH / ih);
7252
7381
  const zoom = fitContain ? Math.max(1, Math.min(3, ct.containScale ?? 1)) : Math.max(1, img._ct.zoom ?? 1);
@@ -7283,6 +7412,42 @@ function updateCoverLayout(g) {
7283
7412
  offsetY = overflowY > 0 ? -overflowY * (panY - 0.5) : 0;
7284
7413
  }
7285
7414
  img.set({ left: offsetX, top: offsetY });
7415
+ const wantsOutline = bw > 0 || pad > 0;
7416
+ const shapedMask = shape === "circle" || shape === "roundRect";
7417
+ if (wantsOutline && shapedMask) {
7418
+ if (g.clipPath) {
7419
+ img.clipPath = g.clipPath;
7420
+ g.clipPath = void 0;
7421
+ }
7422
+ if (img.cropX || img.cropY || img.width !== iw) {
7423
+ img.set({ width: iw, height: ih, cropX: 0, cropY: 0 });
7424
+ }
7425
+ } else if (wantsOutline && !fitContain) {
7426
+ const regionW = Math.min(iw, frameW / finalScale);
7427
+ const regionH = Math.min(ih, frameH / finalScale);
7428
+ img.set({
7429
+ width: regionW,
7430
+ height: regionH,
7431
+ cropX: (iw - regionW) * panX,
7432
+ cropY: (ih - regionH) * panY,
7433
+ left: 0,
7434
+ top: 0
7435
+ });
7436
+ img.clipPath = void 0;
7437
+ g.clipPath = void 0;
7438
+ } else {
7439
+ if (img.clipPath) img.clipPath = void 0;
7440
+ if (img.cropX || img.cropY || img.width !== iw) {
7441
+ img.set({ width: iw, height: ih, cropX: 0, cropY: 0 });
7442
+ }
7443
+ }
7444
+ const imgClip = img.clipPath;
7445
+ if (imgClip && typeof imgClip.set === "function") {
7446
+ const csx = img.scaleX || 1;
7447
+ const csy = img.scaleY || 1;
7448
+ imgClip.set({ left: -offsetX / csx, top: -offsetY / csy, scaleX: 1 / csx, scaleY: 1 / csy });
7449
+ imgClip.dirty = true;
7450
+ }
7286
7451
  g.dirty = true;
7287
7452
  img.dirty = true;
7288
7453
  if (g.clipPath) {
@@ -7633,6 +7798,51 @@ function installCanvaMaskControls(g) {
7633
7798
  g.set(controlStyle);
7634
7799
  g.setCoords();
7635
7800
  }
7801
+ function ensureRingShapeClass(g, ct, key, wantCircle) {
7802
+ const cur = ct[key];
7803
+ if (!cur) return void 0;
7804
+ const isEllipse = cur instanceof fabric__namespace.Ellipse;
7805
+ if (isEllipse === wantCircle) return cur;
7806
+ const common = {
7807
+ left: 0,
7808
+ top: 0,
7809
+ originX: "center",
7810
+ originY: "center",
7811
+ fill: "transparent",
7812
+ stroke: cur.stroke,
7813
+ strokeWidth: cur.strokeWidth,
7814
+ selectable: false,
7815
+ evented: false
7816
+ };
7817
+ const next = wantCircle ? new fabric__namespace.Ellipse({ ...common, rx: 1, ry: 1, objectCaching: false }) : new fabric__namespace.Rect({ ...common, width: 1, height: 1, objectCaching: false });
7818
+ const objs = g._objects;
7819
+ const idx = objs ? objs.indexOf(cur) : -1;
7820
+ if (objs && idx >= 0) objs.splice(idx, 1, next);
7821
+ else return cur;
7822
+ next.group = cur.group ?? g;
7823
+ next.canvas = cur.canvas ?? g.canvas;
7824
+ cur.group = void 0;
7825
+ next.setCoords();
7826
+ ct[key] = next;
7827
+ g.dirty = true;
7828
+ return next;
7829
+ }
7830
+ function computeOutlineGeometry(opts) {
7831
+ const { frameW, frameH, bw, pad, rx, outward } = opts;
7832
+ if (outward) {
7833
+ const grow = (r, by) => r > 0 ? r + by : 0;
7834
+ return {
7835
+ // Ring centre-line is pad + bw/2 past the frame → inner edge exactly `pad` out.
7836
+ border: { width: frameW + 2 * pad + bw, height: frameH + 2 * pad + bw, rx: grow(rx, pad + bw / 2) },
7837
+ // The gap band is transparent; it exists only to keep the child list stable.
7838
+ mat: { width: frameW + pad, height: frameH + pad, rx: grow(rx, pad / 2) }
7839
+ };
7840
+ }
7841
+ return {
7842
+ border: { width: frameW - bw, height: frameH - bw, rx },
7843
+ mat: { width: frameW - 2 * bw - pad, height: frameH - 2 * bw - pad, rx: Math.max(0, rx - bw) }
7844
+ };
7845
+ }
7636
7846
  async function createMaskedImageElement({
7637
7847
  url,
7638
7848
  image,
@@ -7661,7 +7871,8 @@ async function createMaskedImageElement({
7661
7871
  containScale = 1,
7662
7872
  shadow = null,
7663
7873
  borderPadding = 0,
7664
- borderPaddingColor = "#FFFFFF"
7874
+ borderPaddingColor = "transparent",
7875
+ borderPosition = "outside"
7665
7876
  }) {
7666
7877
  const img = image || (url ? await fabric__namespace.FabricImage.fromURL(getProxiedImageUrl(url), { crossOrigin: "anonymous" }) : null);
7667
7878
  if (!img) {
@@ -7686,36 +7897,47 @@ async function createMaskedImageElement({
7686
7897
  });
7687
7898
  const bw = strokeWidth || 0;
7688
7899
  const pad = Math.max(0, borderPadding || 0);
7900
+ const outward = borderPosition === "outside";
7901
+ const geo = computeOutlineGeometry({ frameW, frameH, bw, pad, rx, outward });
7902
+ const matStroke = outward ? borderPaddingColor || "transparent" : borderPaddingColor;
7903
+ const matW = geo.mat.width;
7904
+ const matH = geo.mat.height;
7905
+ const matRx = geo.mat.rx;
7689
7906
  const mat = shape === "circle" ? new fabric__namespace.Ellipse({
7690
- rx: (frameW - 2 * bw - pad) / 2,
7691
- ry: (frameH - 2 * bw - pad) / 2,
7907
+ rx: matW / 2,
7908
+ ry: matH / 2,
7692
7909
  left: 0,
7693
7910
  top: 0,
7694
7911
  originX: "center",
7695
7912
  originY: "center",
7696
7913
  fill: "transparent",
7697
- stroke: borderPaddingColor,
7914
+ stroke: matStroke,
7698
7915
  strokeWidth: pad,
7699
7916
  selectable: false,
7700
- evented: false
7917
+ evented: false,
7918
+ objectCaching: false
7701
7919
  }) : new fabric__namespace.Rect({
7702
- width: frameW - 2 * bw - pad,
7703
- height: frameH - 2 * bw - pad,
7704
- rx: Math.max(0, rx - bw),
7705
- ry: Math.max(0, rx - bw),
7920
+ width: matW,
7921
+ height: matH,
7922
+ rx: matRx,
7923
+ ry: matRx,
7706
7924
  left: 0,
7707
7925
  top: 0,
7708
7926
  originX: "center",
7709
7927
  originY: "center",
7710
7928
  fill: "transparent",
7711
- stroke: borderPaddingColor,
7929
+ stroke: matStroke,
7712
7930
  strokeWidth: pad,
7713
7931
  selectable: false,
7714
- evented: false
7932
+ evented: false,
7933
+ objectCaching: false
7715
7934
  });
7935
+ const bordW = geo.border.width;
7936
+ const bordH = geo.border.height;
7937
+ const bordRx = geo.border.rx;
7716
7938
  const border = shape === "circle" ? new fabric__namespace.Ellipse({
7717
- rx: (frameW - bw) / 2,
7718
- ry: (frameH - bw) / 2,
7939
+ rx: bordW / 2,
7940
+ ry: bordH / 2,
7719
7941
  left: 0,
7720
7942
  top: 0,
7721
7943
  originX: "center",
@@ -7724,12 +7946,13 @@ async function createMaskedImageElement({
7724
7946
  stroke: stroke || "transparent",
7725
7947
  strokeWidth: bw,
7726
7948
  selectable: false,
7727
- evented: false
7949
+ evented: false,
7950
+ objectCaching: false
7728
7951
  }) : new fabric__namespace.Rect({
7729
- width: frameW - bw,
7730
- height: frameH - bw,
7731
- rx,
7732
- ry: rx,
7952
+ width: bordW,
7953
+ height: bordH,
7954
+ rx: bordRx,
7955
+ ry: bordRx,
7733
7956
  left: 0,
7734
7957
  top: 0,
7735
7958
  originX: "center",
@@ -7738,7 +7961,8 @@ async function createMaskedImageElement({
7738
7961
  stroke: stroke || "transparent",
7739
7962
  strokeWidth: bw,
7740
7963
  selectable: false,
7741
- evented: false
7964
+ evented: false,
7965
+ objectCaching: false
7742
7966
  });
7743
7967
  img.set({
7744
7968
  left: 0,
@@ -7832,7 +8056,9 @@ async function createMaskedImageElement({
7832
8056
  _border: border,
7833
8057
  _mat: mat,
7834
8058
  borderPadding: pad,
7835
- borderPaddingColor
8059
+ borderPaddingColor,
8060
+ // Read back by refreshMaskedImage so it recomputes with the same geometry.
8061
+ borderPosition
7836
8062
  };
7837
8063
  g._ct = g._ct || {};
7838
8064
  g._ct.isCropGroup = true;
@@ -7866,7 +8092,12 @@ async function createMaskedImageElement({
7866
8092
  });
7867
8093
  clip.absolutePositioned = false;
7868
8094
  clip.excludeFromExport = true;
7869
- g.clipPath = clip;
8095
+ if (outward) {
8096
+ img.clipPath = clip;
8097
+ g.clipPath = void 0;
8098
+ } else {
8099
+ g.clipPath = clip;
8100
+ }
7870
8101
  g.set({
7871
8102
  selectable: true,
7872
8103
  evented: true,
@@ -17967,6 +18198,7 @@ const PageCanvas = react.forwardRef(
17967
18198
  // Transparent so underlay (page bg + group bgs) shows through
17968
18199
  backgroundColor: "transparent"
17969
18200
  });
18201
+ fabricCanvas.__pixldocsEditMode = isEditorMode && !isPreviewMode;
17970
18202
  fabricCanvas.hoverCursor = "default";
17971
18203
  fabricCanvas.moveCursor = "move";
17972
18204
  const suppressTextEditForClick = (textbox) => {
@@ -23956,11 +24188,33 @@ const PageCanvas = react.forwardRef(
23956
24188
  const wantBW = Math.max(0, Number(element.imageBorderWidth) || 0);
23957
24189
  const wantBC = element.imageBorderColor ?? "#FFFFFF";
23958
24190
  const wantPad = Math.max(0, Number(element.imageBorderPadding) || 0);
23959
- const wantPC = element.imageBorderPaddingColor ?? "#FFFFFF";
24191
+ const wantPC = element.imageBorderPaddingColor ?? "transparent";
24192
+ const wantPos = element.imageBorderPosition ?? "outside";
23960
24193
  if (ct._border) ct._border.set({ stroke: wantBW > 0 ? wantBC : "transparent", strokeWidth: wantBW, dirty: true });
23961
24194
  if (ct._mat) ct._mat.set({ stroke: wantPC, strokeWidth: wantPad, dirty: true });
23962
24195
  ct.borderPadding = wantPad;
23963
24196
  ct.borderPaddingColor = wantPC;
24197
+ ct.borderPosition = wantPos;
24198
+ {
24199
+ const gMinDim = Math.min(ct.frameW, ct.frameH);
24200
+ const rxRaw = ct.rx || 0;
24201
+ const gRx = Math.max(0, Math.min(rxRaw > 0.5 ? rxRaw : rxRaw * gMinDim, ct.frameW / 2, ct.frameH / 2));
24202
+ const gGeo = computeOutlineGeometry({
24203
+ frameW: ct.frameW,
24204
+ frameH: ct.frameH,
24205
+ bw: wantBW,
24206
+ pad: wantPad,
24207
+ rx: gRx,
24208
+ outward: wantPos === "outside"
24209
+ });
24210
+ const isCircle = (ct.shape || "rect") === "circle";
24211
+ if (ct._border) {
24212
+ 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 });
24213
+ }
24214
+ if (ct._mat) {
24215
+ 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 });
24216
+ }
24217
+ }
23964
24218
  const sBlur = Math.max(0, Number(element.imageShadowBlur) || 0);
23965
24219
  const sColor = element.imageShadowColor;
23966
24220
  if (sBlur > 0 || sColor) {
@@ -25270,6 +25524,7 @@ const PageCanvas = react.forwardRef(
25270
25524
  } else if (obj instanceof fabric__namespace.Textbox) {
25271
25525
  const overflowPolicy = element.overflowPolicy || "grow-and-push";
25272
25526
  let text = element.text != null && element.text !== "" ? element.text : " ";
25527
+ text = applyElementTextCase(text, element.textCase);
25273
25528
  let parsedStyles = null;
25274
25529
  if (element.formattingEnabled === true) {
25275
25530
  const parsed = parseTextMarkdown(text);
@@ -25914,6 +26169,13 @@ const PageCanvas = react.forwardRef(
25914
26169
  const img = await fabric__namespace.FabricImage.fromURL(url, { crossOrigin: "anonymous" });
25915
26170
  if (!fabricRef.current || !isLatestRequest()) return;
25916
26171
  await normalizeSvgImageDimensions(img, imageUrl, element.sourceFormat, svgDecodeTarget);
26172
+ {
26173
+ const natW = img.width ?? 0;
26174
+ const natH = img.height ?? 0;
26175
+ if (natW <= 1 && natH <= 1 && Number(element.width) > 8 && Number(element.height) > 8) {
26176
+ throw new Error(`image decoded to ${natW}x${natH} — treating as failed load`);
26177
+ }
26178
+ }
25917
26179
  if (!isLatestRequest()) return;
25918
26180
  const imageFitForFade = element.imageFit || ((_a2 = element.style) == null ? void 0 : _a2.imageFit) || "cover";
25919
26181
  const clipShapeForFade = element.clipShape ?? ((_b2 = element.style) == null ? void 0 : _b2.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
@@ -26121,7 +26383,9 @@ const PageCanvas = react.forwardRef(
26121
26383
  strokeWidth: element.imageBorderWidth ?? 0,
26122
26384
  // Gap between the image and the outline (the "matted photo" look).
26123
26385
  borderPadding: element.imageBorderPadding ?? 0,
26124
- borderPaddingColor: element.imageBorderPaddingColor ?? "#FFFFFF",
26386
+ // Gap is empty space by default, not a white mat.
26387
+ borderPaddingColor: element.imageBorderPaddingColor ?? "transparent",
26388
+ borderPosition: element.imageBorderPosition ?? "outside",
26125
26389
  // Optional image drop shadow (element property).
26126
26390
  shadow: (element.imageShadowBlur ?? 0) > 0 || element.imageShadowColor ? {
26127
26391
  color: element.imageShadowColor ?? "rgba(0,0,0,0.25)",
@@ -26278,6 +26542,19 @@ const PageCanvas = react.forwardRef(
26278
26542
  }
26279
26543
  fc.requestRenderAll();
26280
26544
  } catch (error) {
26545
+ console.error("[PageCanvas] image failed to load", {
26546
+ id: element.id,
26547
+ url: (element.src || element.imageUrl || "").slice(0, 200),
26548
+ error: error instanceof Error ? error.message : String(error)
26549
+ });
26550
+ const fcNow = fabricRef.current;
26551
+ if (fcNow && fcNow.__pixldocsEditMode === true) {
26552
+ try {
26553
+ markPlaceholderAsBrokenImage(placeholder);
26554
+ fcNow.requestRenderAll();
26555
+ } catch {
26556
+ }
26557
+ }
26281
26558
  }
26282
26559
  };
26283
26560
  const handleCanvasClick = react.useCallback(
@@ -28883,7 +29160,9 @@ function expandBoundTables(pages, formValues) {
28883
29160
  const t = node;
28884
29161
  if (t.type === "table" && ((_a2 = t.tableData) == null ? void 0 : _a2.repeat)) {
28885
29162
  const entries = tableEntriesFor(t.tableData.repeat.from, formValues);
28886
- t.tableData = expandTableRepeat(t.tableData, entries);
29163
+ if (entries.length > 0) {
29164
+ t.tableData = expandTableRepeat(t.tableData, entries);
29165
+ }
28887
29166
  for (const row of t.tableData.cells) {
28888
29167
  for (const cell of row) {
28889
29168
  if (!cell.field) continue;
@@ -30038,7 +30317,12 @@ function pagesHaveConstantTokens(pages) {
30038
30317
  return pages.some((p) => walk(p.children));
30039
30318
  }
30040
30319
  function substituteConstantTokens(pages, constants) {
30041
- const sub = (s) => s.replace(/\{\{\s*\$([a-zA-Z0-9_]+)\s*\}\}/g, (m, name) => Object.prototype.hasOwnProperty.call(constants, name) ? String(constants[name]) : m);
30320
+ const sub = (s) => s.replace(/\{\{\s*\$([a-zA-Z0-9_]+)\s*\}\}/g, (m, name) => (
30321
+ // Unresolved tokens are ERASED rather than left as literal braces: on a
30322
+ // money cell a missing symbol still reads as a number, whereas
30323
+ // "{{$currency}}9,500.00" is visibly broken output.
30324
+ Object.prototype.hasOwnProperty.call(constants, name) ? String(constants[name]) : ""
30325
+ ));
30042
30326
  const walk = (nodes) => {
30043
30327
  for (const n of nodes ?? []) {
30044
30328
  if (n.type === "text") {
@@ -30069,10 +30353,9 @@ function applyContentBoundsPagination(config) {
30069
30353
  }
30070
30354
  let mutated = continuationMasters.length > 0;
30071
30355
  const constants = config.constants;
30072
- const hasConstants = !!constants && typeof constants === "object" && Object.keys(constants).length > 0;
30073
- if (hasConstants && pagesHaveConstantTokens(pages)) {
30356
+ if (pagesHaveConstantTokens(pages)) {
30074
30357
  pages = pages.map((p) => JSON.parse(JSON.stringify(p)));
30075
- substituteConstantTokens(pages, constants);
30358
+ substituteConstantTokens(pages, constants && typeof constants === "object" ? constants : {});
30076
30359
  mutated = true;
30077
30360
  }
30078
30361
  let resultPages = [];
@@ -32242,8 +32525,9 @@ async function resolveTemplateData(options) {
32242
32525
  void 0,
32243
32526
  repeatablePagesInput.length > 0 ? repeatablePagesInput : void 0
32244
32527
  );
32528
+ const paginated = applyContentBoundsPagination(resolvedConfig);
32245
32529
  return {
32246
- config: applyThemeIfNeeded(resolvedConfig),
32530
+ config: applyThemeIfNeeded(paginated),
32247
32531
  templateName: template.name || "Untitled",
32248
32532
  templateId,
32249
32533
  price: template.price ?? 0
@@ -33884,6 +34168,43 @@ function stampPixldocsImageIdOnSvg(svg, id) {
33884
34168
  const attr = ` data-pixldocs-image-id="${escapeSvgDataAttr(id)}"`;
33885
34169
  return svg.replace(/<image\b/i, `<image${attr}`);
33886
34170
  }
34171
+ function colorHasAlpha(color) {
34172
+ if (typeof color !== "string") return false;
34173
+ const c = color.trim().toLowerCase();
34174
+ if (c === "transparent") return true;
34175
+ const rgba = c.match(/^rgba?\(([^)]+)\)$/);
34176
+ if (rgba) {
34177
+ const parts = rgba[1].split(",").map((x) => x.trim());
34178
+ return parts.length > 3 && Number(parts[3]) < 1;
34179
+ }
34180
+ if (/^#[0-9a-f]{8}$/.test(c)) return parseInt(c.slice(7, 9), 16) < 255;
34181
+ if (/^#[0-9a-f]{4}$/.test(c)) return parseInt(c[4] + c[4], 16) < 255;
34182
+ return false;
34183
+ }
34184
+ function paintIsAlphaGradient(paint) {
34185
+ const stops = paint == null ? void 0 : paint.colorStops;
34186
+ if (!Array.isArray(stops) || !stops.length) return false;
34187
+ return stops.some((st) => colorHasAlpha(st == null ? void 0 : st.color) || typeof (st == null ? void 0 : st.opacity) === "number" && st.opacity < 1);
34188
+ }
34189
+ function objectNeedsAlphaGradientRaster(obj) {
34190
+ if (!obj) return false;
34191
+ return paintIsAlphaGradient(obj.fill) || paintIsAlphaGradient(obj.stroke);
34192
+ }
34193
+ function rasterizeObjectToSvgImage(obj, multiplier) {
34194
+ try {
34195
+ if (typeof obj.toDataURL !== "function") return null;
34196
+ const rect = obj.getBoundingRect();
34197
+ if (!rect || !(rect.width > 0) || !(rect.height > 0)) return null;
34198
+ const url = obj.toDataURL({ multiplier, enableRetinaScaling: false });
34199
+ if (typeof url !== "string" || !url.startsWith("data:image")) return null;
34200
+ const n = (v) => Number(v.toFixed(4));
34201
+ return `<image x="${n(rect.left)}" y="${n(rect.top)}" width="${n(rect.width)}" height="${n(rect.height)}" preserveAspectRatio="none" xlink:href="${url}"></image>`;
34202
+ } catch (e) {
34203
+ console.warn("[canvas-svg-capture][alphaGradient] raster failed:", e);
34204
+ return null;
34205
+ }
34206
+ }
34207
+ const ALPHA_GRADIENT_RASTER_MULTIPLIER = 3;
33887
34208
  function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight) {
33888
34209
  const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
33889
34210
  const prevSvgVPT = fabricInstance.svgViewportTransformation;
@@ -33904,10 +34225,23 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
33904
34225
  try {
33905
34226
  const visit = (obj) => {
33906
34227
  if (!obj) return;
34228
+ if (obj.__pixldocsEditorChrome) {
34229
+ const originalToSVG = typeof obj.toSVG === "function" ? obj.toSVG.bind(obj) : null;
34230
+ if (originalToSVG) {
34231
+ obj.toSVG = () => "";
34232
+ svgPatchRecords.push({ obj, originalToSVG });
34233
+ }
34234
+ return;
34235
+ }
33907
34236
  const imageId = typeof obj.__docuforgeId === "string" && hasRenderableRasterCandidate(obj) ? obj.__docuforgeId : "";
33908
- if ((isTextboxLike(obj) || imageId) && typeof obj.toSVG === "function") {
34237
+ const alphaGradient = objectNeedsAlphaGradientRaster(obj);
34238
+ if ((isTextboxLike(obj) || imageId || alphaGradient) && typeof obj.toSVG === "function") {
33909
34239
  const originalToSVG = obj.toSVG.bind(obj);
33910
34240
  obj.toSVG = (reviver) => {
34241
+ if (alphaGradient) {
34242
+ const raster = rasterizeObjectToSvgImage(obj, ALPHA_GRADIENT_RASTER_MULTIPLIER);
34243
+ if (raster) return raster;
34244
+ }
33911
34245
  let svg = originalToSVG(reviver);
33912
34246
  if (isTextboxLike(obj)) svg = stampFabricLineMetricsOnTextSvg(svg, obj);
33913
34247
  if (isTextboxLike(obj)) svg = warpTextboxSvgAlongPath(svg, obj);
@@ -34006,9 +34340,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
34006
34340
  }
34007
34341
  return svgString;
34008
34342
  }
34009
- const resolvedPackageVersion = "0.5.495";
34343
+ const resolvedPackageVersion = "0.5.497";
34010
34344
  const PACKAGE_VERSION = resolvedPackageVersion;
34011
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.495";
34345
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.497";
34012
34346
  const roundParityValue = (value) => {
34013
34347
  if (typeof value !== "number") return value;
34014
34348
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -34447,6 +34781,11 @@ class PixldocsRenderer {
34447
34781
  * Mounts a hidden PreviewCanvas component and captures the Fabric canvas output.
34448
34782
  */
34449
34783
  async render(templateConfig, options = {}) {
34784
+ if (options.watermark === true && !templateConfig.__pixldocsWatermarked) {
34785
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34786
+ templateConfig = injectWatermark2(templateConfig, options.watermarkOptions);
34787
+ templateConfig.__pixldocsWatermarked = true;
34788
+ }
34450
34789
  const pageIndex = options.pageIndex ?? 0;
34451
34790
  const format = options.format ?? "png";
34452
34791
  const quality = options.quality ?? 0.92;
@@ -34462,6 +34801,7 @@ class PixldocsRenderer {
34462
34801
  const hasAutoShrink = configHasAutoShrinkText(templateConfig);
34463
34802
  const defaultWait = hasAutoShrink ? 4e3 : 1800;
34464
34803
  await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
34804
+ this.flushTextMeasurementCaches();
34465
34805
  }
34466
34806
  const { setPackageApiUrl: setPackageApiUrl2 } = await Promise.resolve().then(() => appApi);
34467
34807
  setPackageApiUrl2(this.config.imageProxyUrl);
@@ -34489,6 +34829,7 @@ class PixldocsRenderer {
34489
34829
  const hasAutoShrink = configHasAutoShrinkText(templateConfig);
34490
34830
  const defaultWait = hasAutoShrink ? 4e3 : 1800;
34491
34831
  await this.awaitFontsForConfig(templateConfig, options.waitForFontsMs ?? defaultWait);
34832
+ this.flushTextMeasurementCaches();
34492
34833
  }
34493
34834
  const results = [];
34494
34835
  for (let i = 0; i < templateConfig.pages.length; i++) {
@@ -34515,8 +34856,8 @@ class PixldocsRenderer {
34515
34856
  const shouldWatermark = watermark ?? resolved.price > 0;
34516
34857
  let configToRender = resolved.config;
34517
34858
  if (shouldWatermark) {
34518
- const { injectWatermark } = await Promise.resolve().then(() => require("./canvasWatermark-B0ab38Ok.cjs"));
34519
- configToRender = injectWatermark(configToRender, watermarkOptions);
34859
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34860
+ configToRender = injectWatermark2(configToRender, watermarkOptions);
34520
34861
  const { injectPreviewBlur: injectPreviewBlur2 } = await Promise.resolve().then(() => previewBlur);
34521
34862
  configToRender = injectPreviewBlur2(configToRender);
34522
34863
  }
@@ -34575,8 +34916,8 @@ class PixldocsRenderer {
34575
34916
  const shouldWatermark = watermark ?? resolved.price > 0;
34576
34917
  let configToRender = resolved.config;
34577
34918
  if (shouldWatermark) {
34578
- const { injectWatermark } = await Promise.resolve().then(() => require("./canvasWatermark-B0ab38Ok.cjs"));
34579
- configToRender = injectWatermark(configToRender, watermarkOptions);
34919
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34920
+ configToRender = injectWatermark2(configToRender, watermarkOptions);
34580
34921
  const { injectPreviewBlur: injectPreviewBlur2 } = await Promise.resolve().then(() => previewBlur);
34581
34922
  configToRender = injectPreviewBlur2(configToRender);
34582
34923
  }
@@ -34596,6 +34937,10 @@ class PixldocsRenderer {
34596
34937
  * exporter, which is what `renderPdfViaClientExport` does below.
34597
34938
  */
34598
34939
  async renderPdf(templateConfig, options) {
34940
+ if ((options == null ? void 0 : options.watermark) === true) {
34941
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34942
+ templateConfig = injectWatermark2(templateConfig, options.watermarkOptions);
34943
+ }
34599
34944
  return this.renderPdfViaClientExport(templateConfig, {
34600
34945
  title: options == null ? void 0 : options.title,
34601
34946
  textMode: options == null ? void 0 : options.textMode,
@@ -34624,8 +34969,8 @@ class PixldocsRenderer {
34624
34969
  const shouldWatermark = watermark ?? resolved.price > 0;
34625
34970
  let configToRender = resolved.config;
34626
34971
  if (shouldWatermark) {
34627
- const { injectWatermark } = await Promise.resolve().then(() => require("./canvasWatermark-B0ab38Ok.cjs"));
34628
- configToRender = injectWatermark(configToRender, watermarkOptions);
34972
+ const { injectWatermark: injectWatermark2 } = await Promise.resolve().then(() => canvasWatermark);
34973
+ configToRender = injectWatermark2(configToRender, watermarkOptions);
34629
34974
  const { injectPreviewBlur: injectPreviewBlur2 } = await Promise.resolve().then(() => previewBlur);
34630
34975
  configToRender = injectPreviewBlur2(configToRender);
34631
34976
  }
@@ -34822,7 +35167,7 @@ class PixldocsRenderer {
34822
35167
  await this.waitForCanvasScene(container, cloned, i);
34823
35168
  }
34824
35169
  console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
34825
- const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-roj_Ddzd.cjs"));
35170
+ const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-De8UFmDy.cjs"));
34826
35171
  const prepared = preparePagesForExport(
34827
35172
  cloned.pages,
34828
35173
  canvasWidth,
@@ -35100,6 +35445,37 @@ class PixldocsRenderer {
35100
35445
  new Promise((r) => setTimeout(r, Math.min(500, maxWaitMs)))
35101
35446
  ]);
35102
35447
  }
35448
+ /**
35449
+ * Flush BOTH process-global text-measurement caches after fonts are ready
35450
+ * and BEFORE the capture canvas mounts.
35451
+ *
35452
+ * Why: anything that measured text before the webfonts finished loading —
35453
+ * most notably `applyContentBoundsPagination` running inside
35454
+ * `resolveFromForm` on a cold page (the EC2/MCP flow: resolve + render in
35455
+ * one browser context) — populates the app measurement cache and Fabric's
35456
+ * global char-width cache with FALLBACK metrics. `createText` then places
35457
+ * center/right-aligned lines from those stale widths, so a right-aligned
35458
+ * grid cell on a continuation page drew shifted left by
35459
+ * (fallbackWidth − realWidth) px vs the editor (caught by
35460
+ * tests/parity `auto-paginate`, a 9px shift). The later
35461
+ * `waitForStableTextMetrics` pass re-primes glyph bounds but never moves
35462
+ * the already-positioned box, so the flush must happen pre-mount.
35463
+ *
35464
+ * Only called from the headless PNG paths (`render`/`renderAllPages`),
35465
+ * which already default to clearing the global char cache in
35466
+ * `waitForStableTextMetrics` — the live-preview-adjacent PDF path keeps
35467
+ * its `clearGlobalCharCache: false` behaviour untouched.
35468
+ */
35469
+ flushTextMeasurementCaches() {
35470
+ try {
35471
+ clearMeasurementCache();
35472
+ } catch {
35473
+ }
35474
+ try {
35475
+ clearFabricCharCache();
35476
+ } catch {
35477
+ }
35478
+ }
35103
35479
  getNormalizedGradientStops(gradient) {
35104
35480
  const stops = Array.isArray(gradient == null ? void 0 : gradient.stops) ? gradient.stops.map((stop) => ({
35105
35481
  offset: Math.max(0, Math.min(1, Number((stop == null ? void 0 : stop.offset) ?? 0))),
@@ -37142,7 +37518,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
37142
37518
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
37143
37519
  sanitizeSvgTreeForPdf(svgToDraw);
37144
37520
  try {
37145
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-roj_Ddzd.cjs"));
37521
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-De8UFmDy.cjs"));
37146
37522
  try {
37147
37523
  await logTextMeasurementDiagnostic(svgToDraw);
37148
37524
  } catch {
@@ -37390,6 +37766,129 @@ async function getPublishedTemplate(options) {
37390
37766
  const rows = await res.json();
37391
37767
  return rows[0] ?? null;
37392
37768
  }
37769
+ const WATERMARK_ID_PREFIX = "__wm_";
37770
+ const DEFAULT_WATERMARK_OPACITY = 0.14;
37771
+ const DEFAULT_WATERMARK_FONT_SIZE = 18;
37772
+ const DEFAULT_WATERMARK_ANGLE = -30;
37773
+ function getWatermarkText() {
37774
+ try {
37775
+ const host = window.location.hostname;
37776
+ if (host.includes("biomaker")) return "biomaker.app";
37777
+ } catch {
37778
+ }
37779
+ return "pixldocs.com";
37780
+ }
37781
+ function getLuminance(color) {
37782
+ if (!color || color === "transparent" || color === "none") return 1;
37783
+ let r = 255, g = 255, b = 255;
37784
+ if (color.startsWith("#")) {
37785
+ const hex = color.slice(1);
37786
+ const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
37787
+ const n = parseInt(full, 16);
37788
+ r = n >> 16 & 255;
37789
+ g = n >> 8 & 255;
37790
+ b = n & 255;
37791
+ } else {
37792
+ const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
37793
+ if (m) {
37794
+ r = +m[1];
37795
+ g = +m[2];
37796
+ b = +m[3];
37797
+ }
37798
+ }
37799
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
37800
+ }
37801
+ function getWatermarkColor(bgColor) {
37802
+ const lum = getLuminance(bgColor);
37803
+ return lum < 0.45 ? "#cccccc" : "#808080";
37804
+ }
37805
+ function getPageBgColor(page, canvasBg) {
37806
+ var _a2, _b2;
37807
+ const settingsBg = (_a2 = page.settings) == null ? void 0 : _a2.backgroundColor;
37808
+ if (settingsBg && settingsBg !== "transparent" && settingsBg !== "none") {
37809
+ return settingsBg;
37810
+ }
37811
+ const children = page.children || [];
37812
+ for (const child of children) {
37813
+ if (child.type === "shape" && child.fill && child.fill !== "transparent" && child.fill !== "none") {
37814
+ return child.fill;
37815
+ }
37816
+ if ((_b2 = child.children) == null ? void 0 : _b2.length) {
37817
+ for (const gc of child.children) {
37818
+ if (gc.type === "shape" && gc.fill && gc.fill !== "transparent" && gc.fill !== "none") {
37819
+ return gc.fill;
37820
+ }
37821
+ }
37822
+ }
37823
+ }
37824
+ return canvasBg;
37825
+ }
37826
+ function generateWatermarkElements(canvasWidth, canvasHeight, wmColor, options = {}) {
37827
+ const elements = [];
37828
+ const text = options.text ?? getWatermarkText();
37829
+ const fontSize = options.fontSize ?? DEFAULT_WATERMARK_FONT_SIZE;
37830
+ const angle = options.angle ?? DEFAULT_WATERMARK_ANGLE;
37831
+ const opacity = options.opacity ?? DEFAULT_WATERMARK_OPACITY;
37832
+ const estimatedTextWidth = text.length * fontSize * 0.55;
37833
+ const spacingX = Math.max(160, estimatedTextWidth + 30);
37834
+ const spacingY = 90;
37835
+ const startX = -canvasWidth * 0.3;
37836
+ const startY = -canvasHeight * 0.3;
37837
+ const endX = canvasWidth * 1.5;
37838
+ const endY = canvasHeight * 1.5;
37839
+ let idx = 0;
37840
+ for (let y = startY; y < endY; y += spacingY) {
37841
+ for (let x = startX; x < endX; x += spacingX) {
37842
+ elements.push({
37843
+ id: `${WATERMARK_ID_PREFIX}${idx++}`,
37844
+ type: "text",
37845
+ left: x,
37846
+ top: y,
37847
+ width: 200,
37848
+ height: 40,
37849
+ text,
37850
+ fontFamily: "Montserrat",
37851
+ fontSize,
37852
+ fontWeight: "600",
37853
+ fill: wmColor,
37854
+ angle,
37855
+ selectable: false,
37856
+ locked: true,
37857
+ visible: true,
37858
+ opacity,
37859
+ scaleX: 1,
37860
+ scaleY: 1,
37861
+ letterSpacing: 4
37862
+ });
37863
+ }
37864
+ }
37865
+ return elements;
37866
+ }
37867
+ function injectWatermark(config, options = {}) {
37868
+ var _a2, _b2, _c2;
37869
+ const canvasWidth = ((_a2 = config.canvas) == null ? void 0 : _a2.width) || 612;
37870
+ const canvasHeight = ((_b2 = config.canvas) == null ? void 0 : _b2.height) || 792;
37871
+ const canvasBg = (_c2 = config.canvas) == null ? void 0 : _c2.backgroundColor;
37872
+ return {
37873
+ ...config,
37874
+ pages: config.pages.map((page) => {
37875
+ const pageBg = getPageBgColor(page, canvasBg);
37876
+ const wmColor = getWatermarkColor(pageBg);
37877
+ const watermarkElements = generateWatermarkElements(canvasWidth, canvasHeight, wmColor, options);
37878
+ return {
37879
+ ...page,
37880
+ children: [
37881
+ ...page.children || [],
37882
+ ...watermarkElements
37883
+ ]
37884
+ };
37885
+ })
37886
+ };
37887
+ }
37888
+ const canvasWatermark = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
37889
+ __proto__: null,
37890
+ injectWatermark
37891
+ }, Symbol.toStringTag, { value: "Module" }));
37393
37892
  function setAutoShrinkDebug(enabled) {
37394
37893
  if (typeof window !== "undefined") {
37395
37894
  window.__pixldocsDebugAutoShrink = !!enabled;
@@ -37436,6 +37935,7 @@ exports.getWorldAngleDeg = getWorldAngleDeg$1;
37436
37935
  exports.hasAnyPreviewBlur = hasAnyPreviewBlur;
37437
37936
  exports.hasEdgeFade = hasEdgeFade;
37438
37937
  exports.injectPreviewBlur = injectPreviewBlur;
37938
+ exports.injectWatermark = injectWatermark;
37439
37939
  exports.isBundledAssetUrl = isBundledAssetUrl;
37440
37940
  exports.isElement = isElement;
37441
37941
  exports.isFontAvailable = isFontAvailable;
@@ -37457,4 +37957,4 @@ exports.setAutoShrinkDebug = setAutoShrinkDebug;
37457
37957
  exports.setBundledAssetPrefixes = setBundledAssetPrefixes;
37458
37958
  exports.warmResolvedTemplateForPreview = warmResolvedTemplateForPreview;
37459
37959
  exports.warmTemplateFromForm = warmTemplateFromForm;
37460
- //# sourceMappingURL=index-DaaT4Mo7.cjs.map
37960
+ //# sourceMappingURL=index-CZm1oVfa.cjs.map