@cj-tech-master/excelts 6.2.0 → 7.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 (56) hide show
  1. package/README.md +45 -17
  2. package/README_zh.md +43 -15
  3. package/dist/browser/index.browser.d.ts +1 -1
  4. package/dist/browser/index.browser.js +1 -1
  5. package/dist/browser/index.d.ts +2 -2
  6. package/dist/browser/index.js +1 -1
  7. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +0 -2
  8. package/dist/browser/modules/excel/stream/workbook-writer.d.ts +2 -2
  9. package/dist/browser/modules/excel/types.d.ts +0 -2
  10. package/dist/browser/modules/pdf/excel-bridge.d.ts +29 -0
  11. package/dist/browser/modules/pdf/excel-bridge.js +423 -0
  12. package/dist/browser/modules/pdf/index.d.ts +22 -24
  13. package/dist/browser/modules/pdf/index.js +22 -25
  14. package/dist/browser/modules/pdf/pdf.d.ts +121 -0
  15. package/dist/browser/modules/pdf/pdf.js +255 -0
  16. package/dist/browser/modules/pdf/render/layout-engine.d.ts +10 -8
  17. package/dist/browser/modules/pdf/render/layout-engine.js +115 -209
  18. package/dist/browser/modules/pdf/render/pdf-exporter.d.ts +9 -62
  19. package/dist/browser/modules/pdf/render/pdf-exporter.js +38 -78
  20. package/dist/browser/modules/pdf/render/style-converter.d.ts +20 -18
  21. package/dist/browser/modules/pdf/render/style-converter.js +24 -23
  22. package/dist/browser/modules/pdf/types.d.ts +193 -11
  23. package/dist/browser/modules/pdf/types.js +22 -1
  24. package/dist/cjs/index.js +3 -3
  25. package/dist/cjs/modules/pdf/excel-bridge.js +426 -0
  26. package/dist/cjs/modules/pdf/index.js +25 -28
  27. package/dist/cjs/modules/pdf/pdf.js +258 -0
  28. package/dist/cjs/modules/pdf/render/layout-engine.js +116 -210
  29. package/dist/cjs/modules/pdf/render/pdf-exporter.js +37 -79
  30. package/dist/cjs/modules/pdf/render/style-converter.js +24 -23
  31. package/dist/cjs/modules/pdf/types.js +23 -2
  32. package/dist/esm/index.browser.js +1 -1
  33. package/dist/esm/index.js +1 -1
  34. package/dist/esm/modules/pdf/excel-bridge.js +423 -0
  35. package/dist/esm/modules/pdf/index.js +22 -25
  36. package/dist/esm/modules/pdf/pdf.js +255 -0
  37. package/dist/esm/modules/pdf/render/layout-engine.js +115 -209
  38. package/dist/esm/modules/pdf/render/pdf-exporter.js +38 -78
  39. package/dist/esm/modules/pdf/render/style-converter.js +24 -23
  40. package/dist/esm/modules/pdf/types.js +22 -1
  41. package/dist/iife/excelts.iife.js +728 -251
  42. package/dist/iife/excelts.iife.js.map +1 -1
  43. package/dist/iife/excelts.iife.min.js +34 -34
  44. package/dist/types/index.browser.d.ts +1 -1
  45. package/dist/types/index.d.ts +2 -2
  46. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +0 -2
  47. package/dist/types/modules/excel/stream/workbook-writer.d.ts +2 -2
  48. package/dist/types/modules/excel/types.d.ts +0 -2
  49. package/dist/types/modules/pdf/excel-bridge.d.ts +29 -0
  50. package/dist/types/modules/pdf/index.d.ts +22 -24
  51. package/dist/types/modules/pdf/pdf.d.ts +121 -0
  52. package/dist/types/modules/pdf/render/layout-engine.d.ts +10 -8
  53. package/dist/types/modules/pdf/render/pdf-exporter.d.ts +9 -62
  54. package/dist/types/modules/pdf/render/style-converter.d.ts +20 -18
  55. package/dist/types/modules/pdf/types.d.ts +193 -11
  56. package/package.json +1 -1
