@cj-tech-master/excelts 8.1.2 → 9.0.0

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.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/README_zh.md +2 -2
  3. package/dist/browser/modules/excel/cell.js +11 -7
  4. package/dist/browser/modules/excel/column.js +7 -6
  5. package/dist/browser/modules/excel/row.js +5 -1
  6. package/dist/browser/modules/excel/stream/worksheet-reader.js +3 -2
  7. package/dist/browser/modules/excel/utils/cell-format.js +64 -2
  8. package/dist/browser/modules/pdf/excel-bridge.d.ts +4 -3
  9. package/dist/browser/modules/pdf/excel-bridge.js +18 -5
  10. package/dist/browser/modules/pdf/index.d.ts +3 -3
  11. package/dist/browser/modules/pdf/index.js +3 -3
  12. package/dist/browser/modules/pdf/pdf.d.ts +7 -6
  13. package/dist/browser/modules/pdf/pdf.js +7 -6
  14. package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +8 -7
  15. package/dist/browser/modules/pdf/reader/pdf-reader.js +81 -74
  16. package/dist/browser/modules/pdf/render/constants.d.ts +30 -0
  17. package/dist/browser/modules/pdf/render/constants.js +30 -0
  18. package/dist/browser/modules/pdf/render/layout-engine.d.ts +2 -1
  19. package/dist/browser/modules/pdf/render/layout-engine.js +359 -156
  20. package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -2
  21. package/dist/browser/modules/pdf/render/page-renderer.js +245 -107
  22. package/dist/browser/modules/pdf/render/pdf-exporter.d.ts +3 -2
  23. package/dist/browser/modules/pdf/render/pdf-exporter.js +145 -105
  24. package/dist/browser/modules/pdf/render/style-converter.js +27 -26
  25. package/dist/browser/modules/pdf/types.d.ts +8 -0
  26. package/dist/browser/utils/utils.base.d.ts +5 -0
  27. package/dist/browser/utils/utils.base.js +10 -0
  28. package/dist/cjs/modules/excel/cell.js +11 -7
  29. package/dist/cjs/modules/excel/column.js +7 -6
  30. package/dist/cjs/modules/excel/row.js +5 -1
  31. package/dist/cjs/modules/excel/stream/worksheet-reader.js +3 -2
  32. package/dist/cjs/modules/excel/utils/cell-format.js +64 -2
  33. package/dist/cjs/modules/pdf/excel-bridge.js +18 -5
  34. package/dist/cjs/modules/pdf/index.js +3 -3
  35. package/dist/cjs/modules/pdf/pdf.js +7 -6
  36. package/dist/cjs/modules/pdf/reader/pdf-reader.js +81 -74
  37. package/dist/cjs/modules/pdf/render/constants.js +33 -0
  38. package/dist/cjs/modules/pdf/render/layout-engine.js +359 -156
  39. package/dist/cjs/modules/pdf/render/page-renderer.js +245 -107
  40. package/dist/cjs/modules/pdf/render/pdf-exporter.js +145 -105
  41. package/dist/cjs/modules/pdf/render/style-converter.js +27 -26
  42. package/dist/cjs/utils/utils.base.js +11 -0
  43. package/dist/esm/modules/excel/cell.js +11 -7
  44. package/dist/esm/modules/excel/column.js +7 -6
  45. package/dist/esm/modules/excel/row.js +5 -1
  46. package/dist/esm/modules/excel/stream/worksheet-reader.js +3 -2
  47. package/dist/esm/modules/excel/utils/cell-format.js +64 -2
  48. package/dist/esm/modules/pdf/excel-bridge.js +18 -5
  49. package/dist/esm/modules/pdf/index.js +3 -3
  50. package/dist/esm/modules/pdf/pdf.js +7 -6
  51. package/dist/esm/modules/pdf/reader/pdf-reader.js +81 -74
  52. package/dist/esm/modules/pdf/render/constants.js +30 -0
  53. package/dist/esm/modules/pdf/render/layout-engine.js +359 -156
  54. package/dist/esm/modules/pdf/render/page-renderer.js +245 -107
  55. package/dist/esm/modules/pdf/render/pdf-exporter.js +145 -105
  56. package/dist/esm/modules/pdf/render/style-converter.js +27 -26
  57. package/dist/esm/utils/utils.base.js +10 -0
  58. package/dist/iife/excelts.iife.js +1022 -677
  59. package/dist/iife/excelts.iife.js.map +1 -1
  60. package/dist/iife/excelts.iife.min.js +48 -48
  61. package/dist/types/modules/pdf/excel-bridge.d.ts +4 -3
  62. package/dist/types/modules/pdf/index.d.ts +3 -3
  63. package/dist/types/modules/pdf/pdf.d.ts +7 -6
  64. package/dist/types/modules/pdf/reader/pdf-reader.d.ts +8 -7
  65. package/dist/types/modules/pdf/render/constants.d.ts +30 -0
  66. package/dist/types/modules/pdf/render/layout-engine.d.ts +2 -1
  67. package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -2
  68. package/dist/types/modules/pdf/render/pdf-exporter.d.ts +3 -2
  69. package/dist/types/modules/pdf/types.d.ts +8 -0
  70. package/dist/types/utils/utils.base.d.ts +5 -0
  71. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @cj-tech-master/excelts v8.1.2
2
+ * @cj-tech-master/excelts v9.0.0
3
3
  * Zero-dependency TypeScript toolkit — Excel (XLSX), PDF, CSV, Markdown, XML, ZIP/TAR, and streaming.
4
4
  * (c) 2026 cjnoname
5
5
  * Released under the MIT License
@@ -942,6 +942,44 @@ var ExcelTS = (function(exports) {
942
942
  });
943
943
  }
944
944
  //#endregion
945
+ //#region src/modules/excel/utils/copy-style.ts
946
+ const oneDepthCopy = (obj, nestKeys) => ({
947
+ ...obj,
948
+ ...nestKeys.reduce((memo, key) => {
949
+ if (obj[key]) memo[key] = { ...obj[key] };
950
+ return memo;
951
+ }, {})
952
+ });
953
+ const setIfExists = (src, dst, key, nestKeys = []) => {
954
+ if (src[key]) dst[key] = oneDepthCopy(src[key], nestKeys);
955
+ };
956
+ const isEmptyObj = (obj) => Object.keys(obj).length === 0;
957
+ const copyStyle = (style) => {
958
+ if (!style) return style;
959
+ if (isEmptyObj(style)) return {};
960
+ const copied = { ...style };
961
+ setIfExists(style, copied, "font", ["color"]);
962
+ setIfExists(style, copied, "alignment");
963
+ setIfExists(style, copied, "protection");
964
+ if (style.border) {
965
+ setIfExists(style, copied, "border");
966
+ setIfExists(style.border, copied.border, "top", ["color"]);
967
+ setIfExists(style.border, copied.border, "left", ["color"]);
968
+ setIfExists(style.border, copied.border, "bottom", ["color"]);
969
+ setIfExists(style.border, copied.border, "right", ["color"]);
970
+ setIfExists(style.border, copied.border, "diagonal", ["color"]);
971
+ }
972
+ if (style.fill) {
973
+ setIfExists(style, copied, "fill", [
974
+ "fgColor",
975
+ "bgColor",
976
+ "center"
977
+ ]);
978
+ if (style.fill.stops) copied.fill.stops = style.fill.stops.map((s) => oneDepthCopy(s, ["color"]));
979
+ }
980
+ return copied;
981
+ };
982
+ //#endregion
945
983
  //#region src/modules/excel/cell.ts
946
984
  const hasOwnKeys = (v) => !!v && (typeof v !== "object" || Object.keys(v).length > 0);
