@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
@@ -19,20 +19,86 @@
19
19
  import { PdfCellType } from "../types.js";
20
20
  import { resolvePdfFontName } from "../font/font-manager.js";
21
21
  import { extractFontProperties, excelFillToPdfColor, excelBordersToPdf, excelHAlignToPdf, excelVAlignToPdf } from "./style-converter.js";
22
+ import { wrapTextLines } from "./page-renderer.js";
23
+ import { CELL_PADDING_H, CELL_PADDING_V, LINE_HEIGHT_FACTOR, INDENT_WIDTH, MAX_DIGIT_WIDTH_PX, EXCEL_COLUMN_PADDING_PX, PX_TO_PT } from "./constants.js";
24
+ import { yieldToEventLoop } from "../../../utils/utils.base.js";
22
25
  // =============================================================================
23
26
  // Constants
24
27
  // =============================================================================
25
- const EXCEL_CHAR_WIDTH_TO_POINTS = 7;
26
28
  const DEFAULT_COLUMN_WIDTH = 8.43;
27
29
  const DEFAULT_ROW_HEIGHT = 15;
28
- const MIN_COLUMN_WIDTH = 5;
30
+ const MIN_COLUMN_WIDTH = 3;
31
+ // =============================================================================
32
+ // Type-based Default Alignment
33
+ // =============================================================================
34
+ /**
35
+ * Resolve horizontal alignment, using Excel's type-based defaults when
36
+ * no explicit alignment is set (or when alignment is "general"):
37
+ * - Numbers/Dates → right
38
+ * - Booleans/Errors → center
39
+ * - Text/RichText/Hyperlink → left
40
+ * - Formulas → based on result type
41
+ */
42
+ function resolveHorizontalAlign(alignment, cellType, formulaResult) {
43
+ // If explicitly set (and not "general"), use the explicit alignment
44
+ if (alignment?.horizontal && alignment.horizontal !== "general") {
45
+ return excelHAlignToPdf(alignment);
46
+ }
47
+ // Use type-based default
48
+ if (cellType !== undefined) {
49
+ switch (cellType) {
50
+ case PdfCellType.Number:
51
+ case PdfCellType.Date:
52
+ return "right";
53
+ case PdfCellType.Boolean:
54
+ case PdfCellType.Error:
55
+ return "center";
56
+ case PdfCellType.Formula:
57
+ if (typeof formulaResult === "number" || formulaResult instanceof Date) {
58
+ return "right";
59
+ }
60
+ if (typeof formulaResult === "boolean") {
61
+ return "center";
62
+ }
63
+ return "left";
64
+ default:
65
+ return "left";
66
+ }
67
+ }
68
+ return "left";
69
+ }
29
70
  // =============================================================================
30
71
  // Layout Engine
31
72
  // =============================================================================
32
73
  /**
33
74
  * Compute the layout for a sheet across one or more PDF pages.
75
+ * Yields to the event loop between each output page.
34
76
  */
