@pixldocs/canvas-renderer 0.5.82 → 0.5.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -494,29 +494,39 @@ function resolveStackGroupEffectivePositions(group, pageChildren, options) {
494
494
  const mode = group.layoutMode ?? "absolute";
495
495
  if (!isStackLayoutMode(mode)) return /* @__PURE__ */ new Map();
496
496
  const gap = group.stackSpacing ?? 8;
497
+ const padTop = group.paddingTop ?? 0;
498
+ const padLeft = group.paddingLeft ?? 0;
497
499
  const kids = group.children ?? [];
498
500
  const out = /* @__PURE__ */ new Map();
499
501
  if (isVerticalStackLayoutMode(mode)) {
500
- let prevBottom = 0;
502
+ let prevBottom = padTop;
503
+ let firstSeen = false;
501
504
  for (let i = 0; i < kids.length; i++) {
502
505
  const child = kids[i];
503
506
  const storedTop = getNodeTop(child);
504
507
  const storedLeft = getNodeLeft(child);
505
- const effectiveTop = i === 0 ? storedTop : prevBottom + gap + storedTop;
506
- out.set(child.id, { top: effectiveTop, left: storedLeft });
508
+ const mTop = child.marginTop ?? 0;
509
+ const mLeft = child.marginLeft ?? 0;
510
+ const effectiveTop = !firstSeen ? padTop + storedTop + mTop : prevBottom + gap + storedTop + mTop;
511
+ firstSeen = true;
512
+ out.set(child.id, { top: effectiveTop, left: padLeft + storedLeft + mLeft });
507
513
  const h = getNodeBounds(child, pageChildren).height;
508
- prevBottom = effectiveTop + h;
514
+ prevBottom = effectiveTop + h + (child.marginBottom ?? 0);
509
515
  }
510
516
  } else {
511
- let prevRight = 0;
517
+ let prevRight = padLeft;
518
+ let firstSeen = false;
512
519
  for (let i = 0; i < kids.length; i++) {
513
520
  const child = kids[i];
514
521
  const storedLeft = getNodeLeft(child);
515
522
  const storedTop = getNodeTop(child);
516
- const effectiveLeft = i === 0 ? storedLeft : prevRight + gap + storedLeft;
517
- out.set(child.id, { top: storedTop, left: effectiveLeft });
523
+ const mTop = child.marginTop ?? 0;
524
+ const mLeft = child.marginLeft ?? 0;
525
+ const effectiveLeft = !firstSeen ? padLeft + storedLeft + mLeft : prevRight + gap + storedLeft + mLeft;
526
+ firstSeen = true;
527
+ out.set(child.id, { top: padTop + storedTop + mTop, left: effectiveLeft });
518
528
  const w = getNodeBounds(child, pageChildren).width;
519
- prevRight = effectiveLeft + w;
529
+ prevRight = effectiveLeft + w + (child.marginRight ?? 0);
520
530
  }
521
531
  }
522
532
  return out;
@@ -541,6 +551,14 @@ function groupBoundsFromChildren(group, pageChildren, options) {
541
551
  maxX = Math.max(maxX, cl + b.width);
542
552
  maxY = Math.max(maxY, ct + b.height);
543
553
  }
554
+ if (isStack) {
555
+ const padRight = group.paddingRight ?? 0;
556
+ const padBottom = group.paddingBottom ?? 0;
557
+ return {
558
+ width: Math.max(1, maxX + padRight),
559
+ height: Math.max(1, maxY + padBottom)
560
+ };
561
+ }
544
562
  return {
545
563
  width: Math.max(1, maxX - minX),
546
564
  height: Math.max(1, maxY - minY)
@@ -603,7 +621,7 @@ function absoluteToStorePosition(absoluteLeft, absoluteTop, nodeId, pageChildren
603
621
  const prev = kids[idx - 1];
604
622
  const prevResolved = resolved.get(prev.id);
605
623
  const prevHeight = getNodeBounds(prev, pageChildren).height;
606
- const prevBottom = prevResolved ? prevResolved.top + prevHeight : 0;
624
+ const prevBottom = prevResolved ? prevResolved.top + prevHeight + (prev.marginBottom ?? 0) : 0;
607
625
  const storeTop = inParentTop - prevBottom - gap;
608
626
  return { left: storeLeft, top: Math.max(0, storeTop) };
609
627
  }
@@ -625,7 +643,7 @@ function reflowStackGroup(group, pageChildren, spacing) {
625
643
  const b = getNodeBounds(child, pageChildren);
626
644
  const currentTop = getNodeTop(child);
627
645
  const effectiveTop = i === 0 ? currentTop : prevBottom + gap;
628
- prevBottom = effectiveTop + b.height;
646
+ prevBottom = effectiveTop + b.height + (child.marginBottom ?? 0);
629
647
  if (i > 0) updates.set(child.id, { top: 0, left: firstLeft });
630
648
  }
631
649
  } else {
@@ -636,7 +654,7 @@ function reflowStackGroup(group, pageChildren, spacing) {
636
654
  const b = getNodeBounds(child, pageChildren);
637
655
  const currentLeft = getNodeLeft(child);
638
656
  const effectiveLeft = i === 0 ? currentLeft : prevRight + gap;
639
- prevRight = effectiveLeft + b.width;
657
+ prevRight = effectiveLeft + b.width + (child.marginRight ?? 0);
640
658
  if (i > 0) updates.set(child.id, { left: 0, top: firstTop });
641
659
  }
642
660
  }
@@ -788,7 +806,8 @@ const cloneCanvas = (canvas) => ({
788
806
  themeConfig: canvas.themeConfig ? {
789
807
  properties: canvas.themeConfig.properties.map((p) => ({ ...p })),
790
808
  variants: canvas.themeConfig.variants.map((v) => ({ ...v, values: { ...v.values } }))
791
- } : void 0
809
+ } : void 0,
810
+ pdfTextMode: canvas.pdfTextMode
792
811
  });