@@ -1,22 +1,22 @@
1
1
  /**
2
- * Layout engine for Excel-to-PDF conversion.
2
+ * Layout engine for PDF generation.
3
3
  *
4
- * Takes a worksheet and produces LayoutPage objects that describe exactly
4
+ * Takes a PdfSheetData and produces LayoutPage objects that describe exactly
5
5
  * where each cell, border, and piece of text should be drawn on each PDF page.
6
6
  *
7
+ * This module is fully independent of the Excel module — it works with
8
+ * the PDF module's own data model (PdfSheetData, PdfCellData, etc.).
9
+ *
7
10
  * Key responsibilities:
8
- * - Convert Excel column widths (character units) to PDF points
9
- * - Convert Excel row heights (points already, but may need scaling)
11
+ * - Convert column widths (character units) to PDF points
12
+ * - Convert row heights (points already, but may need scaling)
10
13
  * - Handle merged cells spanning multiple rows/columns
11
14
  * - Paginate content across multiple pages
12
15
  * - Handle fitToPage scaling
13
16
  * - Handle repeated header rows
14
17
  * - Skip hidden rows and columns
15
18
  */
16
- import { ValueType } from "../../excel/enums.js";
17
- import { decodeRange } from "../../excel/utils/address.js";
18
- import { formatCellValue } from "../../excel/utils/cell-format.js";
19
- import { base64ToUint8Array } from "../../../utils/utils.base.js";
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
22
  // =============================================================================
@@ -30,9 +30,9 @@ const MIN_COLUMN_WIDTH = 5;
30
30
  // Layout Engine
31
31
  // =============================================================================
32
32
  /**
33
- * Compute the layout for a worksheet across one or more PDF pages.
33
+ * Compute the layout for a sheet across one or more PDF pages.
34
34
  */
