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
@@ -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) {
@@ -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;
@@ -28,3 +28,48 @@ export const INDENT_WIDTH = 10;
28
28
  */
29
29
  export const MAX_DIGIT_WIDTH_PX = 7;
30
30
  export const PX_TO_PT = 72 / 96; // 0.75
31
+ /**
32
+ * Font size of the printed row-number / column-letter headings, in points.
33
+ *
34
+ * Deliberately independent of the grid's print scale: headings are a reading
35
+ * aid rather than content, so a fixed size stays legible even when the grid is
36
+ * shrunk to fit, and lets the layout reserve exactly the space it draws.
37
+ */
38
+ export const HEADING_FONT_SIZE = 8;
39
+ /** Padding around a heading label, in points (applied on both sides). */
40
+ export const HEADING_PADDING = 3;
41
+ /** Background shade and rule color of the heading bands. */
42
+ export const HEADING_FILL = { r: 0.94, g: 0.94, b: 0.94 };
43
+ export const HEADING_RULE = { r: 0.6, g: 0.6, b: 0.6 };
44
+ export const HEADING_TEXT = { r: 0.2, g: 0.2, b: 0.2 };
45
+ /**
46
+ * Lower bound when solving "fit to N pages", matching the 10% floor of Excel's
47
+ * Page Setup scaling. A target that is unreachable at this scale — typically
48
+ * because manual page breaks force more pages than requested — yields more than
49
+ * N pages rather than shrinking the grid into illegibility.
50
+ */
51
+ export const FIT_MIN_SCALE = 0.1;
52
+ /**
53
+ * Side length of the red corner marker Excel draws on a commented cell, in
54
+ * points at 100% scale.
55
+ */
56
+ export const COMMENT_MARKER_SIZE = 4;
57
+ /** Comment box fill, border and text colors, matching Excel's note styling. */
58
+ export const COMMENT_FILL = { r: 1, g: 1, b: 0.88 };
59
+ export const COMMENT_BORDER = { r: 0.4, g: 0.4, b: 0.4 };
60
+ export const COMMENT_MARKER_COLOR = { r: 0.8, g: 0, b: 0 };
61
+ /**
62
+ * Overflow tolerance when packing columns into a page, in points.
63
+ *
64
+ * Scaled point widths accumulate rounding error, so a column that mathematically
65
+ * fits can measure a hair too wide. Rows need no such slack because their
66
+ * heights are not derived from a character-width conversion.
67
+ */
68
+ export const COLUMN_FIT_EPSILON = 0.01;
69
+ /**
70
+ * Height of the bands reserved for the fallback sheet-name header and
71
+ * page-number footer, in points. Excel header/footers use their own margins;
72
+ * these are the simple `showSheetNames` / `showPageNumbers` bands.
73
+ */
74
+ export const SHEET_NAME_BAND_HEIGHT = 20;
75
+ export const PAGE_NUMBER_BAND_HEIGHT = 20;