947
985
  var Cell = class Cell {
@@ -1011,15 +1049,15 @@ var ExcelTS = (function(exports) {
1011
1049
  const numFmt = rowStyle && rowStyle.numFmt || colStyle && colStyle.numFmt;
1012
1050
  if (numFmt) style.numFmt = numFmt;
1013
1051
  const font = rowStyle && hasOwnKeys(rowStyle.font) && rowStyle.font || colStyle && hasOwnKeys(colStyle.font) && colStyle.font;
1014
- if (font) style.font = font;
1052
+ if (font) style.font = structuredClone(font);
1015
1053
  const alignment = rowStyle && hasOwnKeys(rowStyle.alignment) && rowStyle.alignment || colStyle && hasOwnKeys(colStyle.alignment) && colStyle.alignment;
1016
- if (alignment) style.alignment = alignment;
1054
+ if (alignment) style.alignment = structuredClone(alignment);
1017
1055
  const border = rowStyle && hasOwnKeys(rowStyle.border) && rowStyle.border || colStyle && hasOwnKeys(colStyle.border) && colStyle.border;
1018
- if (border) style.border = border;
1056
+ if (border) style.border = structuredClone(border);
1019
1057
  const fill = rowStyle && hasOwnKeys(rowStyle.fill) && rowStyle.fill || colStyle && hasOwnKeys(colStyle.fill) && colStyle.fill;
1020
- if (fill) style.fill = fill;
1058
+ if (fill) style.fill = structuredClone(fill);
1021
1059
  const protection = rowStyle && hasOwnKeys(rowStyle.protection) && rowStyle.protection || colStyle && hasOwnKeys(colStyle.protection) && colStyle.protection;
1022
- if (protection) style.protection = protection;
1060
+ if (protection) style.protection = structuredClone(protection);
1023
1061
  return style;
1024
1062
  }
1025
1063
  get address() {
@@ -1055,7 +1093,7 @@ var ExcelTS = (function(exports) {
1055
1093
  merge(master, ignoreStyle) {
1056
1094
  this._value.release();
1057
1095
  this._value = Value.create(Cell.Types.Merge, this, master);
1058
- if (!ignoreStyle) this.style = master.style;
1096
+ if (!ignoreStyle) this.style = copyStyle(master.style) ?? {};
1059
1097
  }
1060
1098
  unmerge() {
1061
1099
  if (this.type === Cell.Types.Merge) {
@@ -1187,7 +1225,7 @@ var ExcelTS = (function(exports) {
1187
1225
  this._comment = Note.fromModel(value.comment);
1188
1226
  break;
1189
1227
  }
1190
- if (value.style) this.style = value.style;
1228
+ if (value.style) this.style = copyStyle(value.style) ?? {};
1191
1229
  else this.style = {};
1192
1230
  }
1193
1231
  };
@@ -1816,44 +1854,6 @@ var ExcelTS = (function(exports) {
1816
1854
  }
1817
1855
  };
1818
1856
  //#endregion
1819
- //#region src/modules/excel/utils/copy-style.ts
1820
- const oneDepthCopy = (obj, nestKeys) => ({
1821
- ...obj,
1822
- ...nestKeys.reduce((memo, key) => {
1823
- if (obj[key]) memo[key] = { ...obj[key] };
1824
- return memo;
1825
- }, {})
1826
- });
1827
- const setIfExists = (src, dst, key, nestKeys = []) => {
1828
- if (src[key]) dst[key] = oneDepthCopy(src[key], nestKeys);
1829
- };
1830
- const isEmptyObj = (obj) => Object.keys(obj).length === 0;
1831
- const copyStyle = (style) => {
1832
- if (!style) return style;
1833
- if (isEmptyObj(style)) return {};
1834
- const copied = { ...style };
1835
- setIfExists(style, copied, "font", ["color"]);
1836
- setIfExists(style, copied, "alignment");
1837
- setIfExists(style, copied, "protection");
1838
- if (style.border) {
1839
- setIfExists(style, copied, "border");
1840
- setIfExists(style.border, copied.border, "top", ["color"]);
1841
- setIfExists(style.border, copied.border, "left", ["color"]);
1842
- setIfExists(style.border, copied.border, "bottom", ["color"]);
1843
- setIfExists(style.border, copied.border, "right", ["color"]);
1844
- setIfExists(style.border, copied.border, "diagonal", ["color"]);
1845
- }
1846
- if (style.fill) {
1847
- setIfExists(style, copied, "fill", [
1848
- "fgColor",
1849
- "bgColor",
1850
- "center"
1851
- ]);
1852
- if (style.fill.stops) copied.fill.stops = style.fill.stops.map((s) => oneDepthCopy(s, ["color"]));
1853
- }
1854
- return copied;
1855
- };
1856
- //#endregion
1857
1857
  //#region src/modules/excel/row.ts
1858
1858
  var Row = class {
1859
1859
  constructor(worksheet, number) {
@@ -2084,7 +2084,7 @@ var ExcelTS = (function(exports) {
2084
2084
  _applyStyle(name, value) {
2085
2085
  this.style[name] = value;
2086
2086
  this._cells.forEach((cell) => {
2087
- if (cell) cell.style[name] = value;
2087
+ if (cell) cell.style[name] = typeof value === "object" && value !== null ? structuredClone(value) : value;
2088
2088
  });
2089
2089
  }
2090
2090
  get numFmt() {
@@ -2249,7 +2249,7 @@ var ExcelTS = (function(exports) {
2249
2249
  this.key = value.key;
2250
2250
  this.width = value.width !== void 0 ? value.width : DEFAULT_COLUMN_WIDTH$1;
2251
2251
  this.outlineLevel = value.outlineLevel;
2252
- if (value.style) this.style = value.style;
2252
+ if (value.style) this.style = copyStyle(value.style) ?? {};
2253
2253
  else this.style = {};
2254
2254
  this.header = value.header;
2255
2255
  this._hidden = !!value.hidden;
@@ -2412,7 +2412,7 @@ var ExcelTS = (function(exports) {
2412
2412
  set font(value) {
2413
2413
  this.style.font = value;
2414
2414
  this.eachCell((cell) => {
2415
- cell.font = value;
2415
+ cell.font = value ? structuredClone(value) : value;
2416
2416
  });
2417
2417
  }
2418
2418
  get alignment() {
@@ -2421,7 +2421,7 @@ var ExcelTS = (function(exports) {
2421
2421
  set alignment(value) {
2422
2422
  this.style.alignment = value;
2423
2423
  this.eachCell((cell) => {
2424
- cell.alignment = value;
2424
+ cell.alignment = value ? structuredClone(value) : value;
2425
2425
  });
2426
2426
  }
2427
2427
  get protection() {
@@ -2430,7 +2430,7 @@ var ExcelTS = (function(exports) {
2430
2430
  set protection(value) {
2431
2431
  this.style.protection = value;
2432
2432
  this.eachCell((cell) => {
2433
- cell.protection = value;
2433
+ cell.protection = value ? structuredClone(value) : value;
2434
2434
  });
2435
2435
  }
2436
2436
  get border() {
@@ -2439,7 +2439,7 @@ var ExcelTS = (function(exports) {
2439
2439
  set border(value) {
2440
2440
  this.style.border = value;
2441
2441
  this.eachCell((cell) => {
2442
- cell.border = value;
2442
+ cell.border = value ? structuredClone(value) : value;
2443
2443
  });
2444
2444
  }
2445
2445
  get fill() {
@@ -2448,7 +2448,7 @@ var ExcelTS = (function(exports) {
2448
2448
  set fill(value) {
2449
2449
  this.style.fill = value;
2450
2450
  this.eachCell((cell) => {
2451
- cell.fill = value;
2451
+ cell.fill = value ? structuredClone(value) : value;
2452
2452
  });
2453
2453
  }
2454
2454
  static toModel(columns) {
@@ -3837,6 +3837,13 @@ var ExcelTS = (function(exports) {
3837
3837
  }
3838
3838
  return bytes;
3839
3839
  }
3840
+ /**
3841
+ * Yield to the event loop via a macrotask.
3842
+ * Uses `setTimeout(0)` which works in both Node.js and browsers.
3843
+ */
3844
+ function yieldToEventLoop() {
3845
+ return new Promise((resolve) => setTimeout(resolve, 0));
3846
+ }
3840
3847
  //#endregion
3841
3848
  //#region src/modules/excel/pivot-table.ts
3842
3849
  /**
@@ -4537,7 +4544,20 @@ var ExcelTS = (function(exports) {
4537
4544
  decFmt = workFmt.substring(decimalIdx + 1);
4538
4545
  }
4539
4546
  const decimalPlaces = decFmt.replace(/[^0#?]/g, "").length;
4540
- const [intPart, decPart = ""] = roundTo(scaledVal, decimalPlaces).toString().split(".");
4547
+ const roundedVal = roundTo(scaledVal, decimalPlaces);
4548
+ if (roundedVal === 0 && !intFmt.includes("0") && !decFmt.includes("0")) {
4549
+ let result = "";
4550
+ for (const ch of intFmt) if (ch === "?") result += " ";
4551
+ else if (ch !== "#" && ch !== ",") result += ch;
4552
+ if (decimalPlaces > 0) {
4553
+ if (/[0?]/.test(decFmt)) {
4554
+ result += ".";
4555
+ for (const ch of decFmt) if (ch === "?") result += " ";
4556
+ }
4557
+ }
4558
+ return sign + result;
4559
+ }
4560
+ const [intPart, decPart = ""] = roundedVal.toString().split(".");
4541
4561
  const hasLiteralInFormat = /[0#?][^0#?,.\s][0#?]/.test(intFmt);
4542
4562
  let formattedInt;
4543
4563
  if (hasLiteralInFormat) {
@@ -4559,10 +4579,23 @@ var ExcelTS = (function(exports) {
4559
4579
  formattedInt = intPart;
4560
4580
  if (intFmt.includes(",")) formattedInt = commaify(intPart);
4561
4581
  const minIntDigits = (intFmt.match(/0/g) ?? []).length;
4582
+ const totalIntSlots = (intFmt.match(/[0?]/g) ?? []).length;
4562
4583
  if (formattedInt.length < minIntDigits) formattedInt = "0".repeat(minIntDigits - formattedInt.length) + formattedInt;
4584
+ if (formattedInt.length < totalIntSlots) formattedInt = " ".repeat(totalIntSlots - formattedInt.length) + formattedInt;
4585
+ if (formattedInt === "0" && minIntDigits === 0 && totalIntSlots === 0) formattedInt = "";
4563
4586
  }
4564
4587
  let formattedDec = "";
4565
- if (decimalPlaces > 0) formattedDec = "." + (decPart + "0".repeat(decimalPlaces)).substring(0, decimalPlaces);
4588
+ if (decimalPlaces > 0) {
4589
+ const decChars = (decPart + "0".repeat(decimalPlaces)).substring(0, decimalPlaces).split("");
4590
+ for (let i = decFmt.length - 1; i >= 0; i--) {
4591
+ if (i >= decChars.length) continue;
4592
+ if (decFmt[i] === "#" && decChars[i] === "0") decChars[i] = "";
4593
+ else if (decFmt[i] === "?" && decChars[i] === "0") decChars[i] = " ";
4594
+ else break;
4595
+ }
4596
+ const decStr = decChars.join("");
4597
+ if (decStr.length > 0) formattedDec = "." + decStr;
4598
+ }
4566
4599
  return sign + formattedInt + formattedDec;
4567
4600
  }
4568
4601
  /**
@@ -38704,7 +38737,7 @@ onmessage = async (ev) => {
38704
38737
  if (node.attributes.s !== void 0) {
38705
38738
  const styleId = parseInt(node.attributes.s, 10);
38706
38739
  const style = styles.getStyleModel(styleId);
38707
- if (style) row.style = style;
38740
+ if (style) row.style = copyStyle(style) ?? {};
38708
38741
  }
38709
38742
  }
38710
38743
  break;
@@ -38786,7 +38819,7 @@ onmessage = async (ev) => {
38786
38819
  const cell = row.getCell(address.col);
38787
38820
  if (c.s !== void 0) {
38788
38821
  const style = styles.getStyleModel(c.s);
38789
- if (style) cell.style = style;
38822
+ if (style) cell.style = copyStyle(style) ?? {};
38790
38823
  }
38791
38824
  if (c.f) {
38792
38825
  const cellValue = { formula: c.f.text };
@@ -47524,29 +47557,29 @@ onmessage = async (ev) => {
47524
47557
  b: 0
47525
47558
  },
47526
47559
  {
47527
- r: .918,
47528
- g: .929,
47529
- b: .941
47560
+ r: .906,
47561
+ g: .902,
47562
+ b: .902
47530
47563
  },
47531
47564
  {
47532
47565
  r: .267,
47533
- g: .278,
47534
- b: .298
47566
+ g: .329,
47567
+ b: .416
47535
47568
  },
47536
47569
  {
47537
- r: .263,
47538
- g: .522,
47539
- b: .839
47570
+ r: .267,
47571
+ g: .447,
47572
+ b: .769
47540
47573
  },
47541
47574
  {
47542
- r: .922,
47543
- g: .494,
47544
- b: .196
47575
+ r: .929,
47576
+ g: .49,
47577
+ b: .192
47545
47578
  },
47546
47579
  {
47547
- r: .624,
47548
- g: .624,
47549
- b: .624
47580
+ r: .647,
47581
+ g: .647,
47582
+ b: .647
47550
47583
  },
47551
47584
  {
47552
47585
  r: 1,
@@ -47554,9 +47587,9 @@ onmessage = async (ev) => {
47554
47587
  b: 0
47555
47588
  },
47556
47589
  {
47557
- r: .314,
47558
- g: .686,
47559
- b: .886
47590
+ r: .357,
47591
+ g: .608,
47592
+ b: .835
47560
47593
  },
47561
47594
  {
47562
47595
  r: .439,
@@ -47641,20 +47674,20 @@ onmessage = async (ev) => {
47641
47674
  */
47642
47675
  function borderStyleToLineWidth(style) {
47643
47676
  switch (style) {
47644
- case "thin": return .5;
47645
- case "medium": return 1;
47646
- case "thick": return 1.5;
47647
- case "double": return .5;
47648
- case "hair": return .25;
47649
- case "dotted": return .5;
47650
- case "dashed": return .5;
47651
- case "dashDot": return .5;
47652
- case "dashDotDot": return .5;
47653
- case "slantDashDot": return .5;
47654
- case "mediumDashed": return 1;
47655
- case "mediumDashDot": return 1;
47656
- case "mediumDashDotDot": return 1;
47657
- default: return .5;
47677
+ case "thin": return .25;
47678
+ case "medium": return .5;
47679
+ case "thick": return 1;
47680
+ case "double": return .25;
47681
+ case "hair": return .1;
47682
+ case "dotted": return .25;
47683
+ case "dashed": return .25;
47684
+ case "dashDot": return .25;
47685
+ case "dashDotDot": return .25;
47686
+ case "slantDashDot": return .25;
47687
+ case "mediumDashed": return .5;
47688
+ case "mediumDashDot": return .5;
47689
+ case "mediumDashDotDot": return .5;
47690
+ default: return .25;
47658
47691
  }
47659
47692
  }
47660
47693
  /**
@@ -47700,7 +47733,8 @@ onmessage = async (ev) => {
47700
47733
  return {
47701
47734
  width: borderStyleToLineWidth(border.style),
47702
47735
  color: excelColorToPdf(border.color) ?? DEFAULT_COLORS.black,
47703
- dashPattern: borderStyleToDashPattern(border.style)
47736
+ dashPattern: borderStyleToDashPattern(border.style),
47737
+ isDouble: border.style === "double"
47704
47738
  };
47705
47739
  }
47706
47740
  /**
@@ -47744,489 +47778,6 @@ onmessage = async (ev) => {
47744
47778
  }
47745
47779
  }
47746
47780
  //#endregion
47747
- //#region src/modules/pdf/render/layout-engine.ts
47748
- const EXCEL_CHAR_WIDTH_TO_POINTS = 7;
47749
- const DEFAULT_COLUMN_WIDTH = 8.43;
47750
- const DEFAULT_ROW_HEIGHT = 15;
47751
- const MIN_COLUMN_WIDTH = 5;
47752
- /**
47753
- * Compute the layout for a sheet across one or more PDF pages.
47754
- */
47755
- function layoutSheet(sheet, options, fontManager) {
47756
- const { margins } = options;
47757
- let pageWidth = options.pageSize.width;
47758
- let pageHeight = options.pageSize.height;
47759
- if (options.orientation === "landscape") [pageWidth, pageHeight] = [pageHeight, pageWidth];
47760
- const contentWidth = pageWidth - margins.left - margins.right;
47761
- const contentHeight = pageHeight - margins.top - margins.bottom;
47762
- const headerHeight = options.showSheetNames ? 20 : 0;
47763
- const footerHeight = options.showPageNumbers ? 20 : 0;
47764
- const availableHeight = contentHeight - headerHeight - footerHeight;
47765
- const printRange = getPrintRange(sheet);
47766
- const { columnWidths, visibleCols } = computeColumnWidths(sheet, printRange);
47767
- if (visibleCols.length === 0) return [emptyPage(pageWidth, pageHeight, sheet.name, options)];
47768
- const totalTableWidth = columnWidths.reduce((sum, w) => sum + w, 0);
47769
- let scaleFactor = options.scale;
47770
- if (options.fitToPage && totalTableWidth > 0) {
47771
- const fitScale = contentWidth / totalTableWidth;
47772
- if (fitScale < 1) scaleFactor *= fitScale;
47773
- }
47774
- const scaledColumnWidths = columnWidths.map((w) => w * scaleFactor);
47775
- const { rowHeights, visibleRows } = computeRowHeights(sheet, scaleFactor, printRange);
47776
- const mergeMap = buildMergeMap(sheet);
47777
- const rowPages = paginateRows(rowHeights, availableHeight, typeof options.repeatRows === "number" ? options.repeatRows : 0, buildRowBreakSet(sheet, visibleRows));
47778
- const colGroups = paginateColumns(scaledColumnWidths, contentWidth, sheet, visibleCols);
47779
- const layoutPages = [];
47780
- for (const rowPage of rowPages) for (const colGroup of colGroups) {
47781
- const cells = [];
47782
- const groupColWidths = colGroup.map((ci) => scaledColumnWidths[ci]);
47783
- const groupTotalWidth = groupColWidths.reduce((s, w) => s + w, 0);
47784
- const groupColOffsets = [];
47785
- let gx = margins.left;
47786
- if (groupTotalWidth < contentWidth) gx = margins.left + (contentWidth - groupTotalWidth) / 2;
47787
- for (const w of groupColWidths) {
47788
- groupColOffsets.push(gx);
47789
- gx += w;
47790
- }
47791
- const rowYPositions = [];
47792
- const pageRowHeights = [];
47793
- let currentY = pageHeight - margins.top - headerHeight;
47794
- for (const rowIdx of rowPage) {
47795
- const rowH = rowHeights[rowIdx] ?? DEFAULT_ROW_HEIGHT * scaleFactor;
47796
- rowYPositions.push(currentY);
47797
- pageRowHeights.push(rowH);
47798
- currentY -= rowH;
47799
- }
47800
- for (let ri = 0; ri < rowPage.length; ri++) {
47801
- const visibleRowIdx = rowPage[ri];
47802
- const wsRowNumber = visibleRows[visibleRowIdx];
47803
- for (let gci = 0; gci < colGroup.length; gci++) {
47804
- const wsColNumber = visibleCols[colGroup[gci]];
47805
- const mergeKey = `${wsRowNumber}:${wsColNumber}`;
47806
- const mergeInfo = mergeMap.get(mergeKey);
47807
- if (mergeInfo && !mergeInfo.isMaster) continue;
47808
- const cell = sheet.rows.get(wsRowNumber)?.cells.get(wsColNumber);
47809
- let colSpan = 1;
47810
- let rowSpan = 1;
47811
- if (mergeInfo && mergeInfo.isMaster) {
47812
- const mergeEndCol = wsColNumber + mergeInfo.colSpan - 1;
47813
- colSpan = 0;
47814
- for (let s = gci; s < colGroup.length; s++) if (visibleCols[colGroup[s]] <= mergeEndCol) colSpan++;
47815
- else break;
47816
- const mergeEndRow = wsRowNumber + mergeInfo.rowSpan - 1;
47817
- rowSpan = 0;
47818
- for (let s = visibleRowIdx; s < visibleRows.length; s++) if (visibleRows[s] <= mergeEndRow) rowSpan++;
47819
- else break;
47820
- colSpan = Math.max(colSpan, 1);
47821
- rowSpan = Math.max(rowSpan, 1);
47822
- }
47823
- const cellX = groupColOffsets[gci];
47824
- const cellY = rowYPositions[ri];
47825
- let cellWidth = 0;
47826
- for (let s = 0; s < colSpan && gci + s < groupColWidths.length; s++) cellWidth += groupColWidths[gci + s];
47827
- let cellHeight = 0;
47828
- for (let s = 0; s < rowSpan && ri + s < pageRowHeights.length; s++) cellHeight += pageRowHeights[ri + s];
47829
- const rectY = cellY - cellHeight;
47830
- cells.push(buildLayoutCell(cell, cellX, rectY, cellWidth, cellHeight, colSpan, rowSpan, options, fontManager, scaleFactor));
47831
- }
47832
- }
47833
- layoutPages.push({
47834
- pageNumber: layoutPages.length + 1,
47835
- options,
47836
- cells,
47837
- width: pageWidth,
47838
- height: pageHeight,
47839
- sheetName: sheet.name,
47840
- sheetCols: colGroup.map((ci) => visibleCols[ci]),
47841
- columnOffsets: groupColOffsets,
47842
- columnWidths: groupColWidths,
47843
- sheetRows: rowPage.map((ri) => visibleRows[ri]),
47844
- rowYPositions,
47845
- rowHeights: pageRowHeights,
47846
- images: []
47847
- });
47848
- }
47849
- if (layoutPages.length > 0 && sheet.images) assignImagesToPages(sheet.images, layoutPages);
47850
- return layoutPages;
47851
- }
47852
- function emptyPage(width, height, sheetName, options) {
47853
- return {
47854
- pageNumber: 1,
47855
- options,
47856
- cells: [],
47857
- width,
47858
- height,
47859
- sheetName,
47860
- sheetCols: [],
47861
- columnOffsets: [],
47862
- columnWidths: [],
47863
- sheetRows: [],
47864
- rowYPositions: [],
47865
- rowHeights: [],
47866
- images: []
47867
- };
47868
- }
47869
- /**
47870
- * Parse a cell reference like "A1" into 0-indexed { c, r }.
47871
- */
47872
- function parseCellRef(ref) {
47873
- const upper = ref.replace(/\$/g, "").toUpperCase();
47874
- let col = 0;
47875
- let i = 0;
47876
- while (i < upper.length && upper.charCodeAt(i) >= 65 && upper.charCodeAt(i) <= 90) {
47877
- col = col * 26 + (upper.charCodeAt(i) - 64);
47878
- i++;
47879
- }
47880
- const row = parseInt(upper.substring(i), 10);
47881
- return {
47882
- c: col - 1,
47883
- r: row - 1
47884
- };
47885
- }
47886
- /**
47887
- * Parse a range string like "A1:B2" into 0-indexed start/end.
47888
- */
47889
- function parseRangeRef(range) {
47890
- const idx = range.indexOf(":");
47891
- if (idx === -1) {
47892
- const cell = parseCellRef(range);
47893
- return {
47894
- s: cell,
47895
- e: { ...cell }
47896
- };
47897
- }
47898
- return {
47899
- s: parseCellRef(range.slice(0, idx)),
47900
- e: parseCellRef(range.slice(idx + 1))
47901
- };
47902
- }
47903
- /**
47904
- * Get the print area range from the sheet's pageSetup.
47905
- * Returns null if no print area is set.
47906
- */
47907
- function getPrintRange(sheet) {
47908
- const printArea = sheet.pageSetup?.printArea;
47909
- if (!printArea || typeof printArea !== "string") return null;
47910
- const firstRange = printArea.split("&&")[0].trim();
47911
- if (!firstRange) return null;
47912
- try {
47913
- const range = parseRangeRef(firstRange);
47914
- return {
47915
- startRow: range.s.r + 1,
47916
- endRow: range.e.r + 1,
47917
- startCol: range.s.c + 1,
47918
- endCol: range.e.c + 1
47919
- };
47920
- } catch {
47921
- return null;
47922
- }
47923
- }
47924
- function computeColumnWidths(sheet, printRange) {
47925
- const bounds = sheet.bounds;
47926
- if (!(bounds.top > 0 && bounds.left > 0)) return {
47927
- columnWidths: [],
47928
- visibleCols: []
47929
- };
47930
- const startCol = printRange?.startCol ?? bounds.left;
47931
- const endCol = printRange?.endCol ?? bounds.right;
47932
- const columnWidths = [];
47933
- const visibleCols = [];
47934
- for (let c = startCol; c <= endCol; c++) {
47935
- const col = sheet.columns.get(c);
47936
- if (col?.hidden) continue;
47937
- const excelWidth = col?.width ?? DEFAULT_COLUMN_WIDTH;
47938
- const pointWidth = Math.max(excelWidth * EXCEL_CHAR_WIDTH_TO_POINTS, MIN_COLUMN_WIDTH);
47939
- columnWidths.push(pointWidth);
47940
- visibleCols.push(c);
47941
- }
47942
- return {
47943
- columnWidths,
47944
- visibleCols
47945
- };
47946
- }
47947
- function computeRowHeights(sheet, scaleFactor, printRange) {
47948
- const bounds = sheet.bounds;
47949
- if (bounds.top <= 0) return {
47950
- rowHeights: [],
47951
- visibleRows: []
47952
- };
47953
- const startRow = printRange?.startRow ?? bounds.top;
47954
- const endRow = printRange?.endRow ?? bounds.bottom;
47955
- const rowHeights = [];
47956
- const visibleRows = [];
47957
- for (let r = startRow; r <= endRow; r++) {
47958
- const row = sheet.rows.get(r);
47959
- if (row && row.hidden) continue;
47960
- let height;
47961
- if (row?.height) height = row.height;
47962
- else {
47963
- height = DEFAULT_ROW_HEIGHT;
47964
- if (row) for (const cell of row.cells.values()) {
47965
- let fontSize = cell.style?.font?.size ?? 11;
47966
- const rtValue = cell.value;
47967
- if (rtValue?.richText) for (const run of rtValue.richText) {
47968
- const runSize = run.font?.size ?? fontSize;
47969
- if (runSize > fontSize) fontSize = runSize;
47970
- }
47971
- const lineHeight = fontSize * 1.5;
47972
- const text = cell.text ?? "";
47973
- const lineCount = Math.max(1, (text.match(/\n/g) ?? []).length + 1);
47974
- let wrapLineCount = lineCount;
47975
- if (cell.style?.alignment?.wrapText && lineCount === 1 && text.length > 0) {
47976
- const colPts = (sheet.columns.get(cell.col)?.width ?? DEFAULT_COLUMN_WIDTH) * EXCEL_CHAR_WIDTH_TO_POINTS * scaleFactor;
47977
- const avgCharWidth = fontSize * .55;
47978
- const charsPerLine = Math.max(1, Math.floor(colPts / avgCharWidth));
47979
- wrapLineCount = Math.ceil(text.length / charsPerLine);
47980
- }
47981
- const neededHeight = lineHeight * wrapLineCount;
47982
- if (neededHeight > height) height = neededHeight;
47983
- }
47984
- }
47985
- rowHeights.push(height * scaleFactor);
47986
- visibleRows.push(r);
47987
- }
47988
- return {
47989
- rowHeights,
47990
- visibleRows
47991
- };
47992
- }
47993
- /**
47994
- * Build a set of visible-row indices where manual page breaks occur.
47995
- */
47996
- function buildRowBreakSet(sheet, visibleRows) {
47997
- const breaks = /* @__PURE__ */ new Set();
47998
- const rowBreaks = sheet.rowBreaks ?? [];
47999
- if (rowBreaks.length === 0) return breaks;
48000
- const rowToIndex = /* @__PURE__ */ new Map();
48001
- for (let i = 0; i < visibleRows.length; i++) rowToIndex.set(visibleRows[i], i);
48002
- for (const brk of rowBreaks) {
48003
- const idx = rowToIndex.get(brk);
48004
- if (idx !== void 0) breaks.add(idx + 1);
48005
- }
48006
- return breaks;
48007
- }
48008
- /**
48009
- * Build a map of all merged cell regions.
48010
- * Key: "row:col" (1-based), Value: merge info
48011
- */
48012
- function buildMergeMap(sheet) {
48013
- const map = /* @__PURE__ */ new Map();
48014
- const merges = sheet.merges;
48015
- if (!merges || merges.length === 0) return map;
48016
- for (const rangeStr of merges) {
48017
- const range = parseRangeRef(rangeStr);
48018
- const top = range.s.r + 1;
48019
- const left = range.s.c + 1;
48020
- const bottom = range.e.r + 1;
48021
- const right = range.e.c + 1;
48022
- const rowSpan = bottom - top + 1;
48023
- const colSpan = right - left + 1;
48024
- for (let r = top; r <= bottom; r++) for (let c = left; c <= right; c++) map.set(`${r}:${c}`, {
48025
- isMaster: r === top && c === left,
48026
- rowSpan,
48027
- colSpan
48028
- });
48029
- }
48030
- return map;
48031
- }
48032
- function paginateRows(rowHeights, availableHeight, repeatRowCount, rowBreaks) {
48033
- if (rowHeights.length === 0) return [[]];
48034
- const pages = [];
48035
- let currentPage = [];
48036
- let currentPageHeight = 0;
48037
- let isFirstPage = true;
48038
- let repeatedPrefixCount = 0;
48039
- const addRepeatRows = () => {
48040
- repeatedPrefixCount = 0;
48041
- for (let h = 0; h < repeatRowCount && h < rowHeights.length; h++) {
48042
- if (currentPageHeight + rowHeights[h] > availableHeight && currentPage.length > 0) break;
48043
- currentPage.push(h);
48044
- currentPageHeight += rowHeights[h];
48045
- repeatedPrefixCount++;
48046
- }
48047
- };
48048
- for (let i = 0; i < rowHeights.length; i++) {
48049
- const rowHeight = rowHeights[i];
48050
- const pageAvailable = availableHeight;
48051
- let skipRepeatedRow = false;
48052
- while (true) {
48053
- const forceBreak = rowBreaks.has(i) && currentPage.length > 0;
48054
- if ((forceBreak || currentPageHeight + rowHeight > pageAvailable) && currentPage.length > 0) {
48055
- if (!forceBreak && !isFirstPage && currentPage.length > 0 && currentPage.length === repeatedPrefixCount) {
48056
- currentPage = [];
48057
- currentPageHeight = 0;
48058
- repeatedPrefixCount = 0;
48059
- continue;
48060
- }
48061
- pages.push(currentPage);
48062
- currentPage = [];
48063
- currentPageHeight = 0;
48064
- repeatedPrefixCount = 0;
48065
- isFirstPage = false;
48066
- addRepeatRows();
48067
- continue;
48068
- }
48069
- if (!isFirstPage && i < repeatRowCount && currentPage.includes(i)) {
48070
- skipRepeatedRow = true;
48071
- break;
48072
- }
48073
- currentPage.push(i);
48074
- currentPageHeight += rowHeight;
48075
- break;
48076
- }
48077
- if (skipRepeatedRow) continue;
48078
- }
48079
- if (currentPage.length > 0) pages.push(currentPage);
48080
- return pages.length > 0 ? pages : [[]];
48081
- }
48082
- /**
48083
- * Split columns into groups for horizontal pagination.
48084
- */
48085
- function paginateColumns(columnWidths, contentWidth, sheet, visibleCols) {
48086
- if (columnWidths.length === 0) return [[]];
48087
- const colBreaks = /* @__PURE__ */ new Set();
48088
- const wsColBreaks = sheet.colBreaks ?? [];
48089
- if (wsColBreaks.length > 0) {
48090
- const colToIndex = /* @__PURE__ */ new Map();
48091
- for (let i = 0; i < visibleCols.length; i++) colToIndex.set(visibleCols[i], i);
48092
- for (const brk of wsColBreaks) {
48093
- const idx = colToIndex.get(brk);
48094
- if (idx !== void 0) colBreaks.add(idx + 1);
48095
- }
48096
- }
48097
- const groups = [];
48098
- let currentGroup = [];
48099
- let currentWidth = 0;
48100
- for (let i = 0; i < columnWidths.length; i++) {
48101
- const colWidth = columnWidths[i];
48102
- if ((colBreaks.has(i) && currentGroup.length > 0 || currentWidth + colWidth > contentWidth + .01) && currentGroup.length > 0) {
48103
- groups.push(currentGroup);
48104
- currentGroup = [];
48105
- currentWidth = 0;
48106
- }
48107
- currentGroup.push(i);
48108
- currentWidth += colWidth;
48109
- }
48110
- if (currentGroup.length > 0) groups.push(currentGroup);
48111
- return groups.length > 0 ? groups : [Array.from({ length: columnWidths.length }, (_, i) => i)];
48112
- }
48113
- function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, fontManager, scaleFactor) {
48114
- const text = cell?.text ?? "";
48115
- const style = cell?.style ?? {};
48116
- const fontProps = extractFontProperties(style.font, options.defaultFontFamily, options.defaultFontSize);
48117
- const scaledFontSize = fontProps.fontSize * scaleFactor;
48118
- if (fontManager.hasEmbeddedFont()) fontManager.trackText(text);
48119
- else {
48120
- const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
48121
- fontManager.ensureFont(pdfFontName);
48122
- }
48123
- const richText = buildRichTextRuns(cell, options, fontManager, scaleFactor);
48124
- return {
48125
- text,
48126
- rect: {
48127
- x,
48128
- y,
48129
- width,
48130
- height
48131
- },
48132
- fontFamily: fontProps.fontFamily,
48133
- fontSize: scaledFontSize,
48134
- bold: fontProps.bold,
48135
- italic: fontProps.italic,
48136
- strike: fontProps.strike,
48137
- underline: fontProps.underline,
48138
- textColor: fontProps.textColor,
48139
- fillColor: excelFillToPdfColor(style.fill),
48140
- horizontalAlign: excelHAlignToPdf(style.alignment),
48141
- verticalAlign: excelVAlignToPdf(style.alignment),
48142
- wrapText: style.alignment?.wrapText ?? false,
48143
- borders: excelBordersToPdf(style.border),
48144
- colSpan,
48145
- rowSpan,
48146
- hyperlink: cell?.hyperlink ?? null,
48147
- richText,
48148
- indent: style.alignment?.indent ?? 0,
48149
- textRotation: style.alignment?.textRotation ?? 0
48150
- };
48151
- }
48152
- /**
48153
- * Assign pre-collected images to the pages that contain their top-left anchor.
48154
- */
48155
- function assignImagesToPages(images, layoutPages) {
48156
- for (const img of images) {
48157
- const tl = img.range.tl;
48158
- const tlCol = (tl.nativeCol ?? tl.col ?? 0) + 1;
48159
- const tlRow = (tl.nativeRow ?? tl.row ?? 0) + 1;
48160
- const targetPage = layoutPages.find((page) => page.sheetCols.includes(tlCol) && page.sheetRows.includes(tlRow));
48161
- if (!targetPage) continue;
48162
- const pageColIndex = targetPage.sheetCols.indexOf(tlCol);
48163
- const pageRowIndex = targetPage.sheetRows.indexOf(tlRow);
48164
- const baseX = targetPage.columnOffsets[pageColIndex] ?? targetPage.options.margins.left;
48165
- const baseY = targetPage.rowYPositions[pageRowIndex] ?? targetPage.height - targetPage.options.margins.top - (targetPage.options.showSheetNames ? 20 : 0);
48166
- const tlColOff = (tl.nativeColOff ?? 0) / 12700 || 0;
48167
- const tlRowOff = (tl.nativeRowOff ?? 0) / 12700 || 0;
48168
- const imgX = baseX + tlColOff;
48169
- const imgY = baseY - tlRowOff;
48170
- let imgWidth = 100;
48171
- let imgHeight = 100;
48172
- if (img.range.ext) {
48173
- imgWidth = (img.range.ext.width ?? 100) * .75;
48174
- imgHeight = (img.range.ext.height ?? 100) * .75;
48175
- } else if (img.range.br) {
48176
- const br = img.range.br;
48177
- const brCol = (br.nativeCol ?? br.col ?? 0) + 1;
48178
- const brRow = (br.nativeRow ?? br.row ?? 0) + 1;
48179
- const brPageColIndex = targetPage.sheetCols.indexOf(brCol);
48180
- const brPageRowIndex = targetPage.sheetRows.indexOf(brRow);
48181
- const brBaseX = brPageColIndex >= 0 ? targetPage.columnOffsets[brPageColIndex] : imgX + (targetPage.columnWidths[pageColIndex] ?? 100);
48182
- const brBaseY = brPageRowIndex >= 0 ? targetPage.rowYPositions[brPageRowIndex] : imgY - (targetPage.rowHeights[pageRowIndex] ?? 100);
48183
- const brColOff = (br.nativeColOff ?? 0) / 12700 || 0;
48184
- const brRowOff = (br.nativeRowOff ?? 0) / 12700 || 0;
48185
- const brX = brBaseX + brColOff;
48186
- const brY = brBaseY - brRowOff;
48187
- imgWidth = brX - imgX;
48188
- imgHeight = imgY - brY;
48189
- }
48190
- targetPage.images.push({
48191
- data: img.data,
48192
- format: img.format,
48193
- rect: {
48194
- x: imgX,
48195
- y: imgY - imgHeight,
48196
- width: Math.abs(imgWidth),
48197
- height: Math.abs(imgHeight)
48198
- }
48199
- });
48200
- }
48201
- }
48202
- /**
48203
- * Build rich text runs from a RichText cell.
48204
- * Returns null for non-RichText cells.
48205
- */
48206
- function buildRichTextRuns(cell, options, fontManager, scaleFactor) {
48207
- if (!cell || cell.type !== PdfCellType.RichText) return null;
48208
- const rtValue = cell.value;
48209
- if (!rtValue?.richText || rtValue.richText.length === 0) return null;
48210
- return rtValue.richText.map((run) => {
48211
- const fontProps = extractFontProperties(run.font, options.defaultFontFamily, options.defaultFontSize);
48212
- if (fontManager.hasEmbeddedFont()) fontManager.trackText(run.text);
48213
- else {
48214
- const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
48215
- fontManager.ensureFont(pdfFontName);
48216
- }
48217
- return {
48218
- text: run.text,
48219
- fontFamily: fontProps.fontFamily,
48220
- fontSize: fontProps.fontSize * scaleFactor,
48221
- bold: fontProps.bold,
48222
- italic: fontProps.italic,
48223
- strike: fontProps.strike,
48224
- underline: fontProps.underline,
48225
- textColor: fontProps.textColor
48226
- };
48227
- });
48228
- }
48229
- //#endregion
48230
47781
  //#region src/modules/pdf/core/pdf-stream.ts
48231
47782
  /**
48232
47783
  * PDF content stream builder.
@@ -48617,6 +48168,18 @@ onmessage = async (ev) => {
48617
48168
  return 63;
48618
48169
  }
48619
48170
  //#endregion
48171
+ //#region src/modules/pdf/render/constants.ts
48172
+ /**
48173
+ * Line-height multiplier applied to the font size.
48174
+ *
48175
+ * Excel's default row height for an 11pt font is 15pt, which after removing
48176
+ * vertical padding (2 × 2 = 4pt) leaves 11pt × 1.0 — but Excel also adds
48177
+ * internal leading. A factor of 1.2 matches standard PDF/typographic practice
48178
+ * and keeps text readable without inflating row heights.
48179
+ */
48180
+ const LINE_HEIGHT_FACTOR = 1.2;
48181
+ const PX_TO_PT = 72 / 96;
48182
+ //#endregion
48620
48183
  //#region src/modules/pdf/render/page-renderer.ts
48621
48184
  /**
48622
48185
  * Page renderer for PDF generation.
@@ -48629,9 +48192,6 @@ onmessage = async (ev) => {
48629
48192
  * - Grid lines
48630
48193
  * - Page headers (sheet names) and footers (page numbers)
48631
48194
  */
48632
- /** Internal cell padding in points */
48633
- const CELL_PADDING_H = 3;
48634
- const CELL_PADDING_V = 2;
48635
48195
  /**
48636
48196
  * Render a single page to a PDF content stream.
48637
48197
  */
@@ -48641,7 +48201,8 @@ onmessage = async (ev) => {
48641
48201
  if (options.showGridLines) drawGridLines(stream, page, options);
48642
48202
  for (const cell of page.cells) if (cell.fillColor) drawCellFill(stream, cell, alphaValues);
48643
48203
  for (const cell of page.cells) drawCellBorders(stream, cell);
48644
- for (const cell of page.cells) if (cell.text) drawCellText(stream, cell, fontManager, alphaValues);
48204
+ const sf = page.scaleFactor;
48205
+ for (const cell of page.cells) if (cell.text) drawCellText(stream, cell, fontManager, alphaValues, sf);
48645
48206
  if (options.showSheetNames) drawPageHeader(stream, page, options, fontManager);
48646
48207
  if (options.showPageNumbers) drawPageFooter(stream, page, options, fontManager, totalPages);
48647
48208
  return {
@@ -48690,23 +48251,36 @@ onmessage = async (ev) => {
48690
48251
  function drawCellBorders(stream, cell) {
48691
48252
  const { rect, borders } = cell;
48692
48253
  const { x, y, width, height } = rect;
48693
- if (borders.top) drawBorderLine(stream, borders.top, x, y + height, x + width, y + height);
48694
- if (borders.bottom) drawBorderLine(stream, borders.bottom, x, y, x + width, y);
48695
- if (borders.left) drawBorderLine(stream, borders.left, x, y, x, y + height);
48696
- if (borders.right) drawBorderLine(stream, borders.right, x + width, y, x + width, y + height);
48697
- }
48698
- function drawBorderLine(stream, border, x1, y1, x2, y2) {
48699
- stream.drawLine(x1, y1, x2, y2, border.color, border.width, border.dashPattern);
48254
+ if (borders.top) drawBorderLine(stream, borders.top, x, y + height, x + width, y + height, true);
48255
+ if (borders.bottom) drawBorderLine(stream, borders.bottom, x, y, x + width, y, true);
48256
+ if (borders.left) drawBorderLine(stream, borders.left, x, y, x, y + height, false);
48257
+ if (borders.right) drawBorderLine(stream, borders.right, x + width, y, x + width, y + height, false);
48258
+ }
48259
+ function drawBorderLine(stream, border, x1, y1, x2, y2, isHorizontal) {
48260
+ if (border.isDouble) {
48261
+ const offset = .4;
48262
+ const thinWidth = Math.min(border.width, .25);
48263
+ if (isHorizontal) {
48264
+ stream.drawLine(x1, y1 + offset, x2, y2 + offset, border.color, thinWidth, border.dashPattern);
48265
+ stream.drawLine(x1, y1 - offset, x2, y2 - offset, border.color, thinWidth, border.dashPattern);
48266
+ } else {
48267
+ stream.drawLine(x1 + offset, y1, x2 + offset, y2, border.color, thinWidth, border.dashPattern);
48268
+ stream.drawLine(x1 - offset, y1, x2 - offset, y2, border.color, thinWidth, border.dashPattern);
48269
+ }
48270
+ } else stream.drawLine(x1, y1, x2, y2, border.color, border.width, border.dashPattern);
48700
48271
  }
48701
- function drawCellText(stream, cell, fontManager, alphaValues) {
48272
+ function drawCellText(stream, cell, fontManager, alphaValues, scaleFactor = 1) {
48702
48273
  const { rect, text, fontSize, horizontalAlign, verticalAlign, wrapText } = cell;
48703
48274
  if (!text && !cell.richText) return;
48704
- const availWidth = rect.width - CELL_PADDING_H * 2;
48705
- const availHeight = rect.height - CELL_PADDING_V * 2;
48275
+ const padH = 3 * scaleFactor;
48276
+ const padV = 2 * scaleFactor;
48277
+ const availWidth = rect.width - padH * 2;
48278
+ const availHeight = rect.height - padV * 2;
48706
48279
  if (availWidth <= 0 || availHeight <= 0) return;
48707
- const indentPts = cell.indent * INDENT_WIDTH;
48280
+ const indentPts = cell.indent * 10 * scaleFactor;
48281
+ const clipWidth = rect.width + (cell.textOverflowWidth || 0);
48708
48282
  stream.save();
48709
- stream.rect(rect.x, rect.y, rect.width, rect.height);
48283
+ stream.rect(rect.x, rect.y, clipWidth, rect.height);
48710
48284
  stream.clip();
48711
48285
  stream.endPath();
48712
48286
  const textAlpha = cell.textColor.a;
@@ -48715,34 +48289,34 @@ onmessage = async (ev) => {
48715
48289
  stream.setGraphicsState(alphaGsName(textAlpha));
48716
48290
  }
48717
48291
  if (cell.textRotation === "vertical") {
48718
- drawVerticalStackedText(stream, cell, fontManager, indentPts);
48292
+ drawVerticalStackedText(stream, cell, fontManager, indentPts, scaleFactor);
48719
48293
  stream.restore();
48720
48294
  return;
48721
48295
  }
48722
48296
  if (typeof cell.textRotation === "number" && cell.textRotation !== 0) {
48723
- drawRotatedText(stream, cell, fontManager, indentPts);
48297
+ drawRotatedText(stream, cell, fontManager, indentPts, scaleFactor);
48724
48298
  stream.restore();
48725
48299
  return;
48726
48300
  }
48727
48301
  if (cell.richText && cell.richText.length > 0) {
48728
- drawRichText(stream, cell, fontManager, indentPts);
48302
+ drawRichText(stream, cell, fontManager, indentPts, scaleFactor);
48729
48303
  stream.restore();
48730
48304
  return;
48731
48305
  }
48732
48306
  const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
48733
48307
  const measure = (s) => fontManager.measureText(s, resourceName, fontSize);
48734
- const effectiveWidth = availWidth - indentPts - 1;
48735
- const lines = wrapText ? wrapTextLines(text, measure, effectiveWidth) : [text];
48736
- const lineHeight = fontSize * 1.2;
48308
+ const effectiveWidth = availWidth - indentPts;
48309
+ const lines = wrapText ? wrapTextLines(text, measure, effectiveWidth) : text.split(/\r?\n/);
48310
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
48737
48311
  const ascent = fontManager.getFontAscent(resourceName, fontSize);
48738
- const textStartY = computeTextStartY(verticalAlign, rect, lines.length * lineHeight, ascent);
48312
+ const textStartY = computeTextStartY(verticalAlign, rect, lines.length * lineHeight, ascent, padV);
48739
48313
  stream.setFillColor(cell.textColor);
48740
48314
  stream.beginText();
48741
48315
  stream.setFont(resourceName, fontSize);
48742
48316
  for (let i = 0; i < lines.length; i++) {
48743
48317
  const line = lines[i];
48744
48318
  const lineY = textStartY - i * lineHeight;
48745
- const textX = computeTextX(horizontalAlign, rect, measure(line), indentPts);
48319
+ const textX = computeTextX(horizontalAlign, rect, measure(line), indentPts, padH);
48746
48320
  stream.setTextMatrix(1, 0, 0, 1, textX, lineY);
48747
48321
  const hexEncoded = fontManager.encodeText(line, resourceName);
48748
48322
  if (hexEncoded) stream.showTextHex(hexEncoded);
@@ -48752,17 +48326,19 @@ onmessage = async (ev) => {
48752
48326
  drawTextDecorations(stream, cell, lines, lineHeight, textStartY, measure, resourceName, fontManager, indentPts);
48753
48327
  stream.restore();
48754
48328
  }
48755
- function drawRichText(stream, cell, fontManager, indentPts) {
48329
+ function drawRichText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
48756
48330
  const { rect, horizontalAlign, verticalAlign, wrapText } = cell;
48757
48331
  const runs = cell.richText;
48332
+ const padH = 3 * scaleFactor;
48333
+ const padV = 2 * scaleFactor;
48758
48334
  let maxFontSize = cell.fontSize;
48759
48335
  for (const run of runs) if (run.fontSize > maxFontSize) maxFontSize = run.fontSize;
48760
48336
  const primaryFontSize = maxFontSize;
48761
- const lineHeight = primaryFontSize * 1.2;
48337
+ const lineHeight = primaryFontSize * LINE_HEIGHT_FACTOR;
48762
48338
  const isEmbedded = fontManager.hasEmbeddedFont();
48763
48339
  const runResource = (run) => isEmbedded ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(run.fontFamily, run.bold, run.italic));
48764
48340
  if (wrapText) {
48765
- const availWidth = rect.width - CELL_PADDING_H * 2 - indentPts - 1;
48341
+ const availWidth = rect.width - padH * 2 - indentPts;
48766
48342
  if (availWidth <= 0) return;
48767
48343
  const fullText = runs.map((r) => r.text).join("");
48768
48344
  const primaryResource = runResource(runs[0]);
@@ -48772,7 +48348,7 @@ onmessage = async (ev) => {
48772
48348
  for (let ri = 0; ri < runs.length; ri++) for (let ci = 0; ci < runs[ri].text.length; ci++) runForChar.push(ri);
48773
48349
  const primaryResourceName = runResource(runs[0]);
48774
48350
  const ascent = fontManager.getFontAscent(primaryResourceName, primaryFontSize);
48775
- const textStartY = computeTextStartY(verticalAlign, rect, lines.length * lineHeight, ascent);
48351
+ const textStartY = computeTextStartY(verticalAlign, rect, lines.length * lineHeight, ascent, padV);
48776
48352
  let charPos = 0;
48777
48353
  for (let li = 0; li < lines.length; li++) {
48778
48354
  const lineY = textStartY - li * lineHeight;
@@ -48800,7 +48376,7 @@ onmessage = async (ev) => {
48800
48376
  }
48801
48377
  let lineWidth = 0;
48802
48378
  for (const seg of segments) lineWidth += fontManager.measureText(seg.text, seg.resourceName, seg.run.fontSize);
48803
- let textX = computeTextX(horizontalAlign, rect, lineWidth, indentPts);
48379
+ let textX = computeTextX(horizontalAlign, rect, lineWidth, indentPts, padH);
48804
48380
  for (const seg of segments) {
48805
48381
  const { run, text, resourceName } = seg;
48806
48382
  const segWidth = fontManager.measureText(text, resourceName, run.fontSize);
@@ -48837,8 +48413,8 @@ onmessage = async (ev) => {
48837
48413
  totalWidth += w;
48838
48414
  }
48839
48415
  const primaryResourceName = runMetrics[0]?.resourceName ?? "F1";
48840
- const textStartY = computeTextStartY(verticalAlign, rect, lineHeight, fontManager.getFontAscent(primaryResourceName, primaryFontSize));
48841
- let textX = computeTextX(horizontalAlign, rect, totalWidth, indentPts);
48416
+ const textStartY = computeTextStartY(verticalAlign, rect, lineHeight, fontManager.getFontAscent(primaryResourceName, primaryFontSize), padV);
48417
+ let textX = computeTextX(horizontalAlign, rect, totalWidth, indentPts, padH);
48842
48418
  for (let i = 0; i < runs.length; i++) {
48843
48419
  const run = runs[i];
48844
48420
  const { resourceName } = runMetrics[i];
@@ -48862,69 +48438,158 @@ onmessage = async (ev) => {
48862
48438
  textX += runMetrics[i].width;
48863
48439
  }
48864
48440
  }
48865
- function drawRotatedText(stream, cell, fontManager, indentPts) {
48866
- const { rect, text } = cell;
48441
+ function drawRotatedText(stream, cell, fontManager, indentPts, scaleFactor = 1) {
48442
+ const { rect, wrapText } = cell;
48867
48443
  let { fontSize } = cell;
48444
+ const padH = 3 * scaleFactor;
48445
+ const padV = 2 * scaleFactor;
48868
48446
  const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
48869
48447
  let degrees;
48870
- if (typeof cell.textRotation === "number") if (cell.textRotation <= 90) degrees = cell.textRotation;
48871
- else degrees = -(cell.textRotation - 90);
48448
+ if (typeof cell.textRotation === "number") degrees = cell.textRotation <= 90 ? cell.textRotation : -(cell.textRotation - 90);
48872
48449
  else degrees = 0;
48873
48450
  const radians = degrees * Math.PI / 180;
48874
48451
  const cos = Math.cos(radians);
48875
48452
  const sin = Math.sin(radians);
48876
- const textWidth = fontManager.measureText(text, resourceName, fontSize);
48877
48453
  const absSin = Math.abs(sin);
48878
48454
  const absCos = Math.abs(cos);
48879
- const rotatedWidth = textWidth * absCos + fontSize * absSin;
48880
- const rotatedHeight = textWidth * absSin + fontSize * absCos;
48881
- const maxWidth = rect.width - CELL_PADDING_H * 2;
48882
- const maxHeight = rect.height - CELL_PADDING_V * 2;
48883
- if (maxWidth > 0 && maxHeight > 0 && (rotatedWidth > maxWidth || rotatedHeight > maxHeight)) {
48884
- const fitScale = Math.min(maxWidth / rotatedWidth, maxHeight / rotatedHeight);
48885
- if (fitScale < 1) fontSize = fontSize * fitScale;
48886
- }
48887
- const indentOffset = cell.horizontalAlign === "left" ? indentPts / 2 : cell.horizontalAlign === "right" ? -indentPts / 2 : 0;
48888
- const cx = rect.x + rect.width / 2 + indentOffset;
48889
- const cy = rect.y + rect.height / 2;
48890
- const finalTextWidth = fontManager.measureText(text, resourceName, fontSize);
48455
+ const maxWidth = rect.width - padH * 2;
48456
+ const maxHeight = rect.height - padV * 2;
48457
+ let availTextLength;
48458
+ if (absSin > .01 && absCos > .01) availTextLength = Math.min(maxHeight / absSin, maxWidth / absCos);
48459
+ else if (absSin > .01) availTextLength = maxHeight / absSin;
48460
+ else availTextLength = maxWidth;
48461
+ const measure = (s) => fontManager.measureText(s, resourceName, fontSize);
48462
+ let lines;
48463
+ if (wrapText) lines = wrapTextLines(cell.text, measure, Math.max(availTextLength - 1, 1));
48464
+ else lines = cell.text.split(/\r?\n/);
48465
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
48466
+ const totalTextHeight = lines.length * lineHeight;
48467
+ if (!wrapText) {
48468
+ let maxLineWidth = 0;
48469
+ for (const line of lines) {
48470
+ const w = measure(line);
48471
+ if (w > maxLineWidth) maxLineWidth = w;
48472
+ }
48473
+ const rotatedWidth = maxLineWidth * absCos + totalTextHeight * absSin;
48474
+ const rotatedHeight = maxLineWidth * absSin + totalTextHeight * absCos;
48475
+ if (maxWidth > 0 && maxHeight > 0 && (rotatedWidth > maxWidth || rotatedHeight > maxHeight)) {
48476
+ const fitScale = Math.min(maxWidth / rotatedWidth, maxHeight / rotatedHeight);
48477
+ if (fitScale < 1) fontSize = fontSize * fitScale;
48478
+ }
48479
+ }
48480
+ const scaledLineHeight = fontSize * LINE_HEIGHT_FACTOR;
48891
48481
  const ascent = fontManager.getFontAscent(resourceName, fontSize);
48892
- const offsetX = -finalTextWidth / 2;
48893
- const offsetY = -ascent / 2;
48894
- const tx = cx + offsetX * cos - offsetY * sin;
48895
- const ty = cy + offsetX * sin + offsetY * cos;
48482
+ const is90 = Math.abs(degrees - 90) < .01;
48483
+ const isMinus90 = Math.abs(degrees + 90) < .01;
48896
48484
  stream.setFillColor(cell.textColor);
48897
- stream.beginText();
48898
- stream.setFont(resourceName, fontSize);
48899
- stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
48900
- const hexEncoded = fontManager.encodeText(text, resourceName);
48901
- if (hexEncoded) stream.showTextHex(hexEncoded);
48485
+ if (is90) drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize, scaledLineHeight, ascent, padH, padV);
48486
+ else if (isMinus90) drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, fontSize, scaledLineHeight, ascent, padH, padV);
48487
+ else drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, fontSize, scaledLineHeight, ascent, cos, sin, indentPts);
48488
+ }
48489
+ /** 90° CCW: text reads bottom-to-top, lines stack left-to-right. */
48490
+ function drawRotated90(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, padH, padV) {
48491
+ const { rect, horizontalAlign, verticalAlign } = cell;
48492
+ const totalColumnsWidth = lines.length * lineHeight;
48493
+ let startX;
48494
+ if (horizontalAlign === "center" || lines.length === 1) startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + ascent;
48495
+ else if (horizontalAlign === "right") startX = rect.x + rect.width - padH - totalColumnsWidth + ascent;
48496
+ else startX = rect.x + padH + ascent;
48497
+ for (let i = 0; i < lines.length; i++) {
48498
+ const line = lines[i];
48499
+ const lineWidth = fontManager.measureText(line, resourceName, fontSize);
48500
+ const colX = startX + i * lineHeight;
48501
+ let ty;
48502
+ if (verticalAlign === "top") ty = rect.y + padV;
48503
+ else if (verticalAlign === "middle") ty = rect.y + (rect.height - lineWidth) / 2;
48504
+ else ty = rect.y + rect.height - padV - lineWidth;
48505
+ ty = Math.max(ty, rect.y + padV);
48506
+ stream.beginText();
48507
+ stream.setFont(resourceName, fontSize);
48508
+ stream.setTextMatrix(0, 1, -1, 0, colX, ty);
48509
+ emitText(stream, fontManager, line, resourceName);
48510
+ stream.endText();
48511
+ }
48512
+ }
48513
+ /** -90° (270° CW): text reads top-to-bottom, lines stack right-to-left. */
48514
+ function drawRotatedMinus90(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, padH, padV) {
48515
+ const { rect, horizontalAlign, verticalAlign } = cell;
48516
+ const totalColumnsWidth = lines.length * lineHeight;
48517
+ let startX;
48518
+ if (horizontalAlign === "center" || lines.length === 1) startX = rect.x + rect.width / 2 + totalColumnsWidth / 2 - lineHeight + ascent;
48519
+ else if (horizontalAlign === "right") startX = rect.x + rect.width - padH - lineHeight + ascent;
48520
+ else startX = rect.x + padH + totalColumnsWidth - lineHeight + ascent;
48521
+ for (let i = 0; i < lines.length; i++) {
48522
+ const line = lines[i];
48523
+ const lineWidth = fontManager.measureText(line, resourceName, fontSize);
48524
+ const colX = startX - i * lineHeight;
48525
+ let ty;
48526
+ if (verticalAlign === "top") ty = rect.y + rect.height - padV;
48527
+ else if (verticalAlign === "middle") ty = rect.y + (rect.height + lineWidth) / 2;
48528
+ else ty = rect.y + padV + lineWidth;
48529
+ ty = Math.min(ty, rect.y + rect.height - padV);
48530
+ stream.beginText();
48531
+ stream.setFont(resourceName, fontSize);
48532
+ stream.setTextMatrix(0, -1, 1, 0, colX, ty);
48533
+ emitText(stream, fontManager, line, resourceName);
48534
+ stream.endText();
48535
+ }
48536
+ }
48537
+ /** General rotation — center a multi-line text block in the cell. */
48538
+ function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, fontSize, lineHeight, ascent, cos, sin, indentPts) {
48539
+ const { rect, horizontalAlign } = cell;
48540
+ const indentOffset = horizontalAlign === "left" ? indentPts / 2 : horizontalAlign === "right" ? -indentPts / 2 : 0;
48541
+ const cx = rect.x + rect.width / 2 + indentOffset;
48542
+ const cy = rect.y + rect.height / 2;
48543
+ for (let i = 0; i < lines.length; i++) {
48544
+ const line = lines[i];
48545
+ const lineWidth = fontManager.measureText(line, resourceName, fontSize);
48546
+ const lineOffset = (i - (lines.length - 1) / 2) * lineHeight;
48547
+ const offsetX = -lineWidth / 2;
48548
+ const offsetY = -ascent / 2 - lineOffset;
48549
+ const tx = cx + offsetX * cos - offsetY * sin;
48550
+ const ty = cy + offsetX * sin + offsetY * cos;
48551
+ stream.beginText();
48552
+ stream.setFont(resourceName, fontSize);
48553
+ stream.setTextMatrix(cos, sin, -sin, cos, tx, ty);
48554
+ emitText(stream, fontManager, line, resourceName);
48555
+ stream.endText();
48556
+ }
48557
+ }
48558
+ /** Emit a text string with hex encoding if available. */
48559
+ function emitText(stream, fontManager, text, resourceName) {
48560
+ const hex = fontManager.encodeText(text, resourceName);
48561
+ if (hex) stream.showTextHex(hex);
48902
48562
  else stream.showText(text);
48903
- stream.endText();
48904
48563
  }
48905
48564
  /**
48906
48565
  * Draw vertical stacked text (each character top-to-bottom).
48566
+ * Newlines (\n) start a new column to the right.
48907
48567
  */
48908
- function drawVerticalStackedText(stream, cell, fontManager, _indentPts) {
48568
+ function drawVerticalStackedText(stream, cell, fontManager, _indentPts, scaleFactor = 1) {
48909
48569
  const { rect, text, fontSize } = cell;
48570
+ const padV = 2 * scaleFactor;
48910
48571
  const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
48911
48572
  const charHeight = fontSize * 1.3;
48912
48573
  const ascent = fontManager.getFontAscent(resourceName, fontSize);
48913
- const startX = rect.x + rect.width / 2;
48914
- let currentY = rect.y + rect.height - CELL_PADDING_V - ascent;
48574
+ const columns = text.split(/\r?\n/);
48575
+ const columnWidth = fontSize * 1.4;
48576
+ const totalColumnsWidth = columns.length * columnWidth;
48577
+ const startX = rect.x + rect.width / 2 - totalColumnsWidth / 2 + columnWidth / 2;
48915
48578
  stream.setFillColor(cell.textColor);
48916
- for (let i = 0; i < text.length; i++) {
48917
- if (currentY < rect.y + CELL_PADDING_V) break;
48918
- const ch = text[i];
48919
- const charWidth = fontManager.measureText(ch, resourceName, fontSize);
48920
- stream.beginText();
48921
- stream.setFont(resourceName, fontSize);
48922
- stream.setTextMatrix(1, 0, 0, 1, startX - charWidth / 2, currentY);
48923
- const hexEncoded = fontManager.encodeText(ch, resourceName);
48924
- if (hexEncoded) stream.showTextHex(hexEncoded);
48925
- else stream.showText(ch);
48926
- stream.endText();
48927
- currentY -= charHeight;
48579
+ for (let colIdx = 0; colIdx < columns.length; colIdx++) {
48580
+ const colText = columns[colIdx];
48581
+ const colX = startX + colIdx * columnWidth;
48582
+ let currentY = rect.y + rect.height - padV - ascent;
48583
+ for (const ch of colText) {
48584
+ if (currentY < rect.y + padV) break;
48585
+ const charWidth = fontManager.measureText(ch, resourceName, fontSize);
48586
+ stream.beginText();
48587
+ stream.setFont(resourceName, fontSize);
48588
+ stream.setTextMatrix(1, 0, 0, 1, colX - charWidth / 2, currentY);
48589
+ emitText(stream, fontManager, ch, resourceName);
48590
+ stream.endText();
48591
+ currentY -= charHeight;
48592
+ }
48928
48593
  }
48929
48594
  }
48930
48595
  /**
@@ -48935,41 +48600,39 @@ onmessage = async (ev) => {
48935
48600
  function alphaGsName(alpha) {
48936
48601
  return `GS${Math.round(alpha * 1e4)}`;
48937
48602
  }
48938
- /** Indent width per level in points (~3 characters at 11pt) */
48939
- const INDENT_WIDTH = 10;
48940
- function computeTextStartY(verticalAlign, rect, totalTextHeight, ascent) {
48603
+ function computeTextStartY(verticalAlign, rect, totalTextHeight, ascent, padV = 2) {
48941
48604
  let y;
48942
48605
  switch (verticalAlign) {
48943
48606
  case "top":
48944
- y = rect.y + rect.height - CELL_PADDING_V - ascent;
48607
+ y = rect.y + rect.height - padV - ascent;
48945
48608
  break;
48946
48609
  case "middle":
48947
48610
  y = rect.y + rect.height / 2 + totalTextHeight / 2 - ascent;
48948
48611
  break;
48949
48612
  default:
48950
- y = rect.y + CELL_PADDING_V + (totalTextHeight - ascent);
48613
+ y = rect.y + padV + (totalTextHeight - ascent);
48951
48614
  break;
48952
48615
  }
48953
- const maxY = rect.y + rect.height - CELL_PADDING_V - ascent;
48616
+ const maxY = rect.y + rect.height - padV - ascent;
48954
48617
  if (y > maxY) y = maxY;
48955
- const minY = rect.y + CELL_PADDING_V;
48618
+ const minY = rect.y + padV;
48956
48619
  if (y < minY) y = minY;
48957
48620
  return y;
48958
48621
  }
48959
- function computeTextX(align, rect, textWidth, indentPts = 0) {
48622
+ function computeTextX(align, rect, textWidth, indentPts = 0, padH = 3) {
48960
48623
  let x;
48961
48624
  switch (align) {
48962
48625
  case "center":
48963
48626
  x = rect.x + (rect.width - textWidth) / 2;
48964
48627
  break;
48965
48628
  case "right":
48966
- x = rect.x + rect.width - CELL_PADDING_H - textWidth;
48629
+ x = rect.x + rect.width - padH - textWidth;
48967
48630
  break;
48968
48631
  default:
48969
- x = rect.x + CELL_PADDING_H + indentPts;
48632
+ x = rect.x + padH + indentPts;
48970
48633
  break;
48971
48634
  }
48972
- const minX = rect.x + CELL_PADDING_H;
48635
+ const minX = rect.x + padH;
48973
48636
  if (x < minX) x = minX;
48974
48637
  return x;
48975
48638
  }
@@ -49067,6 +48730,630 @@ onmessage = async (ev) => {
49067
48730
  stream.restore();
49068
48731
  }
49069
48732
  //#endregion
48733
+ //#region src/modules/pdf/render/layout-engine.ts
48734
+ const DEFAULT_COLUMN_WIDTH = 8.43;
48735
+ const DEFAULT_ROW_HEIGHT = 15;
48736
+ const MIN_COLUMN_WIDTH = 3;
48737
+ /**
48738
+ * Resolve horizontal alignment, using Excel's type-based defaults when
48739
+ * no explicit alignment is set (or when alignment is "general"):
48740
+ * - Numbers/Dates → right
48741
+ * - Booleans/Errors → center
48742
+ * - Text/RichText/Hyperlink → left
48743
+ * - Formulas → based on result type
48744
+ */
48745
+ function resolveHorizontalAlign(alignment, cellType, formulaResult) {
48746
+ if (alignment?.horizontal && alignment.horizontal !== "general") return excelHAlignToPdf(alignment);
48747
+ if (cellType !== void 0) switch (cellType) {
48748
+ case PdfCellType.Number:
48749
+ case PdfCellType.Date: return "right";
48750
+ case PdfCellType.Boolean:
48751
+ case PdfCellType.Error: return "center";
48752
+ case PdfCellType.Formula:
48753
+ if (typeof formulaResult === "number" || formulaResult instanceof Date) return "right";
48754
+ if (typeof formulaResult === "boolean") return "center";
48755
+ return "left";
48756
+ default: return "left";
48757
+ }
48758
+ return "left";
48759
+ }
48760
+ /**
48761
+ * Compute the layout for a sheet across one or more PDF pages.
48762
+ * Yields to the event loop between each output page.
48763
+ */
48764
+ async function layoutSheet(sheet, options, fontManager) {
48765
+ const ctx = prepareLayout(sheet, options, fontManager);
48766
+ if (!ctx) return [createEmptyPage(sheet, options)];
48767
+ const layoutPages = [];
48768
+ const totalOutputPages = ctx.rowPages.length * ctx.colGroups.length;
48769
+ for (const rowPage of ctx.rowPages) for (const colGroup of ctx.colGroups) {
48770
+ layoutPages.push(buildPageLayout(ctx, rowPage, colGroup, layoutPages.length, sheet, options, fontManager));
48771
+ if (layoutPages.length < totalOutputPages) await yieldToEventLoop();
48772
+ }
48773
+ if (layoutPages.length > 0 && sheet.images) assignImagesToPages(sheet.images, layoutPages, ctx.scaleFactor);
48774
+ return layoutPages;
48775
+ }
48776
+ /**
48777
+ * Steps 1–5: compute columns, scale, rows, merges, pagination.
48778
+ * Returns null if the sheet has no visible columns (→ caller should emit an empty page).
48779
+ */
48780
+ function prepareLayout(sheet, options, fontManager) {
48781
+ const { margins } = options;
48782
+ let pageWidth = options.pageSize.width;
48783
+ let pageHeight = options.pageSize.height;
48784
+ if (options.orientation === "landscape") [pageWidth, pageHeight] = [pageHeight, pageWidth];
48785
+ const contentWidth = pageWidth - margins.left - margins.right;
48786
+ const contentHeight = pageHeight - margins.top - margins.bottom;
48787
+ const headerHeight = options.showSheetNames ? 20 : 0;
48788
+ const footerHeight = options.showPageNumbers ? 20 : 0;
48789
+ const availableHeight = contentHeight - headerHeight - footerHeight;
48790
+ const printRange = getPrintRange(sheet);
48791
+ const { columnWidths, visibleCols } = computeColumnWidths(sheet, printRange);
48792
+ if (visibleCols.length === 0) return null;
48793
+ const totalTableWidth = columnWidths.reduce((sum, w) => sum + w, 0);
48794
+ let scaleFactor = options.scale;
48795
+ if (options.fitToPage && totalTableWidth > 0) {
48796
+ const fitScale = contentWidth / totalTableWidth;
48797
+ if (fitScale < 1) scaleFactor *= fitScale;
48798
+ }
48799
+ const scaledColumnWidths = columnWidths.map((w) => w * scaleFactor);
48800
+ const { rowHeights, visibleRows } = computeRowHeights(sheet, scaleFactor, printRange, fontManager, options);
48801
+ const mergeMap = buildMergeMap(sheet);
48802
+ const rowPages = paginateRows(rowHeights, availableHeight, typeof options.repeatRows === "number" ? options.repeatRows : 0, buildRowBreakSet(sheet, visibleRows));
48803
+ const colGroups = paginateColumns(scaledColumnWidths, contentWidth, sheet, visibleCols);
48804
+ return {
48805
+ pageWidth,
48806
+ pageHeight,
48807
+ contentWidth,
48808
+ headerHeight,
48809
+ scaleFactor,
48810
+ scaledColumnWidths,
48811
+ rowHeights,
48812
+ visibleRows,
48813
+ visibleCols,
48814
+ mergeMap,
48815
+ rowPages,
48816
+ colGroups,
48817
+ margins
48818
+ };
48819
+ }
48820
+ /**
48821
+ * Build the LayoutPage for a single rowPage × colGroup combination.
48822
+ */
48823
+ function buildPageLayout(ctx, rowPage, colGroup, currentPageCount, sheet, options, fontManager) {
48824
+ const { scaledColumnWidths, rowHeights, visibleRows, visibleCols, mergeMap, pageWidth, pageHeight, contentWidth, headerHeight, scaleFactor, margins } = ctx;
48825
+ const cells = [];
48826
+ const groupColWidths = colGroup.map((ci) => scaledColumnWidths[ci]);
48827
+ const groupTotalWidth = groupColWidths.reduce((s, w) => s + w, 0);
48828
+ const groupColOffsets = [];
48829
+ let gx = margins.left;
48830
+ if (groupTotalWidth < contentWidth) gx = margins.left + (contentWidth - groupTotalWidth) / 2;
48831
+ for (const w of groupColWidths) {
48832
+ groupColOffsets.push(gx);
48833
+ gx += w;
48834
+ }
48835
+ const rowYPositions = [];
48836
+ const pageRowHeights = [];
48837
+ let currentY = pageHeight - margins.top - headerHeight;
48838
+ for (const rowIdx of rowPage) {
48839
+ const rowH = rowHeights[rowIdx] ?? DEFAULT_ROW_HEIGHT * scaleFactor;
48840
+ rowYPositions.push(currentY);
48841
+ pageRowHeights.push(rowH);
48842
+ currentY -= rowH;
48843
+ }
48844
+ const cellGrid = /* @__PURE__ */ new Map();
48845
+ for (let ri = 0; ri < rowPage.length; ri++) {
48846
+ const visibleRowIdx = rowPage[ri];
48847
+ const wsRowNumber = visibleRows[visibleRowIdx];
48848
+ for (let gci = 0; gci < colGroup.length; gci++) {
48849
+ const wsColNumber = visibleCols[colGroup[gci]];
48850
+ const mergeKey = `${wsRowNumber}:${wsColNumber}`;
48851
+ const mergeInfo = mergeMap.get(mergeKey);
48852
+ if (mergeInfo && !mergeInfo.isMaster) continue;
48853
+ const cell = sheet.rows.get(wsRowNumber)?.cells.get(wsColNumber);
48854
+ let colSpan = 1;
48855
+ let rowSpan = 1;
48856
+ if (mergeInfo && mergeInfo.isMaster) {
48857
+ const mergeEndCol = wsColNumber + mergeInfo.colSpan - 1;
48858
+ colSpan = 0;
48859
+ for (let s = gci; s < colGroup.length; s++) if (visibleCols[colGroup[s]] <= mergeEndCol) colSpan++;
48860
+ else break;
48861
+ const mergeEndRow = wsRowNumber + mergeInfo.rowSpan - 1;
48862
+ rowSpan = 0;
48863
+ for (let s = visibleRowIdx; s < visibleRows.length; s++) if (visibleRows[s] <= mergeEndRow) rowSpan++;
48864
+ else break;
48865
+ colSpan = Math.max(colSpan, 1);
48866
+ rowSpan = Math.max(rowSpan, 1);
48867
+ }
48868
+ const cellX = groupColOffsets[gci];
48869
+ const cellY = rowYPositions[ri];
48870
+ let cellWidth = 0;
48871
+ for (let s = 0; s < colSpan && gci + s < groupColWidths.length; s++) cellWidth += groupColWidths[gci + s];
48872
+ let cellHeight = 0;
48873
+ for (let s = 0; s < rowSpan && ri + s < pageRowHeights.length; s++) cellHeight += pageRowHeights[ri + s];
48874
+ const rectY = cellY - cellHeight;
48875
+ cells.push(buildLayoutCell(cell, cellX, rectY, cellWidth, cellHeight, colSpan, rowSpan, options, fontManager, scaleFactor));
48876
+ const layoutCell = cells[cells.length - 1];
48877
+ if (mergeInfo?.isMaster) propagateMergeBorders(layoutCell, mergeInfo, wsRowNumber, wsColNumber, sheet);
48878
+ cellGrid.set(`${ri}:${gci}`, layoutCell);
48879
+ }
48880
+ }
48881
+ computeTextOverflows(cellGrid, rowPage, colGroup, visibleRows, visibleCols, groupColWidths, mergeMap, fontManager);
48882
+ return {
48883
+ pageNumber: currentPageCount + 1,
48884
+ options,
48885
+ cells,
48886
+ width: pageWidth,
48887
+ height: pageHeight,
48888
+ sheetName: sheet.name,
48889
+ sheetCols: colGroup.map((ci) => visibleCols[ci]),
48890
+ columnOffsets: groupColOffsets,
48891
+ columnWidths: groupColWidths,
48892
+ sheetRows: rowPage.map((ri) => visibleRows[ri]),
48893
+ rowYPositions,
48894
+ rowHeights: pageRowHeights,
48895
+ images: [],
48896
+ scaleFactor
48897
+ };
48898
+ }
48899
+ function createEmptyPage(sheet, options) {
48900
+ let pageWidth = options.pageSize.width;
48901
+ let pageHeight = options.pageSize.height;
48902
+ if (options.orientation === "landscape") [pageWidth, pageHeight] = [pageHeight, pageWidth];
48903
+ return {
48904
+ pageNumber: 1,
48905
+ options,
48906
+ cells: [],
48907
+ width: pageWidth,
48908
+ height: pageHeight,
48909
+ sheetName: sheet.name,
48910
+ sheetCols: [],
48911
+ columnOffsets: [],
48912
+ columnWidths: [],
48913
+ sheetRows: [],
48914
+ rowYPositions: [],
48915
+ rowHeights: [],
48916
+ images: [],
48917
+ scaleFactor: 1
48918
+ };
48919
+ }
48920
+ /**
48921
+ * Parse a cell reference like "A1" into 0-indexed { c, r }.
48922
+ */
48923
+ function parseCellRef(ref) {
48924
+ const upper = ref.replace(/\$/g, "").toUpperCase();
48925
+ let col = 0;
48926
+ let i = 0;
48927
+ while (i < upper.length && upper.charCodeAt(i) >= 65 && upper.charCodeAt(i) <= 90) {
48928
+ col = col * 26 + (upper.charCodeAt(i) - 64);
48929
+ i++;
48930
+ }
48931
+ const row = parseInt(upper.substring(i), 10);
48932
+ return {
48933
+ c: col - 1,
48934
+ r: row - 1
48935
+ };
48936
+ }
48937
+ /**
48938
+ * Parse a range string like "A1:B2" into 0-indexed start/end.
48939
+ */
48940
+ function parseRangeRef(range) {
48941
+ const idx = range.indexOf(":");
48942
+ if (idx === -1) {
48943
+ const cell = parseCellRef(range);
48944
+ return {
48945
+ s: cell,
48946
+ e: { ...cell }
48947
+ };
48948
+ }
48949
+ return {
48950
+ s: parseCellRef(range.slice(0, idx)),
48951
+ e: parseCellRef(range.slice(idx + 1))
48952
+ };
48953
+ }
48954
+ /**
48955
+ * Get the print area range from the sheet's pageSetup.
48956
+ * Returns null if no print area is set.
48957
+ */
48958
+ function getPrintRange(sheet) {
48959
+ const printArea = sheet.pageSetup?.printArea;
48960
+ if (!printArea || typeof printArea !== "string") return null;
48961
+ const firstRange = printArea.split("&&")[0].trim();
48962
+ if (!firstRange) return null;
48963
+ try {
48964
+ const range = parseRangeRef(firstRange);
48965
+ return {
48966
+ startRow: range.s.r + 1,
48967
+ endRow: range.e.r + 1,
48968
+ startCol: range.s.c + 1,
48969
+ endCol: range.e.c + 1
48970
+ };
48971
+ } catch {
48972
+ return null;
48973
+ }
48974
+ }
48975
+ function computeColumnWidths(sheet, printRange) {
48976
+ const bounds = sheet.bounds;
48977
+ if (!(bounds.top > 0 && bounds.left > 0)) return {
48978
+ columnWidths: [],
48979
+ visibleCols: []
48980
+ };
48981
+ const startCol = printRange?.startCol ?? bounds.left;
48982
+ const endCol = printRange?.endCol ?? bounds.right;
48983
+ const columnWidths = [];
48984
+ const visibleCols = [];
48985
+ for (let c = startCol; c <= endCol; c++) {
48986
+ const col = sheet.columns.get(c);
48987
+ if (col?.hidden) continue;
48988
+ const pixelWidth = (col?.width ?? DEFAULT_COLUMN_WIDTH) * 7 + 5;
48989
+ const pointWidth = Math.max(pixelWidth * PX_TO_PT, MIN_COLUMN_WIDTH);
48990
+ columnWidths.push(pointWidth);
48991
+ visibleCols.push(c);
48992
+ }
48993
+ return {
48994
+ columnWidths,
48995
+ visibleCols
48996
+ };
48997
+ }
48998
+ function computeRowHeights(sheet, scaleFactor, printRange, fontManager, options) {
48999
+ const bounds = sheet.bounds;
49000
+ if (bounds.top <= 0) return {
49001
+ rowHeights: [],
49002
+ visibleRows: []
49003
+ };
49004
+ const startRow = printRange?.startRow ?? bounds.top;
49005
+ const endRow = printRange?.endRow ?? bounds.bottom;
49006
+ const rowHeights = [];
49007
+ const visibleRows = [];
49008
+ for (let r = startRow; r <= endRow; r++) {
49009
+ const row = sheet.rows.get(r);
49010
+ if (row?.hidden) continue;
49011
+ let height;
49012
+ if (row?.height && row.customHeight) height = row.height;
49013
+ else if (row?.height) height = row.height;
49014
+ else {
49015
+ height = DEFAULT_ROW_HEIGHT;
49016
+ if (row) for (const cell of row.cells.values()) {
49017
+ const fontSize = getCellFontSize(cell);
49018
+ const wrapLineCount = countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options);
49019
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
49020
+ const neededHeight = fontSize + (wrapLineCount - 1) * lineHeight + 4;
49021
+ if (neededHeight > height) height = neededHeight;
49022
+ }
49023
+ }
49024
+ rowHeights.push(height * scaleFactor);
49025
+ visibleRows.push(r);
49026
+ }
49027
+ return {
49028
+ rowHeights,
49029
+ visibleRows
49030
+ };
49031
+ }
49032
+ /**
49033
+ * Get the largest font size for a cell, checking rich text runs.
49034
+ */
49035
+ function getCellFontSize(cell) {
49036
+ let fontSize = cell.style?.font?.size ?? 11;
49037
+ if (cell.type === PdfCellType.RichText) {
49038
+ const value = cell.value;
49039
+ if (value && typeof value === "object" && "richText" in value) {
49040
+ const runs = value.richText;
49041
+ for (const run of runs) {
49042
+ const runSize = run.font?.size ?? fontSize;
49043
+ if (runSize > fontSize) fontSize = runSize;
49044
+ }
49045
+ }
49046
+ }
49047
+ return fontSize;
49048
+ }
49049
+ /**
49050
+ * Count the wrap-line count for a cell, using actual font measurements
49051
+ * so row heights match the page renderer exactly.
49052
+ */
49053
+ function countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options) {
49054
+ const text = typeof cell.text === "string" ? cell.text : String(cell.text ?? "");
49055
+ const lineCount = Math.max(1, (text.match(/\n/g) ?? []).length + 1);
49056
+ if (!cell.style?.alignment?.wrapText || text.length === 0) return lineCount;
49057
+ const scaledColPts = ((sheet.columns.get(cell.col)?.width ?? DEFAULT_COLUMN_WIDTH) * 7 + 5) * PX_TO_PT * scaleFactor;
49058
+ const padding = 6 + (cell.style.alignment.indent ?? 0) * 10;
49059
+ const effectiveWidth = Math.max(scaledColPts - padding, 1);
49060
+ const scaledFontSize = fontSize * scaleFactor;
49061
+ const fontProps = extractFontProperties(cell.style.font, options.defaultFontFamily, options.defaultFontSize);
49062
+ const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
49063
+ const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(pdfFontName);
49064
+ const measure = (s) => fontManager.measureText(s, resourceName, scaledFontSize);
49065
+ const wrappedLines = wrapTextLines(text, measure, effectiveWidth);
49066
+ return Math.max(lineCount, wrappedLines.length);
49067
+ }
49068
+ /**
49069
+ * Build a set of visible-row indices where manual page breaks occur.
49070
+ */
49071
+ function buildRowBreakSet(sheet, visibleRows) {
49072
+ const breaks = /* @__PURE__ */ new Set();
49073
+ const rowBreaks = sheet.rowBreaks ?? [];
49074
+ if (rowBreaks.length === 0) return breaks;
49075
+ const rowToIndex = /* @__PURE__ */ new Map();
49076
+ for (let i = 0; i < visibleRows.length; i++) rowToIndex.set(visibleRows[i], i);
49077
+ for (const brk of rowBreaks) {
49078
+ const idx = rowToIndex.get(brk);
49079
+ if (idx !== void 0) breaks.add(idx + 1);
49080
+ }
49081
+ return breaks;
49082
+ }
49083
+ /**
49084
+ * Build a map of all merged cell regions.
49085
+ * Key: "row:col" (1-based), Value: merge info
49086
+ */
49087
+ function buildMergeMap(sheet) {
49088
+ const map = /* @__PURE__ */ new Map();
49089
+ const merges = sheet.merges;
49090
+ if (!merges || merges.length === 0) return map;
49091
+ for (const rangeStr of merges) {
49092
+ const range = parseRangeRef(rangeStr);
49093
+ const top = range.s.r + 1;
49094
+ const left = range.s.c + 1;
49095
+ const bottom = range.e.r + 1;
49096
+ const right = range.e.c + 1;
49097
+ const rowSpan = bottom - top + 1;
49098
+ const colSpan = right - left + 1;
49099
+ for (let r = top; r <= bottom; r++) for (let c = left; c <= right; c++) map.set(`${r}:${c}`, {
49100
+ isMaster: r === top && c === left,
49101
+ rowSpan,
49102
+ colSpan
49103
+ });
49104
+ }
49105
+ return map;
49106
+ }
49107
+ function paginateRows(rowHeights, availableHeight, repeatRowCount, rowBreaks) {
49108
+ if (rowHeights.length === 0) return [[]];
49109
+ const pages = [];
49110
+ let currentPage = [];
49111
+ let currentPageHeight = 0;
49112
+ let isFirstPage = true;
49113
+ let repeatedPrefixCount = 0;
49114
+ const addRepeatRows = () => {
49115
+ repeatedPrefixCount = 0;
49116
+ for (let h = 0; h < repeatRowCount && h < rowHeights.length; h++) {
49117
+ if (currentPageHeight + rowHeights[h] > availableHeight && currentPage.length > 0) break;
49118
+ currentPage.push(h);
49119
+ currentPageHeight += rowHeights[h];
49120
+ repeatedPrefixCount++;
49121
+ }
49122
+ };
49123
+ for (let i = 0; i < rowHeights.length; i++) {
49124
+ const rowHeight = rowHeights[i];
49125
+ const pageAvailable = availableHeight;
49126
+ let skipRepeatedRow = false;
49127
+ while (true) {
49128
+ const forceBreak = rowBreaks.has(i) && currentPage.length > 0;
49129
+ if ((forceBreak || currentPageHeight + rowHeight > pageAvailable) && currentPage.length > 0) {
49130
+ if (!forceBreak && !isFirstPage && currentPage.length > 0 && currentPage.length === repeatedPrefixCount) {
49131
+ currentPage = [];
49132
+ currentPageHeight = 0;
49133
+ repeatedPrefixCount = 0;
49134
+ continue;
49135
+ }
49136
+ pages.push(currentPage);
49137
+ currentPage = [];
49138
+ currentPageHeight = 0;
49139
+ repeatedPrefixCount = 0;
49140
+ isFirstPage = false;
49141
+ addRepeatRows();
49142
+ continue;
49143
+ }
49144
+ if (!isFirstPage && i < repeatRowCount && currentPage.includes(i)) {
49145
+ skipRepeatedRow = true;
49146
+ break;
49147
+ }
49148
+ currentPage.push(i);
49149
+ currentPageHeight += rowHeight;
49150
+ break;
49151
+ }
49152
+ if (skipRepeatedRow) continue;
49153
+ }
49154
+ if (currentPage.length > 0) pages.push(currentPage);
49155
+ return pages.length > 0 ? pages : [[]];
49156
+ }
49157
+ /**
49158
+ * Split columns into groups for horizontal pagination.
49159
+ */
49160
+ function paginateColumns(columnWidths, contentWidth, sheet, visibleCols) {
49161
+ if (columnWidths.length === 0) return [[]];
49162
+ const colBreaks = /* @__PURE__ */ new Set();
49163
+ const wsColBreaks = sheet.colBreaks ?? [];
49164
+ if (wsColBreaks.length > 0) {
49165
+ const colToIndex = /* @__PURE__ */ new Map();
49166
+ for (let i = 0; i < visibleCols.length; i++) colToIndex.set(visibleCols[i], i);
49167
+ for (const brk of wsColBreaks) {
49168
+ const idx = colToIndex.get(brk);
49169
+ if (idx !== void 0) colBreaks.add(idx + 1);
49170
+ }
49171
+ }
49172
+ const groups = [];
49173
+ let currentGroup = [];
49174
+ let currentWidth = 0;
49175
+ for (let i = 0; i < columnWidths.length; i++) {
49176
+ const colWidth = columnWidths[i];
49177
+ if ((colBreaks.has(i) && currentGroup.length > 0 || currentWidth + colWidth > contentWidth + .01) && currentGroup.length > 0) {
49178
+ groups.push(currentGroup);
49179
+ currentGroup = [];
49180
+ currentWidth = 0;
49181
+ }
49182
+ currentGroup.push(i);
49183
+ currentWidth += colWidth;
49184
+ }
49185
+ if (currentGroup.length > 0) groups.push(currentGroup);
49186
+ return groups.length > 0 ? groups : [Array.from({ length: columnWidths.length }, (_, i) => i)];
49187
+ }
49188
+ function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, fontManager, scaleFactor) {
49189
+ const text = cell?.text ?? "";
49190
+ const style = cell?.style ?? {};
49191
+ const fontProps = extractFontProperties(style.font, options.defaultFontFamily, options.defaultFontSize);
49192
+ const scaledFontSize = fontProps.fontSize * scaleFactor;
49193
+ if (fontManager.hasEmbeddedFont()) fontManager.trackText(text);
49194
+ else {
49195
+ const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
49196
+ fontManager.ensureFont(pdfFontName);
49197
+ }
49198
+ const richText = buildRichTextRuns(cell, options, fontManager, scaleFactor);
49199
+ return {
49200
+ text,
49201
+ rect: {
49202
+ x,
49203
+ y,
49204
+ width,
49205
+ height
49206
+ },
49207
+ fontFamily: fontProps.fontFamily,
49208
+ fontSize: scaledFontSize,
49209
+ bold: fontProps.bold,
49210
+ italic: fontProps.italic,
49211
+ strike: fontProps.strike,
49212
+ underline: fontProps.underline,
49213
+ textColor: fontProps.textColor,
49214
+ fillColor: excelFillToPdfColor(style.fill),
49215
+ horizontalAlign: resolveHorizontalAlign(style.alignment, cell?.type, cell?.result),
49216
+ verticalAlign: excelVAlignToPdf(style.alignment),
49217
+ wrapText: style.alignment?.wrapText ?? false,
49218
+ borders: excelBordersToPdf(style.border),
49219
+ colSpan,
49220
+ rowSpan,
49221
+ hyperlink: cell?.hyperlink ?? null,
49222
+ richText,
49223
+ indent: style.alignment?.indent ?? 0,
49224
+ textRotation: style.alignment?.textRotation === 255 ? "vertical" : style.alignment?.textRotation ?? 0,
49225
+ textOverflowWidth: 0
49226
+ };
49227
+ }
49228
+ /**
49229
+ * Assign pre-collected images to the pages that contain their top-left anchor.
49230
+ */
49231
+ function assignImagesToPages(images, layoutPages, scaleFactor) {
49232
+ for (const img of images) {
49233
+ const tl = img.range.tl;
49234
+ const tlCol = (tl.nativeCol ?? tl.col ?? 0) + 1;
49235
+ const tlRow = (tl.nativeRow ?? tl.row ?? 0) + 1;
49236
+ const targetPage = layoutPages.find((page) => page.sheetCols.includes(tlCol) && page.sheetRows.includes(tlRow));
49237
+ if (!targetPage) continue;
49238
+ const pageColIndex = targetPage.sheetCols.indexOf(tlCol);
49239
+ const pageRowIndex = targetPage.sheetRows.indexOf(tlRow);
49240
+ const baseX = targetPage.columnOffsets[pageColIndex] ?? targetPage.options.margins.left;
49241
+ const baseY = targetPage.rowYPositions[pageRowIndex] ?? targetPage.height - targetPage.options.margins.top - (targetPage.options.showSheetNames ? 20 : 0);
49242
+ const tlColOff = ((tl.nativeColOff ?? 0) / 12700 || 0) * scaleFactor;
49243
+ const tlRowOff = ((tl.nativeRowOff ?? 0) / 12700 || 0) * scaleFactor;
49244
+ const imgX = baseX + tlColOff;
49245
+ const imgY = baseY - tlRowOff;
49246
+ let imgWidth = 100;
49247
+ let imgHeight = 100;
49248
+ if (img.range.ext) {
49249
+ imgWidth = (img.range.ext.width ?? 100) * .75 * scaleFactor;
49250
+ imgHeight = (img.range.ext.height ?? 100) * .75 * scaleFactor;
49251
+ } else if (img.range.br) {
49252
+ const br = img.range.br;
49253
+ const brCol = (br.nativeCol ?? br.col ?? 0) + 1;
49254
+ const brRow = (br.nativeRow ?? br.row ?? 0) + 1;
49255
+ const brPageColIndex = targetPage.sheetCols.indexOf(brCol);
49256
+ const brPageRowIndex = targetPage.sheetRows.indexOf(brRow);
49257
+ const brBaseX = brPageColIndex >= 0 ? targetPage.columnOffsets[brPageColIndex] : imgX + (targetPage.columnWidths[pageColIndex] ?? 100);
49258
+ const brBaseY = brPageRowIndex >= 0 ? targetPage.rowYPositions[brPageRowIndex] : imgY - (targetPage.rowHeights[pageRowIndex] ?? 100);
49259
+ const brColOff = ((br.nativeColOff ?? 0) / 12700 || 0) * scaleFactor;
49260
+ const brRowOff = ((br.nativeRowOff ?? 0) / 12700 || 0) * scaleFactor;
49261
+ const brX = brBaseX + brColOff;
49262
+ const brY = brBaseY - brRowOff;
49263
+ imgWidth = brX - imgX;
49264
+ imgHeight = imgY - brY;
49265
+ }
49266
+ targetPage.images.push({
49267
+ data: img.data,
49268
+ format: img.format,
49269
+ rect: {
49270
+ x: imgX,
49271
+ y: imgY - imgHeight,
49272
+ width: Math.abs(imgWidth),
49273
+ height: Math.abs(imgHeight)
49274
+ }
49275
+ });
49276
+ }
49277
+ }
49278
+ /**
49279
+ * Excel stores merged-cell borders on the boundary cells, not on the master.
49280
+ * Copy the right border from the rightmost column cell and the bottom border
49281
+ * from the bottom row cell so the layout cell renders them correctly.
49282
+ */
49283
+ function propagateMergeBorders(layoutCell, mergeInfo, wsRowNumber, wsColNumber, sheet) {
49284
+ if (mergeInfo.colSpan > 1) {
49285
+ const rightCol = wsColNumber + mergeInfo.colSpan - 1;
49286
+ const rightCellData = sheet.rows.get(wsRowNumber)?.cells.get(rightCol);
49287
+ if (rightCellData?.style?.border?.right) {
49288
+ const converted = excelBordersToPdf({ right: rightCellData.style.border.right });
49289
+ if (converted.right) layoutCell.borders.right = converted.right;
49290
+ }
49291
+ }
49292
+ if (mergeInfo.rowSpan > 1) {
49293
+ const bottomRowNum = wsRowNumber + mergeInfo.rowSpan - 1;
49294
+ const bottomCellData = sheet.rows.get(bottomRowNum)?.cells.get(wsColNumber);
49295
+ if (bottomCellData?.style?.border?.bottom) {
49296
+ const converted = excelBordersToPdf({ bottom: bottomCellData.style.border.bottom });
49297
+ if (converted.bottom) layoutCell.borders.bottom = converted.bottom;
49298
+ }
49299
+ }
49300
+ }
49301
+ /**
49302
+ * In Excel, non-wrapped text overflows into adjacent empty cells.
49303
+ * Fill color alone does NOT block overflow — only text content does.
49304
+ * Computes `textOverflowWidth` for cells whose text exceeds the cell width.
49305
+ */
49306
+ function computeTextOverflows(cellGrid, rowPage, colGroup, visibleRows, visibleCols, groupColWidths, mergeMap, fontManager) {
49307
+ for (let ri = 0; ri < rowPage.length; ri++) for (let gci = 0; gci < colGroup.length; gci++) {
49308
+ const cell = cellGrid.get(`${ri}:${gci}`);
49309
+ if (!cell || cell.wrapText || cell.colSpan > 1 || !cell.text || cell.richText || typeof cell.textRotation === "number" && cell.textRotation !== 0 || cell.textRotation === "vertical") continue;
49310
+ const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
49311
+ const textWidth = fontManager.measureText(cell.text, resourceName, cell.fontSize);
49312
+ const cellContentWidth = cell.rect.width - 6;
49313
+ if (textWidth <= cellContentWidth) continue;
49314
+ const overflowNeeded = textWidth - cellContentWidth;
49315
+ let overflowAvailable = 0;
49316
+ for (let j = gci + 1; j < colGroup.length; j++) {
49317
+ const wsRow = visibleRows[rowPage[ri]];
49318
+ const wsCol = visibleCols[colGroup[j]];
49319
+ if (mergeMap.has(`${wsRow}:${wsCol}`)) break;
49320
+ if (cellGrid.get(`${ri}:${j}`)?.text) break;
49321
+ overflowAvailable += groupColWidths[j];
49322
+ if (overflowAvailable >= overflowNeeded) break;
49323
+ }
49324
+ if (overflowAvailable > 0) cell.textOverflowWidth = Math.min(overflowNeeded, overflowAvailable);
49325
+ }
49326
+ }
49327
+ /**
49328
+ * Build rich text runs from a RichText cell.
49329
+ * Returns null for non-RichText cells.
49330
+ */
49331
+ function buildRichTextRuns(cell, options, fontManager, scaleFactor) {
49332
+ if (!cell || cell.type !== PdfCellType.RichText) return null;
49333
+ const value = cell.value;
49334
+ if (!value || typeof value !== "object" || !("richText" in value)) return null;
49335
+ const runs = value.richText;
49336
+ if (runs.length === 0) return null;
49337
+ return runs.map((run) => {
49338
+ const fontProps = extractFontProperties(run.font, options.defaultFontFamily, options.defaultFontSize);
49339
+ if (fontManager.hasEmbeddedFont()) fontManager.trackText(run.text);
49340
+ else {
49341
+ const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
49342
+ fontManager.ensureFont(pdfFontName);
49343
+ }
49344
+ return {
49345
+ text: run.text,
49346
+ fontFamily: fontProps.fontFamily,
49347
+ fontSize: fontProps.fontSize * scaleFactor,
49348
+ bold: fontProps.bold,
49349
+ italic: fontProps.italic,
49350
+ strike: fontProps.strike,
49351
+ underline: fontProps.underline,
49352
+ textColor: fontProps.textColor
49353
+ };
49354
+ });
49355
+ }
49356
+ //#endregion
49070
49357
  //#region src/modules/pdf/render/png-decoder.ts
49071
49358
  /**
49072
49359
  * Minimal PNG decoder for PDF image embedding.
@@ -49289,13 +49576,23 @@ onmessage = async (ev) => {
49289
49576
  */
49290
49577
  /**
49291
49578
  * Export a PdfWorkbook to PDF format.
49579
+ * Yields to the event loop between each output page during layout and rendering.
49292
49580
  *
49293
49581
  * @param workbook - The workbook data to export
49294
49582
  * @param options - Export options controlling layout, pagination, and appearance
49295
- * @returns PDF file as a Uint8Array
49583
+ * @returns Promise of PDF file as a Uint8Array
49296
49584
  * @throws {PdfError} If the workbook has no sheets or export fails
49297
49585
  */
49298
- function exportPdf(workbook, options) {
49586
+ async function exportPdf(workbook, options) {
49587
+ const ctx = prepareExport(workbook, options);
49588
+ for (const sheet of ctx.sheets) await layoutSheetInto(ctx, sheet, options);
49589
+ return finishExport(ctx, workbook, options);
49590
+ }
49591
+ /**
49592
+ * Shared setup: validate sheets, create font manager and writer,
49593
+ * register embedded font.
49594
+ */
49595
+ function prepareExport(workbook, options) {
49299
49596
  const sheets = selectSheets(workbook, options?.sheets);
49300
49597
  if (sheets.length === 0) throw new PdfError("No sheets to export. The workbook is empty or no sheets matched.");
49301
49598
  const fontManager = new FontManager();
@@ -49306,14 +49603,38 @@ onmessage = async (ev) => {
49306
49603
  } catch (err) {
49307
49604
  throw new PdfRenderError("Failed to parse TrueType font", { cause: err });
49308
49605
  }
49309
- const allPages = [];
49310
- for (const sheet of sheets) try {
49311
- const pages = layoutSheet(sheet, resolveOptions(options, sheet), fontManager);
49312
- allPages.push(...pages);
49606
+ return {
49607
+ sheets,
49608
+ fontManager,
49609
+ writer,
49610
+ allPages: []
49611
+ };
49612
+ }
49613
+ /**
49614
+ * Layout a single sheet and append its pages to the context.
49615
+ */
49616
+ async function layoutSheetInto(ctx, sheet, options) {
49617
+ try {
49618
+ const pages = await layoutSheet(sheet, resolveOptions(options, sheet), ctx.fontManager);
49619
+ ctx.allPages.push(...pages);
49313
49620
  } catch (err) {
49314
49621
  throw new PdfRenderError(`Failed to layout sheet "${sheet.name}"`, { cause: err });
49315
49622
  }
49623
+ }
49624
+ /**
49625
+ * After layout: fix page numbers, track fonts, write resources,
49626
+ * render pages, and build the final PDF binary.
49627
+ */
49628
+ async function finishExport(ctx, workbook, options) {
49629
+ const { allPages, fontManager, writer, sheets } = ctx;
49316
49630
  const documentOptions = resolveOptions(options, sheets[0]);
49631
+ ensureAtLeastOnePage(allPages, documentOptions, sheets);
49632
+ fixPageNumbers(allPages);
49633
+ trackFontsForHeaders(allPages, fontManager);
49634
+ const { pageObjNums, sheetFirstPage, pagesTreeObjNum } = await renderAllPages(allPages, fontManager, writer, fontManager.writeFontResources(writer));
49635
+ return buildFinalPdf(writer, pageObjNums, pagesTreeObjNum, sheetFirstPage, documentOptions, workbook, options);
49636
+ }
49637
+ function ensureAtLeastOnePage(allPages, documentOptions, sheets) {
49317
49638
  if (allPages.length === 0) allPages.push({
49318
49639
  pageNumber: 1,
49319
49640
  options: documentOptions,
@@ -49327,10 +49648,14 @@ onmessage = async (ev) => {
49327
49648
  sheetRows: [],
49328
49649
  rowYPositions: [],
49329
49650
  rowHeights: [],
49330
- images: []
49651
+ images: [],
49652
+ scaleFactor: 1
49331
49653
  });
49654
+ }
49655
+ function fixPageNumbers(allPages) {
49332
49656
  for (let i = 0; i < allPages.length; i++) allPages[i].pageNumber = i + 1;
49333
- const totalPages = allPages.length;
49657
+ }
49658
+ function trackFontsForHeaders(allPages, fontManager) {
49334
49659
  if (fontManager.hasEmbeddedFont()) {
49335
49660
  for (const page of allPages) if (page.options.showSheetNames) fontManager.trackText(page.sheetName);
49336
49661
  }
@@ -49338,11 +49663,24 @@ onmessage = async (ev) => {
49338
49663
  if (!fontManager.hasEmbeddedFont()) {
49339
49664
  for (const page of allPages) if (page.options.showSheetNames) fontManager.ensureFont(resolvePdfFontName(page.options.defaultFontFamily, true, false));
49340
49665
  }
49341
- const fontObjectMap = fontManager.writeFontResources(writer);
49666
+ }
49667
+ async function renderAllPages(allPages, fontManager, writer, fontObjectMap) {
49342
49668
  const pageObjNums = [];
49343
49669
  const pagesTreeObjNum = writer.allocObject();
49344
49670
  const sheetFirstPage = /* @__PURE__ */ new Map();
49345
- for (const page of allPages) try {
49671
+ const totalPages = allPages.length;
49672
+ for (let i = 0; i < allPages.length; i++) {
49673
+ renderSinglePage(allPages[i], fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage);
49674
+ if (i < allPages.length - 1) await yieldToEventLoop();
49675
+ }
49676
+ return {
49677
+ pageObjNums,
49678
+ sheetFirstPage,
49679
+ pagesTreeObjNum
49680
+ };
49681
+ }
49682
+ function renderSinglePage(page, fontManager, writer, fontObjectMap, totalPages, pageObjNums, pagesTreeObjNum, sheetFirstPage) {
49683
+ try {
49346
49684
  const { stream: contentStream, alphaValues } = renderPage(page, page.options, fontManager, totalPages);
49347
49685
  const imageXObjects = /* @__PURE__ */ new Map();
49348
49686
  if (page.images.length > 0) for (let imgIdx = 0; imgIdx < page.images.length; imgIdx++) {
@@ -49397,6 +49735,8 @@ onmessage = async (ev) => {
49397
49735
  } catch (err) {
49398
49736
  throw new PdfRenderError(`Failed to render page ${page.pageNumber} of "${page.sheetName}"`, { cause: err });
49399
49737
  }
49738
+ }
49739
+ function buildFinalPdf(writer, pageObjNums, pagesTreeObjNum, sheetFirstPage, documentOptions, workbook, options) {
49400
49740
  const pagesKids = "[" + pageObjNums.map((n) => pdfRef(n)).join(" ") + "]";
49401
49741
  const pagesDict = new PdfDict().set("Type", "/Pages").set("Kids", pagesKids).set("Count", String(pageObjNums.length));
49402
49742
  writer.addObject(pagesTreeObjNum, pagesDict);
@@ -49454,7 +49794,7 @@ onmessage = async (ev) => {
49454
49794
  orientation,
49455
49795
  margins,
49456
49796
  fitToPage: options?.fitToPage !== void 0 ? options.fitToPage : true,
49457
- scale: Math.max(.1, Math.min(3, options?.scale ?? (ps?.scale ? ps.scale / 100 : 1))),
49797
+ scale: Math.max(.1, Math.min(3, options?.scale ?? ((options?.fitToPage !== void 0 ? options.fitToPage : true) ? 1 : ps?.scale ? ps.scale / 100 : 1))),
49458
49798
  showGridLines: options?.showGridLines ?? ps?.showGridLines ?? false,
49459
49799
  gridLineColor,
49460
49800
  repeatRows,
@@ -49607,7 +49947,7 @@ onmessage = async (ev) => {
49607
49947
  * ```typescript
49608
49948
  * import { pdf } from "@cj-tech-master/excelts/pdf";
49609
49949
  *
49610
- * const bytes = pdf([
49950
+ * const bytes = await pdf([
49611
49951
  * ["Product", "Revenue"],
49612
49952
  * ["Widget", 1000],
49613
49953
  * ["Gadget", 2500]
@@ -49616,7 +49956,7 @@ onmessage = async (ev) => {
49616
49956
  *
49617
49957
  * @example With options:
49618
49958
  * ```typescript
49619
- * const bytes = pdf([
49959
+ * const bytes = await pdf([
49620
49960
  * ["Name", "Score"],
49621
49961
  * ["Alice", 95],
49622
49962
  * ["Bob", 87]
@@ -49625,7 +49965,7 @@ onmessage = async (ev) => {
49625
49965
  *
49626
49966
  * @example Multiple sheets:
49627
49967
  * ```typescript
49628
- * const bytes = pdf({
49968
+ * const bytes = await pdf({
49629
49969
  * sheets: [
49630
49970
  * { name: "Sales", data: [["Product", "Revenue"], ["Widget", 1000]] },
49631
49971
  * { name: "Costs", data: [["Item", "Amount"], ["Rent", 500]] }
@@ -49635,7 +49975,7 @@ onmessage = async (ev) => {
49635
49975
  *
49636
49976
  * @example With column widths and styles:
49637
49977
  * ```typescript
49638
- * const bytes = pdf({
49978
+ * const bytes = await pdf({
49639
49979
  * name: "Report",
49640
49980
  * columns: [{ width: 25 }, { width: 15 }],
49641
49981
  * data: [
@@ -49649,12 +49989,13 @@ onmessage = async (ev) => {
49649
49989
  * Generate a PDF.
49650
49990
  *
49651
49991
  * Accepts anything from a plain 2D array to a multi-sheet workbook.
49992
+ * Yields to the event loop between each output page during layout and rendering.
49652
49993
  *
49653
49994
  * @param input - 2D array, sheet object, or workbook object
49654
49995
  * @param options - PDF export options (page size, margins, etc.)
49655
- * @returns PDF file as Uint8Array
49996
+ * @returns Promise of PDF file as Uint8Array
49656
49997
  */
49657
- function pdf(input, options) {
49998
+ async function pdf(input, options) {
49658
49999
  return exportPdf(normalizeInput(input), options);
49659
50000
  }
49660
50001
  function normalizeInput(input) {
@@ -49814,12 +50155,13 @@ onmessage = async (ev) => {
49814
50155
  *
49815
50156
  * This is a convenience function that converts the Workbook to the PDF module's
49816
50157
  * data model and then generates the PDF.
50158
+ * Yields to the event loop between each output page during layout and rendering.
49817
50159
  *
49818
50160
  * @param workbook - An Excel Workbook instance
49819
50161
  * @param options - PDF export options
49820
- * @returns PDF file as a Uint8Array
50162
+ * @returns Promise of PDF file as a Uint8Array
49821
50163
  */
49822
- function excelToPdf(workbook, options) {
50164
+ async function excelToPdf(workbook, options) {
49823
50165
  return exportPdf(excelWorkbookToPdf(workbook), options);
49824
50166
  }
49825
50167
  /**
@@ -49860,12 +50202,15 @@ onmessage = async (ev) => {
49860
50202
  const row = ws.findRow(r);
49861
50203
  if (!row) continue;
49862
50204
  const cells = /* @__PURE__ */ new Map();
49863
- row.eachCell({ includeEmpty: false }, (cell) => {
49864
- cells.set(cell.col, convertCell(cell));
50205
+ row.eachCell({ includeEmpty: true }, (cell) => {
50206
+ const hasValue = cell.type !== ValueType.Null && cell.type !== ValueType.Merge;
50207
+ const hasStyle = cell.style && (cell.style.border && (cell.style.border.top || cell.style.border.right || cell.style.border.bottom || cell.style.border.left) || cell.style.fill || cell.style.font);
50208
+ if (hasValue || hasStyle) cells.set(cell.col, convertCell(cell));
49865
50209
  });
49866
50210
  rows.set(r, {
49867
50211
  hidden: row.hidden || void 0,
49868
50212
  height: row.height ?? void 0,
50213
+ customHeight: row.customHeight || void 0,
49869
50214
  cells
49870
50215
  });
49871
50216
  }