@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.
- package/README.md +45 -17
- package/README_zh.md +43 -15
- package/dist/browser/index.browser.d.ts +1 -1
- package/dist/browser/index.browser.js +1 -1
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +0 -2
- package/dist/browser/modules/excel/stream/workbook-writer.d.ts +2 -2
- package/dist/browser/modules/excel/types.d.ts +0 -2
- package/dist/browser/modules/pdf/excel-bridge.d.ts +29 -0
- package/dist/browser/modules/pdf/excel-bridge.js +423 -0
- package/dist/browser/modules/pdf/index.d.ts +22 -24
- package/dist/browser/modules/pdf/index.js +22 -25
- package/dist/browser/modules/pdf/pdf.d.ts +121 -0
- package/dist/browser/modules/pdf/pdf.js +255 -0
- package/dist/browser/modules/pdf/render/layout-engine.d.ts +10 -8
- package/dist/browser/modules/pdf/render/layout-engine.js +115 -209
- package/dist/browser/modules/pdf/render/pdf-exporter.d.ts +9 -62
- package/dist/browser/modules/pdf/render/pdf-exporter.js +38 -78
- package/dist/browser/modules/pdf/render/style-converter.d.ts +20 -18
- package/dist/browser/modules/pdf/render/style-converter.js +24 -23
- package/dist/browser/modules/pdf/types.d.ts +193 -11
- package/dist/browser/modules/pdf/types.js +22 -1
- package/dist/cjs/index.js +3 -3
- package/dist/cjs/modules/pdf/excel-bridge.js +426 -0
- package/dist/cjs/modules/pdf/index.js +25 -28
- package/dist/cjs/modules/pdf/pdf.js +258 -0
- package/dist/cjs/modules/pdf/render/layout-engine.js +116 -210
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +37 -79
- package/dist/cjs/modules/pdf/render/style-converter.js +24 -23
- package/dist/cjs/modules/pdf/types.js +23 -2
- package/dist/esm/index.browser.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/modules/pdf/excel-bridge.js +423 -0
- package/dist/esm/modules/pdf/index.js +22 -25
- package/dist/esm/modules/pdf/pdf.js +255 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +115 -209
- package/dist/esm/modules/pdf/render/pdf-exporter.js +38 -78
- package/dist/esm/modules/pdf/render/style-converter.js +24 -23
- package/dist/esm/modules/pdf/types.js +22 -1
- package/dist/iife/excelts.iife.js +728 -251
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +34 -34
- package/dist/types/index.browser.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +0 -2
- package/dist/types/modules/excel/stream/workbook-writer.d.ts +2 -2
- package/dist/types/modules/excel/types.d.ts +0 -2
- package/dist/types/modules/pdf/excel-bridge.d.ts +29 -0
- package/dist/types/modules/pdf/index.d.ts +22 -24
- package/dist/types/modules/pdf/pdf.d.ts +121 -0
- package/dist/types/modules/pdf/render/layout-engine.d.ts +10 -8
- package/dist/types/modules/pdf/render/pdf-exporter.d.ts +9 -62
- package/dist/types/modules/pdf/render/style-converter.d.ts +20 -18
- package/dist/types/modules/pdf/types.d.ts +193 -11
- package/package.json +1 -1
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Layout engine for
|
|
2
|
+
* Layout engine for PDF generation.
|
|
3
3
|
*
|
|
4
|
-
* Takes a
|
|
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
|
|
9
|
-
* - Convert
|
|
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 {
|
|
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
|
|
33
|
+
* Compute the layout for a sheet across one or more PDF pages.
|
|
34
34
|
*/
|
|
35
|
-
export function
|
|
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(
|
|
48
|
+
const printRange = getPrintRange(sheet);
|
|
49
49
|
// --- Step 1: Visible columns and widths ---
|
|
50
|
-
const { columnWidths, visibleCols } = computeColumnWidths(
|
|
50
|
+
const { columnWidths, visibleCols } = computeColumnWidths(sheet, printRange);
|
|
51
51
|
const columnCount = visibleCols.length;
|
|
52
52
|
if (columnCount === 0) {
|
|
53
|
-
return [emptyPage(pageWidth, pageHeight,
|
|
53
|
+
return [emptyPage(pageWidth, pageHeight, sheet.name, options)];
|
|
54
54
|
}
|
|
55
55
|
// --- Step 2: Scale ---
|
|
56
|
-
|
|
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(
|
|
66
|
+
const { rowHeights, visibleRows } = computeRowHeights(sheet, scaleFactor, printRange);
|
|
78
67
|
// --- Step 4: Merge map ---
|
|
79
|
-
const mergeMap = buildMergeMap(
|
|
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(
|
|
71
|
+
const rowBreakSet = buildRowBreakSet(sheet, visibleRows);
|
|
83
72
|
const rowPages = paginateRows(rowHeights, availableHeight, repeatRowCount, rowBreakSet);
|
|
84
|
-
const colGroups = paginateColumns(scaledColumnWidths, contentWidth,
|
|
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 =
|
|
125
|
-
const cell = row
|
|
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,
|
|
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:
|
|
173
|
-
|
|
161
|
+
sheetName: sheet.name,
|
|
162
|
+
sheetCols: colGroup.map(ci => visibleCols[ci]),
|
|
174
163
|
columnOffsets: groupColOffsets,
|
|
175
164
|
columnWidths: groupColWidths,
|
|
176
|
-
|
|
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:
|
|
184
|
-
if (layoutPages.length > 0) {
|
|
185
|
-
assignImagesToPages(
|
|
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
|
-
|
|
189
|
+
sheetCols: [],
|
|
201
190
|
columnOffsets: [],
|
|
202
191
|
columnWidths: [],
|
|
203
|
-
|
|
192
|
+
sheetRows: [],
|
|
204
193
|
rowYPositions: [],
|
|
205
194
|
rowHeights: [],
|
|
206
195
|
images: []
|
|
207
196
|
};
|
|
208
197
|
}
|
|
209
198
|
/**
|
|
210
|
-
*
|
|
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(
|
|
214
|
-
const 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 =
|
|
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(
|
|
241
|
-
const
|
|
242
|
-
const hasData =
|
|
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 ??
|
|
247
|
-
const endCol = printRange?.endCol ??
|
|
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 =
|
|
252
|
-
if (col
|
|
268
|
+
const col = sheet.columns.get(c);
|
|
269
|
+
if (col?.hidden) {
|
|
253
270
|
continue;
|
|
254
271
|
}
|
|
255
|
-
const excelWidth = col
|
|
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(
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
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 ??
|
|
271
|
-
const endRow = printRange?.endRow ??
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
349
|
+
function buildRowBreakSet(sheet, visibleRows) {
|
|
333
350
|
const breaks = new Set();
|
|
334
|
-
const rowBreaks =
|
|
351
|
+
const rowBreaks = sheet.rowBreaks ?? [];
|
|
335
352
|
if (rowBreaks.length === 0) {
|
|
336
353
|
return breaks;
|
|
337
354
|
}
|
|
338
|
-
// Map
|
|
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
|
|
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
|
|
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(
|
|
373
|
+
function buildMergeMap(sheet) {
|
|
358
374
|
const map = new Map();
|
|
359
|
-
|
|
375
|
+
const merges = sheet.merges;
|
|
376
|
+
if (!merges || merges.length === 0) {
|
|
360
377
|
return map;
|
|
361
378
|
}
|
|
362
|
-
const
|
|
363
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
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,
|
|
506
|
-
const text =
|
|
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
|
|
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
|
|
549
|
+
// Image Placement
|
|
546
550
|
// =============================================================================
|
|
547
551
|
/**
|
|
548
|
-
*
|
|
549
|
-
* their top-left anchor.
|
|
552
|
+
* Assign pre-collected images to the pages that contain their top-left anchor.
|
|
550
553
|
*/
|
|
551
|
-
function assignImagesToPages(
|
|
552
|
-
|
|
553
|
-
|
|
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.
|
|
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.
|
|
595
|
-
const pageRowIndex = targetPage.
|
|
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;
|
|
574
|
+
const imgY = baseY - tlRowOff;
|
|
606
575
|
// Determine image size
|
|
607
576
|
let imgWidth = 100;
|
|
608
577
|
let imgHeight = 100;
|
|
609
|
-
if (
|
|
610
|
-
|
|
611
|
-
|
|
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 (
|
|
615
|
-
|
|
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.
|
|
620
|
-
const brPageRowIndex = targetPage.
|
|
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 !==
|
|
621
|
+
if (!cell || cell.type !== PdfCellType.RichText) {
|
|
716
622
|
return null;
|
|
717
623
|
}
|
|
718
624
|
const rtValue = cell.value;
|