@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.
@@ -2239,6 +2239,40 @@ function adaptLayoutGrid(grid, mode) {
2239
2239
  const cells = (grid.cells ?? []).map((c) => c && c.grid ? { ...c, grid: adaptLayoutGrid(c.grid, mode) } : c);
2240
2240
  return { ...grid, dir: grid.dir === from ? to : grid.dir, cells };
2241
2241
  }
2242
+ function isAspectLockedNode(node) {
2243
+ const e = node;
2244
+ if (!e) return false;
2245
+ if (e.lockAspect === true) return true;
2246
+ if (e.clipShape === "circle") return true;
2247
+ const kind = e.componentKind || e.smartType;
2248
+ return kind === "qrcode" || kind === "barcode";
2249
+ }
2250
+ function scaleNodeForReflow(node, sx, sy, fscale) {
2251
+ const n = node;
2252
+ const el = { ...n };
2253
+ const hasBox = typeof n.width === "number" && typeof n.height === "number";
2254
+ const f = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
2255
+ if (hasBox && isAspectLockedNode(n) && sx !== sy) {
2256
+ const s = Math.min(sx, sy);
2257
+ const cx = (f(n.left) + n.width / 2) * sx;
2258
+ const cy = (f(n.top) + n.height / 2) * sy;
2259
+ el.width = n.width * s;
2260
+ el.height = n.height * s;
2261
+ el.left = cx - el.width / 2;
2262
+ el.top = cy - el.height / 2;
2263
+ } else {
2264
+ if (typeof el.left === "number") el.left = el.left * sx;
2265
+ if (typeof el.top === "number") el.top = el.top * sy;
2266
+ if (typeof el.width === "number") el.width = el.width * sx;
2267
+ if (typeof el.height === "number") el.height = el.height * sy;
2268
+ }
2269
+ if (typeof el.fontSize === "number") el.fontSize = Math.max(4, el.fontSize * fscale);
2270
+ if (typeof el.minBoxHeight === "number") el.minBoxHeight = el.minBoxHeight * sy;
2271
+ if (Array.isArray(el.children)) {
2272
+ el.children = el.children.map((c) => scaleNodeForReflow(c, sx, sy, fscale));
2273
+ }
2274
+ return el;
2275
+ }
2242
2276
  function computeGridHandles(grid, box, gridPath = []) {
2243
2277
  const out = [];
2244
2278
  const n = grid.cells.length;
@@ -2641,16 +2675,7 @@ const reflowPagesFromBase = (base, W, H) => {
2641
2675
  const fscale = Math.sqrt(Math.max(0.01, sx * sy));
2642
2676
  const adapt = layoutAdaptationFor(base.width, base.height, W, H);
2643
2677
  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
- };
2678
+ const scaleNode = (node) => scaleNodeForReflow(node, sx, sy, fscale);
2654
2679
  return base.pages.map((p) => {
2655
2680
  const pg = p;
2656
2681
  const page = {
@@ -3239,6 +3264,13 @@ const useEditorStore = create((set, get) => ({
3239
3264
  return { canvas: nextCanvas, ...committed };
3240
3265
  });
3241
3266
  },
3267
+ getCanvasForSave: () => {
3268
+ const { canvas } = get();
3269
+ const base = sizeBaseSnapshot;
3270
+ if (!base) return canvas;
3271
+ if (canvas.width === base.width && canvas.height === base.height) return canvas;
3272
+ return { ...canvas, width: base.width, height: base.height, pages: base.pages };
3273
+ },
3242
3274
  addSizeVariant: (variant) => set((state) => {
3243
3275
  const sizes = state.canvas.sizes ?? [];
3244
3276
  if (sizes.some((s) => s.id === variant.id)) return {};
@@ -6492,8 +6524,14 @@ async function loadImageAsync(element, placeholder, fc, fabricRef, syncLockedRef
6492
6524
  shape,
6493
6525
  rx: rxRatio,
6494
6526
  // Pass ratio, not pixel value
6495
- stroke: element.stroke,
6496
- strokeWidth: element.strokeWidth,
6527
+ // Image outline is an ELEMENT property (imageBorder*), not the generic
6528
+ // shape stroke — this builder forwarded only stroke/strokeWidth, so
6529
+ // outline/gap never reached it.
6530
+ stroke: (element.imageBorderWidth ?? 0) > 0 ? element.imageBorderColor ?? "#FFFFFF" : element.stroke,
6531
+ strokeWidth: (element.imageBorderWidth ?? 0) > 0 ? element.imageBorderWidth : element.strokeWidth,
6532
+ borderPadding: element.imageBorderPadding ?? 0,
6533
+ borderPaddingColor: element.imageBorderPaddingColor ?? "transparent",
6534
+ borderPosition: element.imageBorderPosition ?? "outside",
6497
6535
  panX,
6498
6536
  panY,
6499
6537
  zoom
@@ -7044,10 +7082,10 @@ function finalizeCropGroupCoords(g) {
7044
7082
  g.setCoords();
7045
7083
  }
7046
7084
  function updateCoverLayout(g) {
7047
- var _a2;
7085
+ var _a2, _b2;
7048
7086
  const ct = g.__cropData;
7049
7087
  if (!ct) return;
7050
- const { frameW, frameH, shape, rx: rxRatio, _img: img, _border: border } = ct;
7088
+ const { frameW, frameH, shape, rx: rxRatio, _img: img } = ct;
7051
7089
  const minDim = Math.min(frameW, frameH);
7052
7090
  let rx = rxRatio > 0.5 ? rxRatio : rxRatio * minDim;
7053
7091
  rx = Math.max(0, Math.min(rx, frameW / 2, frameH / 2));
@@ -7180,25 +7218,31 @@ function updateCoverLayout(g) {
7180
7218
  g.clipPath.excludeFromExport = true;
7181
7219
  }
7182
7220
  }
7221
+ const wantCircle = shape === "circle";
7222
+ const border = ensureRingShapeClass(g, ct, "_border", wantCircle);
7223
+ const mat = ensureRingShapeClass(g, ct, "_mat", wantCircle);
7183
7224
  const bw = border ? Number(border.strokeWidth) || 0 : 0;
7184
- const mat = ct._mat;
7185
7225
  const pad = Math.max(0, Number(ct.borderPadding) || 0);
7226
+ const outward = (ct.borderPosition ?? "outside") === "outside";
7186
7227
  const borderMinDim = Math.min(frameW, frameH);
7187
7228
  let borderRx = rxRatio > 0.5 ? rxRatio : rxRatio * borderMinDim;
7188
7229
  borderRx = Math.max(0, Math.min(borderRx, frameW / 2, frameH / 2));
7230
+ const geo = computeOutlineGeometry({ frameW, frameH, bw, pad, rx: borderRx, outward });
7189
7231
  if (border) {
7190
7232
  if (shape === "circle") {
7191
- border.set({ rx: (frameW - bw) / 2, ry: (frameH - bw) / 2 });
7233
+ border.set({ rx: geo.border.width / 2, ry: geo.border.height / 2 });
7192
7234
  } else {
7193
- border.set({ width: frameW - bw, height: frameH - bw, rx: borderRx, ry: borderRx });
7235
+ border.set({ width: geo.border.width, height: geo.border.height, rx: geo.border.rx, ry: geo.border.rx });
7194
7236
  }
7237
+ border.dirty = true;
7195
7238
  }
7196
7239
  if (mat) {
7197
7240
  if (shape === "circle") {
7198
- mat.set({ rx: (frameW - 2 * bw - pad) / 2, ry: (frameH - 2 * bw - pad) / 2 });
7241
+ mat.set({ rx: geo.mat.width / 2, ry: geo.mat.height / 2 });
7199
7242
  } 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) });
7243
+ mat.set({ width: geo.mat.width, height: geo.mat.height, rx: geo.mat.rx, ry: geo.mat.rx });
7201
7244
  }
7245
+ mat.dirty = true;
7202
7246
  }
7203
7247
  if (!img) {
7204
7248
  const placeholderFrame = ct._placeholderFrame ?? ((_a2 = g._objects) == null ? void 0 : _a2.find((obj) => obj.__isPlaceholderFrame));
@@ -7227,8 +7271,9 @@ function updateCoverLayout(g) {
7227
7271
  img._ct.panX = img.__panX ?? 0.5;
7228
7272
  img._ct.panY = img.__panY ?? 0.5;
7229
7273
  }
7230
- const iw = img.width || 1;
7231
- const ih = img.height || 1;
7274
+ const srcEl = img._originalElement ?? ((_b2 = img.getElement) == null ? void 0 : _b2.call(img));
7275
+ const iw = Number(srcEl == null ? void 0 : srcEl.naturalWidth) || Number(srcEl == null ? void 0 : srcEl.width) || img.width || 1;
7276
+ const ih = Number(srcEl == null ? void 0 : srcEl.naturalHeight) || Number(srcEl == null ? void 0 : srcEl.height) || img.height || 1;
7232
7277
  const fitContain = ct.fit === "contain";
7233
7278
  const baseScale = fitContain ? Math.min(frameW / iw, frameH / ih) : Math.max(frameW / iw, frameH / ih);
7234
7279
  const zoom = fitContain ? Math.max(1, Math.min(3, ct.containScale ?? 1)) : Math.max(1, img._ct.zoom ?? 1);
@@ -7265,6 +7310,42 @@ function updateCoverLayout(g) {
7265
7310
  offsetY = overflowY > 0 ? -overflowY * (panY - 0.5) : 0;
7266
7311
  }
7267
7312
  img.set({ left: offsetX, top: offsetY });
7313
+ const wantsOutline = bw > 0 || pad > 0;
7314
+ const shapedMask = shape === "circle" || shape === "roundRect";
7315
+ if (wantsOutline && shapedMask) {
7316
+ if (g.clipPath) {
7317
+ img.clipPath = g.clipPath;
7318
+ g.clipPath = void 0;
7319
+ }
7320
+ if (img.cropX || img.cropY || img.width !== iw) {
7321
+ img.set({ width: iw, height: ih, cropX: 0, cropY: 0 });
7322
+ }
7323
+ } else if (wantsOutline && !fitContain) {
7324
+ const regionW = Math.min(iw, frameW / finalScale);
7325
+ const regionH = Math.min(ih, frameH / finalScale);
7326
+ img.set({
7327
+ width: regionW,
7328
+ height: regionH,
7329
+ cropX: (iw - regionW) * panX,
7330
+ cropY: (ih - regionH) * panY,
7331
+ left: 0,
7332
+ top: 0
7333
+ });
7334
+ img.clipPath = void 0;
7335
+ g.clipPath = void 0;
7336
+ } else {
7337
+ if (img.clipPath) img.clipPath = void 0;
7338
+ if (img.cropX || img.cropY || img.width !== iw) {
7339
+ img.set({ width: iw, height: ih, cropX: 0, cropY: 0 });
7340
+ }
7341
+ }
7342
+ const imgClip = img.clipPath;
7343
+ if (imgClip && typeof imgClip.set === "function") {
7344
+ const csx = img.scaleX || 1;
7345
+ const csy = img.scaleY || 1;
7346
+ imgClip.set({ left: -offsetX / csx, top: -offsetY / csy, scaleX: 1 / csx, scaleY: 1 / csy });
7347
+ imgClip.dirty = true;
7348
+ }
7268
7349
  g.dirty = true;
7269
7350
  img.dirty = true;
7270
7351
  if (g.clipPath) {
@@ -7615,6 +7696,51 @@ function installCanvaMaskControls(g) {
7615
7696
  g.set(controlStyle);
7616
7697
  g.setCoords();
7617
7698
  }
7699
+ function ensureRingShapeClass(g, ct, key, wantCircle) {
7700
+ const cur = ct[key];
7701
+ if (!cur) return void 0;
7702
+ const isEllipse = cur instanceof fabric.Ellipse;
7703
+ if (isEllipse === wantCircle) return cur;
7704
+ const common = {
7705
+ left: 0,
7706
+ top: 0,
7707
+ originX: "center",
7708
+ originY: "center",
7709
+ fill: "transparent",
7710
+ stroke: cur.stroke,
7711
+ strokeWidth: cur.strokeWidth,
7712
+ selectable: false,
7713
+ evented: false
7714
+ };
7715
+ const next = wantCircle ? new fabric.Ellipse({ ...common, rx: 1, ry: 1, objectCaching: false }) : new fabric.Rect({ ...common, width: 1, height: 1, objectCaching: false });
7716
+ const objs = g._objects;
7717
+ const idx = objs ? objs.indexOf(cur) : -1;
7718
+ if (objs && idx >= 0) objs.splice(idx, 1, next);
7719
+ else return cur;
7720
+ next.group = cur.group ?? g;
7721
+ next.canvas = cur.canvas ?? g.canvas;
7722
+ cur.group = void 0;
7723
+ next.setCoords();
7724
+ ct[key] = next;
7725
+ g.dirty = true;
7726
+ return next;
7727
+ }
7728
+ function computeOutlineGeometry(opts) {
7729
+ const { frameW, frameH, bw, pad, rx, outward } = opts;
7730
+ if (outward) {
7731
+ const grow = (r, by) => r > 0 ? r + by : 0;
7732
+ return {
7733
+ // Ring centre-line is pad + bw/2 past the frame → inner edge exactly `pad` out.
7734
+ border: { width: frameW + 2 * pad + bw, height: frameH + 2 * pad + bw, rx: grow(rx, pad + bw / 2) },
7735
+ // The gap band is transparent; it exists only to keep the child list stable.
7736
+ mat: { width: frameW + pad, height: frameH + pad, rx: grow(rx, pad / 2) }
7737
+ };
7738
+ }
7739
+ return {
7740
+ border: { width: frameW - bw, height: frameH - bw, rx },
7741
+ mat: { width: frameW - 2 * bw - pad, height: frameH - 2 * bw - pad, rx: Math.max(0, rx - bw) }
7742
+ };
7743
+ }
7618
7744
  async function createMaskedImageElement({
7619
7745
  url,
7620
7746
  image,
@@ -7643,7 +7769,8 @@ async function createMaskedImageElement({
7643
7769
  containScale = 1,
7644
7770
  shadow = null,
7645
7771
  borderPadding = 0,
7646
- borderPaddingColor = "#FFFFFF"
7772
+ borderPaddingColor = "transparent",
7773
+ borderPosition = "outside"
7647
7774
  }) {
7648
7775
  const img = image || (url ? await fabric.FabricImage.fromURL(getProxiedImageUrl(url), { crossOrigin: "anonymous" }) : null);
7649
7776
  if (!img) {
@@ -7668,36 +7795,47 @@ async function createMaskedImageElement({
7668
7795
  });
7669
7796
  const bw = strokeWidth || 0;
7670
7797
  const pad = Math.max(0, borderPadding || 0);
7798
+ const outward = borderPosition === "outside";
7799
+ const geo = computeOutlineGeometry({ frameW, frameH, bw, pad, rx, outward });
7800
+ const matStroke = outward ? borderPaddingColor || "transparent" : borderPaddingColor;
7801
+ const matW = geo.mat.width;
7802
+ const matH = geo.mat.height;
7803
+ const matRx = geo.mat.rx;
7671
7804
  const mat = shape === "circle" ? new fabric.Ellipse({
7672
- rx: (frameW - 2 * bw - pad) / 2,
7673
- ry: (frameH - 2 * bw - pad) / 2,
7805
+ rx: matW / 2,
7806
+ ry: matH / 2,
7674
7807
  left: 0,
7675
7808
  top: 0,
7676
7809
  originX: "center",
7677
7810
  originY: "center",
7678
7811
  fill: "transparent",
7679
- stroke: borderPaddingColor,
7812
+ stroke: matStroke,
7680
7813
  strokeWidth: pad,
7681
7814
  selectable: false,
7682
- evented: false
7815
+ evented: false,
7816
+ objectCaching: false
7683
7817
  }) : 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),
7818
+ width: matW,
7819
+ height: matH,
7820
+ rx: matRx,
7821
+ ry: matRx,
7688
7822
  left: 0,
7689
7823
  top: 0,
7690
7824
  originX: "center",
7691
7825
  originY: "center",
7692
7826
  fill: "transparent",
7693
- stroke: borderPaddingColor,
7827
+ stroke: matStroke,
7694
7828
  strokeWidth: pad,
7695
7829
  selectable: false,
7696
- evented: false
7830
+ evented: false,
7831
+ objectCaching: false
7697
7832
  });
7833
+ const bordW = geo.border.width;
7834
+ const bordH = geo.border.height;
7835
+ const bordRx = geo.border.rx;
7698
7836
  const border = shape === "circle" ? new fabric.Ellipse({
7699
- rx: (frameW - bw) / 2,
7700
- ry: (frameH - bw) / 2,
7837
+ rx: bordW / 2,
7838
+ ry: bordH / 2,
7701
7839
  left: 0,
7702
7840
  top: 0,
7703
7841
  originX: "center",
@@ -7706,12 +7844,13 @@ async function createMaskedImageElement({
7706
7844
  stroke: stroke || "transparent",
7707
7845
  strokeWidth: bw,
7708
7846
  selectable: false,
7709
- evented: false
7847
+ evented: false,
7848
+ objectCaching: false
7710
7849
  }) : new fabric.Rect({
7711
- width: frameW - bw,
7712
- height: frameH - bw,
7713
- rx,
7714
- ry: rx,
7850
+ width: bordW,
7851
+ height: bordH,
7852
+ rx: bordRx,
7853
+ ry: bordRx,
7715
7854
  left: 0,
7716
7855
  top: 0,
7717
7856
  originX: "center",
@@ -7720,7 +7859,8 @@ async function createMaskedImageElement({
7720
7859
  stroke: stroke || "transparent",
7721
7860
  strokeWidth: bw,
7722
7861
  selectable: false,
7723
- evented: false
7862
+ evented: false,
7863
+ objectCaching: false
7724
7864
  });
7725
7865
  img.set({
7726
7866
  left: 0,
@@ -7814,7 +7954,9 @@ async function createMaskedImageElement({
7814
7954
  _border: border,
7815
7955
  _mat: mat,
7816
7956
  borderPadding: pad,
7817
- borderPaddingColor
7957
+ borderPaddingColor,
7958
+ // Read back by refreshMaskedImage so it recomputes with the same geometry.
7959
+ borderPosition
7818
7960
  };
7819
7961
  g._ct = g._ct || {};
7820
7962
  g._ct.isCropGroup = true;
@@ -7848,7 +7990,12 @@ async function createMaskedImageElement({
7848
7990
  });
7849
7991
  clip.absolutePositioned = false;
7850
7992
  clip.excludeFromExport = true;
7851
- g.clipPath = clip;
7993
+ if (outward) {
7994
+ img.clipPath = clip;
7995
+ g.clipPath = void 0;
7996
+ } else {
7997
+ g.clipPath = clip;
7998
+ }
7852
7999
  g.set({
7853
8000
  selectable: true,
7854
8001
  evented: true,
@@ -23938,11 +24085,33 @@ const PageCanvas = forwardRef(
23938
24085
  const wantBW = Math.max(0, Number(element.imageBorderWidth) || 0);
23939
24086
  const wantBC = element.imageBorderColor ?? "#FFFFFF";
23940
24087
  const wantPad = Math.max(0, Number(element.imageBorderPadding) || 0);
23941
- const wantPC = element.imageBorderPaddingColor ?? "#FFFFFF";
24088
+ const wantPC = element.imageBorderPaddingColor ?? "transparent";
24089
+ const wantPos = element.imageBorderPosition ?? "outside";
23942
24090
  if (ct._border) ct._border.set({ stroke: wantBW > 0 ? wantBC : "transparent", strokeWidth: wantBW, dirty: true });
23943
24091
  if (ct._mat) ct._mat.set({ stroke: wantPC, strokeWidth: wantPad, dirty: true });
23944
24092
  ct.borderPadding = wantPad;
23945
24093
  ct.borderPaddingColor = wantPC;
24094
+ ct.borderPosition = wantPos;
24095
+ {
24096
+ const gMinDim = Math.min(ct.frameW, ct.frameH);
24097
+ const rxRaw = ct.rx || 0;
24098
+ const gRx = Math.max(0, Math.min(rxRaw > 0.5 ? rxRaw : rxRaw * gMinDim, ct.frameW / 2, ct.frameH / 2));
24099
+ const gGeo = computeOutlineGeometry({
24100
+ frameW: ct.frameW,
24101
+ frameH: ct.frameH,
24102
+ bw: wantBW,
24103
+ pad: wantPad,
24104
+ rx: gRx,
24105
+ outward: wantPos === "outside"
24106
+ });
24107
+ const isCircle = (ct.shape || "rect") === "circle";
24108
+ if (ct._border) {
24109
+ 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 });
24110
+ }
24111
+ if (ct._mat) {
24112
+ 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 });
24113
+ }
24114
+ }
23946
24115
  const sBlur = Math.max(0, Number(element.imageShadowBlur) || 0);
23947
24116
  const sColor = element.imageShadowColor;
23948
24117
  if (sBlur > 0 || sColor) {
@@ -26103,7 +26272,9 @@ const PageCanvas = forwardRef(
26103
26272
  strokeWidth: element.imageBorderWidth ?? 0,
26104
26273
  // Gap between the image and the outline (the "matted photo" look).
26105
26274
  borderPadding: element.imageBorderPadding ?? 0,
26106
- borderPaddingColor: element.imageBorderPaddingColor ?? "#FFFFFF",
26275
+ // Gap is empty space by default, not a white mat.
26276
+ borderPaddingColor: element.imageBorderPaddingColor ?? "transparent",
26277
+ borderPosition: element.imageBorderPosition ?? "outside",
26107
26278
  // Optional image drop shadow (element property).
26108
26279
  shadow: (element.imageShadowBlur ?? 0) > 0 || element.imageShadowColor ? {
26109
26280
  color: element.imageShadowColor ?? "rgba(0,0,0,0.25)",
@@ -28865,7 +29036,9 @@ function expandBoundTables(pages, formValues) {
28865
29036
  const t = node;
28866
29037
  if (t.type === "table" && ((_a2 = t.tableData) == null ? void 0 : _a2.repeat)) {
28867
29038
  const entries = tableEntriesFor(t.tableData.repeat.from, formValues);
28868
- t.tableData = expandTableRepeat(t.tableData, entries);
29039
+ if (entries.length > 0) {
29040
+ t.tableData = expandTableRepeat(t.tableData, entries);
29041
+ }
28869
29042
  for (const row of t.tableData.cells) {
28870
29043
  for (const cell of row) {
28871
29044
  if (!cell.field) continue;
@@ -30020,7 +30193,12 @@ function pagesHaveConstantTokens(pages) {
30020
30193
  return pages.some((p) => walk(p.children));
30021
30194
  }
30022
30195
  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);
30196
+ const sub = (s) => s.replace(/\{\{\s*\$([a-zA-Z0-9_]+)\s*\}\}/g, (m, name) => (
30197
+ // Unresolved tokens are ERASED rather than left as literal braces: on a
30198
+ // money cell a missing symbol still reads as a number, whereas
30199
+ // "{{$currency}}9,500.00" is visibly broken output.
30200
+ Object.prototype.hasOwnProperty.call(constants, name) ? String(constants[name]) : ""
30201
+ ));
30024
30202
  const walk = (nodes) => {
30025
30203
  for (const n of nodes ?? []) {
30026
30204
  if (n.type === "text") {
@@ -30051,10 +30229,9 @@ function applyContentBoundsPagination(config) {
30051
30229
  }
30052
30230
  let mutated = continuationMasters.length > 0;
30053
30231
  const constants = config.constants;
30054
- const hasConstants = !!constants && typeof constants === "object" && Object.keys(constants).length > 0;
30055
- if (hasConstants && pagesHaveConstantTokens(pages)) {
30232
+ if (pagesHaveConstantTokens(pages)) {
30056
30233
  pages = pages.map((p) => JSON.parse(JSON.stringify(p)));
30057
- substituteConstantTokens(pages, constants);
30234
+ substituteConstantTokens(pages, constants && typeof constants === "object" ? constants : {});
30058
30235
  mutated = true;
30059
30236
  }
30060
30237
  let resultPages = [];
@@ -32224,8 +32401,9 @@ async function resolveTemplateData(options) {
32224
32401
  void 0,
32225
32402
  repeatablePagesInput.length > 0 ? repeatablePagesInput : void 0
32226
32403
  );
32404
+ const paginated = applyContentBoundsPagination(resolvedConfig);
32227
32405
  return {
32228
- config: applyThemeIfNeeded(resolvedConfig),
32406
+ config: applyThemeIfNeeded(paginated),
32229
32407
  templateName: template.name || "Untitled",
32230
32408
  templateId,
32231
32409
  price: template.price ?? 0
@@ -33866,6 +34044,43 @@ function stampPixldocsImageIdOnSvg(svg, id) {
33866
34044
  const attr = ` data-pixldocs-image-id="${escapeSvgDataAttr(id)}"`;
33867
34045
  return svg.replace(/<image\b/i, `<image${attr}`);
33868
34046
  }
34047
+ function colorHasAlpha(color) {
34048
+ if (typeof color !== "string") return false;
34049
+ const c = color.trim().toLowerCase();
34050
+ if (c === "transparent") return true;
34051
+ const rgba = c.match(/^rgba?\(([^)]+)\)$/);
34052
+ if (rgba) {
34053
+ const parts = rgba[1].split(",").map((x) => x.trim());
34054
+ return parts.length > 3 && Number(parts[3]) < 1;
34055
+ }
34056
+ if (/^#[0-9a-f]{8}$/.test(c)) return parseInt(c.slice(7, 9), 16) < 255;
34057
+ if (/^#[0-9a-f]{4}$/.test(c)) return parseInt(c[4] + c[4], 16) < 255;
34058
+ return false;
34059
+ }
34060
+ function paintIsAlphaGradient(paint) {
34061
+ const stops = paint == null ? void 0 : paint.colorStops;
34062
+ if (!Array.isArray(stops) || !stops.length) return false;
34063
+ return stops.some((st) => colorHasAlpha(st == null ? void 0 : st.color) || typeof (st == null ? void 0 : st.opacity) === "number" && st.opacity < 1);
34064
+ }
34065
+ function objectNeedsAlphaGradientRaster(obj) {
34066
+ if (!obj) return false;
34067
+ return paintIsAlphaGradient(obj.fill) || paintIsAlphaGradient(obj.stroke);
34068
+ }
34069
+ function rasterizeObjectToSvgImage(obj, multiplier) {
34070
+ try {
34071
+ if (typeof obj.toDataURL !== "function") return null;
34072
+ const rect = obj.getBoundingRect();
34073
+ if (!rect || !(rect.width > 0) || !(rect.height > 0)) return null;
34074
+ const url = obj.toDataURL({ multiplier, enableRetinaScaling: false });
34075
+ if (typeof url !== "string" || !url.startsWith("data:image")) return null;
34076
+ const n = (v) => Number(v.toFixed(4));
34077
+ return `<image x="${n(rect.left)}" y="${n(rect.top)}" width="${n(rect.width)}" height="${n(rect.height)}" preserveAspectRatio="none" xlink:href="${url}"></image>`;
34078
+ } catch (e) {
34079
+ console.warn("[canvas-svg-capture][alphaGradient] raster failed:", e);
34080
+ return null;
34081
+ }
34082
+ }
34083
+ const ALPHA_GRADIENT_RASTER_MULTIPLIER = 3;
33869
34084
  function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight) {
33870
34085
  const prevVPT = fabricInstance.viewportTransform ? [...fabricInstance.viewportTransform] : void 0;
33871
34086
  const prevSvgVPT = fabricInstance.svgViewportTransformation;
@@ -33887,9 +34102,14 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
33887
34102
  const visit = (obj) => {
33888
34103
  if (!obj) return;
33889
34104
  const imageId = typeof obj.__docuforgeId === "string" && hasRenderableRasterCandidate(obj) ? obj.__docuforgeId : "";
33890
- if ((isTextboxLike(obj) || imageId) && typeof obj.toSVG === "function") {
34105
+ const alphaGradient = objectNeedsAlphaGradientRaster(obj);
34106
+ if ((isTextboxLike(obj) || imageId || alphaGradient) && typeof obj.toSVG === "function") {
33891
34107
  const originalToSVG = obj.toSVG.bind(obj);
33892
34108
  obj.toSVG = (reviver) => {
34109
+ if (alphaGradient) {
34110
+ const raster = rasterizeObjectToSvgImage(obj, ALPHA_GRADIENT_RASTER_MULTIPLIER);
34111
+ if (raster) return raster;
34112
+ }
33893
34113
  let svg = originalToSVG(reviver);
33894
34114
  if (isTextboxLike(obj)) svg = stampFabricLineMetricsOnTextSvg(svg, obj);
33895
34115
  if (isTextboxLike(obj)) svg = warpTextboxSvgAlongPath(svg, obj);
@@ -33988,9 +34208,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
33988
34208
  }
33989
34209
  return svgString;
33990
34210
  }
33991
- const resolvedPackageVersion = "0.5.495";
34211
+ const resolvedPackageVersion = "0.5.496";
33992
34212
  const PACKAGE_VERSION = resolvedPackageVersion;
33993
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.495";
34213
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.496";
33994
34214
  const roundParityValue = (value) => {
33995
34215
  if (typeof value !== "number") return value;
33996
34216
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -34804,7 +35024,7 @@ class PixldocsRenderer {
34804
35024
  await this.waitForCanvasScene(container, cloned, i);
34805
35025
  }
34806
35026
  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");
35027
+ const { exportMultiPagePdf, preparePagesForExport } = await import("./vectorPdfExport-E3x8GBto.js");
34808
35028
  const prepared = preparePagesForExport(
34809
35029
  cloned.pages,
34810
35030
  canvasWidth,
@@ -37124,7 +37344,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
37124
37344
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
37125
37345
  sanitizeSvgTreeForPdf(svgToDraw);
37126
37346
  try {
37127
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-B6m8o09g.js");
37347
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-E3x8GBto.js");
37128
37348
  try {
37129
37349
  await logTextMeasurementDiagnostic(svgToDraw);
37130
37350
  } catch {
@@ -37442,4 +37662,4 @@ export {
37442
37662
  awaitFontsForConfig as y,
37443
37663
  buildTeaserBlurFlatKeys as z
37444
37664
  };
37445
- //# sourceMappingURL=index-Cvi3YYOq.js.map
37665
+ //# sourceMappingURL=index-CiSoT2Lk.js.map