@pixldocs/canvas-renderer 0.5.81 → 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.d.ts CHANGED
@@ -276,6 +276,14 @@ export declare interface PdfAssemblyOptions {
276
276
  fontBaseUrl?: string;
277
277
  /** Convert SVG text to paths before svg2pdf. Default: false — keep live Fabric SVG text for preview/PDF metric parity. */
278
278
  outlineText?: boolean;
279
+ /**
280
+ * Per-template text rendering mode.
281
+ * - 'auto' : outline only complex scripts (Indic/Arabic/CJK/emoji); Latin stays selectable.
282
+ * - 'selectable' : never outline; all text remains real selectable text in the PDF.
283
+ * - 'pixel-perfect' : outline every <text> for 100% font parity.
284
+ * When omitted, falls back to `outlineText` (legacy boolean) for backwards compatibility.
285
+ */
286
+ textMode?: 'auto' | 'selectable' | 'pixel-perfect';
279
287
  }
280
288
 
281
289
  /** Options for PDF rendering */
@@ -294,6 +302,8 @@ export declare interface PdfFromFormOptions {
294
302
  title?: string;
295
303
  /** Base URL for TTF font files for PDF font embedding */
296
304
  fontBaseUrl?: string;
305
+ /** PDF text rendering mode. Default: selectable real text. */
306
+ textMode?: 'auto' | 'selectable' | 'pixel-perfect';
297
307
  }
298
308
 
