documonster 0.7.0 → 0.8.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/dist/browser/modules/pdf/builder/image-utils.d.ts +1 -1
- package/dist/browser/modules/pdf/builder/image-utils.js +58 -9
- package/dist/browser/modules/pdf/core/pdf-writer.d.ts +8 -0
- package/dist/browser/modules/pdf/core/pdf-writer.js +3 -0
- package/dist/browser/modules/pdf/excel-bridge.js +126 -19
- package/dist/browser/modules/pdf/index.d.ts +1 -1
- package/dist/browser/modules/pdf/render/chart-surface.d.ts +6 -1
- package/dist/browser/modules/pdf/render/chart-surface.js +86 -47
- package/dist/browser/modules/pdf/render/constants.d.ts +69 -0
- package/dist/browser/modules/pdf/render/constants.js +45 -0
- package/dist/browser/modules/pdf/render/layout-engine.d.ts +5 -1
- package/dist/browser/modules/pdf/render/layout-engine.js +586 -205
- package/dist/browser/modules/pdf/render/page-renderer.js +180 -3
- package/dist/browser/modules/pdf/render/pdf-exporter.js +176 -35
- package/dist/browser/modules/pdf/render/style-converter.d.ts +11 -0
- package/dist/browser/modules/pdf/render/style-converter.js +24 -0
- package/dist/browser/modules/pdf/types.d.ts +284 -5
- package/dist/cjs/modules/pdf/builder/image-utils.js +58 -9
- package/dist/cjs/modules/pdf/core/pdf-writer.js +3 -0
- package/dist/cjs/modules/pdf/excel-bridge.js +125 -18
- package/dist/cjs/modules/pdf/render/chart-surface.js +86 -47
- package/dist/cjs/modules/pdf/render/constants.js +46 -1
- package/dist/cjs/modules/pdf/render/layout-engine.js +583 -202
- package/dist/cjs/modules/pdf/render/page-renderer.js +179 -2
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +175 -34
- package/dist/cjs/modules/pdf/render/style-converter.js +26 -0
- package/dist/esm/modules/pdf/builder/image-utils.js +58 -9
- package/dist/esm/modules/pdf/core/pdf-writer.js +3 -0
- package/dist/esm/modules/pdf/excel-bridge.js +126 -19
- package/dist/esm/modules/pdf/render/chart-surface.js +86 -47
- package/dist/esm/modules/pdf/render/constants.js +45 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +586 -205
- package/dist/esm/modules/pdf/render/page-renderer.js +180 -3
- package/dist/esm/modules/pdf/render/pdf-exporter.js +176 -35
- package/dist/esm/modules/pdf/render/style-converter.js +24 -0
- package/dist/iife/documonster.archive.iife.js +1 -1
- package/dist/iife/documonster.archive.iife.min.js +1 -1
- package/dist/iife/documonster.csv.iife.js +1 -1
- package/dist/iife/documonster.csv.iife.min.js +1 -1
- package/dist/iife/documonster.excel.iife.js +1 -1
- package/dist/iife/documonster.excel.iife.min.js +1 -1
- package/dist/iife/documonster.formula.iife.js +1 -1
- package/dist/iife/documonster.formula.iife.min.js +1 -1
- package/dist/iife/documonster.markdown.iife.js +1 -1
- package/dist/iife/documonster.markdown.iife.min.js +1 -1
- package/dist/iife/documonster.pdf.iife.js +1457 -664
- package/dist/iife/documonster.pdf.iife.js.map +1 -1
- package/dist/iife/documonster.pdf.iife.min.js +33 -32
- package/dist/iife/documonster.stream.iife.js +1 -1
- package/dist/iife/documonster.stream.iife.min.js +1 -1
- package/dist/iife/documonster.word.iife.js +1 -1
- package/dist/iife/documonster.word.iife.min.js +1 -1
- package/dist/iife/documonster.xml.iife.js +1 -1
- package/dist/iife/documonster.xml.iife.min.js +1 -1
- package/dist/types/modules/pdf/builder/image-utils.d.ts +1 -1
- package/dist/types/modules/pdf/core/pdf-writer.d.ts +8 -0
- package/dist/types/modules/pdf/index.d.ts +1 -1
- package/dist/types/modules/pdf/render/chart-surface.d.ts +6 -1
- package/dist/types/modules/pdf/render/constants.d.ts +69 -0
- package/dist/types/modules/pdf/render/layout-engine.d.ts +5 -1
- package/dist/types/modules/pdf/render/style-converter.d.ts +11 -0
- package/dist/types/modules/pdf/types.d.ts +284 -5
- package/package.json +1 -1
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
* - Skip hidden rows and columns
|
|
18
18
|
*/
|
|
19
19
|
import { resolvePdfFontName } from "../font/font-manager.js";
|
|
20
|
-
import { CELL_PADDING_H, CELL_PADDING_V, LINE_HEIGHT_FACTOR, INDENT_WIDTH, MAX_DIGIT_WIDTH_PX, PX_TO_PT } from "./constants.js";
|
|
20
|
+
import { CELL_PADDING_H, CELL_PADDING_V, LINE_HEIGHT_FACTOR, INDENT_WIDTH, MAX_DIGIT_WIDTH_PX, PX_TO_PT, HEADING_FONT_SIZE, HEADING_PADDING, COMMENT_MARKER_SIZE, COLUMN_FIT_EPSILON, SHEET_NAME_BAND_HEIGHT, PAGE_NUMBER_BAND_HEIGHT, FIT_MIN_SCALE } from "./constants.js";
|
|
21
21
|
import { wrapTextLines } from "./page-renderer.js";
|
|
22
|
-
import { extractFontProperties, excelFillToPdfColor, excelBordersToPdf, excelHAlignToPdf, excelVAlignToPdf, borderStyleToLineWidth } from "./style-converter.js";
|
|
22
|
+
import { extractFontProperties, excelFillToPdfColor, excelBordersToPdf, excelHAlignToPdf, excelVAlignToPdf, borderStyleToLineWidth, toGrayscale, grayscaleBorders } from "./style-converter.js";
|
|
23
23
|
import { PdfCellType } from "../types.js";
|
|
24
|
-
import { emuToPt,
|
|
24
|
+
import { emuToPt, charWidthToPixel } from "../../../utils/units.js";
|
|
25
25
|
import { yieldToEventLoop } from "../../../utils/utils.base.js";
|
|
26
26
|
// =============================================================================
|
|
27
27
|
// Constants
|
|
@@ -82,25 +82,39 @@ export async function layoutSheet(sheet, options, fontManager) {
|
|
|
82
82
|
}
|
|
83
83
|
const layoutPages = [];
|
|
84
84
|
const totalOutputPages = ctx.rowPages.length * ctx.colGroups.length;
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// Excel's "Page order". `downThenOver` (Excel's default) walks each column
|
|
86
|
+
// band top to bottom before moving right; `overThenDown` walks each row band
|
|
87
|
+
// left to right before moving down. Only the nesting differs, so pick which
|
|
88
|
+
// axis is outer rather than materialising every pair.
|
|
89
|
+
const overThenDown = options.pageOrder === "overThenDown";
|
|
90
|
+
const outer = overThenDown ? ctx.rowPages : ctx.colGroups;
|
|
91
|
+
const inner = overThenDown ? ctx.colGroups : ctx.rowPages;
|
|
92
|
+
for (const outerTracks of outer) {
|
|
93
|
+
for (const innerTracks of inner) {
|
|
94
|
+
const rowPage = overThenDown ? outerTracks : innerTracks;
|
|
95
|
+
const colGroup = overThenDown ? innerTracks : outerTracks;
|
|
87
96
|
layoutPages.push(buildPageLayout(ctx, rowPage, colGroup, layoutPages.length, sheet, options, fontManager));
|
|
88
97
|
if (layoutPages.length < totalOutputPages) {
|
|
89
98
|
await yieldToEventLoop();
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
|
-
|
|
102
|
+
// Draft quality omits graphics, matching Excel's "Draft quality" option.
|
|
103
|
+
if (layoutPages.length > 0 && sheet.images && !options.draft) {
|
|
94
104
|
assignImagesToPages(sheet.images, layoutPages, ctx.scaleFactor);
|
|
95
105
|
}
|
|
96
|
-
if (layoutPages.length > 0 && sheet.charts) {
|
|
106
|
+
if (layoutPages.length > 0 && sheet.charts && !options.draft) {
|
|
97
107
|
assignChartsToPages(sheet.charts, layoutPages, ctx.scaleFactor);
|
|
98
108
|
}
|
|
99
|
-
|
|
109
|
+
// Excel's "Comments: at end of sheet" — appended after the grid so the page
|
|
110
|
+
// numbering below covers them too.
|
|
111
|
+
if (options.cellComments === "atEnd" && sheet.comments?.length) {
|
|
112
|
+
layoutPages.push(...buildCommentPages(sheet, sheet.comments, options, fontManager));
|
|
113
|
+
}
|
|
114
|
+
// Only the per-sheet index is settled here; `fixPageNumbers` in the exporter
|
|
115
|
+
// owns `pageNumber`, `sheetPageNumber` and the document-wide `sheetPageCount`.
|
|
100
116
|
for (let i = 0; i < layoutPages.length; i++) {
|
|
101
|
-
layoutPages[i].sheetPageNumber = firstPageNumber + i;
|
|
102
117
|
layoutPages[i].sheetPageIndex = i + 1;
|
|
103
|
-
layoutPages[i].sheetPageCount = layoutPages.length;
|
|
104
118
|
}
|
|
105
119
|
return layoutPages;
|
|
106
120
|
}
|
|
@@ -130,14 +144,10 @@ export function layoutChartsheet(sheet, documentOptions) {
|
|
|
130
144
|
// affected when a single chartsheet flips to portrait.
|
|
131
145
|
const orientation = sheet.orientation ?? documentOptions.orientation;
|
|
132
146
|
const options = { ...documentOptions, orientation };
|
|
133
|
-
|
|
134
|
-
let pageHeight = options.pageSize.height;
|
|
135
|
-
if (options.orientation === "landscape") {
|
|
136
|
-
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
|
137
|
-
}
|
|
147
|
+
const { width: pageWidth, height: pageHeight } = pageDimensions(options);
|
|
138
148
|
const margins = options.margins;
|
|
139
|
-
const headerHeight = options.showSheetNames ?
|
|
140
|
-
const footerHeight = options.showPageNumbers ?
|
|
149
|
+
const headerHeight = options.showSheetNames ? SHEET_NAME_BAND_HEIGHT : 0;
|
|
150
|
+
const footerHeight = options.showPageNumbers ? PAGE_NUMBER_BAND_HEIGHT : 0;
|
|
141
151
|
const contentX = margins.left;
|
|
142
152
|
const contentY = margins.bottom + footerHeight;
|
|
143
153
|
const contentWidth = pageWidth - margins.left - margins.right;
|
|
@@ -152,7 +162,38 @@ export function layoutChartsheet(sheet, documentOptions) {
|
|
|
152
162
|
drawVector: sheet.chart.drawVector,
|
|
153
163
|
raster: sheet.chart.raster
|
|
154
164
|
};
|
|
155
|
-
|
|
165
|
+
return [
|
|
166
|
+
{
|
|
167
|
+
...blankPage(sheet, options),
|
|
168
|
+
// Draft quality omits graphics. A chartsheet is nothing but a graphic, so
|
|
169
|
+
// the page is still emitted — Excel likewise prints a blank sheet rather
|
|
170
|
+
// than dropping it from the page count.
|
|
171
|
+
charts: options.draft ? [] : [chart]
|
|
172
|
+
}
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
// =============================================================================
|
|
176
|
+
// Internal — Shared Layout Pipeline
|
|
177
|
+
// =============================================================================
|
|
178
|
+
/**
|
|
179
|
+
* Page dimensions for the resolved options, with landscape applied.
|
|
180
|
+
*
|
|
181
|
+
* `pageSize` is always stored portrait-wise, so every consumer has to swap the
|
|
182
|
+
* axes itself; centralised here so the four layout entry points cannot disagree.
|
|
183
|
+
*/
|
|
184
|
+
function pageDimensions(options) {
|
|
185
|
+
const { width, height } = options.pageSize;
|
|
186
|
+
return options.orientation === "landscape" ? { width: height, height: width } : { width, height };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A `LayoutPage` with every field at its empty value.
|
|
190
|
+
*
|
|
191
|
+
* The shape has ~20 members and four construction sites; building them from one
|
|
192
|
+
* skeleton means a new field cannot be forgotten in three of them.
|
|
193
|
+
*/
|
|
194
|
+
function blankPage(sheet, options) {
|
|
195
|
+
const { width, height } = pageDimensions(options);
|
|
196
|
+
return {
|
|
156
197
|
pageNumber: 1,
|
|
157
198
|
sheetPageNumber: sheet.pageSetup?.firstPageNumber ?? 1,
|
|
158
199
|
sheetPageIndex: 1,
|
|
@@ -160,8 +201,8 @@ export function layoutChartsheet(sheet, documentOptions) {
|
|
|
160
201
|
firstPageNumber: sheet.pageSetup?.firstPageNumber,
|
|
161
202
|
options,
|
|
162
203
|
cells: [],
|
|
163
|
-
width
|
|
164
|
-
height
|
|
204
|
+
width,
|
|
205
|
+
height,
|
|
165
206
|
sheetName: sheet.name,
|
|
166
207
|
sheetCols: [],
|
|
167
208
|
columnOffsets: [],
|
|
@@ -170,11 +211,10 @@ export function layoutChartsheet(sheet, documentOptions) {
|
|
|
170
211
|
rowYPositions: [],
|
|
171
212
|
rowHeights: [],
|
|
172
213
|
images: [],
|
|
173
|
-
charts: [
|
|
214
|
+
charts: [],
|
|
174
215
|
scaleFactor: 1,
|
|
175
216
|
headerFooter: options.includeHeadersFooters ? sheet.headerFooter : undefined
|
|
176
217
|
};
|
|
177
|
-
return [page];
|
|
178
218
|
}
|
|
179
219
|
/**
|
|
180
220
|
* Steps 1–5: compute columns, scale, rows, merges, pagination.
|
|
@@ -182,45 +222,90 @@ export function layoutChartsheet(sheet, documentOptions) {
|
|
|
182
222
|
*/
|
|
183
223
|
function prepareLayout(sheet, options, fontManager) {
|
|
184
224
|
const { margins } = options;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (options.orientation === "landscape") {
|
|
188
|
-
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
|
189
|
-
}
|
|
190
|
-
const contentWidth = pageWidth - margins.left - margins.right;
|
|
191
|
-
const contentHeight = pageHeight - margins.top - margins.bottom;
|
|
192
|
-
const headerHeight = options.showSheetNames ? 20 : 0;
|
|
225
|
+
const { width: pageWidth, height: pageHeight } = pageDimensions(options);
|
|
226
|
+
const headerHeight = options.showSheetNames ? SHEET_NAME_BAND_HEIGHT : 0;
|
|
193
227
|
const printRange = getPrintRange(sheet, options);
|
|
194
|
-
//
|
|
195
|
-
|
|
228
|
+
// Excel's "Row and column headings". The bands are deliberately *not*
|
|
229
|
+
// scaled with the grid: they are a print aid rather than content, and a
|
|
230
|
+
// fixed size stays legible at small print scales while letting us reserve
|
|
231
|
+
// exactly the space we later draw into.
|
|
232
|
+
const headings = options.showRowColHeaders
|
|
233
|
+
? computeHeadingMetrics(sheet, printRange, fontManager, options)
|
|
234
|
+
: undefined;
|
|
235
|
+
const gutterWidth = headings?.gutterWidth ?? 0;
|
|
236
|
+
const bandHeight = headings?.bandHeight ?? 0;
|
|
237
|
+
const contentWidth = pageWidth - margins.left - margins.right - gutterWidth;
|
|
238
|
+
const contentHeight = pageHeight - margins.top - margins.bottom - bandHeight;
|
|
239
|
+
// --- Step 1: Visible columns and widths (title columns lead) ---
|
|
240
|
+
const { columnWidths, visibleCols, repeatColIndices } = computeColumnWidths(sheet, printRange, options.repeatCols);
|
|
196
241
|
if (visibleCols.length === 0) {
|
|
197
242
|
return null;
|
|
198
243
|
}
|
|
199
244
|
// --- Step 2: Scale ---
|
|
245
|
+
// Rows are measured once, unscaled: heights are linear in the print scale, so
|
|
246
|
+
// the fit solver can probe by multiplying instead of re-measuring.
|
|
247
|
+
const natural = computeRowHeights(sheet, printRange, fontManager, options, options.repeatRows);
|
|
248
|
+
// Break sets are derived once: the fit solver paginates ~25 times per axis.
|
|
249
|
+
const rowBreaks = buildBreakSet(sheet.rowBreaks ?? [], natural.visibleRows);
|
|
250
|
+
const colBreaks = buildBreakSet(sheet.colBreaks ?? [], visibleCols);
|
|
200
251
|
const totalTableWidth = columnWidths.reduce((sum, w) => sum + w, 0);
|
|
252
|
+
const footerHeight = options.showPageNumbers ? PAGE_NUMBER_BAND_HEIGHT : 0;
|
|
253
|
+
const availableHeight = contentHeight - headerHeight - footerHeight;
|
|
201
254
|
let scaleFactor = options.scale;
|
|
202
|
-
if (options.
|
|
203
|
-
|
|
255
|
+
if (options.fitToWidth > 0 || options.fitToHeight > 0) {
|
|
256
|
+
// Excel's "Fit to N page(s) wide by M tall". Like Excel, this only ever
|
|
257
|
+
// shrinks — a grid smaller than the target is left at actual size.
|
|
258
|
+
//
|
|
259
|
+
// A total-size ratio alone does not deliver the promise: pagination packs
|
|
260
|
+
// indivisible columns/rows greedily, and repeated title bands consume space
|
|
261
|
+
// on every page after the first. Three columns at 60% of the page width fit
|
|
262
|
+
// "1.8 pages" by area yet still need three pages. So the ratio is only a
|
|
263
|
+
// starting upper bound, which we then tighten against the real packer.
|
|
264
|
+
let fit = 1;
|
|
265
|
+
// Excel's 10% floor applies to the *final* scale, so express it relative to
|
|
266
|
+
// the factor already in play.
|
|
267
|
+
const minFit = Math.min(1, FIT_MIN_SCALE / scaleFactor);
|
|
268
|
+
if (options.fitToWidth > 0 && totalTableWidth > 0) {
|
|
269
|
+
fit = Math.min(fit, (contentWidth * options.fitToWidth) / (totalTableWidth * scaleFactor));
|
|
270
|
+
fit = tightenToPageCount(candidate => paginateTracks(columnWidths.map(w => w * scaleFactor * candidate), contentWidth, repeatColIndices, colBreaks, COLUMN_FIT_EPSILON).length, options.fitToWidth, fit, minFit);
|
|
271
|
+
}
|
|
272
|
+
if (options.fitToHeight > 0 && availableHeight > 0) {
|
|
273
|
+
const totalTableHeight = natural.rowHeights.reduce((sum, h) => sum + h, 0);
|
|
274
|
+
if (totalTableHeight > 0) {
|
|
275
|
+
let heightFit = Math.min(fit, (availableHeight * options.fitToHeight) / (totalTableHeight * scaleFactor));
|
|
276
|
+
heightFit = tightenToPageCount(candidate => paginateTracks(natural.rowHeights.map(h => h * scaleFactor * candidate), availableHeight, natural.repeatRowIndices, rowBreaks).length, options.fitToHeight, heightFit, minFit);
|
|
277
|
+
fit = Math.min(fit, heightFit);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (fit < 1) {
|
|
281
|
+
scaleFactor = Math.max(scaleFactor * fit, FIT_MIN_SCALE);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else if (options.fitToPage && totalTableWidth > 0) {
|
|
285
|
+
// Same contract as `fitToWidth: 1`: it is the *final* width that must fit
|
|
286
|
+
// one page, so `scale` belongs inside the ratio rather than multiplied on
|
|
287
|
+
// top of it. Dividing by the running factor makes this `min(scale,
|
|
288
|
+
// contentWidth / totalTableWidth)`, which neither overflows when `scale`
|
|
289
|
+
// enlarges nor shrinks twice when it already reduces.
|
|
290
|
+
const fitScale = contentWidth / (totalTableWidth * scaleFactor);
|
|
204
291
|
if (fitScale < 1) {
|
|
205
292
|
scaleFactor *= fitScale;
|
|
206
293
|
}
|
|
207
294
|
}
|
|
208
295
|
const scaledColumnWidths = columnWidths.map(w => w * scaleFactor);
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
const { rowHeights, visibleRows } = computeRowHeights(sheet, scaleFactor, printRange, fontManager, options);
|
|
296
|
+
// --- Step 3: Apply the final scale to the measured heights ---
|
|
297
|
+
const rowHeights = natural.rowHeights.map(h => h * scaleFactor);
|
|
298
|
+
const { visibleRows, repeatRowIndices } = natural;
|
|
213
299
|
// --- Step 4: Merge map ---
|
|
214
300
|
const mergeMap = buildMergeMap(sheet);
|
|
215
301
|
// --- Step 5: Paginate ---
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const rowPages = paginateRows(rowHeights, availableHeight, repeatRowCount, rowBreakSet);
|
|
219
|
-
const colGroups = paginateColumns(scaledColumnWidths, contentWidth, sheet, visibleCols);
|
|
302
|
+
const rowPages = paginateTracks(rowHeights, availableHeight, repeatRowIndices, rowBreaks);
|
|
303
|
+
const colGroups = paginateTracks(scaledColumnWidths, contentWidth, repeatColIndices, colBreaks, COLUMN_FIT_EPSILON);
|
|
220
304
|
return {
|
|
221
305
|
pageWidth,
|
|
222
306
|
pageHeight,
|
|
223
307
|
contentWidth,
|
|
308
|
+
availableHeight,
|
|
224
309
|
headerHeight,
|
|
225
310
|
scaleFactor,
|
|
226
311
|
scaledColumnWidths,
|
|
@@ -230,31 +315,179 @@ function prepareLayout(sheet, options, fontManager) {
|
|
|
230
315
|
mergeMap,
|
|
231
316
|
rowPages,
|
|
232
317
|
colGroups,
|
|
233
|
-
margins
|
|
318
|
+
margins,
|
|
319
|
+
headings
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Size the row-number gutter and column-letter band for Excel's "Row and
|
|
324
|
+
* column headings" print option.
|
|
325
|
+
*
|
|
326
|
+
* The gutter is sized from the widest row label that can appear in the printed
|
|
327
|
+
* range, so the grid origin is stable across pages instead of jittering as row
|
|
328
|
+
* numbers gain digits.
|
|
329
|
+
*/
|
|
330
|
+
function computeHeadingMetrics(sheet, printRange, fontManager, options) {
|
|
331
|
+
const fontSize = HEADING_FONT_SIZE;
|
|
332
|
+
const resourceName = fontManager.hasEmbeddedFont()
|
|
333
|
+
? fontManager.getEmbeddedResourceName()
|
|
334
|
+
: fontManager.ensureFont(resolvePdfFontName(options.defaultFontFamily, false, false));
|
|
335
|
+
const lastRow = printRange?.endRow ?? sheet.bounds.bottom;
|
|
336
|
+
const widestLabel = String(Math.max(1, lastRow));
|
|
337
|
+
fontManager.trackText(widestLabel);
|
|
338
|
+
const labelWidth = fontManager.measureText(widestLabel, resourceName, fontSize);
|
|
339
|
+
return {
|
|
340
|
+
gutterWidth: labelWidth + 2 * HEADING_PADDING,
|
|
341
|
+
bandHeight: fontSize + 2 * HEADING_PADDING,
|
|
342
|
+
fontSize
|
|
234
343
|
};
|
|
235
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Largest scale ≤ `startScale` whose real pagination lands within `target`
|
|
347
|
+
* pages, found by bisection.
|
|
348
|
+
*
|
|
349
|
+
* The caller passes a closure that re-paginates at a candidate scale, so the
|
|
350
|
+
* answer respects indivisible columns/rows, manual breaks and repeated title
|
|
351
|
+
* bands instead of trusting a total-size ratio. Returns `startScale` untouched
|
|
352
|
+
* when it already fits, so nothing is shrunk needlessly.
|
|
353
|
+
*
|
|
354
|
+
* `minScale` is the floor the result may not go below. When the target is
|
|
355
|
+
* unreachable even there — manual page breaks alone can force more pages than
|
|
356
|
+
* requested — the floor is returned and the sheet simply spans more than
|
|
357
|
+
* `target` pages, which is preferable to shrinking it into illegibility.
|
|
358
|
+
*/
|
|
359
|
+
function tightenToPageCount(pageCountAt, target, startScale, minScale) {
|
|
360
|
+
const start = Math.max(startScale, minScale);
|
|
361
|
+
if (pageCountAt(start) <= target || start <= minScale) {
|
|
362
|
+
return start;
|
|
363
|
+
}
|
|
364
|
+
let lo = minScale;
|
|
365
|
+
let hi = start;
|
|
366
|
+
if (pageCountAt(lo) > target) {
|
|
367
|
+
return lo;
|
|
368
|
+
}
|
|
369
|
+
// 24 halvings resolve the scale to ~6e-8 of the starting bound, far below one
|
|
370
|
+
// device pixel, and each probe is pure arithmetic over the track sizes.
|
|
371
|
+
for (let i = 0; i < 24; i++) {
|
|
372
|
+
const mid = (lo + hi) / 2;
|
|
373
|
+
if (pageCountAt(mid) <= target) {
|
|
374
|
+
lo = mid;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
hi = mid;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return lo;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Lay out Excel's "Comments at end of sheet" as extra pages.
|
|
384
|
+
*
|
|
385
|
+
* Each entry becomes an ordinary {@link LayoutCell} spanning the content width,
|
|
386
|
+
* so the existing page renderer draws them with no special casing — including
|
|
387
|
+
* word wrapping, which long comment bodies need. Pages are filled top to bottom
|
|
388
|
+
* and a new one starts when the next entry would overflow.
|
|
389
|
+
*/
|
|
390
|
+
function buildCommentPages(sheet, comments, options, fontManager) {
|
|
391
|
+
const { width: pageWidth, height: pageHeight } = pageDimensions(options);
|
|
392
|
+
const { margins } = options;
|
|
393
|
+
const headerHeight = options.showSheetNames ? SHEET_NAME_BAND_HEIGHT : 0;
|
|
394
|
+
const footerHeight = options.showPageNumbers ? PAGE_NUMBER_BAND_HEIGHT : 0;
|
|
395
|
+
const contentWidth = pageWidth - margins.left - margins.right;
|
|
396
|
+
const top = pageHeight - margins.top - headerHeight;
|
|
397
|
+
const bottom = margins.bottom + footerHeight;
|
|
398
|
+
const fontSize = options.defaultFontSize;
|
|
399
|
+
const resourceName = fontManager.hasEmbeddedFont()
|
|
400
|
+
? fontManager.getEmbeddedResourceName()
|
|
401
|
+
: fontManager.ensureFont(resolvePdfFontName(options.defaultFontFamily, false, false));
|
|
402
|
+
const measure = (text) => fontManager.measureText(text, resourceName, fontSize);
|
|
403
|
+
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
404
|
+
const textColor = { r: 0, g: 0, b: 0 };
|
|
405
|
+
const pages = [];
|
|
406
|
+
let cells = [];
|
|
407
|
+
let cursor = top;
|
|
408
|
+
const flush = () => {
|
|
409
|
+
if (cells.length > 0) {
|
|
410
|
+
pages.push({ ...blankPage(sheet, options), cells });
|
|
411
|
+
cells = [];
|
|
412
|
+
cursor = top;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const push = (text, bold) => {
|
|
416
|
+
fontManager.trackText(text);
|
|
417
|
+
const lines = wrapTextLines(text, measure, Math.max(contentWidth - 2 * CELL_PADDING_H, 1));
|
|
418
|
+
const height = Math.max(lines.length, 1) * lineHeight + 2 * CELL_PADDING_V;
|
|
419
|
+
if (cursor - height < bottom) {
|
|
420
|
+
flush();
|
|
421
|
+
}
|
|
422
|
+
cells.push({
|
|
423
|
+
text,
|
|
424
|
+
rect: { x: margins.left, y: cursor - height, width: contentWidth, height },
|
|
425
|
+
fontFamily: options.defaultFontFamily,
|
|
426
|
+
fontSize,
|
|
427
|
+
bold,
|
|
428
|
+
italic: false,
|
|
429
|
+
strike: false,
|
|
430
|
+
underline: false,
|
|
431
|
+
textColor,
|
|
432
|
+
fillColor: null,
|
|
433
|
+
horizontalAlign: "left",
|
|
434
|
+
verticalAlign: "top",
|
|
435
|
+
wrapText: true,
|
|
436
|
+
borders: { top: null, right: null, bottom: null, left: null },
|
|
437
|
+
borderInsets: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
438
|
+
colSpan: 1,
|
|
439
|
+
rowSpan: 1,
|
|
440
|
+
hyperlink: null,
|
|
441
|
+
richText: null,
|
|
442
|
+
textRotation: 0,
|
|
443
|
+
indent: 0,
|
|
444
|
+
textOverflowWidth: 0
|
|
445
|
+
});
|
|
446
|
+
cursor -= height;
|
|
447
|
+
};
|
|
448
|
+
push(`${sheet.name} — comments`, true);
|
|
449
|
+
for (const comment of comments) {
|
|
450
|
+
const author = comment.author ? ` (${comment.author})` : "";
|
|
451
|
+
push(`${comment.ref}${author}: ${comment.text}`, false);
|
|
452
|
+
}
|
|
453
|
+
flush();
|
|
454
|
+
return pages;
|
|
455
|
+
}
|
|
236
456
|
/**
|
|
237
457
|
* Build the LayoutPage for a single rowPage × colGroup combination.
|
|
238
458
|
*/
|
|
239
459
|
function buildPageLayout(ctx, rowPage, colGroup, currentPageCount, sheet, options, fontManager) {
|
|
240
|
-
const { scaledColumnWidths, rowHeights, visibleRows, visibleCols, mergeMap, pageWidth, pageHeight, contentWidth, headerHeight, scaleFactor, margins } = ctx;
|
|
460
|
+
const { scaledColumnWidths, rowHeights, visibleRows, visibleCols, mergeMap, pageWidth, pageHeight, contentWidth, availableHeight, headerHeight, scaleFactor, margins, headings } = ctx;
|
|
241
461
|
const cells = [];
|
|
242
|
-
//
|
|
462
|
+
// The row-number gutter and column-letter band shift the grid origin right
|
|
463
|
+
// and down respectively; both are 0 when headings are not printed.
|
|
464
|
+
const gridLeft = margins.left + (headings?.gutterWidth ?? 0);
|
|
465
|
+
const gridTop = pageHeight - margins.top - headerHeight - (headings?.bandHeight ?? 0);
|
|
466
|
+
// Compute column offsets for this column group. Content starts at the left
|
|
467
|
+
// margin, matching Excel's print behaviour; it is only centered when the
|
|
468
|
+
// sheet (or the caller) asks for it via Excel's "Center on page →
|
|
469
|
+
// Horizontally" print option.
|
|
243
470
|
const groupColWidths = colGroup.map(ci => scaledColumnWidths[ci]);
|
|
244
471
|
const groupTotalWidth = groupColWidths.reduce((s, w) => s + w, 0);
|
|
245
472
|
const groupColOffsets = [];
|
|
246
|
-
let gx =
|
|
247
|
-
if (groupTotalWidth < contentWidth) {
|
|
248
|
-
gx =
|
|
473
|
+
let gx = gridLeft;
|
|
474
|
+
if (options.horizontalCentered && groupTotalWidth < contentWidth) {
|
|
475
|
+
gx = gridLeft + (contentWidth - groupTotalWidth) / 2;
|
|
249
476
|
}
|
|
250
477
|
for (const w of groupColWidths) {
|
|
251
478
|
groupColOffsets.push(gx);
|
|
252
479
|
gx += w;
|
|
253
480
|
}
|
|
254
|
-
// Row Y positions
|
|
481
|
+
// Row Y positions. Same rule as above for the vertical axis.
|
|
255
482
|
const rowYPositions = [];
|
|
256
483
|
const pageRowHeights = [];
|
|
257
|
-
let currentY =
|
|
484
|
+
let currentY = gridTop;
|
|
485
|
+
if (options.verticalCentered) {
|
|
486
|
+
const pageTotalHeight = rowPage.reduce((sum, rowIdx) => sum + (rowHeights[rowIdx] ?? DEFAULT_ROW_HEIGHT * scaleFactor), 0);
|
|
487
|
+
if (pageTotalHeight < availableHeight) {
|
|
488
|
+
currentY -= (availableHeight - pageTotalHeight) / 2;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
258
491
|
for (const rowIdx of rowPage) {
|
|
259
492
|
const rowH = rowHeights[rowIdx] ?? DEFAULT_ROW_HEIGHT * scaleFactor;
|
|
260
493
|
rowYPositions.push(currentY);
|
|
@@ -318,6 +551,12 @@ function buildPageLayout(ctx, rowPage, colGroup, currentPageCount, sheet, option
|
|
|
318
551
|
// Propagate merged cell borders from boundary cells
|
|
319
552
|
if (mergeInfo?.isMaster) {
|
|
320
553
|
propagateMergeBorders(layoutCell, mergeInfo, wsRowNumber, wsColNumber, sheet);
|
|
554
|
+
// Propagation re-converts the boundary cell's border straight from the
|
|
555
|
+
// Excel style, bypassing the conversion in `buildLayoutCell`, so the
|
|
556
|
+
// black-and-white pass has to be reapplied to the result.
|
|
557
|
+
if (options.blackAndWhite) {
|
|
558
|
+
layoutCell.borders = grayscaleBorders(layoutCell.borders);
|
|
559
|
+
}
|
|
321
560
|
}
|
|
322
561
|
cellGrid.set(`${ri}:${gci}`, layoutCell);
|
|
323
562
|
}
|
|
@@ -347,37 +586,101 @@ function buildPageLayout(ctx, rowPage, colGroup, currentPageCount, sheet, option
|
|
|
347
586
|
images: [],
|
|
348
587
|
charts: [],
|
|
349
588
|
scaleFactor,
|
|
589
|
+
headings,
|
|
590
|
+
commentBoxes: options.cellComments === "asDisplayed"
|
|
591
|
+
? placeCommentBoxes(sheet, colGroup.map(ci => visibleCols[ci]), groupColOffsets, groupColWidths, rowPage.map(ri => visibleRows[ri]), rowYPositions, pageRowHeights, options, scaleFactor)
|
|
592
|
+
: undefined,
|
|
350
593
|
headerFooter: options.includeHeadersFooters ? sheet.headerFooter : undefined
|
|
351
594
|
};
|
|
352
595
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
596
|
+
/**
|
|
597
|
+
* Position each comment box on a page, for `cellComments: "asDisplayed"`.
|
|
598
|
+
*
|
|
599
|
+
* A comment is placed when its box overlaps the page's tracks. Fractional VML
|
|
600
|
+
* coordinates are interpolated against the page's own column offsets and row
|
|
601
|
+
* positions, so a box lands correctly even when the scale, hidden tracks or
|
|
602
|
+
* repeated title bands have moved the grid around. Comments whose box falls
|
|
603
|
+
* entirely outside the page are skipped, which keeps each one on a single page
|
|
604
|
+
* rather than slicing it across the seam.
|
|
605
|
+
*/
|
|
606
|
+
function placeCommentBoxes(sheet, sheetCols, columnOffsets, columnWidths, sheetRows, rowYPositions, rowHeights, options, scaleFactor) {
|
|
607
|
+
const comments = sheet.comments;
|
|
608
|
+
if (!comments?.length || sheetCols.length === 0 || sheetRows.length === 0) {
|
|
609
|
+
return undefined;
|
|
358
610
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
sheetRows: [],
|
|
374
|
-
rowYPositions: [],
|
|
375
|
-
rowHeights: [],
|
|
376
|
-
images: [],
|
|
377
|
-
charts: [],
|
|
378
|
-
scaleFactor: 1,
|
|
379
|
-
headerFooter: options.includeHeadersFooters ? sheet.headerFooter : undefined
|
|
611
|
+
// VML coordinates are 0-based; the page tracks are 1-based sheet numbers.
|
|
612
|
+
const xAt = (col) => {
|
|
613
|
+
const index = sheetCols.indexOf(Math.floor(col) + 1);
|
|
614
|
+
if (index < 0) {
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
return columnOffsets[index] + (col - Math.floor(col)) * columnWidths[index];
|
|
618
|
+
};
|
|
619
|
+
const yAt = (row) => {
|
|
620
|
+
const index = sheetRows.indexOf(Math.floor(row) + 1);
|
|
621
|
+
if (index < 0) {
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
return rowYPositions[index] - (row - Math.floor(row)) * rowHeights[index];
|
|
380
625
|
};
|
|
626
|
+
const boxes = [];
|
|
627
|
+
for (const comment of comments) {
|
|
628
|
+
const anchor = comment.anchor ?? defaultCommentAnchor(comment.ref);
|
|
629
|
+
if (!anchor) {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const left = xAt(anchor.left);
|
|
633
|
+
const right = xAt(anchor.right);
|
|
634
|
+
const top = yAt(anchor.top);
|
|
635
|
+
const bottom = yAt(anchor.bottom);
|
|
636
|
+
if (left === undefined || right === undefined || top === undefined || bottom === undefined) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const width = right - left;
|
|
640
|
+
const height = top - bottom;
|
|
641
|
+
if (width <= 0 || height <= 0) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const cell = parseCellRef(comment.ref);
|
|
645
|
+
const markerCol = sheetCols.indexOf(cell.c + 1);
|
|
646
|
+
const markerRow = sheetRows.indexOf(cell.r + 1);
|
|
647
|
+
const author = comment.author ? `${comment.author}:\n` : "";
|
|
648
|
+
boxes.push({
|
|
649
|
+
rect: { x: left, y: bottom, width, height },
|
|
650
|
+
text: `${author}${comment.text}`,
|
|
651
|
+
fontSize: options.defaultFontSize * scaleFactor,
|
|
652
|
+
marker: markerCol >= 0 && markerRow >= 0
|
|
653
|
+
? {
|
|
654
|
+
x: columnOffsets[markerCol] + columnWidths[markerCol],
|
|
655
|
+
y: rowYPositions[markerRow],
|
|
656
|
+
size: COMMENT_MARKER_SIZE * scaleFactor
|
|
657
|
+
}
|
|
658
|
+
: undefined
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
return boxes.length > 0 ? boxes : undefined;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Excel's default comment placement, used when the note carries no VML anchor.
|
|
665
|
+
*
|
|
666
|
+
* Mirrors the geometry Excel writes for a fresh comment: the box starts at the
|
|
667
|
+
* commented cell's column, two rows above it, and spans two columns by four
|
|
668
|
+
* rows.
|
|
669
|
+
*/
|
|
670
|
+
function defaultCommentAnchor(ref) {
|
|
671
|
+
let cell;
|
|
672
|
+
try {
|
|
673
|
+
cell = parseCellRef(ref);
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
const left = cell.c + 6 / 68;
|
|
679
|
+
const top = Math.max(cell.r - 2, 0) + 14 / 18;
|
|
680
|
+
return { left, top, right: left + 2, bottom: top + 4 };
|
|
681
|
+
}
|
|
682
|
+
function createEmptyPage(sheet, options) {
|
|
683
|
+
return blankPage(sheet, options);
|
|
381
684
|
}
|
|
382
685
|
/**
|
|
383
686
|
* Parse a cell reference like "A1" into 0-indexed { c, r }.
|
|
@@ -441,46 +744,80 @@ function getPrintRange(sheet, options) {
|
|
|
441
744
|
// =============================================================================
|
|
442
745
|
// Column Width Computation
|
|
443
746
|
// =============================================================================
|
|
444
|
-
function computeColumnWidths(sheet, printRange) {
|
|
747
|
+
function computeColumnWidths(sheet, printRange, titleBand) {
|
|
445
748
|
const bounds = sheet.bounds;
|
|
446
749
|
const hasData = bounds.top > 0 && bounds.left > 0;
|
|
447
750
|
if (!hasData) {
|
|
448
|
-
return { columnWidths: [], visibleCols: [] };
|
|
751
|
+
return { columnWidths: [], visibleCols: [], repeatColIndices: [] };
|
|
449
752
|
}
|
|
450
753
|
const startCol = printRange?.startCol ?? bounds.left;
|
|
451
754
|
const endCol = printRange?.endCol ?? bounds.right;
|
|
452
755
|
const columnWidths = [];
|
|
453
756
|
const visibleCols = [];
|
|
454
|
-
|
|
757
|
+
const emitted = new Set();
|
|
758
|
+
const push = (c) => {
|
|
759
|
+
if (emitted.has(c)) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
455
762
|
const col = sheet.columns.get(c);
|
|
456
763
|
if (col?.hidden) {
|
|
457
|
-
|
|
764
|
+
return;
|
|
458
765
|
}
|
|
766
|
+
emitted.add(c);
|
|
459
767
|
const excelWidth = col?.width ?? DEFAULT_COLUMN_WIDTH;
|
|
460
768
|
const pixelWidth = charWidthToPixel(excelWidth, MAX_DIGIT_WIDTH_PX);
|
|
461
769
|
const pointWidth = Math.max(pixelWidth * PX_TO_PT, MIN_COLUMN_WIDTH);
|
|
462
770
|
columnWidths.push(pointWidth);
|
|
463
771
|
visibleCols.push(c);
|
|
772
|
+
};
|
|
773
|
+
// Print titles are independent of the print area, so a band that is not
|
|
774
|
+
// fully inside it must be emitted first: it prints down the left of every
|
|
775
|
+
// page. A band wholly inside keeps the sheet's natural order, so the first
|
|
776
|
+
// page is not reshuffled — only later pages get the repeated prefix.
|
|
777
|
+
const titleFullyInside = titleBand && titleBand.first >= startCol && titleBand.last <= endCol;
|
|
778
|
+
if (titleBand && !titleFullyInside) {
|
|
779
|
+
for (let c = titleBand.first; c <= titleBand.last; c++) {
|
|
780
|
+
push(c);
|
|
781
|
+
}
|
|
464
782
|
}
|
|
465
|
-
|
|
783
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
784
|
+
push(c);
|
|
785
|
+
}
|
|
786
|
+
const repeatColIndices = titleBand
|
|
787
|
+
? visibleCols.flatMap((c, i) => (c >= titleBand.first && c <= titleBand.last ? [i] : []))
|
|
788
|
+
: [];
|
|
789
|
+
return { columnWidths, visibleCols, repeatColIndices };
|
|
466
790
|
}
|
|
467
791
|
// =============================================================================
|
|
468
792
|
// Row Height Computation
|
|
469
793
|
// =============================================================================
|
|
470
|
-
|
|
794
|
+
/**
|
|
795
|
+
* Measure every printable row at 100% scale.
|
|
796
|
+
*
|
|
797
|
+
* Heights are deliberately unscaled: `countWrapLines` derives the wrapped line
|
|
798
|
+
* count from ratios that are independent of the print scale, so a scaled height
|
|
799
|
+
* is exactly `unscaled * scale`. Measuring once and multiplying avoids re-running
|
|
800
|
+
* the most expensive step of layout for every probe of the fit solver.
|
|
801
|
+
*/
|
|
802
|
+
function computeRowHeights(sheet, printRange, fontManager, options, titleBand) {
|
|
471
803
|
const bounds = sheet.bounds;
|
|
472
804
|
if (bounds.top <= 0) {
|
|
473
|
-
return { rowHeights: [], visibleRows: [] };
|
|
805
|
+
return { rowHeights: [], visibleRows: [], repeatRowIndices: [] };
|
|
474
806
|
}
|
|
475
807
|
const startRow = printRange?.startRow ?? bounds.top;
|
|
476
808
|
const endRow = printRange?.endRow ?? bounds.bottom;
|
|
477
809
|
const rowHeights = [];
|
|
478
810
|
const visibleRows = [];
|
|
479
|
-
|
|
811
|
+
const emitted = new Set();
|
|
812
|
+
const push = (r) => {
|
|
813
|
+
if (emitted.has(r)) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
480
816
|
const row = sheet.rows.get(r);
|
|
481
817
|
if (row?.hidden) {
|
|
482
|
-
|
|
818
|
+
return;
|
|
483
819
|
}
|
|
820
|
+
emitted.add(r);
|
|
484
821
|
let height;
|
|
485
822
|
if (row?.height && row.customHeight) {
|
|
486
823
|
// Custom height explicitly set by user — use as-is
|
|
@@ -491,27 +828,41 @@ function computeRowHeights(sheet, scaleFactor, printRange, fontManager, options)
|
|
|
491
828
|
// the row is tall enough for wrapped text. The stored height may be
|
|
492
829
|
// stale when columns are narrower in the PDF layout or when the PDF
|
|
493
830
|
// uses different font metrics than the original Excel file.
|
|
494
|
-
height = Math.max(row.height, autoRowHeight(row,
|
|
831
|
+
height = Math.max(row.height, autoRowHeight(row, sheet, fontManager, options));
|
|
495
832
|
}
|
|
496
833
|
else {
|
|
497
834
|
// No height info: auto-size based on cell content
|
|
498
|
-
height = autoRowHeight(row,
|
|
835
|
+
height = autoRowHeight(row, sheet, fontManager, options);
|
|
499
836
|
}
|
|
500
|
-
rowHeights.push(height
|
|
837
|
+
rowHeights.push(height);
|
|
501
838
|
visibleRows.push(r);
|
|
839
|
+
};
|
|
840
|
+
// Mirrors `computeColumnWidths`: a title band not fully inside the print area
|
|
841
|
+
// leads, otherwise the sheet's natural row order is preserved.
|
|
842
|
+
const titleFullyInside = titleBand && titleBand.first >= startRow && titleBand.last <= endRow;
|
|
843
|
+
if (titleBand && !titleFullyInside) {
|
|
844
|
+
for (let r = titleBand.first; r <= titleBand.last; r++) {
|
|
845
|
+
push(r);
|
|
846
|
+
}
|
|
502
847
|
}
|
|
503
|
-
|
|
848
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
849
|
+
push(r);
|
|
850
|
+
}
|
|
851
|
+
const repeatRowIndices = titleBand
|
|
852
|
+
? visibleRows.flatMap((r, i) => (r >= titleBand.first && r <= titleBand.last ? [i] : []))
|
|
853
|
+
: [];
|
|
854
|
+
return { rowHeights, visibleRows, repeatRowIndices };
|
|
504
855
|
}
|
|
505
856
|
/**
|
|
506
857
|
* Compute the minimum row height required to display wrapped cell content.
|
|
507
858
|
* Returns at least `DEFAULT_ROW_HEIGHT`.
|
|
508
859
|
*/
|
|
509
|
-
function autoRowHeight(row,
|
|
860
|
+
function autoRowHeight(row, sheet, fontManager, options) {
|
|
510
861
|
let height = DEFAULT_ROW_HEIGHT;
|
|
511
862
|
if (row) {
|
|
512
863
|
for (const cell of row.cells.values()) {
|
|
513
864
|
const fontSize = getCellFontSize(cell);
|
|
514
|
-
const wrapLineCount = countWrapLines(cell, fontSize,
|
|
865
|
+
const wrapLineCount = countWrapLines(cell, fontSize, sheet, fontManager, options);
|
|
515
866
|
const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
|
|
516
867
|
// Account for border width: half of each border extends inward
|
|
517
868
|
const borderTop = cell.style?.border?.top?.style
|
|
@@ -553,7 +904,7 @@ function getCellFontSize(cell) {
|
|
|
553
904
|
* Count the wrap-line count for a cell, using actual font measurements
|
|
554
905
|
* so row heights match the page renderer exactly.
|
|
555
906
|
*/
|
|
556
|
-
function countWrapLines(cell, fontSize,
|
|
907
|
+
function countWrapLines(cell, fontSize, sheet, fontManager, options) {
|
|
557
908
|
const text = typeof cell.text === "string" ? cell.text : String(cell.text ?? "");
|
|
558
909
|
const lineCount = Math.max(1, (text.match(/\n/g) ?? []).length + 1);
|
|
559
910
|
if (!cell.style?.alignment?.wrapText || text.length === 0) {
|
|
@@ -561,7 +912,7 @@ function countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options
|
|
|
561
912
|
}
|
|
562
913
|
const col = sheet.columns.get(cell.col);
|
|
563
914
|
const colWidth = col?.width ?? DEFAULT_COLUMN_WIDTH;
|
|
564
|
-
const
|
|
915
|
+
const colPts = charWidthToPixel(colWidth, MAX_DIGIT_WIDTH_PX) * PX_TO_PT;
|
|
565
916
|
const indent = cell.style.alignment.indent ?? 0;
|
|
566
917
|
const borderLeft = cell.style?.border?.left?.style
|
|
567
918
|
? borderStyleToLineWidth(cell.style.border.left.style) / 2
|
|
@@ -569,26 +920,27 @@ function countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options
|
|
|
569
920
|
const borderRight = cell.style?.border?.right?.style
|
|
570
921
|
? borderStyleToLineWidth(cell.style.border.right.style) / 2
|
|
571
922
|
: 0;
|
|
923
|
+
// Width, padding and font size are all unscaled here, which is what makes the
|
|
924
|
+
// wrapped line count independent of the print scale.
|
|
572
925
|
const padding = CELL_PADDING_H + borderLeft + (CELL_PADDING_H + borderRight) + indent * INDENT_WIDTH;
|
|
573
|
-
const effectiveWidth = Math.max(
|
|
926
|
+
const effectiveWidth = Math.max(colPts - padding, 1);
|
|
574
927
|
// For rich text cells, use per-run font size measurement to match rendering
|
|
575
928
|
if (cell.type === PdfCellType.RichText) {
|
|
576
929
|
const value = cell.value;
|
|
577
930
|
if (value && typeof value === "object" && "richText" in value) {
|
|
578
931
|
const runs = value.richText;
|
|
579
932
|
if (runs.length > 0) {
|
|
580
|
-
const wrappedCount = countRichTextWrapLines(text, runs,
|
|
933
|
+
const wrappedCount = countRichTextWrapLines(text, runs, effectiveWidth, fontManager, options, cell.style?.font);
|
|
581
934
|
return Math.max(lineCount, wrappedCount);
|
|
582
935
|
}
|
|
583
936
|
}
|
|
584
937
|
}
|
|
585
|
-
const scaledFontSize = fontSize * scaleFactor;
|
|
586
938
|
const fontProps = extractFontProperties(cell.style.font, options.defaultFontFamily, options.defaultFontSize);
|
|
587
939
|
const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
|
|
588
940
|
const resourceName = fontManager.hasEmbeddedFont()
|
|
589
941
|
? fontManager.getEmbeddedResourceName()
|
|
590
942
|
: fontManager.ensureFont(pdfFontName);
|
|
591
|
-
const measure = (s) => fontManager.measureText(s, resourceName,
|
|
943
|
+
const measure = (s) => fontManager.measureText(s, resourceName, fontSize);
|
|
592
944
|
const wrappedLines = wrapTextLines(text, measure, effectiveWidth);
|
|
593
945
|
return Math.max(lineCount, wrappedLines.length);
|
|
594
946
|
}
|
|
@@ -597,7 +949,7 @@ function countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options
|
|
|
597
949
|
* This mirrors the logic in wrapRichTextLines (page-renderer) so that
|
|
598
950
|
* the row height calculation matches the actual rendering.
|
|
599
951
|
*/
|
|
600
|
-
function countRichTextWrapLines(text, runs,
|
|
952
|
+
function countRichTextWrapLines(text, runs, effectiveWidth, fontManager, options, cellFont) {
|
|
601
953
|
// Use cell-level font as fallback for runs without their own font
|
|
602
954
|
const defaultFamily = cellFont?.name ?? options.defaultFontFamily;
|
|
603
955
|
const defaultSize = cellFont?.size ?? options.defaultFontSize;
|
|
@@ -638,7 +990,7 @@ function countRichTextWrapLines(text, runs, scaleFactor, effectiveWidth, fontMan
|
|
|
638
990
|
}
|
|
639
991
|
: cellFont;
|
|
640
992
|
const fontProps = extractFontProperties(effectiveRunFont, defaultFamily, defaultSize);
|
|
641
|
-
return fontProps.fontSize
|
|
993
|
+
return fontProps.fontSize;
|
|
642
994
|
});
|
|
643
995
|
// Measure a range of fullText using per-character run font sizes
|
|
644
996
|
const measureRange = (start, end) => {
|
|
@@ -731,24 +1083,26 @@ function countRichTextWrapLines(text, runs, scaleFactor, effectiveWidth, fontMan
|
|
|
731
1083
|
// Row Breaks
|
|
732
1084
|
// =============================================================================
|
|
733
1085
|
/**
|
|
734
|
-
*
|
|
1086
|
+
* Translate manual break positions from sheet track numbers into indices of the
|
|
1087
|
+
* printed track list.
|
|
1088
|
+
*
|
|
1089
|
+
* A break sits *after* its track, so the following index starts a new page.
|
|
1090
|
+
* Shared by both axes; computed once per sheet because the fit solver paginates
|
|
1091
|
+
* many times and must not rebuild the lookup on every probe.
|
|
735
1092
|
*/
|
|
736
|
-
function
|
|
1093
|
+
function buildBreakSet(breakTracks, visibleTracks) {
|
|
737
1094
|
const breaks = new Set();
|
|
738
|
-
|
|
739
|
-
if (rowBreaks.length === 0) {
|
|
1095
|
+
if (breakTracks.length === 0) {
|
|
740
1096
|
return breaks;
|
|
741
1097
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
rowToIndex.set(visibleRows[i], i);
|
|
1098
|
+
const trackToIndex = new Map();
|
|
1099
|
+
for (let i = 0; i < visibleTracks.length; i++) {
|
|
1100
|
+
trackToIndex.set(visibleTracks[i], i);
|
|
746
1101
|
}
|
|
747
|
-
for (const
|
|
748
|
-
const
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
breaks.add(idx + 1);
|
|
1102
|
+
for (const track of breakTracks) {
|
|
1103
|
+
const index = trackToIndex.get(track);
|
|
1104
|
+
if (index !== undefined) {
|
|
1105
|
+
breaks.add(index + 1);
|
|
752
1106
|
}
|
|
753
1107
|
}
|
|
754
1108
|
return breaks;
|
|
@@ -786,115 +1140,100 @@ function buildMergeMap(sheet) {
|
|
|
786
1140
|
// =============================================================================
|
|
787
1141
|
// Pagination
|
|
788
1142
|
// =============================================================================
|
|
789
|
-
|
|
790
|
-
|
|
1143
|
+
/**
|
|
1144
|
+
* Split a track list (rows or columns) into pages.
|
|
1145
|
+
*
|
|
1146
|
+
* One implementation serves both axes: heights against the available page
|
|
1147
|
+
* height, widths against the content width. The axes differ only in the overflow
|
|
1148
|
+
* tolerance, so `epsilon` is the sole parameter that distinguishes them —
|
|
1149
|
+
* columns need a small slack because scaled point widths accumulate rounding.
|
|
1150
|
+
*
|
|
1151
|
+
* `repeatIndices` are the tracks of a print-title band. They are re-emitted at
|
|
1152
|
+
* the start of every page after the first, which is why they are absolute
|
|
1153
|
+
* indices rather than a count: a band may sit in the middle of the printed range
|
|
1154
|
+
* (Excel allows `printTitlesColumn = "C:D"`).
|
|
1155
|
+
*
|
|
1156
|
+
* Manual breaks are honoured via `breaks`, holding the index that must start a
|
|
1157
|
+
* new page.
|
|
1158
|
+
*/
|
|
1159
|
+
function paginateTracks(sizes, available, repeatIndices, breaks, epsilon = 0) {
|
|
1160
|
+
if (sizes.length === 0) {
|
|
791
1161
|
return [[]];
|
|
792
1162
|
}
|
|
793
1163
|
const pages = [];
|
|
794
|
-
let
|
|
795
|
-
let
|
|
1164
|
+
let current = [];
|
|
1165
|
+
let used = 0;
|
|
796
1166
|
let isFirstPage = true;
|
|
797
1167
|
let repeatedPrefixCount = 0;
|
|
798
|
-
const
|
|
1168
|
+
const repeatSet = new Set(repeatIndices);
|
|
1169
|
+
const addRepeats = () => {
|
|
799
1170
|
repeatedPrefixCount = 0;
|
|
800
|
-
for (
|
|
801
|
-
if (
|
|
1171
|
+
for (const index of repeatIndices) {
|
|
1172
|
+
if (index >= sizes.length) {
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
if (used + sizes[index] > available + epsilon && current.length > 0) {
|
|
802
1176
|
break;
|
|
803
1177
|
}
|
|
804
|
-
|
|
805
|
-
|
|
1178
|
+
current.push(index);
|
|
1179
|
+
used += sizes[index];
|
|
806
1180
|
repeatedPrefixCount++;
|
|
807
1181
|
}
|
|
808
1182
|
};
|
|
809
|
-
for (let i = 0; i <
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1183
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
1184
|
+
const size = sizes[i];
|
|
1185
|
+
// A manual break at `i` must fire at most once. Without this latch the break
|
|
1186
|
+
// stays true after the repeated title tracks are re-added, so the loop keeps
|
|
1187
|
+
// flushing title-only pages forever (heap exhaustion).
|
|
1188
|
+
let breakConsumed = false;
|
|
1189
|
+
for (;;) {
|
|
1190
|
+
const forceBreak = !breakConsumed && breaks.has(i) && current.length > 0;
|
|
1191
|
+
if ((forceBreak || used + size > available + epsilon) && current.length > 0) {
|
|
1192
|
+
if (forceBreak) {
|
|
1193
|
+
breakConsumed = true;
|
|
1194
|
+
}
|
|
1195
|
+
// Never emit a page holding nothing but the repeated prefix.
|
|
1196
|
+
if (!forceBreak && !isFirstPage && current.length === repeatedPrefixCount) {
|
|
1197
|
+
current = [];
|
|
1198
|
+
used = 0;
|
|
824
1199
|
repeatedPrefixCount = 0;
|
|
825
1200
|
continue;
|
|
826
1201
|
}
|
|
827
|
-
pages.push(
|
|
828
|
-
|
|
829
|
-
|
|
1202
|
+
pages.push(current);
|
|
1203
|
+
current = [];
|
|
1204
|
+
used = 0;
|
|
830
1205
|
repeatedPrefixCount = 0;
|
|
831
1206
|
isFirstPage = false;
|
|
832
|
-
|
|
1207
|
+
addRepeats();
|
|
833
1208
|
continue;
|
|
834
1209
|
}
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
|
|
1210
|
+
if (isFirstPage || !repeatSet.has(i) || !current.includes(i)) {
|
|
1211
|
+
current.push(i);
|
|
1212
|
+
used += size;
|
|
838
1213
|
}
|
|
839
|
-
currentPage.push(i);
|
|
840
|
-
currentPageHeight += rowHeight;
|
|
841
1214
|
break;
|
|
842
1215
|
}
|
|
843
|
-
if (skipRepeatedRow) {
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
1216
|
}
|
|
847
|
-
if (
|
|
848
|
-
pages.push(
|
|
1217
|
+
if (current.length > 0) {
|
|
1218
|
+
pages.push(current);
|
|
849
1219
|
}
|
|
850
|
-
return pages
|
|
1220
|
+
return pages;
|
|
851
1221
|
}
|
|
852
1222
|
/**
|
|
853
|
-
*
|
|
1223
|
+
* Row pagination. Thin wrapper over {@link paginateTracks} that also accepts a
|
|
1224
|
+
* plain count, which reads better in the focused unit tests.
|
|
854
1225
|
*/
|
|
855
|
-
function
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const colBreaks = new Set();
|
|
861
|
-
const wsColBreaks = sheet.colBreaks ?? [];
|
|
862
|
-
if (wsColBreaks.length > 0) {
|
|
863
|
-
const colToIndex = new Map();
|
|
864
|
-
for (let i = 0; i < visibleCols.length; i++) {
|
|
865
|
-
colToIndex.set(visibleCols[i], i);
|
|
866
|
-
}
|
|
867
|
-
for (const brk of wsColBreaks) {
|
|
868
|
-
const idx = colToIndex.get(brk);
|
|
869
|
-
if (idx !== undefined) {
|
|
870
|
-
colBreaks.add(idx + 1);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
const groups = [];
|
|
875
|
-
let currentGroup = [];
|
|
876
|
-
let currentWidth = 0;
|
|
877
|
-
for (let i = 0; i < columnWidths.length; i++) {
|
|
878
|
-
const colWidth = columnWidths[i];
|
|
879
|
-
const forceBreak = colBreaks.has(i) && currentGroup.length > 0;
|
|
880
|
-
if ((forceBreak || currentWidth + colWidth > contentWidth + 0.01) && currentGroup.length > 0) {
|
|
881
|
-
groups.push(currentGroup);
|
|
882
|
-
currentGroup = [];
|
|
883
|
-
currentWidth = 0;
|
|
884
|
-
}
|
|
885
|
-
currentGroup.push(i);
|
|
886
|
-
currentWidth += colWidth;
|
|
887
|
-
}
|
|
888
|
-
if (currentGroup.length > 0) {
|
|
889
|
-
groups.push(currentGroup);
|
|
890
|
-
}
|
|
891
|
-
return groups.length > 0 ? groups : [Array.from({ length: columnWidths.length }, (_, i) => i)];
|
|
1226
|
+
export function paginateRows(rowHeights, availableHeight, repeatRows, rowBreaks) {
|
|
1227
|
+
const repeatIndices = Array.isArray(repeatRows)
|
|
1228
|
+
? repeatRows
|
|
1229
|
+
: Array.from({ length: Math.max(0, repeatRows) }, (_, i) => i);
|
|
1230
|
+
return paginateTracks(rowHeights, availableHeight, repeatIndices, rowBreaks);
|
|
892
1231
|
}
|
|
893
1232
|
// =============================================================================
|
|
894
1233
|
// Cell Layout
|
|
895
1234
|
// =============================================================================
|
|
896
1235
|
function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, fontManager, scaleFactor) {
|
|
897
|
-
const text = cell
|
|
1236
|
+
const text = resolveErrorText(cell, options.errors);
|
|
898
1237
|
const style = cell?.style ?? {};
|
|
899
1238
|
const fontProps = extractFontProperties(style.font, options.defaultFontFamily, options.defaultFontSize);
|
|
900
1239
|
// Scale font size proportionally when fitToPage shrinks the layout
|
|
@@ -913,7 +1252,9 @@ function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, f
|
|
|
913
1252
|
// their own font definition (e.g. the first run often has no font object
|
|
914
1253
|
// and should inherit the cell's style font including bold/italic).
|
|
915
1254
|
const richText = buildRichTextRuns(cell, options, fontManager, scaleFactor, style.font);
|
|
916
|
-
const
|
|
1255
|
+
const rawBorders = excelBordersToPdf(style.border);
|
|
1256
|
+
const borders = options.blackAndWhite ? grayscaleBorders(rawBorders) : rawBorders;
|
|
1257
|
+
const rawFill = excelFillToPdfColor(style.fill);
|
|
917
1258
|
return {
|
|
918
1259
|
text,
|
|
919
1260
|
rect: { x, y, width, height },
|
|
@@ -923,8 +1264,8 @@ function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, f
|
|
|
923
1264
|
italic: fontProps.italic,
|
|
924
1265
|
strike: fontProps.strike,
|
|
925
1266
|
underline: fontProps.underline,
|
|
926
|
-
textColor: fontProps.textColor,
|
|
927
|
-
fillColor:
|
|
1267
|
+
textColor: options.blackAndWhite ? toGrayscale(fontProps.textColor) : fontProps.textColor,
|
|
1268
|
+
fillColor: options.blackAndWhite && rawFill !== null ? toGrayscale(rawFill) : rawFill,
|
|
928
1269
|
horizontalAlign: resolveHorizontalAlign(style.alignment, cell?.type, cell?.result),
|
|
929
1270
|
verticalAlign: excelVAlignToPdf(style.alignment),
|
|
930
1271
|
wrapText: style.alignment?.wrapText ?? false,
|
|
@@ -1035,7 +1376,7 @@ export function resolveSharedBorders(cellGrid, rowCount, colCount) {
|
|
|
1035
1376
|
* - `br` — when present — locates the opposite corner, so the rect size is
|
|
1036
1377
|
* the difference.
|
|
1037
1378
|
* - `ext` — when present — overrides the size directly. Images use pixels
|
|
1038
|
-
* (px × 0.75 = pt); charts use EMU (EMU /
|
|
1379
|
+
* (px × 0.75 = pt); charts use EMU (EMU / 12700 = pt). The `extUnit`
|
|
1039
1380
|
* field disambiguates. Historical callers that omit `extUnit` keep
|
|
1040
1381
|
* the legacy px behaviour.
|
|
1041
1382
|
*
|
|
@@ -1056,7 +1397,7 @@ function resolveAnchorRect(range, layoutPages, scaleFactor) {
|
|
|
1056
1397
|
const baseY = targetPage.rowYPositions[pageRowIndex] ??
|
|
1057
1398
|
targetPage.height -
|
|
1058
1399
|
targetPage.options.margins.top -
|
|
1059
|
-
(targetPage.options.showSheetNames ?
|
|
1400
|
+
(targetPage.options.showSheetNames ? SHEET_NAME_BAND_HEIGHT : 0);
|
|
1060
1401
|
// Apply sub-cell offsets, scaled to match page layout.
|
|
1061
1402
|
const tlColOff = (emuToPt(tl.nativeColOff ?? 0) || 0) * scaleFactor;
|
|
1062
1403
|
const tlRowOff = (emuToPt(tl.nativeRowOff ?? 0) || 0) * scaleFactor;
|
|
@@ -1068,9 +1409,11 @@ function resolveAnchorRect(range, layoutPages, scaleFactor) {
|
|
|
1068
1409
|
const extUnit = range.extUnit ?? "px";
|
|
1069
1410
|
if (range.ext) {
|
|
1070
1411
|
if (extUnit === "emu") {
|
|
1071
|
-
// EMU →
|
|
1072
|
-
|
|
1073
|
-
|
|
1412
|
+
// EMU → pt (÷12700). Using the px factor (÷9525) here rendered every
|
|
1413
|
+
// EMU-sized chart 4/3 too large: a 4in chart came out 384pt instead of
|
|
1414
|
+
// 288pt, overflowing the content area and skewing its aspect ratio.
|
|
1415
|
+
width = emuToPt(range.ext.width) * scaleFactor;
|
|
1416
|
+
height = emuToPt(range.ext.height) * scaleFactor;
|
|
1074
1417
|
}
|
|
1075
1418
|
else {
|
|
1076
1419
|
// Legacy pixel → pt (0.75 factor = 72/96 dpi)
|
|
@@ -1227,13 +1570,20 @@ function assignChartsToPages(charts, layoutPages, scaleFactor) {
|
|
|
1227
1570
|
// is near a page break: rather than clipping them to a sliver, we
|
|
1228
1571
|
// push them onto the following page at full size.
|
|
1229
1572
|
const tl = chart.range.tl;
|
|
1573
|
+
const tlCol = (tl.nativeCol ?? tl.col ?? 0) + 1;
|
|
1230
1574
|
const tlRow = (tl.nativeRow ?? tl.row ?? 0) + 1;
|
|
1575
|
+
// Restrict the search to the column band that holds the anchor, then walk
|
|
1576
|
+
// it in row order. Filtering before stepping keeps the result independent
|
|
1577
|
+
// of `pageOrder`: that setting decides the array order, but "the next page
|
|
1578
|
+
// down" is always the next row band within the same column band.
|
|
1579
|
+
const band = layoutPages.filter(page => page.sheetCols.includes(tlCol));
|
|
1580
|
+
const ordered = (band.length > 0 ? band : [...layoutPages]).sort((a, b) => (a.sheetRows[0] ?? 0) - (b.sheetRows[0] ?? 0));
|
|
1231
1581
|
let targetPage;
|
|
1232
|
-
for (let pi = 0; pi <
|
|
1233
|
-
const page =
|
|
1582
|
+
for (let pi = 0; pi < ordered.length; pi++) {
|
|
1583
|
+
const page = ordered[pi];
|
|
1234
1584
|
const lastPageRow = page.sheetRows[page.sheetRows.length - 1] ?? 0;
|
|
1235
|
-
if (lastPageRow >= tlRow - 1 && pi + 1 <
|
|
1236
|
-
targetPage =
|
|
1585
|
+
if (lastPageRow >= tlRow - 1 && pi + 1 < ordered.length) {
|
|
1586
|
+
targetPage = ordered[pi + 1];
|
|
1237
1587
|
break;
|
|
1238
1588
|
}
|
|
1239
1589
|
if (lastPageRow >= tlRow) {
|
|
@@ -1242,11 +1592,11 @@ function assignChartsToPages(charts, layoutPages, scaleFactor) {
|
|
|
1242
1592
|
}
|
|
1243
1593
|
}
|
|
1244
1594
|
if (!targetPage) {
|
|
1245
|
-
targetPage =
|
|
1595
|
+
targetPage = ordered[ordered.length - 1];
|
|
1246
1596
|
}
|
|
1247
1597
|
if (targetPage) {
|
|
1248
1598
|
const margins = targetPage.options.margins;
|
|
1249
|
-
const headerH = targetPage.options.showSheetNames ?
|
|
1599
|
+
const headerH = targetPage.options.showSheetNames ? SHEET_NAME_BAND_HEIGHT : 0;
|
|
1250
1600
|
const contentX = margins.left;
|
|
1251
1601
|
const contentY = margins.bottom;
|
|
1252
1602
|
const contentW = targetPage.width - margins.left - margins.right;
|
|
@@ -1441,7 +1791,38 @@ function buildRichTextRuns(cell, options, fontManager, scaleFactor, cellFont) {
|
|
|
1441
1791
|
italic: fontProps.italic,
|
|
1442
1792
|
strike: fontProps.strike,
|
|
1443
1793
|
underline: fontProps.underline,
|
|
1444
|
-
textColor: fontProps.textColor
|
|
1794
|
+
textColor: options.blackAndWhite ? toGrayscale(fontProps.textColor) : fontProps.textColor
|
|
1445
1795
|
};
|
|
1446
1796
|
});
|
|
1447
1797
|
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Apply Excel's "Cell errors as" print option to a cell's display text.
|
|
1800
|
+
*
|
|
1801
|
+
* Error values reach the PDF model in two shapes: a plain error cell
|
|
1802
|
+
* (`PdfCellType.Error`) and a formula whose computed result is an error
|
|
1803
|
+
* (`PdfCellType.Formula` with an `{ error }` result). Both must be substituted.
|
|
1804
|
+
*/
|
|
1805
|
+
function resolveErrorText(cell, mode) {
|
|
1806
|
+
const text = cell?.text ?? "";
|
|
1807
|
+
if (mode === "displayed" || !cell) {
|
|
1808
|
+
return text;
|
|
1809
|
+
}
|
|
1810
|
+
const isError = cell.type === PdfCellType.Error ||
|
|
1811
|
+
(cell.type === PdfCellType.Formula &&
|
|
1812
|
+
typeof cell.result === "object" &&
|
|
1813
|
+
cell.result !== null &&
|
|
1814
|
+
"error" in cell.result);
|
|
1815
|
+
if (!isError) {
|
|
1816
|
+
return text;
|
|
1817
|
+
}
|
|
1818
|
+
switch (mode) {
|
|
1819
|
+
case "blank":
|
|
1820
|
+
return "";
|
|
1821
|
+
case "dash":
|
|
1822
|
+
return "--";
|
|
1823
|
+
case "NA":
|
|
1824
|
+
return "#N/A";
|
|
1825
|
+
default:
|
|
1826
|
+
return text;
|
|
1827
|
+
}
|
|
1828
|
+
}
|