35
- export function layoutWorksheet(worksheet, options, fontManager) {
35
+ export function layoutSheet(sheet, options, fontManager) {
36
36
  const { margins } = options;
37
37
  let pageWidth = options.pageSize.width;
38
38
  let pageHeight = options.pageSize.height;
@@ -45,15 +45,15 @@ export function layoutWorksheet(worksheet, options, fontManager) {
45
45
  const footerHeight = options.showPageNumbers ? 20 : 0;
46
46
  const availableHeight = contentHeight - headerHeight - footerHeight;
47
47
  // Determine print area bounds (if set)
48
- const printRange = getPrintRange(worksheet);
48
+ const printRange = getPrintRange(sheet);
49
49
  // --- Step 1: Visible columns and widths ---
50
- const { columnWidths, visibleCols } = computeColumnWidths(worksheet, printRange);
50
+ const { columnWidths, visibleCols } = computeColumnWidths(sheet, printRange);
51
51
  const columnCount = visibleCols.length;
52
52
  if (columnCount === 0) {
53
- return [emptyPage(pageWidth, pageHeight, worksheet.name, options)];
53
+ return [emptyPage(pageWidth, pageHeight, sheet.name, options)];
54
54
  }
55
55
  // --- Step 2: Scale ---
56
- let totalTableWidth = columnWidths.reduce((sum, w) => sum + w, 0);
56
+ const totalTableWidth = columnWidths.reduce((sum, w) => sum + w, 0);
57
57
  let scaleFactor = options.scale;
58
58
  if (options.fitToPage && totalTableWidth > 0) {
59
59
  const fitScale = contentWidth / totalTableWidth;
@@ -62,26 +62,15 @@ export function layoutWorksheet(worksheet, options, fontManager) {
62
62
  }
63
63
  }
64
64
  const scaledColumnWidths = columnWidths.map(w => w * scaleFactor);
65
- totalTableWidth = scaledColumnWidths.reduce((sum, w) => sum + w, 0);
66
- // Column x-offsets
67
- const columnOffsets = [];
68
- let xOffset = margins.left;
69
- if (totalTableWidth < contentWidth) {
70
- xOffset = margins.left + (contentWidth - totalTableWidth) / 2;
71
- }
72
- for (let i = 0; i < scaledColumnWidths.length; i++) {
73
- columnOffsets.push(xOffset);
74
- xOffset += scaledColumnWidths[i];
75
- }
76
65
  // --- Step 3: Visible rows and heights ---
77
- const { rowHeights, visibleRows } = computeRowHeights(worksheet, scaleFactor, printRange);
66
+ const { rowHeights, visibleRows } = computeRowHeights(sheet, scaleFactor, printRange);
78
67
  // --- Step 4: Merge map ---
79
- const mergeMap = buildMergeMap(worksheet);
68
+ const mergeMap = buildMergeMap(sheet);
80
69
  // --- Step 5: Paginate vertically (rows) and horizontally (columns) ---
81
70
  const repeatRowCount = typeof options.repeatRows === "number" ? options.repeatRows : 0;
82
- const rowBreakSet = buildRowBreakSet(worksheet, visibleRows);
71
+ const rowBreakSet = buildRowBreakSet(sheet, visibleRows);
83
72
  const rowPages = paginateRows(rowHeights, availableHeight, repeatRowCount, rowBreakSet);
84
- const colGroups = paginateColumns(scaledColumnWidths, contentWidth, worksheet, visibleCols);
73
+ const colGroups = paginateColumns(scaledColumnWidths, contentWidth, sheet, visibleCols);
85
74
  // --- Step 6: Layout cells per page (row page × column page) ---
86
75
  const layoutPages = [];
87
76
  for (const rowPage of rowPages) {
@@ -121,8 +110,8 @@ export function layoutWorksheet(worksheet, options, fontManager) {
121
110
  if (mergeInfo && !mergeInfo.isMaster) {
122
111
  continue;
123
112
  }
124
- const row = worksheet.findRow(wsRowNumber);
125
- const cell = row ? row.findCell(wsColNumber) : undefined;
113
+ const row = sheet.rows.get(wsRowNumber);
114
+ const cell = row?.cells.get(wsColNumber);
126
115
  let colSpan = 1;
127
116
  let rowSpan = 1;
128
117
  if (mergeInfo && mergeInfo.isMaster) {
@@ -160,7 +149,7 @@ export function layoutWorksheet(worksheet, options, fontManager) {
160
149
  cellHeight += pageRowHeights[ri + s];
161
150
  }
162
151
  const rectY = cellY - cellHeight;
163
- cells.push(buildLayoutCell(cell, ci, visibleRowIdx, cellX, rectY, cellWidth, cellHeight, colSpan, rowSpan, options, fontManager, scaleFactor));
152
+ cells.push(buildLayoutCell(cell, cellX, rectY, cellWidth, cellHeight, colSpan, rowSpan, options, fontManager, scaleFactor));
164
153
  }
165
154
  }
166
155
  layoutPages.push({
@@ -169,20 +158,20 @@ export function layoutWorksheet(worksheet, options, fontManager) {
169
158
  cells,
170
159
  width: pageWidth,
171
160
  height: pageHeight,
172
- sheetName: worksheet.name,
173
- worksheetCols: colGroup.map(ci => visibleCols[ci]),
161
+ sheetName: sheet.name,
162
+ sheetCols: colGroup.map(ci => visibleCols[ci]),
174
163
  columnOffsets: groupColOffsets,
175
164
  columnWidths: groupColWidths,
176
- worksheetRows: rowPage.map(ri => visibleRows[ri]),
165
+ sheetRows: rowPage.map(ri => visibleRows[ri]),
177
166
  rowYPositions,
178
167
  rowHeights: pageRowHeights,
179
168
  images: []
180
169
  });
181
170
  }
182
171
  }
183
- // --- Step 7: Collect images and place them on the correct pages ---
184
- if (layoutPages.length > 0) {
185
- assignImagesToPages(worksheet, layoutPages);
172
+ // --- Step 7: Place images on the correct pages ---
173
+ if (layoutPages.length > 0 && sheet.images) {
174
+ assignImagesToPages(sheet.images, layoutPages);
186
175
  }
187
176
  return layoutPages;
188
177
  }
@@ -197,21 +186,49 @@ function emptyPage(width, height, sheetName, options) {
197
186
  width,
198
187
  height,
199
188
  sheetName,
200
- worksheetCols: [],
189
+ sheetCols: [],
201
190
  columnOffsets: [],
202
191
  columnWidths: [],
203
- worksheetRows: [],
192
+ sheetRows: [],
204
193
  rowYPositions: [],
205
194
  rowHeights: [],
206
195
  images: []
207
196
  };
208
197
  }
209
198
  /**
210
- * Get the print area range from the worksheet's pageSetup.
199
+ * Parse a cell reference like "A1" into 0-indexed { c, r }.
200
+ */
201
+ function parseCellRef(ref) {
202
+ const upper = ref.replace(/\$/g, "").toUpperCase();
203
+ let col = 0;
204
+ let i = 0;
205
+ while (i < upper.length && upper.charCodeAt(i) >= 65 && upper.charCodeAt(i) <= 90) {
206
+ col = col * 26 + (upper.charCodeAt(i) - 64);
207
+ i++;
208
+ }
209
+ const row = parseInt(upper.substring(i), 10);
210
+ return { c: col - 1, r: row - 1 };
211
+ }
212
+ /**
213
+ * Parse a range string like "A1:B2" into 0-indexed start/end.
214
+ */
215
+ function parseRangeRef(range) {
216
+ const idx = range.indexOf(":");
217
+ if (idx === -1) {
218
+ const cell = parseCellRef(range);
219
+ return { s: cell, e: { ...cell } };
220
+ }
221
+ return {
222
+ s: parseCellRef(range.slice(0, idx)),
223
+ e: parseCellRef(range.slice(idx + 1))
224
+ };
225
+ }
226
+ /**
227
+ * Get the print area range from the sheet's pageSetup.
211
228
  * Returns null if no print area is set.
212
229
  */
213
- function getPrintRange(worksheet) {
214
- const printArea = worksheet.pageSetup?.printArea;
230
+ function getPrintRange(sheet) {
231
+ const printArea = sheet.pageSetup?.printArea;
215
232
  if (!printArea || typeof printArea !== "string") {
216
233
  return null;
217
234
  }
@@ -222,7 +239,7 @@ function getPrintRange(worksheet) {
222
239
  return null;
223
240
  }
224
241
  try {
225
- const range = decodeRange(firstRange);
242
+ const range = parseRangeRef(firstRange);
226
243
  return {
227
244
  startRow: range.s.r + 1,
228
245
  endRow: range.e.r + 1,
@@ -237,22 +254,22 @@ function getPrintRange(worksheet) {
237
254
  // =============================================================================
238
255
  // Column Width Computation
239
256
  // =============================================================================
240
- function computeColumnWidths(worksheet, printRange) {
241
- const dimensions = worksheet.dimensions;
242
- const hasData = dimensions && dimensions.model.top > 0 && dimensions.model.left > 0;
257
+ function computeColumnWidths(sheet, printRange) {
258
+ const bounds = sheet.bounds;
259
+ const hasData = bounds.top > 0 && bounds.left > 0;
243
260
  if (!hasData) {
244
261
  return { columnWidths: [], visibleCols: [] };
245
262
  }
246
- const startCol = printRange?.startCol ?? dimensions.model.left;
247
- const endCol = printRange?.endCol ?? dimensions.model.right;
263
+ const startCol = printRange?.startCol ?? bounds.left;
264
+ const endCol = printRange?.endCol ?? bounds.right;
248
265
  const columnWidths = [];
249
266
  const visibleCols = [];
250
267
  for (let c = startCol; c <= endCol; c++) {
251
- const col = worksheet.getColumn(c);
252
- if (col.hidden) {
268
+ const col = sheet.columns.get(c);
269
+ if (col?.hidden) {
253
270
  continue;
254
271
  }
255
- const excelWidth = col.width ?? DEFAULT_COLUMN_WIDTH;
272
+ const excelWidth = col?.width ?? DEFAULT_COLUMN_WIDTH;
256
273
  const pointWidth = Math.max(excelWidth * EXCEL_CHAR_WIDTH_TO_POINTS, MIN_COLUMN_WIDTH);
257
274
  columnWidths.push(pointWidth);
258
275
  visibleCols.push(c);
@@ -262,17 +279,17 @@ function computeColumnWidths(worksheet, printRange) {
262
279
  // =============================================================================
263
280
  // Row Height Computation
264
281
  // =============================================================================
265
- function computeRowHeights(worksheet, scaleFactor, printRange) {
266
- const dimensions = worksheet.dimensions;
267
- if (!dimensions || dimensions.model.top <= 0) {
282
+ function computeRowHeights(sheet, scaleFactor, printRange) {
283
+ const bounds = sheet.bounds;
284
+ if (bounds.top <= 0) {
268
285
  return { rowHeights: [], visibleRows: [] };
269
286
  }
270
- const startRow = printRange?.startRow ?? dimensions.model.top;
271
- const endRow = printRange?.endRow ?? dimensions.model.bottom;
287
+ const startRow = printRange?.startRow ?? bounds.top;
288
+ const endRow = printRange?.endRow ?? bounds.bottom;
272
289
  const rowHeights = [];
273
290
  const visibleRows = [];
274
291
  for (let r = startRow; r <= endRow; r++) {
275
- const row = worksheet.findRow(r);
292
+ const row = sheet.rows.get(r);
276
293
  if (row && row.hidden) {
277
294
  continue;
278
295
  }
@@ -283,10 +300,9 @@ function computeRowHeights(worksheet, scaleFactor, printRange) {
283
300
  }
284
301
  else {
285
302
  // Auto-size: scan cells in this row to find the largest needed height.
286
- // Account for font size and multi-line content (explicit newlines or wrapText).
287
303
  height = DEFAULT_ROW_HEIGHT;
288
304
  if (row) {
289
- row.eachCell({ includeEmpty: false }, cell => {
305
+ for (const cell of row.cells.values()) {
290
306
  let fontSize = cell.style?.font?.size ?? 11;
291
307
  // For rich text cells, find the largest font size across all runs
292
308
  const rtValue = cell.value;
@@ -304,8 +320,9 @@ function computeRowHeights(worksheet, scaleFactor, printRange) {
304
320
  const lineCount = Math.max(1, (text.match(/\n/g) ?? []).length + 1);
305
321
  // For wrapText cells, estimate how many lines word-wrapping produces
306
322
  let wrapLineCount = lineCount;
307
- if (cell.alignment?.wrapText && lineCount === 1 && text.length > 0) {
308
- const colWidth = worksheet.getColumn(cell.col).width ?? DEFAULT_COLUMN_WIDTH;
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;
309
326
  const colPts = colWidth * EXCEL_CHAR_WIDTH_TO_POINTS * scaleFactor;
310
327
  const avgCharWidth = fontSize * 0.55; // rough average char width
311
328
  const charsPerLine = Math.max(1, Math.floor(colPts / avgCharWidth));
@@ -315,7 +332,7 @@ function computeRowHeights(worksheet, scaleFactor, printRange) {
315
332
  if (neededHeight > height) {
316
333
  height = neededHeight;
317
334
  }
318
- });
335
+ }
319
336
  }
320
337
  }
321
338
  rowHeights.push(height * scaleFactor);
@@ -329,19 +346,19 @@ function computeRowHeights(worksheet, scaleFactor, printRange) {
329
346
  /**
330
347
  * Build a set of visible-row indices where manual page breaks occur.
331
348
  */
332
- function buildRowBreakSet(worksheet, visibleRows) {
349
+ function buildRowBreakSet(sheet, visibleRows) {
333
350
  const breaks = new Set();
334
- const rowBreaks = worksheet.rowBreaks ?? [];
351
+ const rowBreaks = sheet.rowBreaks ?? [];
335
352
  if (rowBreaks.length === 0) {
336
353
  return breaks;
337
354
  }
338
- // Map worksheet row numbers to visible-row indices
355
+ // Map row numbers to visible-row indices
339
356
  const rowToIndex = new Map();
340
357
  for (let i = 0; i < visibleRows.length; i++) {
341
358
  rowToIndex.set(visibleRows[i], i);
342
359
  }
343
360
  for (const brk of rowBreaks) {
344
- const idx = rowToIndex.get(brk.id);
361
+ const idx = rowToIndex.get(brk);
345
362
  if (idx !== undefined) {
346
363
  // Break AFTER this row, so the next row starts a new page
347
364
  breaks.add(idx + 1);
@@ -350,21 +367,17 @@ function buildRowBreakSet(worksheet, visibleRows) {
350
367
  return breaks;
351
368
  }
352
369
  /**
353
- * Build a map of all merged cell regions in the worksheet.
354
- * Uses the worksheet model's public mergeCells property.
370
+ * Build a map of all merged cell regions.
355
371
  * Key: "row:col" (1-based), Value: merge info
356
372
  */
357
- function buildMergeMap(worksheet) {
373
+ function buildMergeMap(sheet) {
358
374
  const map = new Map();
359
- if (!worksheet.hasMerges) {
375
+ const merges = sheet.merges;
376
+ if (!merges || merges.length === 0) {
360
377
  return map;
361
378
  }
362
- const mergeCells = worksheet.model.mergeCells;
363
- if (!mergeCells) {
364
- return map;
365
- }
366
- for (const rangeStr of mergeCells) {
367
- const range = decodeRange(rangeStr);
379
+ for (const rangeStr of merges) {
380
+ const range = parseRangeRef(rangeStr);
368
381
  const top = range.s.r + 1;
369
382
  const left = range.s.c + 1;
370
383
  const bottom = range.e.r + 1;
@@ -419,9 +432,6 @@ export function paginateRows(rowHeights, availableHeight, repeatRowCount, rowBre
419
432
  currentPage.length > 0 &&
420
433
  currentPage.length === repeatedPrefixCount;
421
434
  if (pageHasOnlyRepeatRows) {
422
- // If repeated header rows consume too much space to allow any body row,
423
- // fall back to placing body rows without repeated headers rather than
424
- // emitting a header-only page or overflowing the page.
425
435
  currentPage = [];
426
436
  currentPageHeight = 0;
427
437
  repeatedPrefixCount = 0;
@@ -454,25 +464,22 @@ export function paginateRows(rowHeights, availableHeight, repeatRowCount, rowBre
454
464
  }
455
465
  /**
456
466
  * Split columns into groups for horizontal pagination.
457
- * Each group is an array of column indices (into the visibleCols/scaledColumnWidths arrays).
458
- * If the total width fits in contentWidth and there are no colBreaks, returns a single group.
459
467
  */
460
- function paginateColumns(columnWidths, contentWidth, worksheet, visibleCols) {
468
+ function paginateColumns(columnWidths, contentWidth, sheet, visibleCols) {
461
469
  if (columnWidths.length === 0) {
462
470
  return [[]];
463
471
  }
464
472
  // Build col break set (indices into visibleCols)
465
473
  const colBreaks = new Set();
466
- const wsColBreaks = worksheet.colBreaks ?? [];
474
+ const wsColBreaks = sheet.colBreaks ?? [];
467
475
  if (wsColBreaks.length > 0) {
468
476
  const colToIndex = new Map();
469
477
  for (let i = 0; i < visibleCols.length; i++) {
470
478
  colToIndex.set(visibleCols[i], i);
471
479
  }
472
480
  for (const brk of wsColBreaks) {
473
- const idx = colToIndex.get(brk.id);
481
+ const idx = colToIndex.get(brk);
474
482
  if (idx !== undefined) {
475
- // Break AFTER this column, so the next column starts a new group
476
483
  colBreaks.add(idx + 1);
477
484
  }
478
485
  }
@@ -482,9 +489,6 @@ function paginateColumns(columnWidths, contentWidth, worksheet, visibleCols) {
482
489
  let currentWidth = 0;
483
490
  for (let i = 0; i < columnWidths.length; i++) {
484
491
  const colWidth = columnWidths[i];
485
- // Force break at column break positions or when exceeding page width
486
- // Use a small epsilon (0.01pt) to avoid floating-point precision issues
487
- // when fitToPage scales columns to exactly match content width
488
492
  const forceBreak = colBreaks.has(i) && currentGroup.length > 0;
489
493
  if ((forceBreak || currentWidth + colWidth > contentWidth + 0.01) && currentGroup.length > 0) {
490
494
  groups.push(currentGroup);
@@ -502,8 +506,8 @@ function paginateColumns(columnWidths, contentWidth, worksheet, visibleCols) {
502
506
  // =============================================================================
503
507
  // Cell Layout
504
508
  // =============================================================================
505
- function buildLayoutCell(cell, _colIndex, _rowIndex, x, y, width, height, colSpan, rowSpan, options, fontManager, scaleFactor) {
506
- const text = getCellText(cell);
509
+ function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, fontManager, scaleFactor) {
510
+ const text = cell?.text ?? "";
507
511
  const style = cell?.style ?? {};
508
512
  const fontProps = extractFontProperties(style.font, options.defaultFontFamily, options.defaultFontSize);
509
513
  // Scale font size proportionally when fitToPage shrinks the layout
@@ -516,7 +520,7 @@ function buildLayoutCell(cell, _colIndex, _rowIndex, x, y, width, height, colSpa
516
520
  const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
517
521
  fontManager.ensureFont(pdfFontName);
518
522
  }
519
- // Rich text runs (buildRichTextRuns handles font tracking internally)
523
+ // Rich text runs
520
524
  const richText = buildRichTextRuns(cell, options, fontManager, scaleFactor);
521
525
  return {
522
526
  text,
@@ -542,57 +546,22 @@ function buildLayoutCell(cell, _colIndex, _rowIndex, x, y, width, height, colSpa
542
546
  };
543
547
  }
544
548
  // =============================================================================
545
- // Image Collection
549
+ // Image Placement
546
550
  // =============================================================================
547
551
  /**
548
- * Collect images from a worksheet and assign them to the page that contains
549
- * their top-left anchor.
552
+ * Assign pre-collected images to the pages that contain their top-left anchor.
550
553
  */
551
- function assignImagesToPages(worksheet, layoutPages) {
552
- // Access worksheet images via getImages()
553
- const wsImages = worksheet.getImages?.();
554
- if (!wsImages || !Array.isArray(wsImages)) {
555
- return;
556
- }
557
- // Access the workbook for image data
558
- const workbook = worksheet.workbook;
559
- if (!workbook) {
560
- return;
561
- }
562
- for (const wsImage of wsImages) {
563
- if (!wsImage.range?.tl) {
564
- continue;
565
- }
566
- const imageId = wsImage.imageId;
567
- const mediaItem = workbook.getImage?.(Number(imageId));
568
- if (!mediaItem) {
569
- continue;
570
- }
571
- // Get image data
572
- let data;
573
- if (mediaItem.buffer instanceof Uint8Array) {
574
- data = mediaItem.buffer;
575
- }
576
- else if (mediaItem.base64) {
577
- data = base64ToUint8Array(mediaItem.base64);
578
- }
579
- if (!data || data.length === 0) {
580
- continue;
581
- }
582
- const format = mediaItem.extension;
583
- if (format !== "jpeg" && format !== "png") {
584
- continue; // Only JPEG and PNG are supported
585
- }
586
- // Calculate position from anchor
587
- const tl = wsImage.range.tl;
554
+ function assignImagesToPages(images, layoutPages) {
555
+ for (const img of images) {
556
+ const tl = img.range.tl;
588
557
  const tlCol = (tl.nativeCol ?? tl.col ?? 0) + 1; // convert 0-indexed to 1-indexed
589
558
  const tlRow = (tl.nativeRow ?? tl.row ?? 0) + 1;
590
- const targetPage = layoutPages.find(page => page.worksheetCols.includes(tlCol) && page.worksheetRows.includes(tlRow));
559
+ const targetPage = layoutPages.find(page => page.sheetCols.includes(tlCol) && page.sheetRows.includes(tlRow));
591
560
  if (!targetPage) {
592
561
  continue;
593
562
  }
594
- const pageColIndex = targetPage.worksheetCols.indexOf(tlCol);
595
- const pageRowIndex = targetPage.worksheetRows.indexOf(tlRow);
563
+ const pageColIndex = targetPage.sheetCols.indexOf(tlCol);
564
+ const pageRowIndex = targetPage.sheetRows.indexOf(tlRow);
596
565
  const baseX = targetPage.columnOffsets[pageColIndex] ?? targetPage.options.margins.left;
597
566
  const baseY = targetPage.rowYPositions[pageRowIndex] ??
598
567
  targetPage.height -
@@ -602,22 +571,20 @@ function assignImagesToPages(worksheet, layoutPages) {
602
571
  const tlColOff = (tl.nativeColOff ?? 0) / 12700 || 0;
603
572
  const tlRowOff = (tl.nativeRowOff ?? 0) / 12700 || 0;
604
573
  const imgX = baseX + tlColOff;
605
- const imgY = baseY - tlRowOff; // PDF y-axis is bottom-up, offset moves down
574
+ const imgY = baseY - tlRowOff;
606
575
  // Determine image size
607
576
  let imgWidth = 100;
608
577
  let imgHeight = 100;
609
- if (wsImage.range.ext) {
610
- // ext.width and ext.height are in pixels; convert to points (1px ≈ 0.75pt)
611
- imgWidth = (wsImage.range.ext.width ?? 100) * 0.75;
612
- imgHeight = (wsImage.range.ext.height ?? 100) * 0.75;
578
+ if (img.range.ext) {
579
+ imgWidth = (img.range.ext.width ?? 100) * 0.75;
580
+ imgHeight = (img.range.ext.height ?? 100) * 0.75;
613
581
  }
614
- else if (wsImage.range.br) {
615
- // Calculate from bottom-right anchor
616
- const br = wsImage.range.br;
582
+ else if (img.range.br) {
583
+ const br = img.range.br;
617
584
  const brCol = (br.nativeCol ?? br.col ?? 0) + 1;
618
585
  const brRow = (br.nativeRow ?? br.row ?? 0) + 1;
619
- const brPageColIndex = targetPage.worksheetCols.indexOf(brCol);
620
- const brPageRowIndex = targetPage.worksheetRows.indexOf(brRow);
586
+ const brPageColIndex = targetPage.sheetCols.indexOf(brCol);
587
+ const brPageRowIndex = targetPage.sheetRows.indexOf(brRow);
621
588
  const brBaseX = brPageColIndex >= 0
622
589
  ? targetPage.columnOffsets[brPageColIndex]
623
590
  : imgX + (targetPage.columnWidths[pageColIndex] ?? 100);
@@ -631,10 +598,9 @@ function assignImagesToPages(worksheet, layoutPages) {
631
598
  imgWidth = brX - imgX;
632
599
  imgHeight = imgY - brY;
633
600
  }
634
- // PDF coordinates: y is bottom of image
635
601
  targetPage.images.push({
636
- data,
637
- format,
602
+ data: img.data,
603
+ format: img.format,
638
604
  rect: {
639
605
  x: imgX,
640
606
  y: imgY - imgHeight,
@@ -644,66 +610,6 @@ function assignImagesToPages(worksheet, layoutPages) {
644
610
  });
645
611
  }
646
612
  }
647
- /**
648
- * Extract display text from a cell, applying numFmt formatting.
649
- */
650
- function getCellText(cell) {
651
- if (!cell) {
652
- return "";
653
- }
654
- switch (cell.type) {
655
- case ValueType.Null:
656
- case ValueType.Merge:
657
- return "";
658
- case ValueType.RichText: {
659
- // RichText cell.value is a CellRichTextValue object; cell.text joins the run texts
660
- return cell.text;
661
- }
662
- case ValueType.Hyperlink:
663
- return cell.text;
664
- case ValueType.Error: {
665
- // Error cells have value = { error: "#N/A" } etc.
666
- const errValue = cell.value;
667
- return errValue?.error ?? cell.text;
668
- }
669
- case ValueType.Formula: {
670
- const result = cell.result;
671
- if (result !== undefined && result !== null) {
672
- if (typeof result === "object" && "error" in result) {
673
- return result.error;
674
- }
675
- return formatCellValueSafe(result, cell.style?.numFmt);
676
- }
677
- return cell.text;
678
- }
679
- default: {
680
- const value = cell.value;
681
- if (value === null || value === undefined) {
682
- return "";
683
- }
684
- return formatCellValueSafe(value, cell.style?.numFmt);
685
- }
686
- }
687
- }
688
- /**
689
- * Safely format a cell value using its numFmt.
690
- * Falls back to toString if formatting fails or no format is specified.
691
- */
692
- function formatCellValueSafe(value, numFmt) {
693
- const fmt = typeof numFmt === "string" ? numFmt : numFmt?.formatCode;
694
- if (fmt && (typeof value === "number" || value instanceof Date || typeof value === "boolean")) {
695
- try {
696
- return formatCellValue(value, fmt);
697
- }
698
- catch {
699
- // Fall through to default
700
- }
701
- }
702
- if (value instanceof Date) {
703
- return value.toLocaleDateString();
704
- }
705
- return String(value);
706
- }
707
613
  // =============================================================================
708
614
  // Rich Text
709
615
  // =============================================================================
@@ -712,7 +618,7 @@ function formatCellValueSafe(value, numFmt) {
712
618
  * Returns null for non-RichText cells.
713
619
  */
714
620
  function buildRichTextRuns(cell, options, fontManager, scaleFactor) {
715
- if (!cell || cell.type !== ValueType.RichText) {
621
+ if (!cell || cell.type !== PdfCellType.RichText) {
716
622
  return null;
717
623
  }
718
624
  const rtValue = cell.value;