35
- export function layoutSheet(sheet, options, fontManager) {
77
+ export async function layoutSheet(sheet, options, fontManager) {
78
+ const ctx = prepareLayout(sheet, options, fontManager);
79
+ if (!ctx) {
80
+ return [createEmptyPage(sheet, options)];
81
+ }
82
+ const layoutPages = [];
83
+ const totalOutputPages = ctx.rowPages.length * ctx.colGroups.length;
84
+ for (const rowPage of ctx.rowPages) {
85
+ for (const colGroup of ctx.colGroups) {
86
+ layoutPages.push(buildPageLayout(ctx, rowPage, colGroup, layoutPages.length, sheet, options, fontManager));
87
+ if (layoutPages.length < totalOutputPages) {
88
+ await yieldToEventLoop();
89
+ }
90
+ }
91
+ }
92
+ if (layoutPages.length > 0 && sheet.images) {
93
+ assignImagesToPages(sheet.images, layoutPages, ctx.scaleFactor);
94
+ }
95
+ return layoutPages;
96
+ }
97
+ /**
98
+ * Steps 1–5: compute columns, scale, rows, merges, pagination.
99
+ * Returns null if the sheet has no visible columns (→ caller should emit an empty page).
100
+ */
101
+ function prepareLayout(sheet, options, fontManager) {
36
102
  const { margins } = options;
37
103
  let pageWidth = options.pageSize.width;
38
104
  let pageHeight = options.pageSize.height;
@@ -44,13 +110,11 @@ export function layoutSheet(sheet, options, fontManager) {
44
110
  const headerHeight = options.showSheetNames ? 20 : 0;
45
111
  const footerHeight = options.showPageNumbers ? 20 : 0;
46
112
  const availableHeight = contentHeight - headerHeight - footerHeight;
47
- // Determine print area bounds (if set)
48
113
  const printRange = getPrintRange(sheet);
49
114
  // --- Step 1: Visible columns and widths ---
50
115
  const { columnWidths, visibleCols } = computeColumnWidths(sheet, printRange);
51
- const columnCount = visibleCols.length;
52
- if (columnCount === 0) {
53
- return [emptyPage(pageWidth, pageHeight, sheet.name, options)];
116
+ if (visibleCols.length === 0) {
117
+ return null;
54
118
  }
55
119
  // --- Step 2: Scale ---
56
120
  const totalTableWidth = columnWidths.reduce((sum, w) => sum + w, 0);
@@ -63,136 +127,159 @@ export function layoutSheet(sheet, options, fontManager) {
63
127
  }
64
128
  const scaledColumnWidths = columnWidths.map(w => w * scaleFactor);
65
129
  // --- Step 3: Visible rows and heights ---
66
- const { rowHeights, visibleRows } = computeRowHeights(sheet, scaleFactor, printRange);
130
+ const { rowHeights, visibleRows } = computeRowHeights(sheet, scaleFactor, printRange, fontManager, options);
67
131
  // --- Step 4: Merge map ---
68
132
  const mergeMap = buildMergeMap(sheet);
69
- // --- Step 5: Paginate vertically (rows) and horizontally (columns) ---
133
+ // --- Step 5: Paginate ---
70
134
  const repeatRowCount = typeof options.repeatRows === "number" ? options.repeatRows : 0;
71
135
  const rowBreakSet = buildRowBreakSet(sheet, visibleRows);
72
136
  const rowPages = paginateRows(rowHeights, availableHeight, repeatRowCount, rowBreakSet);
73
137
  const colGroups = paginateColumns(scaledColumnWidths, contentWidth, sheet, visibleCols);
74
- // --- Step 6: Layout cells per page (row page × column page) ---
75
- const layoutPages = [];
76
- for (const rowPage of rowPages) {
77
- for (const colGroup of colGroups) {
78
- const cells = [];
79
- // Compute column offsets for this column group
80
- const groupColWidths = colGroup.map(ci => scaledColumnWidths[ci]);
81
- const groupTotalWidth = groupColWidths.reduce((s, w) => s + w, 0);
82
- const groupColOffsets = [];
83
- let gx = margins.left;
84
- if (groupTotalWidth < contentWidth) {
85
- gx = margins.left + (contentWidth - groupTotalWidth) / 2;
86
- }
87
- for (const w of groupColWidths) {
88
- groupColOffsets.push(gx);
89
- gx += w;
90
- }
91
- // Row Y positions
92
- const rowYPositions = [];
93
- const pageRowHeights = [];
94
- let currentY = pageHeight - margins.top - headerHeight;
95
- for (const rowIdx of rowPage) {
96
- const rowH = rowHeights[rowIdx] ?? DEFAULT_ROW_HEIGHT * scaleFactor;
97
- rowYPositions.push(currentY);
98
- pageRowHeights.push(rowH);
99
- currentY -= rowH;
138
+ return {
139
+ pageWidth,
140
+ pageHeight,
141
+ contentWidth,
142
+ headerHeight,
143
+ scaleFactor,
144
+ scaledColumnWidths,
145
+ rowHeights,
146
+ visibleRows,
147
+ visibleCols,
148
+ mergeMap,
149
+ rowPages,
150
+ colGroups,
151
+ margins
152
+ };
153
+ }
154
+ /**
155
+ * Build the LayoutPage for a single rowPage × colGroup combination.
156
+ */
157
+ function buildPageLayout(ctx, rowPage, colGroup, currentPageCount, sheet, options, fontManager) {
158
+ const { scaledColumnWidths, rowHeights, visibleRows, visibleCols, mergeMap, pageWidth, pageHeight, contentWidth, headerHeight, scaleFactor, margins } = ctx;
159
+ const cells = [];
160
+ // Compute column offsets for this column group
161
+ const groupColWidths = colGroup.map(ci => scaledColumnWidths[ci]);
162
+ const groupTotalWidth = groupColWidths.reduce((s, w) => s + w, 0);
163
+ const groupColOffsets = [];
164
+ let gx = margins.left;
165
+ if (groupTotalWidth < contentWidth) {
166
+ gx = margins.left + (contentWidth - groupTotalWidth) / 2;
167
+ }
168
+ for (const w of groupColWidths) {
169
+ groupColOffsets.push(gx);
170
+ gx += w;
171
+ }
172
+ // Row Y positions
173
+ const rowYPositions = [];
174
+ const pageRowHeights = [];
175
+ let currentY = pageHeight - margins.top - headerHeight;
176
+ for (const rowIdx of rowPage) {
177
+ const rowH = rowHeights[rowIdx] ?? DEFAULT_ROW_HEIGHT * scaleFactor;
178
+ rowYPositions.push(currentY);
179
+ pageRowHeights.push(rowH);
180
+ currentY -= rowH;
181
+ }
182
+ // Build cells for this row page × column group
183
+ const cellGrid = new Map();
184
+ for (let ri = 0; ri < rowPage.length; ri++) {
185
+ const visibleRowIdx = rowPage[ri];
186
+ const wsRowNumber = visibleRows[visibleRowIdx];
187
+ for (let gci = 0; gci < colGroup.length; gci++) {
188
+ const ci = colGroup[gci];
189
+ const wsColNumber = visibleCols[ci];
190
+ const mergeKey = `${wsRowNumber}:${wsColNumber}`;
191
+ const mergeInfo = mergeMap.get(mergeKey);
192
+ if (mergeInfo && !mergeInfo.isMaster) {
193
+ continue;
100
194
  }
101
- // Build cells for this row page × column group
102
- for (let ri = 0; ri < rowPage.length; ri++) {
103
- const visibleRowIdx = rowPage[ri];
104
- const wsRowNumber = visibleRows[visibleRowIdx];
105
- for (let gci = 0; gci < colGroup.length; gci++) {
106
- const ci = colGroup[gci]; // index into visibleCols
107
- const wsColNumber = visibleCols[ci];
108
- const mergeKey = `${wsRowNumber}:${wsColNumber}`;
109
- const mergeInfo = mergeMap.get(mergeKey);
110
- if (mergeInfo && !mergeInfo.isMaster) {
111
- continue;
195
+ const row = sheet.rows.get(wsRowNumber);
196
+ const cell = row?.cells.get(wsColNumber);
197
+ let colSpan = 1;
198
+ let rowSpan = 1;
199
+ if (mergeInfo && mergeInfo.isMaster) {
200
+ const mergeEndCol = wsColNumber + mergeInfo.colSpan - 1;
201
+ colSpan = 0;
202
+ for (let s = gci; s < colGroup.length; s++) {
203
+ if (visibleCols[colGroup[s]] <= mergeEndCol) {
204
+ colSpan++;
112
205
  }
113
- const row = sheet.rows.get(wsRowNumber);
114
- const cell = row?.cells.get(wsColNumber);
115
- let colSpan = 1;
116
- let rowSpan = 1;
117
- if (mergeInfo && mergeInfo.isMaster) {
118
- const mergeEndCol = wsColNumber + mergeInfo.colSpan - 1;
119
- colSpan = 0;
120
- for (let s = gci; s < colGroup.length; s++) {
121
- if (visibleCols[colGroup[s]] <= mergeEndCol) {
122
- colSpan++;
123
- }
124
- else {
125
- break;
126
- }
127
- }
128
- const mergeEndRow = wsRowNumber + mergeInfo.rowSpan - 1;
129
- rowSpan = 0;
130
- for (let s = visibleRowIdx; s < visibleRows.length; s++) {
131
- if (visibleRows[s] <= mergeEndRow) {
132
- rowSpan++;
133
- }
134
- else {
135
- break;
136
- }
137
- }
138
- colSpan = Math.max(colSpan, 1);
139
- rowSpan = Math.max(rowSpan, 1);
206
+ else {
207
+ break;
140
208
  }
141
- const cellX = groupColOffsets[gci];
142
- const cellY = rowYPositions[ri];
143
- let cellWidth = 0;
144
- for (let s = 0; s < colSpan && gci + s < groupColWidths.length; s++) {
145
- cellWidth += groupColWidths[gci + s];
209
+ }
210
+ const mergeEndRow = wsRowNumber + mergeInfo.rowSpan - 1;
211
+ rowSpan = 0;
212
+ for (let s = visibleRowIdx; s < visibleRows.length; s++) {
213
+ if (visibleRows[s] <= mergeEndRow) {
214
+ rowSpan++;
146
215
  }
147
- let cellHeight = 0;
148
- for (let s = 0; s < rowSpan && ri + s < pageRowHeights.length; s++) {
149
- cellHeight += pageRowHeights[ri + s];
216
+ else {
217
+ break;
150
218
  }
151
- const rectY = cellY - cellHeight;
152
- cells.push(buildLayoutCell(cell, cellX, rectY, cellWidth, cellHeight, colSpan, rowSpan, options, fontManager, scaleFactor));
153
219
  }
220
+ colSpan = Math.max(colSpan, 1);
221
+ rowSpan = Math.max(rowSpan, 1);
222
+ }
223
+ const cellX = groupColOffsets[gci];
224
+ const cellY = rowYPositions[ri];
225
+ let cellWidth = 0;
226
+ for (let s = 0; s < colSpan && gci + s < groupColWidths.length; s++) {
227
+ cellWidth += groupColWidths[gci + s];
228
+ }
229
+ let cellHeight = 0;
230
+ for (let s = 0; s < rowSpan && ri + s < pageRowHeights.length; s++) {
231
+ cellHeight += pageRowHeights[ri + s];
232
+ }
233
+ const rectY = cellY - cellHeight;
234
+ cells.push(buildLayoutCell(cell, cellX, rectY, cellWidth, cellHeight, colSpan, rowSpan, options, fontManager, scaleFactor));
235
+ const layoutCell = cells[cells.length - 1];
236
+ // Propagate merged cell borders from boundary cells
237
+ if (mergeInfo?.isMaster) {
238
+ propagateMergeBorders(layoutCell, mergeInfo, wsRowNumber, wsColNumber, sheet);
154
239
  }
155
- layoutPages.push({
156
- pageNumber: layoutPages.length + 1,
157
- options,
158
- cells,
159
- width: pageWidth,
160
- height: pageHeight,
161
- sheetName: sheet.name,
162
- sheetCols: colGroup.map(ci => visibleCols[ci]),
163
- columnOffsets: groupColOffsets,
164
- columnWidths: groupColWidths,
165
- sheetRows: rowPage.map(ri => visibleRows[ri]),
166
- rowYPositions,
167
- rowHeights: pageRowHeights,
168
- images: []
169
- });
240
+ cellGrid.set(`${ri}:${gci}`, layoutCell);
170
241
  }
171
242
  }
172
- // --- Step 7: Place images on the correct pages ---
173
- if (layoutPages.length > 0 && sheet.images) {
174
- assignImagesToPages(sheet.images, layoutPages);
175
- }
176
- return layoutPages;
243
+ // Compute text overflow widths for non-wrapped cells
244
+ computeTextOverflows(cellGrid, rowPage, colGroup, visibleRows, visibleCols, groupColWidths, mergeMap, fontManager);
245
+ return {
246
+ pageNumber: currentPageCount + 1,
247
+ options,
248
+ cells,
249
+ width: pageWidth,
250
+ height: pageHeight,
251
+ sheetName: sheet.name,
252
+ sheetCols: colGroup.map(ci => visibleCols[ci]),
253
+ columnOffsets: groupColOffsets,
254
+ columnWidths: groupColWidths,
255
+ sheetRows: rowPage.map(ri => visibleRows[ri]),
256
+ rowYPositions,
257
+ rowHeights: pageRowHeights,
258
+ images: [],
259
+ scaleFactor
260
+ };
177
261
  }
178
- // =============================================================================
179
- // Helpers
180
- // =============================================================================
181
- function emptyPage(width, height, sheetName, options) {
262
+ function createEmptyPage(sheet, options) {
263
+ let pageWidth = options.pageSize.width;
264
+ let pageHeight = options.pageSize.height;
265
+ if (options.orientation === "landscape") {
266
+ [pageWidth, pageHeight] = [pageHeight, pageWidth];
267
+ }
182
268
  return {
183
269
  pageNumber: 1,
184
270
  options,
185
271
  cells: [],
186
- width,
187
- height,
188
- sheetName,
272
+ width: pageWidth,
273
+ height: pageHeight,
274
+ sheetName: sheet.name,
189
275
  sheetCols: [],
190
276
  columnOffsets: [],
191
277
  columnWidths: [],
192
278
  sheetRows: [],
193
279
  rowYPositions: [],
194
280
  rowHeights: [],
195
- images: []
281
+ images: [],
282
+ scaleFactor: 1
196
283
  };
197
284
  }
198
285
  /**
@@ -270,7 +357,8 @@ function computeColumnWidths(sheet, printRange) {
270
357
  continue;
271
358
  }
272
359
  const excelWidth = col?.width ?? DEFAULT_COLUMN_WIDTH;
273
- const pointWidth = Math.max(excelWidth * EXCEL_CHAR_WIDTH_TO_POINTS, MIN_COLUMN_WIDTH);
360
+ const pixelWidth = excelWidth * MAX_DIGIT_WIDTH_PX + EXCEL_COLUMN_PADDING_PX;
361
+ const pointWidth = Math.max(pixelWidth * PX_TO_PT, MIN_COLUMN_WIDTH);
274
362
  columnWidths.push(pointWidth);
275
363
  visibleCols.push(c);
276
364
  }
@@ -279,7 +367,7 @@ function computeColumnWidths(sheet, printRange) {
279
367
  // =============================================================================
280
368
  // Row Height Computation
281
369
  // =============================================================================
282
- function computeRowHeights(sheet, scaleFactor, printRange) {
370
+ function computeRowHeights(sheet, scaleFactor, printRange, fontManager, options) {
283
371
  const bounds = sheet.bounds;
284
372
  if (bounds.top <= 0) {
285
373
  return { rowHeights: [], visibleRows: [] };
@@ -290,45 +378,27 @@ function computeRowHeights(sheet, scaleFactor, printRange) {
290
378
  const visibleRows = [];
291
379
  for (let r = startRow; r <= endRow; r++) {
292
380
  const row = sheet.rows.get(r);
293
- if (row && row.hidden) {
381
+ if (row?.hidden) {
294
382
  continue;
295
383
  }
296
384
  let height;
297
- if (row?.height) {
298
- // Explicit row height set by user
385
+ if (row?.height && row.customHeight) {
386
+ // Custom height explicitly set by user — use as-is
387
+ height = row.height;
388
+ }
389
+ else if (row?.height) {
390
+ // Excel auto-calculated height — trust it as-is
299
391
  height = row.height;
300
392
  }
301
393
  else {
302
- // Auto-size: scan cells in this row to find the largest needed height.
394
+ // No height info: auto-size based on cell content
303
395
  height = DEFAULT_ROW_HEIGHT;
304
396
  if (row) {
305
397
  for (const cell of row.cells.values()) {
306
- let fontSize = cell.style?.font?.size ?? 11;
307
- // For rich text cells, find the largest font size across all runs
308
- const rtValue = cell.value;
309
- if (rtValue?.richText) {
310
- for (const run of rtValue.richText) {
311
- const runSize = run.font?.size ?? fontSize;
312
- if (runSize > fontSize) {
313
- fontSize = runSize;
314
- }
315
- }
316
- }
317
- const lineHeight = fontSize * 1.5;
318
- // Count lines: explicit newlines in the text
319
- const text = cell.text ?? "";
320
- const lineCount = Math.max(1, (text.match(/\n/g) ?? []).length + 1);
321
- // For wrapText cells, estimate how many lines word-wrapping produces
322
- let wrapLineCount = lineCount;
323
- if (cell.style?.alignment?.wrapText && lineCount === 1 && text.length > 0) {
324
- const col = sheet.columns.get(cell.col);
325
- const colWidth = col?.width ?? DEFAULT_COLUMN_WIDTH;
326
- const colPts = colWidth * EXCEL_CHAR_WIDTH_TO_POINTS * scaleFactor;
327
- const avgCharWidth = fontSize * 0.55; // rough average char width
328
- const charsPerLine = Math.max(1, Math.floor(colPts / avgCharWidth));
329
- wrapLineCount = Math.ceil(text.length / charsPerLine);
330
- }
331
- const neededHeight = lineHeight * wrapLineCount;
398
+ const fontSize = getCellFontSize(cell);
399
+ const wrapLineCount = countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options);
400
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
401
+ const neededHeight = fontSize + (wrapLineCount - 1) * lineHeight + CELL_PADDING_V * 2;
332
402
  if (neededHeight > height) {
333
403
  height = neededHeight;
334
404
  }
@@ -340,6 +410,51 @@ function computeRowHeights(sheet, scaleFactor, printRange) {
340
410
  }
341
411
  return { rowHeights, visibleRows };
342
412
  }
413
+ /**
414
+ * Get the largest font size for a cell, checking rich text runs.
415
+ */
416
+ function getCellFontSize(cell) {
417
+ let fontSize = cell.style?.font?.size ?? 11;
418
+ if (cell.type === PdfCellType.RichText) {
419
+ const value = cell.value;
420
+ if (value && typeof value === "object" && "richText" in value) {
421
+ const runs = value.richText;
422
+ for (const run of runs) {
423
+ const runSize = run.font?.size ?? fontSize;
424
+ if (runSize > fontSize) {
425
+ fontSize = runSize;
426
+ }
427
+ }
428
+ }
429
+ }
430
+ return fontSize;
431
+ }
432
+ /**
433
+ * Count the wrap-line count for a cell, using actual font measurements
434
+ * so row heights match the page renderer exactly.
435
+ */
436
+ function countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options) {
437
+ const text = typeof cell.text === "string" ? cell.text : String(cell.text ?? "");
438
+ const lineCount = Math.max(1, (text.match(/\n/g) ?? []).length + 1);
439
+ if (!cell.style?.alignment?.wrapText || text.length === 0) {
440
+ return lineCount;
441
+ }
442
+ const col = sheet.columns.get(cell.col);
443
+ const colWidth = col?.width ?? DEFAULT_COLUMN_WIDTH;
444
+ const scaledColPts = (colWidth * MAX_DIGIT_WIDTH_PX + EXCEL_COLUMN_PADDING_PX) * PX_TO_PT * scaleFactor;
445
+ const indent = cell.style.alignment.indent ?? 0;
446
+ const padding = CELL_PADDING_H * 2 + indent * INDENT_WIDTH;
447
+ const effectiveWidth = Math.max(scaledColPts - padding, 1);
448
+ const scaledFontSize = fontSize * scaleFactor;
449
+ const fontProps = extractFontProperties(cell.style.font, options.defaultFontFamily, options.defaultFontSize);
450
+ const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
451
+ const resourceName = fontManager.hasEmbeddedFont()
452
+ ? fontManager.getEmbeddedResourceName()
453
+ : fontManager.ensureFont(pdfFontName);
454
+ const measure = (s) => fontManager.measureText(s, resourceName, scaledFontSize);
455
+ const wrappedLines = wrapTextLines(text, measure, effectiveWidth);
456
+ return Math.max(lineCount, wrappedLines.length);
457
+ }
343
458
  // =============================================================================
344
459
  // Row Breaks
345
460
  // =============================================================================
@@ -533,7 +648,7 @@ function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, f
533
648
  underline: fontProps.underline,
534
649
  textColor: fontProps.textColor,
535
650
  fillColor: excelFillToPdfColor(style.fill),
536
- horizontalAlign: excelHAlignToPdf(style.alignment),
651
+ horizontalAlign: resolveHorizontalAlign(style.alignment, cell?.type, cell?.result),
537
652
  verticalAlign: excelVAlignToPdf(style.alignment),
538
653
  wrapText: style.alignment?.wrapText ?? false,
539
654
  borders: excelBordersToPdf(style.border),
@@ -542,7 +657,8 @@ function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, f
542
657
  hyperlink: cell?.hyperlink ?? null,
543
658
  richText,
544
659
  indent: style.alignment?.indent ?? 0,
545
- textRotation: style.alignment?.textRotation ?? 0
660
+ textRotation: style.alignment?.textRotation === 255 ? "vertical" : (style.alignment?.textRotation ?? 0),
661
+ textOverflowWidth: 0
546
662
  };
547
663
  }
548
664
  // =============================================================================
@@ -551,7 +667,7 @@ function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, f
551
667
  /**
552
668
  * Assign pre-collected images to the pages that contain their top-left anchor.
553
669
  */
554
- function assignImagesToPages(images, layoutPages) {
670
+ function assignImagesToPages(images, layoutPages, scaleFactor) {
555
671
  for (const img of images) {
556
672
  const tl = img.range.tl;
557
673
  const tlCol = (tl.nativeCol ?? tl.col ?? 0) + 1; // convert 0-indexed to 1-indexed
@@ -567,17 +683,17 @@ function assignImagesToPages(images, layoutPages) {
567
683
  targetPage.height -
568
684
  targetPage.options.margins.top -
569
685
  (targetPage.options.showSheetNames ? 20 : 0);
570
- // Apply sub-cell offsets (EMU: 1pt = 12700 EMU)
571
- const tlColOff = (tl.nativeColOff ?? 0) / 12700 || 0;
572
- const tlRowOff = (tl.nativeRowOff ?? 0) / 12700 || 0;
686
+ // Apply sub-cell offsets (EMU: 1pt = 12700 EMU), scaled to match page layout
687
+ const tlColOff = ((tl.nativeColOff ?? 0) / 12700 || 0) * scaleFactor;
688
+ const tlRowOff = ((tl.nativeRowOff ?? 0) / 12700 || 0) * scaleFactor;
573
689
  const imgX = baseX + tlColOff;
574
690
  const imgY = baseY - tlRowOff;
575
691
  // Determine image size
576
692
  let imgWidth = 100;
577
693
  let imgHeight = 100;
578
694
  if (img.range.ext) {
579
- imgWidth = (img.range.ext.width ?? 100) * 0.75;
580
- imgHeight = (img.range.ext.height ?? 100) * 0.75;
695
+ imgWidth = (img.range.ext.width ?? 100) * 0.75 * scaleFactor;
696
+ imgHeight = (img.range.ext.height ?? 100) * 0.75 * scaleFactor;
581
697
  }
582
698
  else if (img.range.br) {
583
699
  const br = img.range.br;
@@ -591,8 +707,8 @@ function assignImagesToPages(images, layoutPages) {
591
707
  const brBaseY = brPageRowIndex >= 0
592
708
  ? targetPage.rowYPositions[brPageRowIndex]
593
709
  : imgY - (targetPage.rowHeights[pageRowIndex] ?? 100);
594
- const brColOff = (br.nativeColOff ?? 0) / 12700 || 0;
595
- const brRowOff = (br.nativeRowOff ?? 0) / 12700 || 0;
710
+ const brColOff = ((br.nativeColOff ?? 0) / 12700 || 0) * scaleFactor;
711
+ const brRowOff = ((br.nativeRowOff ?? 0) / 12700 || 0) * scaleFactor;
596
712
  const brX = brBaseX + brColOff;
597
713
  const brY = brBaseY - brRowOff;
598
714
  imgWidth = brX - imgX;
@@ -611,6 +727,89 @@ function assignImagesToPages(images, layoutPages) {
611
727
  }
612
728
  }
613
729
  // =============================================================================
730
+ // Merge Border Propagation
731
+ // =============================================================================
732
+ /**
733
+ * Excel stores merged-cell borders on the boundary cells, not on the master.
734
+ * Copy the right border from the rightmost column cell and the bottom border
735
+ * from the bottom row cell so the layout cell renders them correctly.
736
+ */
737
+ function propagateMergeBorders(layoutCell, mergeInfo, wsRowNumber, wsColNumber, sheet) {
738
+ if (mergeInfo.colSpan > 1) {
739
+ const rightCol = wsColNumber + mergeInfo.colSpan - 1;
740
+ const rightCellData = sheet.rows.get(wsRowNumber)?.cells.get(rightCol);
741
+ if (rightCellData?.style?.border?.right) {
742
+ const converted = excelBordersToPdf({ right: rightCellData.style.border.right });
743
+ if (converted.right) {
744
+ layoutCell.borders.right = converted.right;
745
+ }
746
+ }
747
+ }
748
+ if (mergeInfo.rowSpan > 1) {
749
+ const bottomRowNum = wsRowNumber + mergeInfo.rowSpan - 1;
750
+ const bottomCellData = sheet.rows.get(bottomRowNum)?.cells.get(wsColNumber);
751
+ if (bottomCellData?.style?.border?.bottom) {
752
+ const converted = excelBordersToPdf({ bottom: bottomCellData.style.border.bottom });
753
+ if (converted.bottom) {
754
+ layoutCell.borders.bottom = converted.bottom;
755
+ }
756
+ }
757
+ }
758
+ }
759
+ // =============================================================================
760
+ // Text Overflow Calculation
761
+ // =============================================================================
762
+ /**
763
+ * In Excel, non-wrapped text overflows into adjacent empty cells.
764
+ * Fill color alone does NOT block overflow — only text content does.
765
+ * Computes `textOverflowWidth` for cells whose text exceeds the cell width.
766
+ */
767
+ function computeTextOverflows(cellGrid, rowPage, colGroup, visibleRows, visibleCols, groupColWidths, mergeMap, fontManager) {
768
+ for (let ri = 0; ri < rowPage.length; ri++) {
769
+ for (let gci = 0; gci < colGroup.length; gci++) {
770
+ const cell = cellGrid.get(`${ri}:${gci}`);
771
+ if (!cell ||
772
+ cell.wrapText ||
773
+ cell.colSpan > 1 ||
774
+ !cell.text ||
775
+ cell.richText ||
776
+ (typeof cell.textRotation === "number" && cell.textRotation !== 0) ||
777
+ cell.textRotation === "vertical") {
778
+ continue;
779
+ }
780
+ const resourceName = fontManager.hasEmbeddedFont()
781
+ ? fontManager.getEmbeddedResourceName()
782
+ : fontManager.ensureFont(resolvePdfFontName(cell.fontFamily, cell.bold, cell.italic));
783
+ const textWidth = fontManager.measureText(cell.text, resourceName, cell.fontSize);
784
+ const cellContentWidth = cell.rect.width - CELL_PADDING_H * 2;
785
+ if (textWidth <= cellContentWidth) {
786
+ continue;
787
+ }
788
+ const overflowNeeded = textWidth - cellContentWidth;
789
+ let overflowAvailable = 0;
790
+ for (let j = gci + 1; j < colGroup.length; j++) {
791
+ const visibleRowIdx = rowPage[ri];
792
+ const wsRow = visibleRows[visibleRowIdx];
793
+ const wsCol = visibleCols[colGroup[j]];
794
+ if (mergeMap.has(`${wsRow}:${wsCol}`)) {
795
+ break;
796
+ }
797
+ const neighborCell = cellGrid.get(`${ri}:${j}`);
798
+ if (neighborCell?.text) {
799
+ break;
800
+ }
801
+ overflowAvailable += groupColWidths[j];
802
+ if (overflowAvailable >= overflowNeeded) {
803
+ break;
804
+ }
805
+ }
806
+ if (overflowAvailable > 0) {
807
+ cell.textOverflowWidth = Math.min(overflowNeeded, overflowAvailable);
808
+ }
809
+ }
810
+ }
811
+ }
812
+ // =============================================================================
614
813
  // Rich Text
615
814
  // =============================================================================
616
815
  /**
@@ -621,11 +820,15 @@ function buildRichTextRuns(cell, options, fontManager, scaleFactor) {
621
820
  if (!cell || cell.type !== PdfCellType.RichText) {
622
821
  return null;
623
822
  }
624
- const rtValue = cell.value;
625
- if (!rtValue?.richText || rtValue.richText.length === 0) {
823
+ const value = cell.value;
824
+ if (!value || typeof value !== "object" || !("richText" in value)) {
825
+ return null;
826
+ }
827
+ const runs = value.richText;
828
+ if (runs.length === 0) {
626
829
  return null;
627
830
  }
628
- return rtValue.richText.map(run => {
831
+ return runs.map(run => {
629
832
  const fontProps = extractFontProperties(run.font, options.defaultFontFamily, options.defaultFontSize);
630
833
  // Register font for this run
631
834
  if (fontManager.hasEmbeddedFont()) {