@pixldocs/canvas-renderer 0.5.495 → 0.5.496

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.
@@ -2257,6 +2257,40 @@ function adaptLayoutGrid(grid, mode) {
2257
2257
  const cells = (grid.cells ?? []).map((c) => c && c.grid ? { ...c, grid: adaptLayoutGrid(c.grid, mode) } : c);
2258
2258
  return { ...grid, dir: grid.dir === from ? to : grid.dir, cells };
2259
2259
  }
2260
+ function isAspectLockedNode(node) {
2261
+ const e = node;
2262
+ if (!e) return false;
2263
+ if (e.lockAspect === true) return true;
2264
+ if (e.clipShape === "circle") return true;
2265
+ const kind = e.componentKind || e.smartType;
2266
+ return kind === "qrcode" || kind === "barcode";
2267
+ }
2268
+ function scaleNodeForReflow(node, sx, sy, fscale) {
2269
+ const n = node;
2270
+ const el = { ...n };
2271
+ const hasBox = typeof n.width === "number" && typeof n.height === "number";
2272
+ const f = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
2273
+ if (hasBox && isAspectLockedNode(n) && sx !== sy) {
2274
+ const s = Math.min(sx, sy);
2275
+ const cx = (f(n.left) + n.width / 2) * sx;
2276
+ const cy = (f(n.top) + n.height / 2) * sy;
2277
+ el.width = n.width * s;
2278
+ el.height = n.height * s;
2279
+ el.left = cx - el.width / 2;
2280
+ el.top = cy - el.height / 2;
2281
+ } else {
2282
+ if (typeof el.left === "number") el.left = el.left * sx;
2283
+ if (typeof el.top === "number") el.top = el.top * sy;
2284
+ if (typeof el.width === "number") el.width = el.width * sx;
2285
+ if (typeof el.height === "number") el.height = el.height * sy;
2286
+ }
2287
+ if (typeof el.fontSize === "number") el.fontSize = Math.max(4, el.fontSize * fscale);
2288
+ if (typeof el.minBoxHeight === "number") el.minBoxHeight = el.minBoxHeight * sy;
2289
+ if (Array.isArray(el.children)) {
2290
+ el.children = el.children.map((c) => scaleNodeForReflow(c, sx, sy, fscale));
2291
+ }
2292
+ return el;
2293
+ }
2260
2294
  function computeGridHandles(grid, box, gridPath = []) {
2261
2295
  const out = [];
2262
2296
  const n = grid.cells.length;
@@ -2659,16 +2693,7 @@ const reflowPagesFromBase = (base, W, H) => {
2659
2693
  const fscale = Math.sqrt(Math.max(0.01, sx * sy));
2660
2694
  const adapt = layoutAdaptationFor(base.width, base.height, W, H);
2661
2695
  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
- };
2696
+ const scaleNode = (node) => scaleNodeForReflow(node, sx, sy, fscale);
2672
2697
  return base.pages.map((p) => {
2673
2698
  const pg = p;
2674
2699
  const page = {
@@ -3257,6 +3282,13 @@ const useEditorStore = zustand.create((set, get) => ({
3257
3282
  return { canvas: nextCanvas, ...committed };
3258
3283
  });
3259
3284
  },
3285
+ getCanvasForSave: () => {
3286
+ const { canvas } = get();
3287
+ const base = sizeBaseSnapshot;
3288
+ if (!base) return canvas;
3289
+ if (canvas.width === base.width && canvas.height === base.height) return canvas;
3290
+ return { ...canvas, width: base.width, height: base.height, pages: base.pages };
3291
+ },
3260
3292
  addSizeVariant: (variant) => set((state) => {
3261
3293
  const sizes = state.canvas.sizes ?? [];
3262
3294
  if (sizes.some((s) => s.id === variant.id)) return {};
@@ -6510,8 +6542,14 @@ async function loadImageAsync(element, placeholder, fc, fabricRef, syncLockedRef
6510
6542
  shape,
6511
6543
  rx: rxRatio,
6512
6544
  // Pass ratio, not pixel value
6513
- stroke: element.stroke,
6514
- strokeWidth: element.strokeWidth,
6545
+ // Image outline is an ELEMENT property (imageBorder*), not the generic
6546
+ // shape stroke — this builder forwarded only stroke/strokeWidth, so
6547
+ // outline/gap never reached it.
6548
+ stroke: (element.imageBorderWidth ?? 0) > 0 ? element.imageBorderColor ?? "#FFFFFF" : element.stroke,
6549
+ strokeWidth: (element.imageBorderWidth ?? 0) > 0 ? element.imageBorderWidth : element.strokeWidth,
6550
+ borderPadding: element.imageBorderPadding ?? 0,
6551
+ borderPaddingColor: element.imageBorderPaddingColor ?? "transparent",
6552
+ borderPosition: element.imageBorderPosition ?? "outside",
6515
6553
  panX,
6516
6554
  panY,
6517
6555
  zoom
@@ -7062,10 +7100,10 @@ function finalizeCropGroupCoords(g) {
7062
7100
  g.setCoords();
7063
7101
  }
7064
7102
  function updateCoverLayout(g) {
7065
- var _a2;
7103
+ var _a2, _b2;
7066
7104
  const ct = g.__cropData;
7067
7105
  if (!ct) return;
7068
- const { frameW, frameH, shape, rx: rxRatio, _img: img, _border: border } = ct;
7106
+ const { frameW, frameH, shape, rx: rxRatio, _img: img } = ct;
7069
7107
  const minDim = Math.min(frameW, frameH);
7070
7108
  let rx = rxRatio > 0.5 ? rxRatio : rxRatio * minDim;
7071
7109
  rx = Math.max(0, Math.min(rx, frameW / 2, frameH / 2));
@@ -7198,25 +7236,31 @@ function updateCoverLayout(g) {
7198
7236
  g.clipPath.excludeFromExport = true;
7199
7237
  }
7200
7238
  }
7239
+ const wantCircle = shape === "circle";
7240
+ const border = ensureRingShapeClass(g, ct, "_border", wantCircle);
7241
+ const mat = ensureRingShapeClass(g, ct, "_mat", wantCircle);
7201
7242
  const bw = border ? Number(border.strokeWidth) || 0 : 0;
7202
- const mat = ct._mat;
7203
7243
  const pad = Math.max(0, Number(ct.borderPadding) || 0);
7244
+ const outward = (ct.borderPosition ?? "outside") === "outside";
7204
7245
  const borderMinDim = Math.min(frameW, frameH);
7205
7246
  let borderRx = rxRatio > 0.5 ? rxRatio : rxRatio * borderMinDim;
7206
7247
  borderRx = Math.max(0, Math.min(borderRx, frameW / 2, frameH / 2));
7248
+ const geo = computeOutlineGeometry({ frameW, frameH, bw, pad, rx: borderRx, outward });
7207
7249
  if (border) {
7208
7250
  if (shape === "circle") {
7209
- border.set({ rx: (frameW - bw) / 2, ry: (frameH - bw) / 2 });
7251
+ border.set({ rx: geo.border.width / 2, ry: geo.border.height / 2 });
7210
7252
  } else {
7211
- border.set({ width: frameW - bw, height: frameH - bw, rx: borderRx, ry: borderRx });
7253
+ border.set({ width: geo.border.width, height: geo.border.height, rx: geo.border.rx, ry: geo.border.rx });
7212
7254
  }
7255
+ border.dirty = true;
7213
7256
  }
7214
7257
  if (mat) {
7215
7258
  if (shape === "circle") {
7216
- mat.set({ rx: (frameW - 2 * bw - pad) / 2, ry: (frameH - 2 * bw - pad) / 2 });
7259
+ mat.set({ rx: geo.mat.width / 2, ry: geo.mat.height / 2 });
7217
7260
  } 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) });
7261
+ mat.set({ width: geo.mat.width, height: geo.mat.height, rx: geo.mat.rx, ry: geo.mat.rx });
7219
7262
  }
7263
+ mat.dirty = true;
7220
7264
  }
7221
7265
  if (!img) {
7222
7266
  const placeholderFrame = ct._placeholderFrame ?? ((_a2 = g._objects) == null ? void 0 : _a2.find((obj) => obj.__isPlaceholderFrame));
@@ -7245,8 +7289,9 @@ function updateCoverLayout(g) {
7245
7289
  img._ct.panX = img.__panX ?? 0.5;
7246
7290
  img._ct.panY = img.__panY ?? 0.5;
7247
7291
  }
7248
- const iw = img.width || 1;
7249
- const ih = img.height || 1;
7292
+ const srcEl = img._originalElement ?? ((_b2 = img.getElement) == null ? void 0 : _b2.call(img));
7293
+ const iw = Number(srcEl == null ? void 0 : srcEl.naturalWidth) || Number(srcEl == null ? void 0 : srcEl.width) || img.width || 1;
7294
+ const ih = Number(srcEl == null ? void 0 : srcEl.naturalHeight) || Number(srcEl == null ? void 0 : srcEl.height) || img.height || 1;
7250
7295
  const fitContain = ct.fit === "contain";
7251
7296
  const baseScale = fitContain ? Math.min(frameW / iw, frameH / ih) : Math.max(frameW / iw, frameH / ih);
7252
7297
  const zoom = fitContain ? Math.max(1, Math.min(3, ct.containScale ?? 1)) : Math.max(1, img._ct.zoom ?? 1);
@@ -7283,6 +7328,42 @@ function updateCoverLayout(g) {
7283
7328
  offsetY = overflowY > 0 ? -overflowY * (panY - 0.5) : 0;
7284
7329
  }
7285
7330
  img.set({ left: offsetX, top: offsetY });
7331
+ const wantsOutline = bw > 0 || pad > 0;
7332
+ const shapedMask = shape === "circle" || shape === "roundRect";
7333
+ if (wantsOutline && shapedMask) {
7334
+ if (g.clipPath) {
7335
+ img.clipPath = g.clipPath;
7336
+ g.clipPath = void 0;
7337
+ }
7338
+ if (img.cropX || img.cropY || img.width !== iw) {
7339
+ img.set({ width: iw, height: ih, cropX: 0, cropY: 0 });
7340
+ }
7341
+ } else if (wantsOutline && !fitContain) {
7342
+ const regionW = Math.min(iw, frameW / finalScale);
7343
+ const regionH = Math.min(ih, frameH / finalScale);
7344
+ img.set({
7345
+ width: regionW,
7346
+ height: regionH,
7347
+ cropX: (iw - regionW) * panX,
7348
+ cropY: (ih - regionH) * panY,
7349
+ left: 0,
7350
+ top: 0
7351
+ });
7352
+ img.clipPath = void 0;
7353
+ g.clipPath = void 0;
7354
+ } else {
7355
+ if (img.clipPath) img.clipPath = void 0;
7356
+ if (img.cropX || img.cropY || img.width !== iw) {
7357
+ img.set({ width: iw, height: ih, cropX: 0, cropY: 0 });
7358
+ }
7359
+ }
7360
+ const imgClip = img.clipPath;
7361
+ if (imgClip && typeof imgClip.set === "function") {
7362
+ const csx = img.scaleX || 1;
7363
+ const csy = img.scaleY || 1;
7364
+ imgClip.set({ left: -offsetX / csx, top: -offsetY / csy, scaleX: 1 / csx, scaleY: 1 / csy });
7365
+ imgClip.dirty = true;
7366
+ }
7286
7367
  g.dirty = true;
7287
7368
  img.dirty = true;
7288
7369
  if (g.clipPath) {
@@ -7633,6 +7714,51 @@ function installCanvaMaskControls(g) {
7633
7714
  g.set(controlStyle);
7634
7715
  g.setCoords();
7635
7716
  }
7717
+ function ensureRingShapeClass(g, ct, key, wantCircle) {
7718
+ const cur = ct[key];
7719
+ if (!cur) return void 0;
7720
+ const isEllipse = cur instanceof fabric__namespace.Ellipse;
7721
+ if (isEllipse === wantCircle) return cur;
7722
+ const common = {
7723
+ left: 0,
7724
+ top: 0,
7725
+ originX: "center",
7726
+ originY: "center",
7727
+ fill: "transparent",
7728
+ stroke: cur.stroke,
7729
+ strokeWidth: cur.strokeWidth,
7730
+ selectable: false,
7731
+ evented: false
7732
+ };
7733
+ 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 });
7734
+ const objs = g._objects;
7735
+ const idx = objs ? objs.indexOf(cur) : -1;
7736
+ if (objs && idx >= 0) objs.splice(idx, 1, next);
7737
+ else return cur;
7738
+ next.group = cur.group ?? g;
7739
+ next.canvas = cur.canvas ?? g.canvas;
7740
+ cur.group = void 0;
7741
+ next.setCoords();
7742
+ ct[key] = next;
7743
+ g.dirty = true;
7744
+ return next;
7745
+ }
7746
+ function computeOutlineGeometry(opts) {
7747
+ const { frameW, frameH, bw, pad, rx, outward } = opts;
7748
+ if (outward) {
7749
+ const grow = (r, by) => r > 0 ? r + by : 0;
7750
+ return {
7751
+ // Ring centre-line is pad + bw/2 past the frame → inner edge exactly `pad` out.
7752
+ border: { width: frameW + 2 * pad + bw, height: frameH + 2 * pad + bw, rx: grow(rx, pad + bw / 2) },
7753
+ // The gap band is transparent; it exists only to keep the child list stable.
7754
+ mat: { width: frameW + pad, height: frameH + pad, rx: grow(rx, pad / 2) }
7755
+ };
7756
+ }
7757
+ return {
7758
+ border: { width: frameW - bw, height: frameH - bw, rx },
7759
+ mat: { width: frameW - 2 * bw - pad, height: frameH - 2 * bw - pad, rx: Math.max(0, rx - bw) }
7760
+ };
7761
+ }
7636
7762
  async function createMaskedImageElement({
7637
7763
  url,
7638
7764
  image,
@@ -7661,7 +7787,8 @@ async function createMaskedImageElement({
7661
7787
  containScale = 1,
7662
7788
  shadow = null,
7663
7789
  borderPadding = 0,
7664
- borderPaddingColor = "#FFFFFF"
7790
+ borderPaddingColor = "transparent",
7791
+ borderPosition = "outside"
7665
7792
  }) {
7666
7793
  const img = image || (url ? await fabric__namespace.FabricImage.fromURL(getProxiedImageUrl(url), { crossOrigin: "anonymous" }) : null);
7667
7794
  if (!img) {
@@ -7686,36 +7813,47 @@ async function createMaskedImageElement({
7686
7813
  });
7687
7814
  const bw = strokeWidth || 0;
7688
7815
  const pad = Math.max(0, borderPadding || 0);
7816
+ const outward = borderPosition === "outside";
7817
+ const geo = computeOutlineGeometry({ frameW, frameH, bw, pad, rx, outward });
7818
+ const matStroke = outward ? borderPaddingColor || "transparent" : borderPaddingColor;
7819
+ const matW = geo.mat.width;
7820
+ const matH = geo.mat.height;
7821
+ const matRx = geo.mat.rx;
7689
7822
  const mat = shape === "circle" ? new fabric__namespace.Ellipse({
7690
- rx: (frameW - 2 * bw - pad) / 2,
7691
- ry: (frameH - 2 * bw - pad) / 2,
7823
+ rx: matW / 2,
7824
+ ry: matH / 2,
7692
7825
  left: 0,
7693
7826
  top: 0,
7694
7827
  originX: "center",
7695
7828
  originY: "center",
7696
7829
  fill: "transparent",
7697
- stroke: borderPaddingColor,
7830
+ stroke: matStroke,
7698
7831
  strokeWidth: pad,
7699
7832
  selectable: false,
7700
- evented: false
7833
+ evented: false,
7834
+ objectCaching: false
7701
7835
  }) : 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),
7836
+ width: matW,
7837
+ height: matH,
7838
+ rx: matRx,
7839
+ ry: matRx,
7706
7840
  left: 0,
7707
7841
  top: 0,
7708
7842
  originX: "center",
7709
7843
  originY: "center",
7710
7844
  fill: "transparent",
7711
- stroke: borderPaddingColor,
7845
+ stroke: matStroke,
7712
7846
  strokeWidth: pad,
7713
7847
  selectable: false,
7714
- evented: false
7848
+ evented: false,
7849
+ objectCaching: false
7715
7850
  });
7851
+ const bordW = geo.border.width;
7852
+ const bordH = geo.border.height;
7853
+ const bordRx = geo.border.rx;
7716
7854
  const border = shape === "circle" ? new fabric__namespace.Ellipse({
7717
- rx: (frameW - bw) / 2,
7718
- ry: (frameH - bw) / 2,
7855
+ rx: bordW / 2,
7856
+ ry: bordH / 2,
7719
7857
  left: 0,
7720
7858
  top: 0,
7721
7859
  originX: "center",
@@ -7724,12 +7862,13 @@ async function createMaskedImageElement({
7724
7862
  stroke: stroke || "transparent",
7725
7863
  strokeWidth: bw,
7726
7864
  selectable: false,
7727
- evented: false
7865
+ evented: false,
7866
+ objectCaching: false
7728
7867
  }) : new fabric__namespace.Rect({
7729
- width: frameW - bw,
7730
- height: frameH - bw,
7731
- rx,
7732
- ry: rx,
7868
+ width: bordW,
7869
+ height: bordH,
7870
+ rx: bordRx,
7871
+ ry: bordRx,
7733
7872
  left: 0,
7734
7873
  top: 0,
7735
7874
  originX: "center",
@@ -7738,7 +7877,8 @@ async function createMaskedImageElement({
7738
7877
  stroke: stroke || "transparent",
7739
7878
  strokeWidth: bw,
7740
7879
  selectable: false,
7741
- evented: false
7880
+ evented: false,
7881
+ objectCaching: false
7742
7882
  });
7743
7883
  img.set({
7744
7884
  left: 0,
@@ -7832,7 +7972,9 @@ async function createMaskedImageElement({
7832
7972
  _border: border,
7833
7973
  _mat: mat,
7834
7974
  borderPadding: pad,
7835
- borderPaddingColor
7975
+ borderPaddingColor,
7976
+ // Read back by refreshMaskedImage so it recomputes with the same geometry.
7977
+ borderPosition
7836
7978
  };
7837
7979
  g._ct = g._ct || {};
7838
7980
  g._ct.isCropGroup = true;
@@ -7866,7 +8008,12 @@ async function createMaskedImageElement({
7866
8008
  });
7867
8009
  clip.absolutePositioned = false;
7868
8010
  clip.excludeFromExport = true;
7869
- g.clipPath = clip;
8011
+ if (outward) {
8012
+ img.clipPath = clip;
8013
+ g.clipPath = void 0;
8014
+ } else {
8015
+ g.clipPath = clip;
8016
+ }
7870
8017
  g.set({
7871
8018
  selectable: true,
7872
8019
  evented: true,
@@ -23956,11 +24103,33 @@ const PageCanvas = react.forwardRef(
23956
24103
  const wantBW = Math.max(0, Number(element.imageBorderWidth) || 0);
23957
24104
  const wantBC = element.imageBorderColor ?? "#FFFFFF";
23958
24105
  const wantPad = Math.max(0, Number(element.imageBorderPadding) || 0);
23959
- const wantPC = element.imageBorderPaddingColor ?? "#FFFFFF";
24106
+ const wantPC = element.imageBorderPaddingColor ?? "transparent";
24107
+ const wantPos = element.imageBorderPosition ?? "outside";
23960
24108
  if (ct._border) ct._border.set({ stroke: wantBW > 0 ? wantBC : "transparent", strokeWidth: wantBW, dirty: true });
23961
24109
  if (ct._mat) ct._mat.set({ stroke: wantPC, strokeWidth: wantPad, dirty: true });
23962
24110
  ct.borderPadding = wantPad;
23963
24111
  ct.borderPaddingColor = wantPC;
24112
+ ct.borderPosition = wantPos;
24113
+ {
24114
+ const gMinDim = Math.min(ct.frameW, ct.frameH);
24115
+ const rxRaw = ct.rx || 0;
24116
+ const gRx = Math.max(0, Math.min(rxRaw > 0.5 ? rxRaw : rxRaw * gMinDim, ct.frameW / 2, ct.frameH / 2));
24117
+ const gGeo = computeOutlineGeometry({
24118
+ frameW: ct.frameW,
24119
+ frameH: ct.frameH,
24120
+ bw: wantBW,
24121
+ pad: wantPad,
24122
+ rx: gRx,
24123
+ outward: wantPos === "outside"
24124
+ });
24125
+ const isCircle = (ct.shape || "rect") === "circle";
24126
+ if (ct._border) {
24127
+ 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 });
24128
+ }
24129
+ if (ct._mat) {
24130
+ 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 });
24131
+ }
24132
+ }
23964
24133
  const sBlur = Math.max(0, Number(element.imageShadowBlur) || 0);
23965
24134
  const sColor = element.imageShadowColor;
23966
24135
  if (sBlur > 0 || sColor) {
@@ -26121,7 +26290,9 @@ const PageCanvas = react.forwardRef(
26121
26290
  strokeWidth: element.imageBorderWidth ?? 0,
26122
26291
  // Gap between the image and the outline (the "matted photo" look).
26123
26292
  borderPadding: element.imageBorderPadding ?? 0,
26124
- borderPaddingColor: element.imageBorderPaddingColor ?? "#FFFFFF",
26293
+ // Gap is empty space by default, not a white mat.
26294
+ borderPaddingColor: element.imageBorderPaddingColor ?? "transparent",
26295
+ borderPosition: element.imageBorderPosition ?? "outside",
26125
26296
  // Optional image drop shadow (element property).
26126
26297
  shadow: (element.imageShadowBlur ?? 0) > 0 || element.imageShadowColor ? {
26127
26298
  color: element.imageShadowColor ?? "rgba(0,0,0,0.25)",
@@ -28883,7 +29054,9 @@ function expandBoundTables(pages, formValues) {
28883
29054
  const t = node;
28884
29055
  if (t.type === "table" && ((_a2 = t.tableData) == null ? void 0 : _a2.repeat)) {
28885
29056
  const entries = tableEntriesFor(t.tableData.repeat.from, formValues);
28886
- t.tableData = expandTableRepeat(t.tableData, entries);
29057
+ if (entries.length > 0) {
29058
+ t.tableData = expandTableRepeat(t.tableData, entries);
29059
+ }
28887
29060
  for (const row of t.tableData.cells) {
28888
29061
  for (const cell of row) {
28889
29062
  if (!cell.field) continue;
@@ -30038,7 +30211,12 @@ function pagesHaveConstantTokens(pages) {
30038
30211
  return pages.some((p) => walk(p.children));
30039
30212
  }
30040
30213
  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);
30214
+ const sub = (s) => s.replace(/\{\{\s*\$([a-zA-Z0-9_]+)\s*\}\}/g, (m, name) => (
30215
+ // Unresolved tokens are ERASED rather than left as literal braces: on a
30216
+ // money cell a missing symbol still reads as a number, whereas
30217
+ // "{{$currency}}9,500.00" is visibly broken output.
30218
+ Object.prototype.hasOwnProperty.call(constants, name) ? String(constants[name]) : ""
30219
+ ));
30042
30220
  const walk = (nodes) => {
30043
30221
  for (const n of nodes ?? []) {
30044
30222
  if (n.type === "text") {
@@ -30069,10 +30247,9 @@ function applyContentBoundsPagination(config) {
30069
30247
  }
30070
30248
  let mutated = continuationMasters.length > 0;
30071
30249
  const constants = config.constants;
30072
- const hasConstants = !!constants && typeof constants === "object" && Object.keys(constants).length > 0;
30073
- if (hasConstants && pagesHaveConstantTokens(pages)) {
30250
+ if (pagesHaveConstantTokens(pages)) {
30074
30251
  pages = pages.map((p) => JSON.parse(JSON.stringify(p)));
30075
- substituteConstantTokens(pages, constants);
30252
+ substituteConstantTokens(pages, constants && typeof constants === "object" ? constants : {});
30076
30253
  mutated = true;
30077
30254
  }
30078
30255
  let resultPages = [];
@@ -32242,8 +32419,9 @@ async function resolveTemplateData(options) {
32242
32419
  void 0,
32243
32420
  repeatablePagesInput.length > 0 ? repeatablePagesInput : void 0
32244
32421
  );
32422
+ const paginated = applyContentBoundsPagination(resolvedConfig);
32245
32423
  return {
32246
- config: applyThemeIfNeeded(resolvedConfig),
32424
+ config: applyThemeIfNeeded(paginated),
32247
32425
  templateName: template.name || "Untitled",
32248
32426
  templateId,
32249
32427
  price: template.price ?? 0
@@ -33884,6 +34062,43 @@ function stampPixldocsImageIdOnSvg(svg, id) {
33884
34062
  const attr = ` data-pixldocs-image-id="${escapeSvgDataAttr(id)}"`;
33885
34063
  return svg.replace(/<image\b/i, `<image${attr}`);
33886
34064
  }
34065
+ function colorHasAlpha(color) {
34066
+ if (typeof color !== "string") return false;
34067
+ const c = color.trim().toLowerCase();
34068
+ if (c === "transparent") return true;
34069
+ const rgba = c.match(/^rgba?\(([^)]+)\)$/);
34070
+ if (rgba) {
34071
+ const parts = rgba[1].split(",").map((x) => x.trim());
34072
+ return parts.length > 3 && Number(parts[3]) < 1;
34073
+ }
34074
+ if (/^#[0-9a-f]{8}$/.test(c)) return parseInt(c.slice(7, 9), 16) < 255;
34075
+ if (/^#[0-9a-f]{4}$/.test(c)) return parseInt(c[4] + c[4], 16) < 255;
34076
+ return false;
34077
+ }
34078
+ function paintIsAlphaGradient(paint) {
34079
+ const stops = paint == null ? void 0 : paint.colorStops;
34080
+ if (!Array.isArray(stops) || !stops.length) return false;
34081
+ return stops.some((st) => colorHasAlpha(st == null ? void 0 : st.color) || typeof (st == null ? void 0 : st.opacity) === "number" && st.opacity < 1);
34082
+ }
34083
+ function objectNeedsAlphaGradientRaster(obj) {
34084
+ if (!obj) return false;
34085
+ return paintIsAlphaGradient(obj.fill) || paintIsAlphaGradient(obj.stroke);
34086
+ }
34087
+ function rasterizeObjectToSvgImage(obj, multiplier) {
34088
+ try {
34089
+ if (typeof obj.toDataURL !== "function") return null;
34090
+ const rect = obj.getBoundingRect();
34091
+ if (!rect || !(rect.width > 0) || !(rect.height > 0)) return null;
34092
+ const url = obj.toDataURL({ multiplier, enableRetinaScaling: false });
34093
+ if (typeof url !== "string" || !url.startsWith("data:image")) return null;
34094
+ const n = (v) => Number(v.toFixed(4));
34095
+ return `<image x="${n(rect.left)}" y="${n(rect.top)}" width="${n(rect.width)}" height="${n(rect.height)}" preserveAspectRatio="none" xlink:href="${url}"></image>`;
34096
+ } catch (e) {
34097
+ console.warn("[canvas-svg-capture][alphaGradient] raster failed:", e);
34098
+ return null;
34099
+ }
34100
+ }
34101
+ const ALPHA_GRADIENT_RASTER_MULTIPLIER = 3;
33887
34102
  function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight) {
33888
34103
  const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
33889
34104
  const prevSvgVPT = fabricInstance.svgViewportTransformation;
@@ -33905,9 +34120,14 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
33905
34120
  const visit = (obj) => {
33906
34121
  if (!obj) return;
33907
34122
  const imageId = typeof obj.__docuforgeId === "string" && hasRenderableRasterCandidate(obj) ? obj.__docuforgeId : "";
33908
- if ((isTextboxLike(obj) || imageId) && typeof obj.toSVG === "function") {
34123
+ const alphaGradient = objectNeedsAlphaGradientRaster(obj);
34124
+ if ((isTextboxLike(obj) || imageId || alphaGradient) && typeof obj.toSVG === "function") {
33909
34125
  const originalToSVG = obj.toSVG.bind(obj);
33910
34126
  obj.toSVG = (reviver) => {
34127
+ if (alphaGradient) {
34128
+ const raster = rasterizeObjectToSvgImage(obj, ALPHA_GRADIENT_RASTER_MULTIPLIER);
34129
+ if (raster) return raster;
34130
+ }
33911
34131
  let svg = originalToSVG(reviver);
33912
34132
  if (isTextboxLike(obj)) svg = stampFabricLineMetricsOnTextSvg(svg, obj);
33913
34133
  if (isTextboxLike(obj)) svg = warpTextboxSvgAlongPath(svg, obj);
@@ -34006,9 +34226,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
34006
34226
  }
34007
34227
  return svgString;
34008
34228
  }
34009
- const resolvedPackageVersion = "0.5.495";
34229
+ const resolvedPackageVersion = "0.5.496";
34010
34230
  const PACKAGE_VERSION = resolvedPackageVersion;
34011
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.495";
34231
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.496";
34012
34232
  const roundParityValue = (value) => {
34013
34233
  if (typeof value !== "number") return value;
34014
34234
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -34822,7 +35042,7 @@ class PixldocsRenderer {
34822
35042
  await this.waitForCanvasScene(container, cloned, i);
34823
35043
  }
34824
35044
  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"));
35045
+ const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-B2KtFs76.cjs"));
34826
35046
  const prepared = preparePagesForExport(
34827
35047
  cloned.pages,
34828
35048
  canvasWidth,
@@ -37142,7 +37362,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
37142
37362
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
37143
37363
  sanitizeSvgTreeForPdf(svgToDraw);
37144
37364
  try {
37145
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-roj_Ddzd.cjs"));
37365
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-B2KtFs76.cjs"));
37146
37366
  try {
37147
37367
  await logTextMeasurementDiagnostic(svgToDraw);
37148
37368
  } catch {
@@ -37457,4 +37677,4 @@ exports.setAutoShrinkDebug = setAutoShrinkDebug;
37457
37677
  exports.setBundledAssetPrefixes = setBundledAssetPrefixes;
37458
37678
  exports.warmResolvedTemplateForPreview = warmResolvedTemplateForPreview;
37459
37679
  exports.warmTemplateFromForm = warmTemplateFromForm;
37460
- //# sourceMappingURL=index-DaaT4Mo7.cjs.map
37680
+ //# sourceMappingURL=index-CZtvER5z.cjs.map