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
|
@@ -36,4 +36,4 @@ export declare function parseJpegDimensions(data: Uint8Array): {
|
|
|
36
36
|
* Write an image XObject (JPEG or PNG) to the writer.
|
|
37
37
|
* Returns the allocated object number.
|
|
38
38
|
*/
|
|
39
|
-
export declare function writeImageXObject(writer: PdfWriter, data: Uint8Array, format: "jpeg" | "png"): number;
|
|
39
|
+
export declare function writeImageXObject(writer: PdfWriter, data: Uint8Array, format: "jpeg" | "png", grayscale?: boolean): number;
|
|
@@ -76,41 +76,90 @@ export function parseJpegDimensions(data) {
|
|
|
76
76
|
* Write an image XObject (JPEG or PNG) to the writer.
|
|
77
77
|
* Returns the allocated object number.
|
|
78
78
|
*/
|
|
79
|
-
export function writeImageXObject(writer, data, format) {
|
|
79
|
+
export function writeImageXObject(writer, data, format, grayscale = false) {
|
|
80
80
|
if (format === "png") {
|
|
81
|
-
return writePngImageXObject(writer, data);
|
|
81
|
+
return writePngImageXObject(writer, data, grayscale);
|
|
82
82
|
}
|
|
83
|
-
return writeJpegImageXObject(writer, data);
|
|
83
|
+
return writeJpegImageXObject(writer, data, grayscale);
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* Write
|
|
86
|
+
* Write the PostScript calculator function that converts an RGB triple to its
|
|
87
|
+
* Rec. 601 luma, and return its object number.
|
|
88
|
+
*
|
|
89
|
+
* Used as the tint transform of a `/DeviceN` space so a JPEG can be recoloured
|
|
90
|
+
* without decoding it: the samples stay untouched and the consumer applies the
|
|
91
|
+
* transform per pixel. Unlike painting a blend on top, this cannot darken
|
|
92
|
+
* transparent areas, because it *is* the color space rather than an overlay.
|
|
87
93
|
*/
|
|
88
|
-
function
|
|
94
|
+
function writeLumaTintTransform(writer) {
|
|
89
95
|
const objNum = writer.allocObject();
|
|
96
|
+
const dict = new PdfDict()
|
|
97
|
+
.set("FunctionType", "4")
|
|
98
|
+
.set("Domain", "[0 1 0 1 0 1]")
|
|
99
|
+
.set("Range", "[0 1]");
|
|
100
|
+
// Stack on entry is R G B (B on top):
|
|
101
|
+
// 0.114 mul -> R G 0.114B
|
|
102
|
+
// exch -> R 0.114B G
|
|
103
|
+
// 0.587 mul -> R 0.114B 0.587G
|
|
104
|
+
// add -> R (0.114B + 0.587G)
|
|
105
|
+
// exch -> (0.114B + 0.587G) R
|
|
106
|
+
// 0.299 mul -> (0.114B + 0.587G) 0.299R
|
|
107
|
+
// add -> luma
|
|
108
|
+
const program = "{ 0.114 mul exch 0.587 mul add exch 0.299 mul add }";
|
|
109
|
+
writer.addStreamObject(objNum, dict, new TextEncoder().encode(program));
|
|
110
|
+
return objNum;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Write a JPEG image using DCTDecode (raw JPEG data embedded directly).
|
|
114
|
+
*/
|
|
115
|
+
function writeJpegImageXObject(writer, data, grayscale = false) {
|
|
90
116
|
const dims = parseJpegDimensions(data);
|
|
117
|
+
// Grayscale without a JPEG decoder: keep the DCTDecode samples and reinterpret
|
|
118
|
+
// the three components through a `/DeviceN` space whose tint transform is the
|
|
119
|
+
// luma formula. Component count still matches the JPEG, as PDF requires.
|
|
120
|
+
const tintRef = grayscale ? writeLumaTintTransform(writer) : undefined;
|
|
121
|
+
const objNum = writer.allocObject();
|
|
91
122
|
const dict = new PdfDict()
|
|
92
123
|
.set("Type", "/XObject")
|
|
93
124
|
.set("Subtype", "/Image")
|
|
94
125
|
.set("Width", pdfNumber(dims.width))
|
|
95
126
|
.set("Height", pdfNumber(dims.height))
|
|
96
|
-
.set("ColorSpace",
|
|
127
|
+
.set("ColorSpace", tintRef === undefined
|
|
128
|
+
? "/DeviceRGB"
|
|
129
|
+
: `[/DeviceN [/C1 /C2 /C3] /DeviceGray ${pdfRef(tintRef)}]`)
|
|
97
130
|
.set("BitsPerComponent", "8")
|
|
98
131
|
.set("Filter", "/DCTDecode");
|
|
99
132
|
writer.addStreamObject(objNum, dict, data);
|
|
100
133
|
return objNum;
|
|
101
134
|
}
|
|
135
|
+
/** Rec. 601 luma of an 8-bit RGB triple. */
|
|
136
|
+
function lumaByte(r, g, b) {
|
|
137
|
+
return Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
|
138
|
+
}
|
|
102
139
|
/**
|
|
103
140
|
* Write a PNG image: decode to raw RGB, create SMask for alpha if needed.
|
|
104
141
|
*/
|
|
105
|
-
function writePngImageXObject(writer, data) {
|
|
142
|
+
function writePngImageXObject(writer, data, grayscale = false) {
|
|
106
143
|
const png = decodePng(data);
|
|
107
144
|
const objNum = writer.allocObject();
|
|
145
|
+
// PNG is already decoded to raw samples here, so grayscale is a real pixel
|
|
146
|
+
// conversion: collapse RGB to one luma component and switch to DeviceGray.
|
|
147
|
+
// The alpha channel lives in a separate SMask and is untouched, so transparent
|
|
148
|
+
// regions stay transparent.
|
|
149
|
+
let pixels = png.pixels;
|
|
150
|
+
if (grayscale) {
|
|
151
|
+
const gray = new Uint8Array(png.width * png.height);
|
|
152
|
+
for (let i = 0, p = 0; i < gray.length; i++, p += 3) {
|
|
153
|
+
gray[i] = lumaByte(pixels[p], pixels[p + 1], pixels[p + 2]);
|
|
154
|
+
}
|
|
155
|
+
pixels = gray;
|
|
156
|
+
}
|
|
108
157
|
const dict = new PdfDict()
|
|
109
158
|
.set("Type", "/XObject")
|
|
110
159
|
.set("Subtype", "/Image")
|
|
111
160
|
.set("Width", pdfNumber(png.width))
|
|
112
161
|
.set("Height", pdfNumber(png.height))
|
|
113
|
-
.set("ColorSpace", "/DeviceRGB")
|
|
162
|
+
.set("ColorSpace", grayscale ? "/DeviceGray" : "/DeviceRGB")
|
|
114
163
|
.set("BitsPerComponent", pdfNumber(png.bitsPerComponent));
|
|
115
164
|
if (png.alpha) {
|
|
116
165
|
const smaskObjNum = writer.allocObject();
|
|
@@ -124,6 +173,6 @@ function writePngImageXObject(writer, data) {
|
|
|
124
173
|
writer.addStreamObject(smaskObjNum, smaskDict, png.alpha);
|
|
125
174
|
dict.set("SMask", pdfRef(smaskObjNum));
|
|
126
175
|
}
|
|
127
|
-
writer.addStreamObject(objNum, dict,
|
|
176
|
+
writer.addStreamObject(objNum, dict, pixels);
|
|
128
177
|
return objNum;
|
|
129
178
|
}
|
|
@@ -100,6 +100,14 @@ export declare class PdfWriter {
|
|
|
100
100
|
contentsRef: number | string;
|
|
101
101
|
resourcesRef: number;
|
|
102
102
|
annotRefs?: number[];
|
|
103
|
+
/**
|
|
104
|
+
* Declare a transparency group for the page. Required whenever the content
|
|
105
|
+
* uses transparency features (constant alpha, soft masks, blend modes):
|
|
106
|
+
* without it the backdrop a blend composites against is implementation
|
|
107
|
+
* defined, and a consumer that ignores the blend mode can paint a
|
|
108
|
+
* `/Saturation` overlay as opaque black.
|
|
109
|
+
*/
|
|
110
|
+
transparencyGroup?: boolean;
|
|
103
111
|
}): number;
|
|
104
112
|
/**
|
|
105
113
|
* Create and add the Catalog dictionary.
|
|
@@ -164,6 +164,9 @@ export class PdfWriter {
|
|
|
164
164
|
.set("MediaBox", mediaBox)
|
|
165
165
|
.set("Contents", contentsValue)
|
|
166
166
|
.set("Resources", pdfRef(options.resourcesRef));
|
|
167
|
+
if (options.transparencyGroup) {
|
|
168
|
+
dict.set("Group", "<< /S /Transparency /CS /DeviceRGB /I false /K false >>");
|
|
169
|
+
}
|
|
167
170
|
if (options.annotRefs && options.annotRefs.length > 0) {
|
|
168
171
|
dict.set("Annots", "[" + options.annotRefs.map(r => pdfRef(r)).join(" ") + "]");
|
|
169
172
|
}
|
|
@@ -27,7 +27,7 @@ import { chartChartExModel, chartChartModel } from "../excel/chart/chart-handle.
|
|
|
27
27
|
import { canRenderChartExAsVectorPdf, drawChartExPdf, renderChartExPng } from "../excel/chart/render/chart-ex-renderer.js";
|
|
28
28
|
import { drawChartPdf, renderChartPng } from "../excel/chart/render/chart-renderer.js";
|
|
29
29
|
import { anchorCol, anchorRow } from "../excel/core/anchor.js";
|
|
30
|
-
import { cellCol, cellGetValue, cellHyperlink, cellResult, cellText, cellType, mergeCellStyle } from "../excel/core/cell.js";
|
|
30
|
+
import { cellCol, cellComment, cellGetValue, cellHyperlink, cellResult, cellText, cellType, mergeCellStyle } from "../excel/core/cell.js";
|
|
31
31
|
import { chartsheetChartExModel, chartsheetChartModel, chartsheetModel, chartsheetName, chartsheetPageSetup, chartsheetPageMargins, chartsheetState } from "../excel/core/chartsheet.js";
|
|
32
32
|
import { ValueType } from "../excel/core/enums.js";
|
|
33
33
|
import { computeSparklineGeometry } from "../excel/core/sparkline.js";
|
|
@@ -62,7 +62,7 @@ export async function excelToPdf(workbook, options) {
|
|
|
62
62
|
// shipped with. This keeps the ~200 KB engine out of bundles that only
|
|
63
63
|
// export already-computed workbooks — no host-registry needed.
|
|
64
64
|
options?.recalculate?.(workbook);
|
|
65
|
-
const pdfWorkbook = await excelWorkbookToPdf(workbook);
|
|
65
|
+
const pdfWorkbook = await excelWorkbookToPdf(workbook, options);
|
|
66
66
|
return exportPdf(pdfWorkbook, options);
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
@@ -174,9 +174,9 @@ export async function chartToPdf(chart, options = {}) {
|
|
|
174
174
|
* Chartsheets without an orderNo fall to the end, mirroring how Excel
|
|
175
175
|
* treats sheets with missing tab positions.
|
|
176
176
|
*/
|
|
177
|
-
async function excelWorkbookToPdf(workbook) {
|
|
178
|
-
const worksheetResults = await Promise.all(getWorksheets(workbook).map(ws => convertSheet(ws, workbook)));
|
|
179
|
-
const chartsheetResults = await Promise.all(getChartsheets(workbook).map(cs => convertChartsheet(cs, workbook)));
|
|
177
|
+
async function excelWorkbookToPdf(workbook, options) {
|
|
178
|
+
const worksheetResults = await Promise.all(getWorksheets(workbook).map(ws => convertSheet(ws, workbook, options)));
|
|
179
|
+
const chartsheetResults = await Promise.all(getChartsheets(workbook).map(cs => convertChartsheet(cs, workbook, options)));
|
|
180
180
|
const combined = [...worksheetResults, ...chartsheetResults];
|
|
181
181
|
combined.sort((a, b) => (a.orderNo ?? Number.POSITIVE_INFINITY) - (b.orderNo ?? Number.POSITIVE_INFINITY));
|
|
182
182
|
return {
|
|
@@ -192,7 +192,7 @@ async function excelWorkbookToPdf(workbook) {
|
|
|
192
192
|
// =============================================================================
|
|
193
193
|
// Sheet Conversion
|
|
194
194
|
// =============================================================================
|
|
195
|
-
async function convertSheet(ws, workbook) {
|
|
195
|
+
async function convertSheet(ws, workbook, options) {
|
|
196
196
|
const dimensions = getSheetDimensions(ws);
|
|
197
197
|
const hasData = dimensions && dimensions.top > 0 && dimensions.left > 0;
|
|
198
198
|
const bounds = hasData
|
|
@@ -312,10 +312,22 @@ async function convertSheet(ws, workbook) {
|
|
|
312
312
|
}
|
|
313
313
|
: undefined,
|
|
314
314
|
scale: ps.scale,
|
|
315
|
+
fitToPage: ps.fitToPage,
|
|
316
|
+
fitToWidth: ps.fitToWidth,
|
|
317
|
+
fitToHeight: ps.fitToHeight,
|
|
315
318
|
printTitlesRow: ps.printTitlesRow,
|
|
319
|
+
printTitlesColumn: ps.printTitlesColumn,
|
|
316
320
|
showGridLines: ps.showGridLines,
|
|
321
|
+
showRowColHeaders: ps.showRowColHeaders,
|
|
317
322
|
printArea: ps.printArea,
|
|
318
|
-
firstPageNumber: ps.useFirstPageNumber === false ? undefined : ps.firstPageNumber
|
|
323
|
+
firstPageNumber: ps.useFirstPageNumber === false ? undefined : ps.firstPageNumber,
|
|
324
|
+
pageOrder: ps.pageOrder,
|
|
325
|
+
blackAndWhite: ps.blackAndWhite,
|
|
326
|
+
draft: ps.draft,
|
|
327
|
+
errors: ps.errors,
|
|
328
|
+
cellComments: ps.cellComments,
|
|
329
|
+
horizontalCentered: ps.horizontalCentered,
|
|
330
|
+
verticalCentered: ps.verticalCentered
|
|
319
331
|
}
|
|
320
332
|
: undefined;
|
|
321
333
|
// Convert row/col breaks
|
|
@@ -324,9 +336,18 @@ async function convertSheet(ws, workbook) {
|
|
|
324
336
|
// Convert images and charts. Both are floating objects anchored to
|
|
325
337
|
// cells, and both need to participate in bounds expansion so the
|
|
326
338
|
// layout engine allocates pages that cover their anchor rows/cols.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
339
|
+
//
|
|
340
|
+
// Draft quality prints no graphics, so skip collection entirely rather than
|
|
341
|
+
// gathering (and rasterizing ChartEx layouts) only to drop the result during
|
|
342
|
+
// layout: draft exists to be cheap, and a rasterizer failure must not sink an
|
|
343
|
+
// export that would not have shown the graphic anyway. The precedence here
|
|
344
|
+
// mirrors `resolveOptions`; the layout engine still gates on the resolved
|
|
345
|
+
// flag, so this stays a pure optimization.
|
|
346
|
+
const comments = collectComments(ws);
|
|
347
|
+
const draftQuality = options?.draft ?? ws.pageSetup?.draft ?? false;
|
|
348
|
+
const images = draftQuality ? undefined : collectImages(ws, workbook);
|
|
349
|
+
const charts = draftQuality ? undefined : await collectCharts(ws);
|
|
350
|
+
const sparklineCharts = draftQuality ? undefined : collectSparklineCharts(ws);
|
|
330
351
|
// Merge sparkline micro-charts with regular charts
|
|
331
352
|
const allCharts = charts
|
|
332
353
|
? sparklineCharts
|
|
@@ -398,14 +419,91 @@ async function convertSheet(ws, workbook) {
|
|
|
398
419
|
rows,
|
|
399
420
|
merges,
|
|
400
421
|
pageSetup,
|
|
401
|
-
headerFooter: convertHeaderFooter(ws, workbook),
|
|
422
|
+
headerFooter: convertHeaderFooter(ws, workbook, draftQuality),
|
|
402
423
|
rowBreaks,
|
|
403
424
|
colBreaks,
|
|
404
425
|
images,
|
|
405
|
-
charts: allCharts
|
|
426
|
+
charts: allCharts,
|
|
427
|
+
comments
|
|
406
428
|
};
|
|
407
429
|
}
|
|
408
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Collect every comment on a worksheet, in row-major order.
|
|
432
|
+
*
|
|
433
|
+
* Two sources are merged: classic notes stored on the cell (`_comment`, the
|
|
434
|
+
* VML-backed kind) and Office 365 threaded comments held on the sheet. Only the
|
|
435
|
+
* text and author are needed — the printed form is an end-of-sheet list, not a
|
|
436
|
+
* positioned box — so the VML geometry is deliberately ignored.
|
|
437
|
+
*/
|
|
438
|
+
function collectComments(ws) {
|
|
439
|
+
const collected = [];
|
|
440
|
+
for (const row of ws._rows ?? []) {
|
|
441
|
+
if (!row) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
for (const cell of row.cells ?? []) {
|
|
445
|
+
if (!cell) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const note = cellComment(cell);
|
|
449
|
+
const text = noteText(note?.note);
|
|
450
|
+
if (text) {
|
|
451
|
+
collected.push({
|
|
452
|
+
ref: cell.address,
|
|
453
|
+
text,
|
|
454
|
+
author: note?.author,
|
|
455
|
+
anchor: parseVmlAnchor(note?.note)
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const entry of ws.threadedComments ?? []) {
|
|
461
|
+
const text = entry.comment.text?.trim();
|
|
462
|
+
if (text) {
|
|
463
|
+
collected.push({ ref: entry.ref, text, author: entry.comment.personId });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return collected.length > 0 ? collected : undefined;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Decode a VML comment anchor into fractional sheet coordinates.
|
|
470
|
+
*
|
|
471
|
+
* `<x:Anchor>` holds eight integers: for each edge a column/row index followed
|
|
472
|
+
* by an offset into it, measured in 1/68 of a column and 1/18 of a row (the
|
|
473
|
+
* reciprocals Excel uses when it writes the value). Returns `undefined` when the
|
|
474
|
+
* note has no anchor, so callers fall back to Excel's default placement.
|
|
475
|
+
*/
|
|
476
|
+
function parseVmlAnchor(note) {
|
|
477
|
+
if (!note || typeof note === "string" || typeof note.anchor !== "string") {
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
const parts = note.anchor.split(",").map(p => Number(p.trim()));
|
|
481
|
+
if (parts.length !== 8 || parts.some(n => !Number.isFinite(n))) {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
const [l, lf, t, tf, r, rf, b, bf] = parts;
|
|
485
|
+
return {
|
|
486
|
+
left: l + lf / 68,
|
|
487
|
+
top: t + tf / 18,
|
|
488
|
+
right: r + rf / 68,
|
|
489
|
+
bottom: b + bf / 18
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/** Flatten a note body, which may be plain text or a rich `NoteConfig`. */
|
|
493
|
+
function noteText(note) {
|
|
494
|
+
if (typeof note === "string") {
|
|
495
|
+
return note.trim() || undefined;
|
|
496
|
+
}
|
|
497
|
+
if (note && typeof note === "object" && Array.isArray(note.texts)) {
|
|
498
|
+
const joined = note.texts
|
|
499
|
+
.map(t => (typeof t === "string" ? t : (t.text ?? "")))
|
|
500
|
+
.join("")
|
|
501
|
+
.trim();
|
|
502
|
+
return joined || undefined;
|
|
503
|
+
}
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
function convertHeaderFooter(ws, workbook, draftQuality = false) {
|
|
409
507
|
const source = ws.headerFooter;
|
|
410
508
|
const fields = [
|
|
411
509
|
source.oddHeader,
|
|
@@ -415,7 +513,9 @@ function convertHeaderFooter(ws, workbook) {
|
|
|
415
513
|
source.firstHeader,
|
|
416
514
|
source.firstFooter
|
|
417
515
|
];
|
|
418
|
-
|
|
516
|
+
// Draft quality prints no graphics, header pictures included, so skip
|
|
517
|
+
// collecting them rather than embedding XObjects nothing will draw.
|
|
518
|
+
const images = draftQuality ? [] : convertHeaderFooterImages(ws, workbook);
|
|
419
519
|
if (!fields.some(Boolean) && images.length === 0) {
|
|
420
520
|
return undefined;
|
|
421
521
|
}
|
|
@@ -1254,7 +1354,7 @@ function chartAnchorRange(chart) {
|
|
|
1254
1354
|
: undefined,
|
|
1255
1355
|
// Chart anchors store ext as EMU (`cx`, `cy`). Pass the values
|
|
1256
1356
|
// through unchanged; the layout engine converts EMU→pt for charts
|
|
1257
|
-
// (
|
|
1357
|
+
// (÷12700) and px→pt for images (×0.75) based on `extUnit`.
|
|
1258
1358
|
ext: r.ext ? { width: r.ext.cx, height: r.ext.cy } : undefined,
|
|
1259
1359
|
extUnit: r.ext ? "emu" : undefined
|
|
1260
1360
|
};
|
|
@@ -1338,11 +1438,16 @@ const CHARTSHEET_RASTER_PX = { width: 1280, height: 720 };
|
|
|
1338
1438
|
* (matches what Excel prints for a chartsheet whose chart was deleted
|
|
1339
1439
|
* but the sheet kept).
|
|
1340
1440
|
*/
|
|
1341
|
-
async function convertChartsheet(cs, workbook) {
|
|
1441
|
+
async function convertChartsheet(cs, workbook, options) {
|
|
1342
1442
|
const classicModel = chartsheetChartModel(cs);
|
|
1343
1443
|
const chartExModel = chartsheetChartExModel(cs);
|
|
1444
|
+
// Resolve draft before touching the chart: a ChartEx outside the vector
|
|
1445
|
+
// whitelist would otherwise be rasterised at cost — and could fail — for a
|
|
1446
|
+
// page that prints no graphics at all. Precedence mirrors `resolveOptions`.
|
|
1447
|
+
const draftQuality = options?.draft ?? chartsheetPageSetup(cs)?.draft ?? false;
|
|
1448
|
+
// Draft quality prints no graphics, so the chart is simply never built.
|
|
1344
1449
|
let chart = {};
|
|
1345
|
-
if (classicModel) {
|
|
1450
|
+
if (!draftQuality && classicModel) {
|
|
1346
1451
|
const model = classicModel;
|
|
1347
1452
|
chart = {
|
|
1348
1453
|
drawVector: (surface, rect) => {
|
|
@@ -1355,7 +1460,7 @@ async function convertChartsheet(cs, workbook) {
|
|
|
1355
1460
|
}
|
|
1356
1461
|
};
|
|
1357
1462
|
}
|
|
1358
|
-
else if (chartExModel) {
|
|
1463
|
+
else if (!draftQuality && chartExModel) {
|
|
1359
1464
|
if (canRenderChartExAsVectorPdf(chartExModel)) {
|
|
1360
1465
|
const model = chartExModel;
|
|
1361
1466
|
chart = {
|
|
@@ -1396,6 +1501,8 @@ async function convertChartsheet(cs, workbook) {
|
|
|
1396
1501
|
orientation: ps?.orientation,
|
|
1397
1502
|
paperSize: ps?.paperSize,
|
|
1398
1503
|
showGridLines: false,
|
|
1504
|
+
blackAndWhite: ps?.blackAndWhite,
|
|
1505
|
+
draft: ps?.draft,
|
|
1399
1506
|
firstPageNumber: ps?.useFirstPageNumber === false ? undefined : ps?.firstPageNumber,
|
|
1400
1507
|
margins: margins
|
|
1401
1508
|
? {
|
|
@@ -1417,7 +1524,7 @@ async function convertChartsheet(cs, workbook) {
|
|
|
1417
1524
|
orientation,
|
|
1418
1525
|
chart,
|
|
1419
1526
|
pageSetup,
|
|
1420
|
-
headerFooter: convertHeaderFooterModel(chartsheetModel(cs).headerFooter, convertChartsheetHeaderImages(chartsheetModel(cs).headerImages, workbook))
|
|
1527
|
+
headerFooter: convertHeaderFooterModel(chartsheetModel(cs).headerFooter, draftQuality ? [] : convertChartsheetHeaderImages(chartsheetModel(cs).headerImages, workbook))
|
|
1421
1528
|
};
|
|
1422
1529
|
}
|
|
1423
1530
|
function convertChartsheetHeaderImages(images, workbook) {
|
|
@@ -44,7 +44,7 @@ export * as Pdf from "./surface/pdf.js";
|
|
|
44
44
|
export type { ChartToPdfOptions, ExcelToPdfOptions } from "./excel-bridge.js";
|
|
45
45
|
export type { DocxToPdfOptions } from "./word-bridge.js";
|
|
46
46
|
export type { PdfCell, PdfRow, PdfColumn, PdfSheet, PdfBook, PdfImage, PdfInput } from "./pdf.js";
|
|
47
|
-
export type { PdfExportOptions, PdfOrientation, PdfPageSize, PdfMargins, PdfColor, PageSizeName, PdfWatermark, PdfTextWatermark, PdfImageWatermark, PdfWatermarkFilter } from "./types.js";
|
|
47
|
+
export type { PdfExportOptions, PdfOrientation, PdfPageOrder, PdfCellErrorMode, PdfPageSize, PdfMargins, PdfColor, PageSizeName, PdfWatermark, PdfTextWatermark, PdfImageWatermark, PdfWatermarkFilter } from "./types.js";
|
|
48
48
|
export type { ReadPdfOptions, ReadPdfResult, ReadPdfPage } from "./reader/pdf-reader.js";
|
|
49
49
|
export type { PdfMetadata } from "./reader/metadata-reader.js";
|
|
50
50
|
export type { ExtractedImage } from "./reader/image-extractor.js";
|
|
@@ -29,5 +29,10 @@ import type { PdfChartDrawingSurface } from "../types.js";
|
|
|
29
29
|
* @param alphaValues Shared set that accumulates transparency values for
|
|
30
30
|
* later `ExtGState` registration. The surface adds any
|
|
31
31
|
* `color.a` values it observes to this set.
|
|
32
|
+
* @param grayscale When true, every color the chart renderer emits is
|
|
33
|
+
* converted to grayscale. Charts bypass the cell style
|
|
34
|
+
* pipeline entirely — the chart engine pushes colors
|
|
35
|
+
* straight into this surface — so Excel's "Black and
|
|
36
|
+
* white" print option has to be applied right here.
|
|
32
37
|
*/
|
|
33
|
-
export declare function createChartSurface(stream: PdfContentStream, fontManager: FontManager, alphaValues: Set<number
|
|
38
|
+
export declare function createChartSurface(stream: PdfContentStream, fontManager: FontManager, alphaValues: Set<number>, grayscale?: boolean): PdfChartDrawingSurface;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { resolvePdfFontName } from "../font/font-manager.js";
|
|
20
20
|
import { alphaGsName, emitTextBlock } from "./page-renderer.js";
|
|
21
|
+
import { toGrayscale } from "./style-converter.js";
|
|
21
22
|
/**
|
|
22
23
|
* Build a {@link PdfChartDrawingSurface} that forwards to the given content
|
|
23
24
|
* stream. The returned surface is stateful — callers should not mix it with
|
|
@@ -28,38 +29,77 @@ import { alphaGsName, emitTextBlock } from "./page-renderer.js";
|
|
|
28
29
|
* @param alphaValues Shared set that accumulates transparency values for
|
|
29
30
|
* later `ExtGState` registration. The surface adds any
|
|
30
31
|
* `color.a` values it observes to this set.
|
|
32
|
+
* @param grayscale When true, every color the chart renderer emits is
|
|
33
|
+
* converted to grayscale. Charts bypass the cell style
|
|
34
|
+
* pipeline entirely — the chart engine pushes colors
|
|
35
|
+
* straight into this surface — so Excel's "Black and
|
|
36
|
+
* white" print option has to be applied right here.
|
|
31
37
|
*/
|
|
32
|
-
export function createChartSurface(stream, fontManager, alphaValues) {
|
|
38
|
+
export function createChartSurface(stream, fontManager, alphaValues, grayscale = false) {
|
|
39
|
+
/** Single choke point for every color entering the surface. */
|
|
40
|
+
const mapColor = (color) => grayscale && color ? toGrayscale(color) : color;
|
|
33
41
|
const applyAlpha = (color) => {
|
|
34
42
|
if (color?.a !== undefined && color.a < 1) {
|
|
35
43
|
alphaValues.add(color.a);
|
|
36
44
|
stream.setGraphicsState(alphaGsName(color.a));
|
|
37
45
|
}
|
|
38
46
|
};
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Fill and/or stroke the path built by `buildPath`.
|
|
49
|
+
*
|
|
50
|
+
* `buildPath` is a closure rather than a pre-built path because a fill and a
|
|
51
|
+
* stroke with *different* opacities cannot share one graphics state: the
|
|
52
|
+
* alpha `ExtGState` sets `/ca` and `/CA` together, and `fillAndStroke()`
|
|
53
|
+
* paints both under whichever was applied last. In that case we paint in two
|
|
54
|
+
* passes and rebuild the path for the second one. (Within a single pass the
|
|
55
|
+
* unused half of the ExtGState is harmless — a fill-only pass never strokes.)
|
|
56
|
+
*/
|
|
57
|
+
const paintFillStroke = (buildPath, options) => {
|
|
58
|
+
const { lineWidth, dashPattern } = options;
|
|
59
|
+
const fill = mapColor(options.fill);
|
|
60
|
+
const stroke = mapColor(options.stroke);
|
|
61
|
+
const applyDash = () => {
|
|
62
|
+
if (dashPattern && dashPattern.length > 0) {
|
|
63
|
+
stream.setDashPattern(dashPattern);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
if (fill && stroke) {
|
|
67
|
+
const fillAlpha = fill.a ?? 1;
|
|
68
|
+
const strokeAlpha = stroke.a ?? 1;
|
|
69
|
+
if (fillAlpha !== strokeAlpha) {
|
|
70
|
+
stream.save();
|
|
71
|
+
buildPath();
|
|
72
|
+
stream.setFillColor(fill);
|
|
73
|
+
applyAlpha(fill);
|
|
74
|
+
stream.fill();
|
|
75
|
+
stream.restore();
|
|
76
|
+
stream.save();
|
|
77
|
+
buildPath();
|
|
78
|
+
stream.setStrokeColor(stroke);
|
|
79
|
+
applyAlpha(stroke);
|
|
80
|
+
applyDash();
|
|
81
|
+
if (lineWidth !== undefined) {
|
|
82
|
+
stream.setLineWidth(lineWidth);
|
|
83
|
+
}
|
|
84
|
+
stream.stroke();
|
|
85
|
+
stream.restore();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
buildPath();
|
|
90
|
+
applyDash();
|
|
91
|
+
// Reaching here means either a single operation, or a fill and stroke that
|
|
92
|
+
// share one opacity — so one ExtGState describes both and no reset is
|
|
93
|
+
// needed (the differing case returned above, each pass in its own q/Q).
|
|
41
94
|
if (fill) {
|
|
42
95
|
stream.setFillColor(fill);
|
|
43
96
|
applyAlpha(fill);
|
|
44
97
|
}
|
|
45
98
|
if (stroke) {
|
|
46
99
|
stream.setStrokeColor(stroke);
|
|
47
|
-
|
|
48
|
-
// `a` field or `a >= 1`), we must explicitly restore opaque alpha
|
|
49
|
-
// so the stroke is not drawn with the fill's transparency. The
|
|
50
|
-
// ExtGState set by `applyAlpha(fill)` applies to both fill and
|
|
51
|
-
// stroke operations in PDF; without this reset the stroke leaks
|
|
52
|
-
// the fill's alpha.
|
|
53
|
-
const strokeNeedsAlpha = stroke.a !== undefined && stroke.a < 1;
|
|
54
|
-
const fillAppliedAlpha = fill?.a !== undefined && fill.a < 1;
|
|
55
|
-
if (strokeNeedsAlpha) {
|
|
100
|
+
if (!fill) {
|
|
56
101
|
applyAlpha(stroke);
|
|
57
102
|
}
|
|
58
|
-
else if (fillAppliedAlpha) {
|
|
59
|
-
// Reset to fully opaque for the stroke
|
|
60
|
-
alphaValues.add(1);
|
|
61
|
-
stream.setGraphicsState(alphaGsName(1));
|
|
62
|
-
}
|
|
63
103
|
if (lineWidth !== undefined) {
|
|
64
104
|
stream.setLineWidth(lineWidth);
|
|
65
105
|
}
|
|
@@ -81,13 +121,13 @@ export function createChartSurface(stream, fontManager, alphaValues) {
|
|
|
81
121
|
drawRect(options) {
|
|
82
122
|
const { x, y, width, height, fill, stroke, lineWidth } = options;
|
|
83
123
|
stream.save();
|
|
84
|
-
stream.rect(x, y, width, height);
|
|
85
|
-
paintFillStroke({ fill, stroke, lineWidth });
|
|
124
|
+
paintFillStroke(() => stream.rect(x, y, width, height), { fill, stroke, lineWidth });
|
|
86
125
|
stream.restore();
|
|
87
126
|
return this;
|
|
88
127
|
},
|
|
89
128
|
drawLine(options) {
|
|
90
|
-
const { x1, y1, x2, y2,
|
|
129
|
+
const { x1, y1, x2, y2, lineWidth, dashPattern } = options;
|
|
130
|
+
const color = mapColor(options.color);
|
|
91
131
|
stream.save();
|
|
92
132
|
if (color) {
|
|
93
133
|
stream.setStrokeColor(color);
|
|
@@ -108,7 +148,7 @@ export function createChartSurface(stream, fontManager, alphaValues) {
|
|
|
108
148
|
return this;
|
|
109
149
|
}
|
|
110
150
|
const fontSize = options.fontSize ?? 10;
|
|
111
|
-
const color = options.color ?? { r: 0, g: 0, b: 0 };
|
|
151
|
+
const color = mapColor(options.color ?? { r: 0, g: 0, b: 0 });
|
|
112
152
|
const bold = options.bold ?? false;
|
|
113
153
|
const italic = options.italic ?? false;
|
|
114
154
|
const fontFamily = options.fontFamily ?? "Helvetica";
|
|
@@ -140,39 +180,38 @@ export function createChartSurface(stream, fontManager, alphaValues) {
|
|
|
140
180
|
drawCircle(options) {
|
|
141
181
|
const { cx, cy, r, fill, stroke, lineWidth } = options;
|
|
142
182
|
stream.save();
|
|
143
|
-
stream.circle(cx, cy, r);
|
|
144
|
-
paintFillStroke({ fill, stroke, lineWidth });
|
|
183
|
+
paintFillStroke(() => stream.circle(cx, cy, r), { fill, stroke, lineWidth });
|
|
145
184
|
stream.restore();
|
|
146
185
|
return this;
|
|
147
186
|
},
|
|
148
187
|
drawPath(ops, options) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
188
|
+
const buildPath = () => {
|
|
189
|
+
for (const op of ops) {
|
|
190
|
+
switch (op.op) {
|
|
191
|
+
case "move":
|
|
192
|
+
stream.moveTo(op.x, op.y);
|
|
193
|
+
break;
|
|
194
|
+
case "line":
|
|
195
|
+
stream.lineTo(op.x, op.y);
|
|
196
|
+
break;
|
|
197
|
+
case "curve":
|
|
198
|
+
stream.curveTo(op.x1, op.y1, op.x2, op.y2, op.x3, op.y3);
|
|
199
|
+
break;
|
|
200
|
+
case "close":
|
|
201
|
+
stream.closePath();
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
164
204
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
paintFillStroke({
|
|
205
|
+
if (options?.closePath) {
|
|
206
|
+
stream.closePath();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
stream.save();
|
|
210
|
+
paintFillStroke(buildPath, {
|
|
173
211
|
fill: options?.fill,
|
|
174
212
|
stroke: options?.stroke,
|
|
175
|
-
lineWidth: options?.lineWidth
|
|
213
|
+
lineWidth: options?.lineWidth,
|
|
214
|
+
dashPattern: options?.dashPattern
|
|
176
215
|
});
|
|
177
216
|
stream.restore();
|
|
178
217
|
return this;
|