299
309
  export declare interface PdfRenderResult {
@@ -403,6 +413,7 @@ export declare class PixldocsRenderer {
403
413
  renderPdf(templateConfig: TemplateConfig, options?: {
404
414
  title?: string;
405
415
  fontBaseUrl?: string;
416
+ textMode?: 'auto' | 'selectable' | 'pixel-perfect';
406
417
  }): Promise<PdfRenderResult>;
407
418
  /**
408
419
  * Resolve from V2 sectionState and render a vector PDF.
@@ -669,6 +680,8 @@ export declare interface TemplateConfig {
669
680
  dynamicFields?: DynamicField[];
670
681
  fieldGroups?: any[];
671
682
  themeConfig?: ThemeConfig;
683
+ /** PDF text rendering mode. Default: selectable real text. */
684
+ pdfTextMode?: 'auto' | 'selectable' | 'pixel-perfect';
672
685
  formBindingMode?: string;
673
686
  boundFormDefId?: string;
674
687
  boundFormDefName?: string;
package/dist/index.js CHANGED
@@ -475,29 +475,39 @@ function resolveStackGroupEffectivePositions(group, pageChildren, options) {
475
475
  const mode = group.layoutMode ?? "absolute";
476
476
  if (!isStackLayoutMode(mode)) return /* @__PURE__ */ new Map();
477
477
  const gap = group.stackSpacing ?? 8;
478
+ const padTop = group.paddingTop ?? 0;
479
+ const padLeft = group.paddingLeft ?? 0;
478
480
  const kids = group.children ?? [];
479
481
  const out = /* @__PURE__ */ new Map();
480
482
  if (isVerticalStackLayoutMode(mode)) {
481
- let prevBottom = 0;
483
+ let prevBottom = padTop;
484
+ let firstSeen = false;
482
485
  for (let i = 0; i < kids.length; i++) {
483
486
  const child = kids[i];
484
487
  const storedTop = getNodeTop(child);
485
488
  const storedLeft = getNodeLeft(child);
486
- const effectiveTop = i === 0 ? storedTop : prevBottom + gap + storedTop;
487
- out.set(child.id, { top: effectiveTop, left: storedLeft });
489
+ const mTop = child.marginTop ?? 0;
490
+ const mLeft = child.marginLeft ?? 0;
491
+ const effectiveTop = !firstSeen ? padTop + storedTop + mTop : prevBottom + gap + storedTop + mTop;
492
+ firstSeen = true;
493
+ out.set(child.id, { top: effectiveTop, left: padLeft + storedLeft + mLeft });
488
494
  const h = getNodeBounds(child, pageChildren).height;
489
- prevBottom = effectiveTop + h;
495
+ prevBottom = effectiveTop + h + (child.marginBottom ?? 0);
490
496
  }
491
497
  } else {
492
- let prevRight = 0;
498
+ let prevRight = padLeft;
499
+ let firstSeen = false;
493
500
  for (let i = 0; i < kids.length; i++) {
494
501
  const child = kids[i];
495
502
  const storedLeft = getNodeLeft(child);
496
503
  const storedTop = getNodeTop(child);
497
- const effectiveLeft = i === 0 ? storedLeft : prevRight + gap + storedLeft;
498
- out.set(child.id, { top: storedTop, left: effectiveLeft });
504
+ const mTop = child.marginTop ?? 0;
505
+ const mLeft = child.marginLeft ?? 0;
506
+ const effectiveLeft = !firstSeen ? padLeft + storedLeft + mLeft : prevRight + gap + storedLeft + mLeft;
507
+ firstSeen = true;
508
+ out.set(child.id, { top: padTop + storedTop + mTop, left: effectiveLeft });
499
509
  const w = getNodeBounds(child, pageChildren).width;
500
- prevRight = effectiveLeft + w;
510
+ prevRight = effectiveLeft + w + (child.marginRight ?? 0);
501
511
  }
502
512
  }
503
513
  return out;
@@ -522,6 +532,14 @@ function groupBoundsFromChildren(group, pageChildren, options) {
522
532
  maxX = Math.max(maxX, cl + b.width);
523
533
  maxY = Math.max(maxY, ct + b.height);
524
534
  }
535
+ if (isStack) {
536
+ const padRight = group.paddingRight ?? 0;
537
+ const padBottom = group.paddingBottom ?? 0;
538
+ return {
539
+ width: Math.max(1, maxX + padRight),
540
+ height: Math.max(1, maxY + padBottom)
541
+ };
542
+ }
525
543
  return {
526
544
  width: Math.max(1, maxX - minX),
527
545
  height: Math.max(1, maxY - minY)
@@ -584,7 +602,7 @@ function absoluteToStorePosition(absoluteLeft, absoluteTop, nodeId, pageChildren
584
602
  const prev = kids[idx - 1];
585
603
  const prevResolved = resolved.get(prev.id);
586
604
  const prevHeight = getNodeBounds(prev, pageChildren).height;
587
- const prevBottom = prevResolved ? prevResolved.top + prevHeight : 0;
605
+ const prevBottom = prevResolved ? prevResolved.top + prevHeight + (prev.marginBottom ?? 0) : 0;
588
606
  const storeTop = inParentTop - prevBottom - gap;
589
607
  return { left: storeLeft, top: Math.max(0, storeTop) };
590
608
  }
@@ -606,7 +624,7 @@ function reflowStackGroup(group, pageChildren, spacing) {
606
624
  const b = getNodeBounds(child, pageChildren);
607
625
  const currentTop = getNodeTop(child);
608
626
  const effectiveTop = i === 0 ? currentTop : prevBottom + gap;
609
- prevBottom = effectiveTop + b.height;
627
+ prevBottom = effectiveTop + b.height + (child.marginBottom ?? 0);
610
628
  if (i > 0) updates.set(child.id, { top: 0, left: firstLeft });
611
629
  }
612
630
  } else {
@@ -617,7 +635,7 @@ function reflowStackGroup(group, pageChildren, spacing) {
617
635
  const b = getNodeBounds(child, pageChildren);
618
636
  const currentLeft = getNodeLeft(child);
619
637
  const effectiveLeft = i === 0 ? currentLeft : prevRight + gap;
620
- prevRight = effectiveLeft + b.width;
638
+ prevRight = effectiveLeft + b.width + (child.marginRight ?? 0);
621
639
  if (i > 0) updates.set(child.id, { left: 0, top: firstTop });
622
640
  }
623
641
  }
@@ -769,7 +787,8 @@ const cloneCanvas = (canvas) => ({
769
787
  themeConfig: canvas.themeConfig ? {
770
788
  properties: canvas.themeConfig.properties.map((p) => ({ ...p })),
771
789
  variants: canvas.themeConfig.variants.map((v) => ({ ...v, values: { ...v.values } }))
772
- } : void 0
790
+ } : void 0,
791
+ pdfTextMode: canvas.pdfTextMode
773
792
  });
774
793
  const sameIdSet = (a, b) => {
775
794
  if (a.length !== b.length) return false;
@@ -1249,6 +1268,12 @@ const useEditorStore = create((set, get) => ({
1249
1268
  const committed = commitFromState(state, nextCanvas);
1250
1269
  return { canvas: nextCanvas, ...committed };
1251
1270
  }),
1271
+ setPdfTextMode: (mode) => set((state) => {
1272
+ if (state.canvas.pdfTextMode === mode) return {};
1273
+ const nextCanvas = { ...state.canvas, pdfTextMode: mode };
1274
+ const committed = commitFromState(state, nextCanvas);
1275
+ return { canvas: nextCanvas, ...committed };
1276
+ }),
1252
1277
  setZoom: (zoom) => set((state) => ({
1253
1278
  canvas: { ...state.canvas, zoom: Math.max(0.1, Math.min(10, zoom)) }
1254
1279
  })),
@@ -2436,7 +2461,8 @@ const useEditorStore = create((set, get) => ({
2436
2461
  formBindingMode: config.formBindingMode,
2437
2462
  boundFormDefId: config.boundFormDefId,
2438
2463
  boundFormDefName: config.boundFormDefName,
2439
- themeConfig: config.themeConfig ?? void 0
2464
+ themeConfig: config.themeConfig ?? void 0,
2465
+ pdfTextMode: config.pdfTextMode ?? "selectable"
2440
2466
  };
2441
2467
  const committed = commitFromState(state, nextCanvas);
2442
2468
  const out = {
@@ -4903,7 +4929,9 @@ function extractTextBgConfig(element) {
4903
4929
  if (!Number.isFinite(n)) return void 0;
4904
4930
  return Math.max(0, Math.min(1, n));
4905
4931
  })(),
4906
- shadowAffectsBg: element.textShadowAffectsBg !== false
4932
+ shadowAffectsBg: element.textShadowAffectsBg !== false,
4933
+ shadowAffectsText: element.textShadowAffectsText !== false,
4934
+ fitToText: element.textBgFitToText === true
4907
4935
  };
4908
4936
  }
4909
4937
  function hasTextBackground(cfg) {
@@ -4959,10 +4987,6 @@ function applyTextBackground(obj, cfg) {
4959
4987
  const pR = Math.max(0, Number(bg.padRight ?? 0));
4960
4988
  const pB = Math.max(0, Number(bg.padBottom ?? 0));
4961
4989
  const pL = Math.max(0, Number(bg.padLeft ?? 0));
4962
- const x = -w / 2 - pL;
4963
- const y = -h / 2 - pT;
4964
- const bgW = w + pL + pR;
4965
- const bgH = h + pT + pB;
4966
4990
  ctx.save();
4967
4991
  const suppressShadowOnBg = bg.shadowAffectsBg === false;
4968
4992
  if (suppressShadowOnBg) {
@@ -4971,24 +4995,38 @@ function applyTextBackground(obj, cfg) {
4971
4995
  ctx.shadowOffsetX = 0;
4972
4996
  ctx.shadowOffsetY = 0;
4973
4997
  }
4974
- buildRoundedRectPath2D(
4975
- ctx,
4976
- x,
4977
- y,
4978
- bgW,
4979
- bgH,
4980
- bg.rxTL ?? 0,
4981
- bg.rxTR ?? 0,
4982
- bg.rxBR ?? 0,
4983
- bg.rxBL ?? 0
4984
- );
4985
4998
  const op = typeof bg.opacity === "number" ? Math.max(0, Math.min(1, bg.opacity)) : 1;
4986
4999
  if (op < 1) ctx.globalAlpha = (ctx.globalAlpha ?? 1) * op;
4987
5000
  ctx.fillStyle = bg.color;
4988
- ctx.fill();
5001
+ const rects = computeBgRects(this, w, h, pT, pR, pB, pL, !!bg.fitToText);
5002
+ for (const r of rects) {
5003
+ buildRoundedRectPath2D(
5004
+ ctx,
5005
+ r.x,
5006
+ r.y,
5007
+ r.w,
5008
+ r.h,
5009
+ bg.rxTL ?? 0,
5010
+ bg.rxTR ?? 0,
5011
+ bg.rxBR ?? 0,
5012
+ bg.rxBL ?? 0
5013
+ );
5014
+ ctx.fill();
5015
+ }
4989
5016
  ctx.restore();
4990
5017
  }
4991
- originalRender(ctx);
5018
+ const suppressShadowOnText = bg && bg.shadowAffectsText === false;
5019
+ if (suppressShadowOnText) {
5020
+ ctx.save();
5021
+ ctx.shadowColor = "transparent";
5022
+ ctx.shadowBlur = 0;
5023
+ ctx.shadowOffsetX = 0;
5024
+ ctx.shadowOffsetY = 0;
5025
+ originalRender(ctx);
5026
+ ctx.restore();
5027
+ } else {
5028
+ originalRender(ctx);
5029
+ }
4992
5030
  };
4993
5031
  const originalToObject = obj.toObject.bind(obj);
4994
5032
  obj.toObject = function(propertiesToInclude) {
@@ -5014,20 +5052,18 @@ function applyTextBackground(obj, cfg) {
5014
5052
  const pR = Math.max(0, Number((bg == null ? void 0 : bg.padRight) ?? 0));
5015
5053
  const pB = Math.max(0, Number((bg == null ? void 0 : bg.padBottom) ?? 0));
5016
5054
  const pL = Math.max(0, Number((bg == null ? void 0 : bg.padLeft) ?? 0));
5017
- const x = -w / 2 - pL;
5018
- const y = -h / 2 - pT;
5019
- const bgW = w + pL + pR;
5020
- const bgH = h + pT + pB;
5021
- const bgD = buildRoundedRectPathD(
5022
- x,
5023
- y,
5024
- bgW,
5025
- bgH,
5055
+ const fit = !!(bg == null ? void 0 : bg.fitToText);
5056
+ const rects = computeBgRects(this, w, h, pT, pR, pB, pL, fit);
5057
+ const bgD = rects.map((r) => buildRoundedRectPathD(
5058
+ r.x,
5059
+ r.y,
5060
+ r.w,
5061
+ r.h,
5026
5062
  (bg == null ? void 0 : bg.rxTL) ?? 0,
5027
5063
  (bg == null ? void 0 : bg.rxTR) ?? 0,
5028
5064
  (bg == null ? void 0 : bg.rxBR) ?? 0,
5029
5065
  (bg == null ? void 0 : bg.rxBL) ?? 0
5030
- );
5066
+ )).join(" ");
5031
5067
  const bgFill = (bg == null ? void 0 : bg.color) || "";
5032
5068
  const bgOpacity = typeof (bg == null ? void 0 : bg.opacity) === "number" ? Math.max(0, Math.min(1, bg.opacity)) : 1;
5033
5069
  const bgOpacityAttr = bgOpacity < 1 ? ` fill-opacity="${bgOpacity}"` : "";
@@ -5052,9 +5088,11 @@ function applyTextBackground(obj, cfg) {
5052
5088
  const shadowBgPath = `<path d="${bgD}" fill="${escapeXmlAttr(shadowColor)}" />`;
5053
5089
  bgShadowMarker = wrapShadow(shadowBgPath);
5054
5090
  }
5055
- const inner = extractGInnerMarkup(svg);
5056
- const recoloredText = recolorSvgFills(inner, shadowColor);
5057
- if (recoloredText) textShadowMarker = wrapShadow(recoloredText);
5091
+ if ((bg == null ? void 0 : bg.shadowAffectsText) !== false) {
5092
+ const inner = extractGInnerMarkup(svg);
5093
+ const recoloredText = recolorSvgFills(inner, shadowColor);
5094
+ if (recoloredText) textShadowMarker = wrapShadow(recoloredText);
5095
+ }
5058
5096
  }
5059
5097
  const openTagMatch = svg.match(/^\s*<g\b[^>]*>/);
5060
5098
  const inserted = bgShadowMarker + bgPath + textShadowMarker;
@@ -5105,6 +5143,74 @@ function buildRoundedRectPathD(x, y, w, h, rTL, rTR, rBR, rBL) {
5105
5143
  parts.push("Z");
5106
5144
  return parts.join(" ");
5107
5145
  }
5146
+ function computeBgRects(obj, w, h, pT, pR, pB, pL, fit) {
5147
+ var _a;
5148
+ if (!fit) {
5149
+ return [{
5150
+ x: -w / 2 - pL,
5151
+ y: -h / 2 - pT,
5152
+ w: w + pL + pR,
5153
+ h: h + pT + pB
5154
+ }];
5155
+ }
5156
+ const lines = (obj == null ? void 0 : obj._textLines) ?? [];
5157
+ if (!lines || lines.length === 0) {
5158
+ return [{
5159
+ x: -w / 2 - pL,
5160
+ y: -h / 2 - pT,
5161
+ w: w + pL + pR,
5162
+ h: h + pT + pB
5163
+ }];
5164
+ }
5165
+ const rects = [];
5166
+ const halfW = w / 2;
5167
+ const halfH = h / 2;
5168
+ const lineHeightRatio = Math.max(0.01, Number((obj == null ? void 0 : obj.lineHeight) ?? 1) || 1);
5169
+ let cursorY = -halfH;
5170
+ for (let i = 0; i < lines.length; i++) {
5171
+ let lineW = 0;
5172
+ let lineLeft = 0;
5173
+ let lineH = 0;
5174
+ try {
5175
+ lineW = obj.getLineWidth(i) || 0;
5176
+ } catch {
5177
+ lineW = 0;
5178
+ }
5179
+ try {
5180
+ lineLeft = ((_a = obj._getLineLeftOffset) == null ? void 0 : _a.call(obj, i)) ?? 0;
5181
+ } catch {
5182
+ lineLeft = 0;
5183
+ }
5184
+ try {
5185
+ lineH = obj.getHeightOfLine(i) || 0;
5186
+ } catch {
5187
+ lineH = 0;
5188
+ }
5189
+ const rawSlotH = i === lines.length - 1 ? lineH / lineHeightRatio : lineH;
5190
+ const usedH = cursorY + halfH;
5191
+ const slotH = Math.max(0, Math.min(rawSlotH, h - usedH));
5192
+ if (lineW <= 0 || slotH <= 0) {
5193
+ cursorY += slotH;
5194
+ continue;
5195
+ }
5196
+ rects.push({
5197
+ x: -halfW + lineLeft - pL,
5198
+ y: cursorY - pT,
5199
+ w: lineW + pL + pR,
5200
+ h: slotH + pT + pB
5201
+ });
5202
+ cursorY += slotH;
5203
+ }
5204
+ if (rects.length === 0) {
5205
+ return [{
5206
+ x: -w / 2 - pL,
5207
+ y: -h / 2 - pT,
5208
+ w: w + pL + pR,
5209
+ h: h + pT + pB
5210
+ }];
5211
+ }
5212
+ return rects;
5213
+ }
5108
5214
  function escapeXmlAttr(s) {
5109
5215
  return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5110
5216
  }
@@ -8124,10 +8230,14 @@ const PageCanvas = forwardRef(
8124
8230
  tr: element.textBgRxTR ?? 0,
8125
8231
  br: element.textBgRxBR ?? 0,
8126
8232
  bl: element.textBgRxBL ?? 0,
8233
+ ft: element.textBgFitToText === true,
8234
+ bo: element.textBgOpacity ?? 1,
8127
8235
  sc: element.textShadowColor ?? null,
8128
8236
  sb: element.textShadowBlur ?? 0,
8129
8237
  sx: element.textShadowOffsetX ?? 0,
8130
- sy: element.textShadowOffsetY ?? 0
8238
+ sy: element.textShadowOffsetY ?? 0,
8239
+ st: element.textShadowAffectsText !== false,
8240
+ sa: element.textShadowAffectsBg !== false
8131
8241
  }) !== (existingObj.__lastTextBgShadowJson ?? "") || // CRITICAL: Detect gradient fill/stroke changes — serialise to JSON for deep comparison
8132
8242
  JSON.stringify(element.fillGradient || null) !== (existingObj.__lastFillGradientJson ?? "null") || JSON.stringify(element.strokeGradient || null) !== (existingObj.__lastStrokeGradientJson ?? "null");
8133
8243
  const forceApplyFromPanel = syncTriggeredByPanelRef.current;
@@ -9041,10 +9151,14 @@ const PageCanvas = forwardRef(
9041
9151
  tr: element.textBgRxTR ?? 0,
9042
9152
  br: element.textBgRxBR ?? 0,
9043
9153
  bl: element.textBgRxBL ?? 0,
9154
+ ft: element.textBgFitToText === true,
9155
+ bo: element.textBgOpacity ?? 1,
9044
9156
  sc: element.textShadowColor ?? null,
9045
9157
  sb: element.textShadowBlur ?? 0,
9046
9158
  sx: element.textShadowOffsetX ?? 0,
9047
- sy: element.textShadowOffsetY ?? 0
9159
+ sy: element.textShadowOffsetY ?? 0,
9160
+ st: element.textShadowAffectsText !== false,
9161
+ sa: element.textShadowAffectsBg !== false
9048
9162
  });
9049
9163
  obj.dirty = true;
9050
9164
  } catch (err) {
@@ -12865,7 +12979,7 @@ class PixldocsRenderer {
12865
12979
  async renderPdf(templateConfig, options) {
12866
12980
  const svgs = await this.renderAllPageSvgs(templateConfig);
12867
12981
  const { assemblePdfFromSvgs: assemblePdfFromSvgs2 } = await Promise.resolve().then(() => pdfExport);
12868
- return assemblePdfFromSvgs2(svgs, { title: options == null ? void 0 : options.title, fontBaseUrl: options == null ? void 0 : options.fontBaseUrl });
12982
+ return assemblePdfFromSvgs2(svgs, { title: options == null ? void 0 : options.title, fontBaseUrl: options == null ? void 0 : options.fontBaseUrl, textMode: (options == null ? void 0 : options.textMode) ?? templateConfig.pdfTextMode ?? "selectable" });
12869
12983
  }
12870
12984
  /**
12871
12985
  * Resolve from V2 sectionState and render a vector PDF.
@@ -12890,7 +13004,7 @@ class PixldocsRenderer {
12890
13004
  }
12891
13005
  const svgs = await this.renderAllPageSvgs(configToRender);
12892
13006
  const { assemblePdfFromSvgs: assemblePdfFromSvgs2 } = await Promise.resolve().then(() => pdfExport);
12893
- return assemblePdfFromSvgs2(svgs, { title: title ?? resolved.config.name, fontBaseUrl });
13007
+ return assemblePdfFromSvgs2(svgs, { title: title ?? resolved.config.name, fontBaseUrl, textMode: options.textMode ?? configToRender.pdfTextMode ?? "selectable" });
12894
13008
  }
12895
13009
  async renderById(templateId, formData, options) {
12896
13010
  const resolved = await resolveTemplateData({
@@ -14096,6 +14210,7 @@ async function fetchTTFAsBase64(url) {
14096
14210
  if (!res.ok) return null;
14097
14211
  const buf = await res.arrayBuffer();
14098
14212
  const bytes = new Uint8Array(buf);
14213
+ if (!isJsPdfEmbeddableTrueType(bytes)) return null;
14099
14214
  let binary = "";
14100
14215
  for (let i = 0; i < bytes.length; i++) {
14101
14216
  binary += String.fromCharCode(bytes[i]);
@@ -14107,6 +14222,33 @@ async function fetchTTFAsBase64(url) {
14107
14222
  return null;
14108
14223
  }
14109
14224
  }
14225
+ function isJsPdfEmbeddableTrueType(bytes) {
14226
+ if (bytes.length < 12) return false;
14227
+ const signature = String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]);
14228
+ if (signature !== "\0\0\0" && signature !== "true") return false;
14229
+ const u16 = (offset) => bytes[offset] << 8 | bytes[offset + 1];
14230
+ const u32 = (offset) => (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
14231
+ const tableCount = u16(4);
14232
+ for (let i = 0; i < tableCount; i++) {
14233
+ const recordOffset = 12 + i * 16;
14234
+ if (recordOffset + 16 > bytes.length) return false;
14235
+ const tag = String.fromCharCode(bytes[recordOffset], bytes[recordOffset + 1], bytes[recordOffset + 2], bytes[recordOffset + 3]);
14236
+ if (tag !== "cmap") continue;
14237
+ const cmapOffset = u32(recordOffset + 8);
14238
+ const cmapLength = u32(recordOffset + 12);
14239
+ if (cmapOffset + Math.min(cmapLength, 4) > bytes.length) return false;
14240
+ const subtables = u16(cmapOffset + 2);
14241
+ for (let j = 0; j < subtables; j++) {
14242
+ const encOffset = cmapOffset + 4 + j * 8;
14243
+ if (encOffset + 8 > bytes.length) return false;
14244
+ const platform = u16(encOffset);
14245
+ const encoding = u16(encOffset + 2);
14246
+ if (platform === 0 || platform === 3 && (encoding === 1 || encoding === 10)) return true;
14247
+ }
14248
+ return false;
14249
+ }
14250
+ return false;
14251
+ }
14110
14252
  async function embedFont(pdf, fontName, weight, fontBaseUrl, isItalic = false) {
14111
14253
  const fontFiles = FONT_FILES[fontName];
14112
14254
  if (!fontFiles) return false;
@@ -14140,10 +14282,22 @@ async function embedFont(pdf, fontName, weight, fontBaseUrl, isItalic = false) {
14140
14282
  async function embedFontsForConfig(pdf, config, fontBaseUrl) {
14141
14283
  const fontKeys = /* @__PURE__ */ new Set();
14142
14284
  const SEP = "";
14285
+ const normalizeWeight = (raw) => {
14286
+ if (raw == null) return 400;
14287
+ if (typeof raw === "number" && Number.isFinite(raw)) return resolveFontWeight(raw);
14288
+ const str = String(raw).trim().toLowerCase();
14289
+ const parsed = Number.parseInt(str, 10);
14290
+ if (Number.isFinite(parsed)) return resolveFontWeight(parsed);
14291
+ if (str === "bold" || str === "bolder") return 700;
14292
+ if (str === "semibold" || str === "demibold") return 600;
14293
+ if (str === "medium") return 500;
14294
+ if (str === "light" || str === "lighter" || str === "thin") return 300;
14295
+ return 400;
14296
+ };
14143
14297
  const walkElements = (elements) => {
14144
14298
  for (const el of elements) {
14145
14299
  if (el.fontFamily) {
14146
- const w = resolveFontWeight(el.fontWeight ?? 400);
14300
+ const w = normalizeWeight(el.fontWeight);
14147
14301
  fontKeys.add(`${el.fontFamily}${SEP}${w}`);
14148
14302
  }
14149
14303
  if (el.styles && typeof el.styles === "object") {
@@ -14153,7 +14307,7 @@ async function embedFontsForConfig(pdf, config, fontBaseUrl) {
14153
14307
  for (const charKey of Object.keys(lineStyles)) {
14154
14308
  const s = lineStyles[charKey];
14155
14309
  if (s == null ? void 0 : s.fontFamily) {
14156
- const w = resolveFontWeight(s.fontWeight ?? 400);
14310
+ const w = normalizeWeight(s.fontWeight);
14157
14311
  fontKeys.add(`${s.fontFamily}${SEP}${w}`);
14158
14312
  }
14159
14313
  }
@@ -15788,7 +15942,9 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
15788
15942
  const hasGradient = !!((_b = (_a = page.backgroundGradient) == null ? void 0 : _a.stops) == null ? void 0 : _b.length);
15789
15943
  drawPageBackground(pdf, i, page.width, page.height, page.backgroundColor, page.backgroundGradient);
15790
15944
  const shouldStripBg = stripPageBackground ?? hasGradient;
15791
- const shouldOutlineText = options.outlineText !== false;
15945
+ const textMode = options.textMode ?? (options.outlineText ? "pixel-perfect" : "selectable");
15946
+ const shouldOutlineText = textMode === "pixel-perfect" || textMode === "auto";
15947
+ const outlineSubMode = textMode === "auto" ? "complex-only" : "all";
15792
15948
  let pageSvg = page.svg;
15793
15949
  try {
15794
15950
  pageSvg = await convertSvgTextDecorationsToLinesString(pageSvg);
@@ -15800,8 +15956,8 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
15800
15956
  }
15801
15957
  if (shouldOutlineText) {
15802
15958
  try {
15803
- const { convertAllTextToPath } = await import("./svgTextToPath-BP0Kppla.js");
15804
- pageSvg = await convertAllTextToPath(pageSvg, fontBaseUrl);
15959
+ const { convertAllTextToPath } = await import("./svgTextToPath-CShDi4Ky.js");
15960
+ pageSvg = await convertAllTextToPath(pageSvg, fontBaseUrl, { mode: outlineSubMode });
15805
15961
  try {
15806
15962
  dumpSvgTextDiagnostics(pageSvg, i, PARITY_TAG, "STAGE-1b-after-text-to-path-raw");
15807
15963
  } catch {