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