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.
Files changed (63) hide show
  1. package/dist/browser/modules/pdf/builder/image-utils.d.ts +1 -1
  2. package/dist/browser/modules/pdf/builder/image-utils.js +58 -9
  3. package/dist/browser/modules/pdf/core/pdf-writer.d.ts +8 -0
  4. package/dist/browser/modules/pdf/core/pdf-writer.js +3 -0
  5. package/dist/browser/modules/pdf/excel-bridge.js +126 -19
  6. package/dist/browser/modules/pdf/index.d.ts +1 -1
  7. package/dist/browser/modules/pdf/render/chart-surface.d.ts +6 -1
  8. package/dist/browser/modules/pdf/render/chart-surface.js +86 -47
  9. package/dist/browser/modules/pdf/render/constants.d.ts +69 -0
  10. package/dist/browser/modules/pdf/render/constants.js +45 -0
  11. package/dist/browser/modules/pdf/render/layout-engine.d.ts +5 -1
  12. package/dist/browser/modules/pdf/render/layout-engine.js +586 -205
  13. package/dist/browser/modules/pdf/render/page-renderer.js +180 -3
  14. package/dist/browser/modules/pdf/render/pdf-exporter.js +176 -35
  15. package/dist/browser/modules/pdf/render/style-converter.d.ts +11 -0
  16. package/dist/browser/modules/pdf/render/style-converter.js +24 -0
  17. package/dist/browser/modules/pdf/types.d.ts +284 -5
  18. package/dist/cjs/modules/pdf/builder/image-utils.js +58 -9
  19. package/dist/cjs/modules/pdf/core/pdf-writer.js +3 -0
  20. package/dist/cjs/modules/pdf/excel-bridge.js +125 -18
  21. package/dist/cjs/modules/pdf/render/chart-surface.js +86 -47
  22. package/dist/cjs/modules/pdf/render/constants.js +46 -1
  23. package/dist/cjs/modules/pdf/render/layout-engine.js +583 -202
  24. package/dist/cjs/modules/pdf/render/page-renderer.js +179 -2
  25. package/dist/cjs/modules/pdf/render/pdf-exporter.js +175 -34
  26. package/dist/cjs/modules/pdf/render/style-converter.js +26 -0
  27. package/dist/esm/modules/pdf/builder/image-utils.js +58 -9
  28. package/dist/esm/modules/pdf/core/pdf-writer.js +3 -0
  29. package/dist/esm/modules/pdf/excel-bridge.js +126 -19
  30. package/dist/esm/modules/pdf/render/chart-surface.js +86 -47
  31. package/dist/esm/modules/pdf/render/constants.js +45 -0
  32. package/dist/esm/modules/pdf/render/layout-engine.js +586 -205
  33. package/dist/esm/modules/pdf/render/page-renderer.js +180 -3
  34. package/dist/esm/modules/pdf/render/pdf-exporter.js +176 -35
  35. package/dist/esm/modules/pdf/render/style-converter.js +24 -0
  36. package/dist/iife/documonster.archive.iife.js +1 -1
  37. package/dist/iife/documonster.archive.iife.min.js +1 -1
  38. package/dist/iife/documonster.csv.iife.js +1 -1
  39. package/dist/iife/documonster.csv.iife.min.js +1 -1
  40. package/dist/iife/documonster.excel.iife.js +1 -1
  41. package/dist/iife/documonster.excel.iife.min.js +1 -1
  42. package/dist/iife/documonster.formula.iife.js +1 -1
  43. package/dist/iife/documonster.formula.iife.min.js +1 -1
  44. package/dist/iife/documonster.markdown.iife.js +1 -1
  45. package/dist/iife/documonster.markdown.iife.min.js +1 -1
  46. package/dist/iife/documonster.pdf.iife.js +1457 -664
  47. package/dist/iife/documonster.pdf.iife.js.map +1 -1
  48. package/dist/iife/documonster.pdf.iife.min.js +33 -32
  49. package/dist/iife/documonster.stream.iife.js +1 -1
  50. package/dist/iife/documonster.stream.iife.min.js +1 -1
  51. package/dist/iife/documonster.word.iife.js +1 -1
  52. package/dist/iife/documonster.word.iife.min.js +1 -1
  53. package/dist/iife/documonster.xml.iife.js +1 -1
  54. package/dist/iife/documonster.xml.iife.min.js +1 -1
  55. package/dist/types/modules/pdf/builder/image-utils.d.ts +1 -1
  56. package/dist/types/modules/pdf/core/pdf-writer.d.ts +8 -0
  57. package/dist/types/modules/pdf/index.d.ts +1 -1
  58. package/dist/types/modules/pdf/render/chart-surface.d.ts +6 -1
  59. package/dist/types/modules/pdf/render/constants.d.ts +69 -0
  60. package/dist/types/modules/pdf/render/layout-engine.d.ts +5 -1
  61. package/dist/types/modules/pdf/render/style-converter.d.ts +11 -0
  62. package/dist/types/modules/pdf/types.d.ts +284 -5
  63. 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 a JPEG image using DCTDecode (raw JPEG data embedded directly).
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 writeJpegImageXObject(writer, data) {
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", "/DeviceRGB")
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, png.pixels);
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
- const images = collectImages(ws, workbook);
328
- const charts = await collectCharts(ws);
329
- const sparklineCharts = collectSparklineCharts(ws);
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
- function convertHeaderFooter(ws, workbook) {
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
- const images = convertHeaderFooterImages(ws, workbook);
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
- // (×1/9525) and px→pt for images (×0.75) based on `extUnit`.
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>): PdfChartDrawingSurface;
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
- const paintFillStroke = (options) => {
40
- const { fill, stroke, lineWidth } = options;
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
- // When fill applied a sub-1 alpha but stroke is fully opaque (no
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, color, lineWidth, dashPattern } = options;
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
- stream.save();
150
- for (const op of ops) {
151
- switch (op.op) {
152
- case "move":
153
- stream.moveTo(op.x, op.y);
154
- break;
155
- case "line":
156
- stream.lineTo(op.x, op.y);
157
- break;
158
- case "curve":
159
- stream.curveTo(op.x1, op.y1, op.x2, op.y2, op.x3, op.y3);
160
- break;
161
- case "close":
162
- stream.closePath();
163
- break;
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
- if (options?.closePath) {
167
- stream.closePath();
168
- }
169
- if (options?.dashPattern && options.dashPattern.length > 0) {
170
- stream.setDashPattern(options.dashPattern);
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;