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
@@ -66,7 +66,7 @@ async function excelToPdf(workbook, options) {
66
66
  // shipped with. This keeps the ~200 KB engine out of bundles that only
67
67
  // export already-computed workbooks — no host-registry needed.
68
68
  options?.recalculate?.(workbook);
69
- const pdfWorkbook = await excelWorkbookToPdf(workbook);
69
+ const pdfWorkbook = await excelWorkbookToPdf(workbook, options);
70
70
  return (0, pdf_exporter_1.exportPdf)(pdfWorkbook, options);
71
71
  }
72
72
  /**
@@ -178,9 +178,9 @@ async function chartToPdf(chart, options = {}) {
178
178
  * Chartsheets without an orderNo fall to the end, mirroring how Excel
179
179
  * treats sheets with missing tab positions.
180
180
  */
181
- async function excelWorkbookToPdf(workbook) {
182
- const worksheetResults = await Promise.all((0, workbook_1.getWorksheets)(workbook).map(ws => convertSheet(ws, workbook)));
183
- const chartsheetResults = await Promise.all((0, workbook_1.getChartsheets)(workbook).map(cs => convertChartsheet(cs, workbook)));
181
+ async function excelWorkbookToPdf(workbook, options) {
182
+ const worksheetResults = await Promise.all((0, workbook_1.getWorksheets)(workbook).map(ws => convertSheet(ws, workbook, options)));
183
+ const chartsheetResults = await Promise.all((0, workbook_1.getChartsheets)(workbook).map(cs => convertChartsheet(cs, workbook, options)));
184
184
  const combined = [...worksheetResults, ...chartsheetResults];
185
185
  combined.sort((a, b) => (a.orderNo ?? Number.POSITIVE_INFINITY) - (b.orderNo ?? Number.POSITIVE_INFINITY));
186
186
  return {
@@ -196,7 +196,7 @@ async function excelWorkbookToPdf(workbook) {
196
196
  // =============================================================================
197
197
  // Sheet Conversion
198
198
  // =============================================================================
199
- async function convertSheet(ws, workbook) {
199
+ async function convertSheet(ws, workbook, options) {
200
200
  const dimensions = (0, worksheet_1.getSheetDimensions)(ws);
201
201
  const hasData = dimensions && dimensions.top > 0 && dimensions.left > 0;
202
202
  const bounds = hasData
@@ -316,10 +316,22 @@ async function convertSheet(ws, workbook) {
316
316
  }
317
317
  : undefined,
318
318
  scale: ps.scale,
319
+ fitToPage: ps.fitToPage,
320
+ fitToWidth: ps.fitToWidth,
321
+ fitToHeight: ps.fitToHeight,
319
322
  printTitlesRow: ps.printTitlesRow,
323
+ printTitlesColumn: ps.printTitlesColumn,
320
324
  showGridLines: ps.showGridLines,
325
+ showRowColHeaders: ps.showRowColHeaders,
321
326
  printArea: ps.printArea,
322
- firstPageNumber: ps.useFirstPageNumber === false ? undefined : ps.firstPageNumber
327
+ firstPageNumber: ps.useFirstPageNumber === false ? undefined : ps.firstPageNumber,
328
+ pageOrder: ps.pageOrder,
329
+ blackAndWhite: ps.blackAndWhite,
330
+ draft: ps.draft,
331
+ errors: ps.errors,
332
+ cellComments: ps.cellComments,
333
+ horizontalCentered: ps.horizontalCentered,
334
+ verticalCentered: ps.verticalCentered
323
335
  }
324
336
  : undefined;
325
337
  // Convert row/col breaks
@@ -328,9 +340,18 @@ async function convertSheet(ws, workbook) {
328
340
  // Convert images and charts. Both are floating objects anchored to
329
341
  // cells, and both need to participate in bounds expansion so the
330
342
  // layout engine allocates pages that cover their anchor rows/cols.
331
- const images = collectImages(ws, workbook);
332
- const charts = await collectCharts(ws);
333
- const sparklineCharts = collectSparklineCharts(ws);
343
+ //
344
+ // Draft quality prints no graphics, so skip collection entirely rather than
345
+ // gathering (and rasterizing ChartEx layouts) only to drop the result during
346
+ // layout: draft exists to be cheap, and a rasterizer failure must not sink an
347
+ // export that would not have shown the graphic anyway. The precedence here
348
+ // mirrors `resolveOptions`; the layout engine still gates on the resolved
349
+ // flag, so this stays a pure optimization.
350
+ const comments = collectComments(ws);
351
+ const draftQuality = options?.draft ?? ws.pageSetup?.draft ?? false;
352
+ const images = draftQuality ? undefined : collectImages(ws, workbook);
353
+ const charts = draftQuality ? undefined : await collectCharts(ws);
354
+ const sparklineCharts = draftQuality ? undefined : collectSparklineCharts(ws);
334
355
  // Merge sparkline micro-charts with regular charts
335
356
  const allCharts = charts
336
357
  ? sparklineCharts
@@ -402,14 +423,91 @@ async function convertSheet(ws, workbook) {
402
423
  rows,
403
424
  merges,
404
425
  pageSetup,
405
- headerFooter: convertHeaderFooter(ws, workbook),
426
+ headerFooter: convertHeaderFooter(ws, workbook, draftQuality),
406
427
  rowBreaks,
407
428
  colBreaks,
408
429
  images,
409
- charts: allCharts
430
+ charts: allCharts,
431
+ comments
410
432
  };
411
433
  }
412
- function convertHeaderFooter(ws, workbook) {
434
+ /**
435
+ * Collect every comment on a worksheet, in row-major order.
436
+ *
437
+ * Two sources are merged: classic notes stored on the cell (`_comment`, the
438
+ * VML-backed kind) and Office 365 threaded comments held on the sheet. Only the
439
+ * text and author are needed — the printed form is an end-of-sheet list, not a
440
+ * positioned box — so the VML geometry is deliberately ignored.
441
+ */
442
+ function collectComments(ws) {
443
+ const collected = [];
444
+ for (const row of ws._rows ?? []) {
445
+ if (!row) {
446
+ continue;
447
+ }
448
+ for (const cell of row.cells ?? []) {
449
+ if (!cell) {
450
+ continue;
451
+ }
452
+ const note = (0, cell_1.cellComment)(cell);
453
+ const text = noteText(note?.note);
454
+ if (text) {
455
+ collected.push({
456
+ ref: cell.address,
457
+ text,
458
+ author: note?.author,
459
+ anchor: parseVmlAnchor(note?.note)
460
+ });
461
+ }
462
+ }
463
+ }
464
+ for (const entry of ws.threadedComments ?? []) {
465
+ const text = entry.comment.text?.trim();
466
+ if (text) {
467
+ collected.push({ ref: entry.ref, text, author: entry.comment.personId });
468
+ }
469
+ }
470
+ return collected.length > 0 ? collected : undefined;
471
+ }
472
+ /**
473
+ * Decode a VML comment anchor into fractional sheet coordinates.
474
+ *
475
+ * `<x:Anchor>` holds eight integers: for each edge a column/row index followed
476
+ * by an offset into it, measured in 1/68 of a column and 1/18 of a row (the
477
+ * reciprocals Excel uses when it writes the value). Returns `undefined` when the
478
+ * note has no anchor, so callers fall back to Excel's default placement.
479
+ */
480
+ function parseVmlAnchor(note) {
481
+ if (!note || typeof note === "string" || typeof note.anchor !== "string") {
482
+ return undefined;
483
+ }
484
+ const parts = note.anchor.split(",").map(p => Number(p.trim()));
485
+ if (parts.length !== 8 || parts.some(n => !Number.isFinite(n))) {
486
+ return undefined;
487
+ }
488
+ const [l, lf, t, tf, r, rf, b, bf] = parts;
489
+ return {
490
+ left: l + lf / 68,
491
+ top: t + tf / 18,
492
+ right: r + rf / 68,
493
+ bottom: b + bf / 18
494
+ };
495
+ }
496
+ /** Flatten a note body, which may be plain text or a rich `NoteConfig`. */
497
+ function noteText(note) {
498
+ if (typeof note === "string") {
499
+ return note.trim() || undefined;
500
+ }
501
+ if (note && typeof note === "object" && Array.isArray(note.texts)) {
502
+ const joined = note.texts
503
+ .map(t => (typeof t === "string" ? t : (t.text ?? "")))
504
+ .join("")
505
+ .trim();
506
+ return joined || undefined;
507
+ }
508
+ return undefined;
509
+ }
510
+ function convertHeaderFooter(ws, workbook, draftQuality = false) {
413
511
  const source = ws.headerFooter;
414
512
  const fields = [
415
513
  source.oddHeader,
@@ -419,7 +517,9 @@ function convertHeaderFooter(ws, workbook) {
419
517
  source.firstHeader,
420
518
  source.firstFooter
421
519
  ];
422
- const images = convertHeaderFooterImages(ws, workbook);
520
+ // Draft quality prints no graphics, header pictures included, so skip
521
+ // collecting them rather than embedding XObjects nothing will draw.
522
+ const images = draftQuality ? [] : convertHeaderFooterImages(ws, workbook);
423
523
  if (!fields.some(Boolean) && images.length === 0) {
424
524
  return undefined;
425
525
  }
@@ -1258,7 +1358,7 @@ function chartAnchorRange(chart) {
1258
1358
  : undefined,
1259
1359
  // Chart anchors store ext as EMU (`cx`, `cy`). Pass the values
1260
1360
  // through unchanged; the layout engine converts EMU→pt for charts
1261
- // (×1/9525) and px→pt for images (×0.75) based on `extUnit`.
1361
+ // (÷12700) and px→pt for images (×0.75) based on `extUnit`.
1262
1362
  ext: r.ext ? { width: r.ext.cx, height: r.ext.cy } : undefined,
1263
1363
  extUnit: r.ext ? "emu" : undefined
1264
1364
  };
@@ -1342,11 +1442,16 @@ const CHARTSHEET_RASTER_PX = { width: 1280, height: 720 };
1342
1442
  * (matches what Excel prints for a chartsheet whose chart was deleted
1343
1443
  * but the sheet kept).
1344
1444
  */
1345
- async function convertChartsheet(cs, workbook) {
1445
+ async function convertChartsheet(cs, workbook, options) {
1346
1446
  const classicModel = (0, chartsheet_1.chartsheetChartModel)(cs);
1347
1447
  const chartExModel = (0, chartsheet_1.chartsheetChartExModel)(cs);
1448
+ // Resolve draft before touching the chart: a ChartEx outside the vector
1449
+ // whitelist would otherwise be rasterised at cost — and could fail — for a
1450
+ // page that prints no graphics at all. Precedence mirrors `resolveOptions`.
1451
+ const draftQuality = options?.draft ?? (0, chartsheet_1.chartsheetPageSetup)(cs)?.draft ?? false;
1452
+ // Draft quality prints no graphics, so the chart is simply never built.
1348
1453
  let chart = {};
1349
- if (classicModel) {
1454
+ if (!draftQuality && classicModel) {
1350
1455
  const model = classicModel;
1351
1456
  chart = {
1352
1457
  drawVector: (surface, rect) => {
@@ -1359,7 +1464,7 @@ async function convertChartsheet(cs, workbook) {
1359
1464
  }
1360
1465
  };
1361
1466
  }
1362
- else if (chartExModel) {
1467
+ else if (!draftQuality && chartExModel) {
1363
1468
  if ((0, chart_ex_renderer_1.canRenderChartExAsVectorPdf)(chartExModel)) {
1364
1469
  const model = chartExModel;
1365
1470
  chart = {
@@ -1400,6 +1505,8 @@ async function convertChartsheet(cs, workbook) {
1400
1505
  orientation: ps?.orientation,
1401
1506
  paperSize: ps?.paperSize,
1402
1507
  showGridLines: false,
1508
+ blackAndWhite: ps?.blackAndWhite,
1509
+ draft: ps?.draft,
1403
1510
  firstPageNumber: ps?.useFirstPageNumber === false ? undefined : ps?.firstPageNumber,
1404
1511
  margins: margins
1405
1512
  ? {
@@ -1421,7 +1528,7 @@ async function convertChartsheet(cs, workbook) {
1421
1528
  orientation,
1422
1529
  chart,
1423
1530
  pageSetup,
1424
- headerFooter: convertHeaderFooterModel((0, chartsheet_1.chartsheetModel)(cs).headerFooter, convertChartsheetHeaderImages((0, chartsheet_1.chartsheetModel)(cs).headerImages, workbook))
1531
+ headerFooter: convertHeaderFooterModel((0, chartsheet_1.chartsheetModel)(cs).headerFooter, draftQuality ? [] : convertChartsheetHeaderImages((0, chartsheet_1.chartsheetModel)(cs).headerImages, workbook))
1425
1532
  };
1426
1533
  }
1427
1534
  function convertChartsheetHeaderImages(images, workbook) {
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.createChartSurface = createChartSurface;
22
22
  const font_manager_1 = require("../font/font-manager.js");
23
23
  const page_renderer_1 = require("./page-renderer.js");
24
+ const style_converter_1 = require("./style-converter.js");
24
25
  /**
25
26
  * Build a {@link PdfChartDrawingSurface} that forwards to the given content
26
27
  * stream. The returned surface is stateful — callers should not mix it with
@@ -31,38 +32,77 @@ const page_renderer_1 = require("./page-renderer.js");
31
32
  * @param alphaValues Shared set that accumulates transparency values for
32
33
  * later `ExtGState` registration. The surface adds any
33
34
  * `color.a` values it observes to this set.
35
+ * @param grayscale When true, every color the chart renderer emits is
36
+ * converted to grayscale. Charts bypass the cell style
37
+ * pipeline entirely — the chart engine pushes colors
38
+ * straight into this surface — so Excel's "Black and
39
+ * white" print option has to be applied right here.
34
40
  */
35
- function createChartSurface(stream, fontManager, alphaValues) {
41
+ function createChartSurface(stream, fontManager, alphaValues, grayscale = false) {
42
+ /** Single choke point for every color entering the surface. */
43
+ const mapColor = (color) => grayscale && color ? (0, style_converter_1.toGrayscale)(color) : color;
36
44
  const applyAlpha = (color) => {
37
45
  if (color?.a !== undefined && color.a < 1) {
38
46
  alphaValues.add(color.a);
39
47
  stream.setGraphicsState((0, page_renderer_1.alphaGsName)(color.a));
40
48
  }
41
49
  };
42
- const paintFillStroke = (options) => {
43
- const { fill, stroke, lineWidth } = options;
50
+ /**
51
+ * Fill and/or stroke the path built by `buildPath`.
52
+ *
53
+ * `buildPath` is a closure rather than a pre-built path because a fill and a
54
+ * stroke with *different* opacities cannot share one graphics state: the
55
+ * alpha `ExtGState` sets `/ca` and `/CA` together, and `fillAndStroke()`
56
+ * paints both under whichever was applied last. In that case we paint in two
57
+ * passes and rebuild the path for the second one. (Within a single pass the
58
+ * unused half of the ExtGState is harmless — a fill-only pass never strokes.)
59
+ */
60
+ const paintFillStroke = (buildPath, options) => {
61
+ const { lineWidth, dashPattern } = options;
62
+ const fill = mapColor(options.fill);
63
+ const stroke = mapColor(options.stroke);
64
+ const applyDash = () => {
65
+ if (dashPattern && dashPattern.length > 0) {
66
+ stream.setDashPattern(dashPattern);
67
+ }
68
+ };
69
+ if (fill && stroke) {
70
+ const fillAlpha = fill.a ?? 1;
71
+ const strokeAlpha = stroke.a ?? 1;
72
+ if (fillAlpha !== strokeAlpha) {
73
+ stream.save();
74
+ buildPath();
75
+ stream.setFillColor(fill);
76
+ applyAlpha(fill);
77
+ stream.fill();
78
+ stream.restore();
79
+ stream.save();
80
+ buildPath();
81
+ stream.setStrokeColor(stroke);
82
+ applyAlpha(stroke);
83
+ applyDash();
84
+ if (lineWidth !== undefined) {
85
+ stream.setLineWidth(lineWidth);
86
+ }
87
+ stream.stroke();
88
+ stream.restore();
89
+ return;
90
+ }
91
+ }
92
+ buildPath();
93
+ applyDash();
94
+ // Reaching here means either a single operation, or a fill and stroke that
95
+ // share one opacity — so one ExtGState describes both and no reset is
96
+ // needed (the differing case returned above, each pass in its own q/Q).
44
97
  if (fill) {
45
98
  stream.setFillColor(fill);
46
99
  applyAlpha(fill);
47
100
  }
48
101
  if (stroke) {
49
102
  stream.setStrokeColor(stroke);
50
- // When fill applied a sub-1 alpha but stroke is fully opaque (no
51
- // `a` field or `a >= 1`), we must explicitly restore opaque alpha
52
- // so the stroke is not drawn with the fill's transparency. The
53
- // ExtGState set by `applyAlpha(fill)` applies to both fill and
54
- // stroke operations in PDF; without this reset the stroke leaks
55
- // the fill's alpha.
56
- const strokeNeedsAlpha = stroke.a !== undefined && stroke.a < 1;
57
- const fillAppliedAlpha = fill?.a !== undefined && fill.a < 1;
58
- if (strokeNeedsAlpha) {
103
+ if (!fill) {
59
104
  applyAlpha(stroke);
60
105
  }
61
- else if (fillAppliedAlpha) {
62
- // Reset to fully opaque for the stroke
63
- alphaValues.add(1);
64
- stream.setGraphicsState((0, page_renderer_1.alphaGsName)(1));
65
- }
66
106
  if (lineWidth !== undefined) {
67
107
  stream.setLineWidth(lineWidth);
68
108
  }
@@ -84,13 +124,13 @@ function createChartSurface(stream, fontManager, alphaValues) {
84
124
  drawRect(options) {
85
125
  const { x, y, width, height, fill, stroke, lineWidth } = options;
86
126
  stream.save();
87
- stream.rect(x, y, width, height);
88
- paintFillStroke({ fill, stroke, lineWidth });
127
+ paintFillStroke(() => stream.rect(x, y, width, height), { fill, stroke, lineWidth });
89
128
  stream.restore();
90
129
  return this;
91
130
  },
92
131
  drawLine(options) {
93
- const { x1, y1, x2, y2, color, lineWidth, dashPattern } = options;
132
+ const { x1, y1, x2, y2, lineWidth, dashPattern } = options;
133
+ const color = mapColor(options.color);
94
134
  stream.save();
95
135
  if (color) {
96
136
  stream.setStrokeColor(color);
@@ -111,7 +151,7 @@ function createChartSurface(stream, fontManager, alphaValues) {
111
151
  return this;
112
152
  }
113
153
  const fontSize = options.fontSize ?? 10;
114
- const color = options.color ?? { r: 0, g: 0, b: 0 };
154
+ const color = mapColor(options.color ?? { r: 0, g: 0, b: 0 });
115
155
  const bold = options.bold ?? false;
116
156
  const italic = options.italic ?? false;
117
157
  const fontFamily = options.fontFamily ?? "Helvetica";
@@ -143,39 +183,38 @@ function createChartSurface(stream, fontManager, alphaValues) {
143
183
  drawCircle(options) {
144
184
  const { cx, cy, r, fill, stroke, lineWidth } = options;
145
185
  stream.save();
146
- stream.circle(cx, cy, r);
147
- paintFillStroke({ fill, stroke, lineWidth });
186
+ paintFillStroke(() => stream.circle(cx, cy, r), { fill, stroke, lineWidth });
148
187
  stream.restore();
149
188
  return this;
150
189
  },
151
190
  drawPath(ops, options) {
152
- stream.save();
153
- for (const op of ops) {
154
- switch (op.op) {
155
- case "move":
156
- stream.moveTo(op.x, op.y);
157
- break;
158
- case "line":
159
- stream.lineTo(op.x, op.y);
160
- break;
161
- case "curve":
162
- stream.curveTo(op.x1, op.y1, op.x2, op.y2, op.x3, op.y3);
163
- break;
164
- case "close":
165
- stream.closePath();
166
- break;
191
+ const buildPath = () => {
192
+ for (const op of ops) {
193
+ switch (op.op) {
194
+ case "move":
195
+ stream.moveTo(op.x, op.y);
196
+ break;
197
+ case "line":
198
+ stream.lineTo(op.x, op.y);
199
+ break;
200
+ case "curve":
201
+ stream.curveTo(op.x1, op.y1, op.x2, op.y2, op.x3, op.y3);
202
+ break;
203
+ case "close":
204
+ stream.closePath();
205
+ break;
206
+ }
167
207
  }
168
- }
169
- if (options?.closePath) {
170
- stream.closePath();
171
- }
172
- if (options?.dashPattern && options.dashPattern.length > 0) {
173
- stream.setDashPattern(options.dashPattern);
174
- }
175
- paintFillStroke({
208
+ if (options?.closePath) {
209
+ stream.closePath();
210
+ }
211
+ };
212
+ stream.save();
213
+ paintFillStroke(buildPath, {
176
214
  fill: options?.fill,
177
215
  stroke: options?.stroke,
178
- lineWidth: options?.lineWidth
216
+ lineWidth: options?.lineWidth,
217
+ dashPattern: options?.dashPattern
179
218
  });
180
219
  stream.restore();
181
220
  return this;
@@ -6,7 +6,7 @@
6
6
  * use exactly the same values, preventing clipped or overlapping content.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.PX_TO_PT = exports.MAX_DIGIT_WIDTH_PX = exports.INDENT_WIDTH = exports.LINE_HEIGHT_FACTOR = exports.CELL_PADDING_V = exports.CELL_PADDING_H = void 0;
9
+ exports.PAGE_NUMBER_BAND_HEIGHT = exports.SHEET_NAME_BAND_HEIGHT = exports.COLUMN_FIT_EPSILON = exports.COMMENT_MARKER_COLOR = exports.COMMENT_BORDER = exports.COMMENT_FILL = exports.COMMENT_MARKER_SIZE = exports.FIT_MIN_SCALE = exports.HEADING_TEXT = exports.HEADING_RULE = exports.HEADING_FILL = exports.HEADING_PADDING = exports.HEADING_FONT_SIZE = exports.PX_TO_PT = exports.MAX_DIGIT_WIDTH_PX = exports.INDENT_WIDTH = exports.LINE_HEIGHT_FACTOR = exports.CELL_PADDING_V = exports.CELL_PADDING_H = void 0;
10
10
  /** Horizontal cell padding in points (left + right = 2 × CELL_PADDING_H). */
11
11
  exports.CELL_PADDING_H = 3;
12
12
  /** Vertical cell padding in points (top + bottom = 2 × CELL_PADDING_V). */
@@ -31,3 +31,48 @@ exports.INDENT_WIDTH = 10;
31
31
  */
32
32
  exports.MAX_DIGIT_WIDTH_PX = 7;
33
33
  exports.PX_TO_PT = 72 / 96; // 0.75
34
+ /**
35
+ * Font size of the printed row-number / column-letter headings, in points.
36
+ *
37
+ * Deliberately independent of the grid's print scale: headings are a reading
38
+ * aid rather than content, so a fixed size stays legible even when the grid is
39
+ * shrunk to fit, and lets the layout reserve exactly the space it draws.
40
+ */
41
+ exports.HEADING_FONT_SIZE = 8;
42
+ /** Padding around a heading label, in points (applied on both sides). */
43
+ exports.HEADING_PADDING = 3;
44
+ /** Background shade and rule color of the heading bands. */
45
+ exports.HEADING_FILL = { r: 0.94, g: 0.94, b: 0.94 };
46
+ exports.HEADING_RULE = { r: 0.6, g: 0.6, b: 0.6 };
47
+ exports.HEADING_TEXT = { r: 0.2, g: 0.2, b: 0.2 };
48
+ /**
49
+ * Lower bound when solving "fit to N pages", matching the 10% floor of Excel's
50
+ * Page Setup scaling. A target that is unreachable at this scale — typically
51
+ * because manual page breaks force more pages than requested — yields more than
52
+ * N pages rather than shrinking the grid into illegibility.
53
+ */
54
+ exports.FIT_MIN_SCALE = 0.1;
55
+ /**
56
+ * Side length of the red corner marker Excel draws on a commented cell, in
57
+ * points at 100% scale.
58
+ */
59
+ exports.COMMENT_MARKER_SIZE = 4;
60
+ /** Comment box fill, border and text colors, matching Excel's note styling. */
61
+ exports.COMMENT_FILL = { r: 1, g: 1, b: 0.88 };
62
+ exports.COMMENT_BORDER = { r: 0.4, g: 0.4, b: 0.4 };
63
+ exports.COMMENT_MARKER_COLOR = { r: 0.8, g: 0, b: 0 };
64
+ /**
65
+ * Overflow tolerance when packing columns into a page, in points.
66
+ *
67
+ * Scaled point widths accumulate rounding error, so a column that mathematically
68
+ * fits can measure a hair too wide. Rows need no such slack because their
69
+ * heights are not derived from a character-width conversion.
70
+ */
71
+ exports.COLUMN_FIT_EPSILON = 0.01;
72
+ /**
73
+ * Height of the bands reserved for the fallback sheet-name header and
74
+ * page-number footer, in points. Excel header/footers use their own margins;
75
+ * these are the simple `showSheetNames` / `showPageNumbers` bands.
76
+ */
77
+ exports.SHEET_NAME_BAND_HEIGHT = 20;
78
+ exports.PAGE_NUMBER_BAND_HEIGHT = 20;