793
812
  const sameIdSet = (a, b) => {
794
813
  if (a.length !== b.length) return false;
@@ -1268,6 +1287,12 @@ const useEditorStore = zustand.create((set, get) => ({
1268
1287
  const committed = commitFromState(state, nextCanvas);
1269
1288
  return { canvas: nextCanvas, ...committed };
1270
1289
  }),
1290
+ setPdfTextMode: (mode) => set((state) => {
1291
+ if (state.canvas.pdfTextMode === mode) return {};
1292
+ const nextCanvas = { ...state.canvas, pdfTextMode: mode };
1293
+ const committed = commitFromState(state, nextCanvas);
1294
+ return { canvas: nextCanvas, ...committed };
1295
+ }),
1271
1296
  setZoom: (zoom) => set((state) => ({
1272
1297
  canvas: { ...state.canvas, zoom: Math.max(0.1, Math.min(10, zoom)) }
1273
1298
  })),
@@ -2455,7 +2480,8 @@ const useEditorStore = zustand.create((set, get) => ({
2455
2480
  formBindingMode: config.formBindingMode,
2456
2481
  boundFormDefId: config.boundFormDefId,
2457
2482
  boundFormDefName: config.boundFormDefName,
2458
- themeConfig: config.themeConfig ?? void 0
2483
+ themeConfig: config.themeConfig ?? void 0,
2484
+ pdfTextMode: config.pdfTextMode ?? "selectable"
2459
2485
  };
2460
2486
  const committed = commitFromState(state, nextCanvas);
2461
2487
  const out = {
@@ -4922,7 +4948,9 @@ function extractTextBgConfig(element) {
4922
4948
  if (!Number.isFinite(n)) return void 0;
4923
4949
  return Math.max(0, Math.min(1, n));
4924
4950
  })(),
4925
- shadowAffectsBg: element.textShadowAffectsBg !== false
4951
+ shadowAffectsBg: element.textShadowAffectsBg !== false,
4952
+ shadowAffectsText: element.textShadowAffectsText !== false,
4953
+ fitToText: element.textBgFitToText === true
4926
4954
  };
4927
4955
  }
4928
4956
  function hasTextBackground(cfg) {
@@ -4978,10 +5006,6 @@ function applyTextBackground(obj, cfg) {
4978
5006
  const pR = Math.max(0, Number(bg.padRight ?? 0));
4979
5007
  const pB = Math.max(0, Number(bg.padBottom ?? 0));
4980
5008
  const pL = Math.max(0, Number(bg.padLeft ?? 0));
4981
- const x = -w / 2 - pL;
4982
- const y = -h / 2 - pT;
4983
- const bgW = w + pL + pR;
4984
- const bgH = h + pT + pB;
4985
5009
  ctx.save();
4986
5010
  const suppressShadowOnBg = bg.shadowAffectsBg === false;
4987
5011
  if (suppressShadowOnBg) {
@@ -4990,24 +5014,38 @@ function applyTextBackground(obj, cfg) {
4990
5014
  ctx.shadowOffsetX = 0;
4991
5015
  ctx.shadowOffsetY = 0;
4992
5016
  }
4993
- buildRoundedRectPath2D(
4994
- ctx,
4995
- x,
4996
- y,
4997
- bgW,
4998
- bgH,
4999
- bg.rxTL ?? 0,
5000
- bg.rxTR ?? 0,
5001
- bg.rxBR ?? 0,
5002
- bg.rxBL ?? 0
5003
- );
5004
5017
  const op = typeof bg.opacity === "number" ? Math.max(0, Math.min(1, bg.opacity)) : 1;
5005
5018
  if (op < 1) ctx.globalAlpha = (ctx.globalAlpha ?? 1) * op;
5006
5019
  ctx.fillStyle = bg.color;
5007
- ctx.fill();
5020
+ const rects = computeBgRects(this, w, h, pT, pR, pB, pL, !!bg.fitToText);
5021
+ for (const r of rects) {
5022
+ buildRoundedRectPath2D(
5023
+ ctx,
5024
+ r.x,
5025
+ r.y,
5026
+ r.w,
5027
+ r.h,
5028
+ bg.rxTL ?? 0,
5029
+ bg.rxTR ?? 0,
5030
+ bg.rxBR ?? 0,
5031
+ bg.rxBL ?? 0
5032
+ );
5033
+ ctx.fill();
5034
+ }
5008
5035
  ctx.restore();
5009
5036
  }
5010
- originalRender(ctx);
5037
+ const suppressShadowOnText = bg && bg.shadowAffectsText === false;
5038
+ if (suppressShadowOnText) {
5039
+ ctx.save();
5040
+ ctx.shadowColor = "transparent";
5041
+ ctx.shadowBlur = 0;
5042
+ ctx.shadowOffsetX = 0;
5043
+ ctx.shadowOffsetY = 0;
5044
+ originalRender(ctx);
5045
+ ctx.restore();
5046
+ } else {
5047
+ originalRender(ctx);
5048
+ }
5011
5049
  };
5012
5050
  const originalToObject = obj.toObject.bind(obj);
5013
5051
  obj.toObject = function(propertiesToInclude) {
@@ -5033,20 +5071,18 @@ function applyTextBackground(obj, cfg) {
5033
5071
  const pR = Math.max(0, Number((bg == null ? void 0 : bg.padRight) ?? 0));
5034
5072
  const pB = Math.max(0, Number((bg == null ? void 0 : bg.padBottom) ?? 0));
5035
5073
  const pL = Math.max(0, Number((bg == null ? void 0 : bg.padLeft) ?? 0));
5036
- const x = -w / 2 - pL;
5037
- const y = -h / 2 - pT;
5038
- const bgW = w + pL + pR;
5039
- const bgH = h + pT + pB;
5040
- const bgD = buildRoundedRectPathD(
5041
- x,
5042
- y,
5043
- bgW,
5044
- bgH,
5074
+ const fit = !!(bg == null ? void 0 : bg.fitToText);
5075
+ const rects = computeBgRects(this, w, h, pT, pR, pB, pL, fit);
5076
+ const bgD = rects.map((r) => buildRoundedRectPathD(
5077
+ r.x,
5078
+ r.y,
5079
+ r.w,
5080
+ r.h,
5045
5081
  (bg == null ? void 0 : bg.rxTL) ?? 0,
5046
5082
  (bg == null ? void 0 : bg.rxTR) ?? 0,
5047
5083
  (bg == null ? void 0 : bg.rxBR) ?? 0,
5048
5084
  (bg == null ? void 0 : bg.rxBL) ?? 0
5049
- );
5085
+ )).join(" ");
5050
5086
  const bgFill = (bg == null ? void 0 : bg.color) || "";
5051
5087
  const bgOpacity = typeof (bg == null ? void 0 : bg.opacity) === "number" ? Math.max(0, Math.min(1, bg.opacity)) : 1;
5052
5088
  const bgOpacityAttr = bgOpacity < 1 ? ` fill-opacity="${bgOpacity}"` : "";
@@ -5071,9 +5107,11 @@ function applyTextBackground(obj, cfg) {
5071
5107
  const shadowBgPath = `<path d="${bgD}" fill="${escapeXmlAttr(shadowColor)}" />`;
5072
5108
  bgShadowMarker = wrapShadow(shadowBgPath);
5073
5109
  }
5074
- const inner = extractGInnerMarkup(svg);
5075
- const recoloredText = recolorSvgFills(inner, shadowColor);
5076
- if (recoloredText) textShadowMarker = wrapShadow(recoloredText);
5110
+ if ((bg == null ? void 0 : bg.shadowAffectsText) !== false) {
5111
+ const inner = extractGInnerMarkup(svg);
5112
+ const recoloredText = recolorSvgFills(inner, shadowColor);
5113
+ if (recoloredText) textShadowMarker = wrapShadow(recoloredText);
5114
+ }
5077
5115
  }
5078
5116
  const openTagMatch = svg.match(/^\s*<g\b[^>]*>/);
5079
5117
  const inserted = bgShadowMarker + bgPath + textShadowMarker;
@@ -5124,6 +5162,74 @@ function buildRoundedRectPathD(x, y, w, h, rTL, rTR, rBR, rBL) {
5124
5162
  parts.push("Z");
5125
5163
  return parts.join(" ");
5126
5164
  }
5165
+ function computeBgRects(obj, w, h, pT, pR, pB, pL, fit) {
5166
+ var _a;
5167
+ if (!fit) {
5168
+ return [{
5169
+ x: -w / 2 - pL,
5170
+ y: -h / 2 - pT,
5171
+ w: w + pL + pR,
5172
+ h: h + pT + pB
5173
+ }];
5174
+ }
5175
+ const lines = (obj == null ? void 0 : obj._textLines) ?? [];
5176
+ if (!lines || lines.length === 0) {
5177
+ return [{
5178
+ x: -w / 2 - pL,
5179
+ y: -h / 2 - pT,
5180
+ w: w + pL + pR,
5181
+ h: h + pT + pB
5182
+ }];
5183
+ }
5184
+ const rects = [];
5185
+ const halfW = w / 2;
5186
+ const halfH = h / 2;
5187
+ const lineHeightRatio = Math.max(0.01, Number((obj == null ? void 0 : obj.lineHeight) ?? 1) || 1);
5188
+ let cursorY = -halfH;
5189
+ for (let i = 0; i < lines.length; i++) {
5190
+ let lineW = 0;
5191
+ let lineLeft = 0;
5192
+ let lineH = 0;
5193
+ try {
5194
+ lineW = obj.getLineWidth(i) || 0;
5195
+ } catch {
5196
+ lineW = 0;
5197
+ }
5198
+ try {
5199
+ lineLeft = ((_a = obj._getLineLeftOffset) == null ? void 0 : _a.call(obj, i)) ?? 0;
5200
+ } catch {
5201
+ lineLeft = 0;
5202
+ }
5203
+ try {
5204
+ lineH = obj.getHeightOfLine(i) || 0;
5205
+ } catch {
5206
+ lineH = 0;
5207
+ }
5208
+ const rawSlotH = i === lines.length - 1 ? lineH / lineHeightRatio : lineH;
5209
+ const usedH = cursorY + halfH;
5210
+ const slotH = Math.max(0, Math.min(rawSlotH, h - usedH));
5211
+ if (lineW <= 0 || slotH <= 0) {
5212
+ cursorY += slotH;
5213
+ continue;
5214
+ }
5215
+ rects.push({
5216
+ x: -halfW + lineLeft - pL,
5217
+ y: cursorY - pT,
5218
+ w: lineW + pL + pR,
5219
+ h: slotH + pT + pB
5220
+ });
5221
+ cursorY += slotH;
5222
+ }
5223
+ if (rects.length === 0) {
5224
+ return [{
5225
+ x: -w / 2 - pL,
5226
+ y: -h / 2 - pT,
5227
+ w: w + pL + pR,
5228
+ h: h + pT + pB
5229
+ }];
5230
+ }
5231
+ return rects;
5232
+ }
5127
5233
  function escapeXmlAttr(s) {
5128
5234
  return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5129
5235
  }
@@ -8143,10 +8249,14 @@ const PageCanvas = react.forwardRef(
8143
8249
  tr: element.textBgRxTR ?? 0,
8144
8250
  br: element.textBgRxBR ?? 0,
8145
8251
  bl: element.textBgRxBL ?? 0,
8252
+ ft: element.textBgFitToText === true,
8253
+ bo: element.textBgOpacity ?? 1,
8146
8254
  sc: element.textShadowColor ?? null,
8147
8255
  sb: element.textShadowBlur ?? 0,
8148
8256
  sx: element.textShadowOffsetX ?? 0,
8149
- sy: element.textShadowOffsetY ?? 0
8257
+ sy: element.textShadowOffsetY ?? 0,
8258
+ st: element.textShadowAffectsText !== false,
8259
+ sa: element.textShadowAffectsBg !== false
8150
8260
  }) !== (existingObj.__lastTextBgShadowJson ?? "") || // CRITICAL: Detect gradient fill/stroke changes — serialise to JSON for deep comparison
8151
8261
  JSON.stringify(element.fillGradient || null) !== (existingObj.__lastFillGradientJson ?? "null") || JSON.stringify(element.strokeGradient || null) !== (existingObj.__lastStrokeGradientJson ?? "null");
8152
8262
  const forceApplyFromPanel = syncTriggeredByPanelRef.current;
@@ -9060,10 +9170,14 @@ const PageCanvas = react.forwardRef(
9060
9170
  tr: element.textBgRxTR ?? 0,
9061
9171
  br: element.textBgRxBR ?? 0,
9062
9172
  bl: element.textBgRxBL ?? 0,
9173
+ ft: element.textBgFitToText === true,
9174
+ bo: element.textBgOpacity ?? 1,
9063
9175
  sc: element.textShadowColor ?? null,
9064
9176
  sb: element.textShadowBlur ?? 0,
9065
9177
  sx: element.textShadowOffsetX ?? 0,
9066
- sy: element.textShadowOffsetY ?? 0
9178
+ sy: element.textShadowOffsetY ?? 0,
9179
+ st: element.textShadowAffectsText !== false,
9180
+ sa: element.textShadowAffectsBg !== false
9067
9181
  });
9068
9182
  obj.dirty = true;
9069
9183
  } catch (err) {
@@ -14115,6 +14229,7 @@ async function fetchTTFAsBase64(url) {
14115
14229
  if (!res.ok) return null;
14116
14230
  const buf = await res.arrayBuffer();
14117
14231
  const bytes = new Uint8Array(buf);
14232
+ if (!isJsPdfEmbeddableTrueType(bytes)) return null;
14118
14233
  let binary = "";
14119
14234
  for (let i = 0; i < bytes.length; i++) {
14120
14235
  binary += String.fromCharCode(bytes[i]);
@@ -14126,6 +14241,33 @@ async function fetchTTFAsBase64(url) {
14126
14241
  return null;
14127
14242
  }
14128
14243
  }
14244
+ function isJsPdfEmbeddableTrueType(bytes) {
14245
+ if (bytes.length < 12) return false;
14246
+ const signature = String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]);
14247
+ if (signature !== "\0\0\0" && signature !== "true") return false;
14248
+ const u16 = (offset) => bytes[offset] << 8 | bytes[offset + 1];
14249
+ const u32 = (offset) => (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
14250
+ const tableCount = u16(4);
14251
+ for (let i = 0; i < tableCount; i++) {
14252
+ const recordOffset = 12 + i * 16;
14253
+ if (recordOffset + 16 > bytes.length) return false;
14254
+ const tag = String.fromCharCode(bytes[recordOffset], bytes[recordOffset + 1], bytes[recordOffset + 2], bytes[recordOffset + 3]);
14255
+ if (tag !== "cmap") continue;
14256
+ const cmapOffset = u32(recordOffset + 8);
14257
+ const cmapLength = u32(recordOffset + 12);
14258
+ if (cmapOffset + Math.min(cmapLength, 4) > bytes.length) return false;
14259
+ const subtables = u16(cmapOffset + 2);
14260
+ for (let j = 0; j < subtables; j++) {
14261
+ const encOffset = cmapOffset + 4 + j * 8;
14262
+ if (encOffset + 8 > bytes.length) return false;
14263
+ const platform = u16(encOffset);
14264
+ const encoding = u16(encOffset + 2);
14265
+ if (platform === 0 || platform === 3 && (encoding === 1 || encoding === 10)) return true;
14266
+ }
14267
+ return false;
14268
+ }
14269
+ return false;
14270
+ }
14129
14271
  async function embedFont(pdf, fontName, weight, fontBaseUrl, isItalic = false) {
14130
14272
  const fontFiles = FONT_FILES[fontName];
14131
14273
  if (!fontFiles) return false;
@@ -15833,7 +15975,7 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
15833
15975
  }
15834
15976
  if (shouldOutlineText) {
15835
15977
  try {
15836
- const { convertAllTextToPath } = await Promise.resolve().then(() => require("./svgTextToPath-DHAXuVMF.cjs"));
15978
+ const { convertAllTextToPath } = await Promise.resolve().then(() => require("./svgTextToPath-CBcIgtk1.cjs"));
15837
15979
  pageSvg = await convertAllTextToPath(pageSvg, fontBaseUrl, { mode: outlineSubMode });
15838
15980
  try {
15839
15981
  dumpSvgTextDiagnostics(pageSvg, i, PARITY_TAG, "STAGE-1b-after-text-to-path-raw");