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