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
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * documonster v0.7.0
2
+ * documonster v0.8.0
3
3
  * Zero-dependency TypeScript toolkit for Excel, Word, PDF, CSV, Markdown, XML & ZIP — one API across Node.js, Bun & browsers.
4
4
  * (c) 2026 cjnoname
5
5
  * Released under the Apache-2.0 License
@@ -1814,34 +1814,60 @@ this.Documonster = this.Documonster || {};
1814
1814
  * Write an image XObject (JPEG or PNG) to the writer.
1815
1815
  * Returns the allocated object number.
1816
1816
  */
1817
- function writeImageXObject(writer, data, format) {
1818
- if (format === "png") return writePngImageXObject(writer, data);
1819
- return writeJpegImageXObject(writer, data);
1817
+ function writeImageXObject(writer, data, format, grayscale = false) {
1818
+ if (format === "png") return writePngImageXObject(writer, data, grayscale);
1819
+ return writeJpegImageXObject(writer, data, grayscale);
1820
1820
  }
1821
1821
  /**
1822
- * Write a JPEG image using DCTDecode (raw JPEG data embedded directly).
1822
+ * Write the PostScript calculator function that converts an RGB triple to its
1823
+ * Rec. 601 luma, and return its object number.
1824
+ *
1825
+ * Used as the tint transform of a `/DeviceN` space so a JPEG can be recoloured
1826
+ * without decoding it: the samples stay untouched and the consumer applies the
1827
+ * transform per pixel. Unlike painting a blend on top, this cannot darken
1828
+ * transparent areas, because it *is* the color space rather than an overlay.
1823
1829
  */
1824
- function writeJpegImageXObject(writer, data) {
1830
+ function writeLumaTintTransform(writer) {
1825
1831
  const objNum = writer.allocObject();
1832
+ const dict = new PdfDict().set("FunctionType", "4").set("Domain", "[0 1 0 1 0 1]").set("Range", "[0 1]");
1833
+ writer.addStreamObject(objNum, dict, new TextEncoder().encode("{ 0.114 mul exch 0.587 mul add exch 0.299 mul add }"));
1834
+ return objNum;
1835
+ }
1836
+ /**
1837
+ * Write a JPEG image using DCTDecode (raw JPEG data embedded directly).
1838
+ */
1839
+ function writeJpegImageXObject(writer, data, grayscale = false) {
1826
1840
  const dims = parseJpegDimensions(data);
1827
- const dict = new PdfDict().set("Type", "/XObject").set("Subtype", "/Image").set("Width", pdfNumber(dims.width)).set("Height", pdfNumber(dims.height)).set("ColorSpace", "/DeviceRGB").set("BitsPerComponent", "8").set("Filter", "/DCTDecode");
1841
+ const tintRef = grayscale ? writeLumaTintTransform(writer) : void 0;
1842
+ const objNum = writer.allocObject();
1843
+ const dict = new PdfDict().set("Type", "/XObject").set("Subtype", "/Image").set("Width", pdfNumber(dims.width)).set("Height", pdfNumber(dims.height)).set("ColorSpace", tintRef === void 0 ? "/DeviceRGB" : `[/DeviceN [/C1 /C2 /C3] /DeviceGray ${pdfRef(tintRef)}]`).set("BitsPerComponent", "8").set("Filter", "/DCTDecode");
1828
1844
  writer.addStreamObject(objNum, dict, data);
1829
1845
  return objNum;
1830
1846
  }
1847
+ /** Rec. 601 luma of an 8-bit RGB triple. */
1848
+ function lumaByte(r, g, b) {
1849
+ return Math.round(.299 * r + .587 * g + .114 * b);
1850
+ }
1831
1851
  /**
1832
1852
  * Write a PNG image: decode to raw RGB, create SMask for alpha if needed.
1833
1853
  */
1834
- function writePngImageXObject(writer, data) {
1854
+ function writePngImageXObject(writer, data, grayscale = false) {
1835
1855
  const png = decodePng(data);
1836
1856
  const objNum = writer.allocObject();
1837
- const dict = new PdfDict().set("Type", "/XObject").set("Subtype", "/Image").set("Width", pdfNumber(png.width)).set("Height", pdfNumber(png.height)).set("ColorSpace", "/DeviceRGB").set("BitsPerComponent", pdfNumber(png.bitsPerComponent));
1857
+ let pixels = png.pixels;
1858
+ if (grayscale) {
1859
+ const gray = new Uint8Array(png.width * png.height);
1860
+ for (let i = 0, p = 0; i < gray.length; i++, p += 3) gray[i] = lumaByte(pixels[p], pixels[p + 1], pixels[p + 2]);
1861
+ pixels = gray;
1862
+ }
1863
+ const dict = new PdfDict().set("Type", "/XObject").set("Subtype", "/Image").set("Width", pdfNumber(png.width)).set("Height", pdfNumber(png.height)).set("ColorSpace", grayscale ? "/DeviceGray" : "/DeviceRGB").set("BitsPerComponent", pdfNumber(png.bitsPerComponent));
1838
1864
  if (png.alpha) {
1839
1865
  const smaskObjNum = writer.allocObject();
1840
1866
  const smaskDict = new PdfDict().set("Type", "/XObject").set("Subtype", "/Image").set("Width", pdfNumber(png.width)).set("Height", pdfNumber(png.height)).set("ColorSpace", "/DeviceGray").set("BitsPerComponent", "8");
1841
1867
  writer.addStreamObject(smaskObjNum, smaskDict, png.alpha);
1842
1868
  dict.set("SMask", pdfRef(smaskObjNum));
1843
1869
  }
1844
- writer.addStreamObject(objNum, dict, png.pixels);
1870
+ writer.addStreamObject(objNum, dict, pixels);
1845
1871
  return objNum;
1846
1872
  }
1847
1873
  var init_image_utils = __esmMin((() => {
@@ -3821,6 +3847,7 @@ this.Documonster = this.Documonster || {};
3821
3847
  const mediaBox = `[0 0 ${pdfNumber(options.width)} ${pdfNumber(options.height)}]`;
3822
3848
  const contentsValue = typeof options.contentsRef === "string" ? options.contentsRef : pdfRef(options.contentsRef);
3823
3849
  const dict = new PdfDict().set("Type", "/Page").set("Parent", pdfRef(options.parentRef)).set("MediaBox", mediaBox).set("Contents", contentsValue).set("Resources", pdfRef(options.resourcesRef));
3850
+ if (options.transparencyGroup) dict.set("Group", "<< /S /Transparency /CS /DeviceRGB /I false /K false >>");
3824
3851
  if (options.annotRefs && options.annotRefs.length > 0) dict.set("Annots", "[" + options.annotRefs.map((r) => pdfRef(r)).join(" ") + "]");
3825
3852
  this.addObject(objNum, dict);
3826
3853
  return objNum;
@@ -31020,10 +31047,488 @@ this.Documonster = this.Documonster || {};
31020
31047
  }));
31021
31048
  //#endregion
31022
31049
  //#region src/modules/pdf/render/constants.ts
31023
- var LINE_HEIGHT_FACTOR, PX_TO_PT;
31050
+ var LINE_HEIGHT_FACTOR, PX_TO_PT, HEADING_FILL, HEADING_RULE, HEADING_TEXT, FIT_MIN_SCALE, COMMENT_FILL, COMMENT_BORDER, COMMENT_MARKER_COLOR, COLUMN_FIT_EPSILON;
31024
31051
  var init_constants = __esmMin((() => {
31025
31052
  LINE_HEIGHT_FACTOR = 1.2;
31026
31053
  PX_TO_PT = 72 / 96;
31054
+ HEADING_FILL = {
31055
+ r: .94,
31056
+ g: .94,
31057
+ b: .94
31058
+ };
31059
+ HEADING_RULE = {
31060
+ r: .6,
31061
+ g: .6,
31062
+ b: .6
31063
+ };
31064
+ HEADING_TEXT = {
31065
+ r: .2,
31066
+ g: .2,
31067
+ b: .2
31068
+ };
31069
+ FIT_MIN_SCALE = .1;
31070
+ COMMENT_FILL = {
31071
+ r: 1,
31072
+ g: 1,
31073
+ b: .88
31074
+ };
31075
+ COMMENT_BORDER = {
31076
+ r: .4,
31077
+ g: .4,
31078
+ b: .4
31079
+ };
31080
+ COMMENT_MARKER_COLOR = {
31081
+ r: .8,
31082
+ g: 0,
31083
+ b: 0
31084
+ };
31085
+ COLUMN_FIT_EPSILON = .01;
31086
+ }));
31087
+ //#endregion
31088
+ //#region src/utils/theme-colors.ts
31089
+ /**
31090
+ * Parse a hex colour string into 0–1 float RGB components.
31091
+ *
31092
+ * Accepts `RRGGBB` (6 digits), `AARRGGBB` (8 digits, alpha first — the form
31093
+ * Excel uses for cell fills), and `RGB` (3 digits, CSS shorthand). A leading
31094
+ * `#` is tolerated. Returns `null` for anything else.
31095
+ */
31096
+ function hexToRgb01(hex) {
31097
+ if (!hex) return null;
31098
+ let h = hex.startsWith("#") ? hex.slice(1) : hex;
31099
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
31100
+ let a = 255;
31101
+ let rHex;
31102
+ let gHex;
31103
+ let bHex;
31104
+ if (h.length === 8) {
31105
+ a = parseInt(h.slice(0, 2), 16);
31106
+ rHex = h.slice(2, 4);
31107
+ gHex = h.slice(4, 6);
31108
+ bHex = h.slice(6, 8);
31109
+ } else if (h.length === 6) {
31110
+ rHex = h.slice(0, 2);
31111
+ gHex = h.slice(2, 4);
31112
+ bHex = h.slice(4, 6);
31113
+ } else return null;
31114
+ const r = parseInt(rHex, 16);
31115
+ const g = parseInt(gHex, 16);
31116
+ const b = parseInt(bHex, 16);
31117
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) || Number.isNaN(a)) return null;
31118
+ const alpha = a / 255;
31119
+ return alpha < 1 ? {
31120
+ r: r / 255,
31121
+ g: g / 255,
31122
+ b: b / 255,
31123
+ a: alpha
31124
+ } : {
31125
+ r: r / 255,
31126
+ g: g / 255,
31127
+ b: b / 255
31128
+ };
31129
+ }
31130
+ /**
31131
+ * Apply a tint to a 0–1 float RGB colour, per OOXML §18.8.19.
31132
+ *
31133
+ * Tint ∈ [-1, 1]: positive lightens toward white
31134
+ * (`c + (1 - c) * tint`), negative darkens toward black
31135
+ * (`c * (1 + tint)`). `tint = 0` returns the colour unchanged. Result
31136
+ * components are clamped to [0, 1]; alpha is preserved.
31137
+ */
31138
+ function applyTintRgb01(color, tint) {
31139
+ const apply = (c) => {
31140
+ const v = tint < 0 ? c * (1 + tint) : c + (1 - c) * tint;
31141
+ return Math.max(0, Math.min(1, v));
31142
+ };
31143
+ const out = {
31144
+ r: apply(color.r),
31145
+ g: apply(color.g),
31146
+ b: apply(color.b)
31147
+ };
31148
+ if (color.a !== void 0) out.a = color.a;
31149
+ return out;
31150
+ }
31151
+ var DEFAULT_OFFICE_THEME, CHART_THEME_PALETTE, CELL_THEME_PALETTE;
31152
+ var init_theme_colors = __esmMin((() => {
31153
+ DEFAULT_OFFICE_THEME = Object.freeze({
31154
+ dk1: "000000",
31155
+ lt1: "FFFFFF",
31156
+ dk2: "44546A",
31157
+ lt2: "E7E6E6",
31158
+ accent1: "4472C4",
31159
+ accent2: "ED7D31",
31160
+ accent3: "A5A5A5",
31161
+ accent4: "FFC000",
31162
+ accent5: "5B9BD5",
31163
+ accent6: "70AD47",
31164
+ hlink: "0563C1",
31165
+ folHlink: "954F72"
31166
+ });
31167
+ CHART_THEME_PALETTE = Object.freeze([
31168
+ DEFAULT_OFFICE_THEME.dk1,
31169
+ DEFAULT_OFFICE_THEME.lt1,
31170
+ DEFAULT_OFFICE_THEME.dk2,
31171
+ DEFAULT_OFFICE_THEME.lt2,
31172
+ DEFAULT_OFFICE_THEME.accent1,
31173
+ DEFAULT_OFFICE_THEME.accent2,
31174
+ DEFAULT_OFFICE_THEME.accent3,
31175
+ DEFAULT_OFFICE_THEME.accent4,
31176
+ DEFAULT_OFFICE_THEME.accent5,
31177
+ DEFAULT_OFFICE_THEME.accent6,
31178
+ DEFAULT_OFFICE_THEME.hlink,
31179
+ DEFAULT_OFFICE_THEME.folHlink
31180
+ ]);
31181
+ CELL_THEME_PALETTE = Object.freeze([
31182
+ DEFAULT_OFFICE_THEME.lt1,
31183
+ DEFAULT_OFFICE_THEME.dk1,
31184
+ DEFAULT_OFFICE_THEME.lt2,
31185
+ DEFAULT_OFFICE_THEME.dk2,
31186
+ DEFAULT_OFFICE_THEME.accent1,
31187
+ DEFAULT_OFFICE_THEME.accent2,
31188
+ DEFAULT_OFFICE_THEME.accent3,
31189
+ DEFAULT_OFFICE_THEME.accent4,
31190
+ DEFAULT_OFFICE_THEME.accent5,
31191
+ DEFAULT_OFFICE_THEME.accent6,
31192
+ DEFAULT_OFFICE_THEME.hlink,
31193
+ DEFAULT_OFFICE_THEME.folHlink
31194
+ ]);
31195
+ }));
31196
+ //#endregion
31197
+ //#region src/modules/pdf/render/style-converter.ts
31198
+ /**
31199
+ * Convert an ARGB color string to PDF RGB color.
31200
+ * Handles "AARRGGBB" (8-char) and "RRGGBB" (6-char) formats.
31201
+ * PDF uses 0-1 floats for each component.
31202
+ */
31203
+ function argbToPdfColor(argb) {
31204
+ if (!argb || argb.length < 6) return null;
31205
+ return hexToRgb01(argb);
31206
+ }
31207
+ /**
31208
+ * Convert a color data object to PDF color.
31209
+ * Handles ARGB, theme-based, and indexed colors.
31210
+ */
31211
+ function excelColorToPdf(color) {
31212
+ if (!color) return null;
31213
+ if (color.argb) return argbToPdfColor(color.argb);
31214
+ if (color.theme !== void 0) {
31215
+ const base = themeColorToPdf(color.theme);
31216
+ if (!base) return null;
31217
+ const tint = color.tint;
31218
+ if (tint !== void 0 && tint !== 0) return applyTint(base, tint);
31219
+ return base;
31220
+ }
31221
+ if (color.indexed !== void 0) return indexedColorToPdf(color.indexed);
31222
+ return null;
31223
+ }
31224
+ /**
31225
+ * Map a cell-colour theme index to a PDF colour using the default Office
31226
+ * palette in SpreadsheetML cell order (0=lt1, 1=dk1, 2=lt2, 3=dk2, …).
31227
+ */
31228
+ function themeColorToPdf(themeIndex) {
31229
+ if (themeIndex >= 0 && themeIndex < CELL_THEME_PALETTE.length) return hexToRgb01(CELL_THEME_PALETTE[themeIndex]);
31230
+ return null;
31231
+ }
31232
+ /**
31233
+ * Convert an indexed color to PDF color.
31234
+ * Index 64 = system foreground (black), 65 = system background (white).
31235
+ */
31236
+ function indexedColorToPdf(index) {
31237
+ if (index === 64) return {
31238
+ r: 0,
31239
+ g: 0,
31240
+ b: 0
31241
+ };
31242
+ if (index === 65) return {
31243
+ r: 1,
31244
+ g: 1,
31245
+ b: 1
31246
+ };
31247
+ if (index >= 0 && index < INDEXED_COLORS.length) return argbToPdfColor(INDEXED_COLORS[index]) ?? null;
31248
+ return null;
31249
+ }
31250
+ /**
31251
+ * Apply a tint value to a color.
31252
+ * Tint range: -1.0 (fully dark) to +1.0 (fully light).
31253
+ * Negative tint darkens, positive tint lightens.
31254
+ *
31255
+ * @see OOXML §18.8.19 - tint formula
31256
+ */
31257
+ function applyTint(color, tint) {
31258
+ return applyTintRgb01(color, tint);
31259
+ }
31260
+ /**
31261
+ * Convert a color to its luminance-preserving grayscale equivalent, using the
31262
+ * Rec. 601 luma weights. Backs Excel's "Black and white" print option: hues are
31263
+ * discarded while relative lightness — and therefore contrast — is kept.
31264
+ *
31265
+ * Opacity is carried through untouched; dropping it would make transparent
31266
+ * chart fills opaque.
31267
+ */
31268
+ function toGrayscale(color) {
31269
+ const luma = .299 * color.r + .587 * color.g + .114 * color.b;
31270
+ return color.a === void 0 ? {
31271
+ r: luma,
31272
+ g: luma,
31273
+ b: luma
31274
+ } : {
31275
+ r: luma,
31276
+ g: luma,
31277
+ b: luma,
31278
+ a: color.a
31279
+ };
31280
+ }
31281
+ /** Grayscale every edge of a border set, preserving `null` edges. */
31282
+ function grayscaleBorders(borders) {
31283
+ const convert = (b) => b === null ? null : {
31284
+ ...b,
31285
+ color: toGrayscale(b.color)
31286
+ };
31287
+ return {
31288
+ top: convert(borders.top),
31289
+ right: convert(borders.right),
31290
+ bottom: convert(borders.bottom),
31291
+ left: convert(borders.left)
31292
+ };
31293
+ }
31294
+ /**
31295
+ * Extract font properties for PDF rendering.
31296
+ */
31297
+ function extractFontProperties(font, defaultFamily, defaultSize) {
31298
+ return {
31299
+ fontFamily: font?.name ?? defaultFamily,
31300
+ fontSize: font?.size ?? defaultSize,
31301
+ bold: font?.bold ?? false,
31302
+ italic: font?.italic ?? false,
31303
+ strike: font?.strike ?? false,
31304
+ underline: !!(font?.underline && font.underline !== "none"),
31305
+ textColor: excelColorToPdf(font?.color) ?? DEFAULT_COLORS.black
31306
+ };
31307
+ }
31308
+ /**
31309
+ * Convert a fill to a PDF background color.
31310
+ * Only pattern fills with "solid" pattern are supported as PDF backgrounds.
31311
+ * Other patterns are approximated or ignored.
31312
+ */
31313
+ function excelFillToPdfColor(fill) {
31314
+ if (!fill) return null;
31315
+ if (fill.type === "pattern") {
31316
+ if (fill.pattern === "solid" && fill.fgColor) return excelColorToPdf(fill.fgColor);
31317
+ if (fill.pattern === "none") return null;
31318
+ if (fill.fgColor) return excelColorToPdf(fill.fgColor);
31319
+ }
31320
+ if (fill.type === "gradient") {
31321
+ if (fill.stops && fill.stops.length > 0) return excelColorToPdf(fill.stops[0].color);
31322
+ }
31323
+ return null;
31324
+ }
31325
+ /**
31326
+ * Map border styles to PDF line widths (in points).
31327
+ *
31328
+ * Values match Excel's actual border weights as used historically by this
31329
+ * library. An earlier change doubled every width (0.25 → 0.5, 0.5 → 1,
31330
+ * 1 → 2) to make `thin` and `medium` more visually distinct in PDF
31331
+ * viewers, but that change made all borders heavier than Excel itself.
31332
+ * The 2× ratio between thin/medium and the 4× ratio between
31333
+ * thin/thick are preserved with the lighter values, so styles remain
31334
+ * distinguishable while matching Excel.
31335
+ *
31336
+ * hair = 0.1 pt
31337
+ * thin = 0.25 pt (also dotted, dashed, dashDot, dashDotDot, slantDashDot, double)
31338
+ * medium = 0.5 pt (also mediumDashed, mediumDashDot, mediumDashDotDot)
31339
+ * thick = 1 pt
31340
+ */
31341
+ function borderStyleToLineWidth(style) {
31342
+ switch (style) {
31343
+ case "hair": return .1;
31344
+ case "thin": return .25;
31345
+ case "medium": return .5;
31346
+ case "thick": return 1;
31347
+ case "double": return .25;
31348
+ case "dotted": return .25;
31349
+ case "dashed": return .25;
31350
+ case "dashDot": return .25;
31351
+ case "dashDotDot": return .25;
31352
+ case "slantDashDot": return .25;
31353
+ case "mediumDashed": return .5;
31354
+ case "mediumDashDot": return .5;
31355
+ case "mediumDashDotDot": return .5;
31356
+ default: return .25;
31357
+ }
31358
+ }
31359
+ /**
31360
+ * Map border styles to PDF dash patterns.
31361
+ * An empty array means a solid line.
31362
+ */
31363
+ function borderStyleToDashPattern(style) {
31364
+ switch (style) {
31365
+ case "dotted": return [1, 1];
31366
+ case "dashed":
31367
+ case "mediumDashed": return [3, 2];
31368
+ case "dashDot":
31369
+ case "mediumDashDot": return [
31370
+ 3,
31371
+ 1,
31372
+ 1,
31373
+ 1
31374
+ ];
31375
+ case "dashDotDot":
31376
+ case "mediumDashDotDot": return [
31377
+ 3,
31378
+ 1,
31379
+ 1,
31380
+ 1,
31381
+ 1,
31382
+ 1
31383
+ ];
31384
+ case "slantDashDot": return [
31385
+ 4,
31386
+ 2,
31387
+ 1,
31388
+ 2
31389
+ ];
31390
+ case "hair": return [.5, .5];
31391
+ default: return [];
31392
+ }
31393
+ }
31394
+ /**
31395
+ * Convert a single border side to a PDF LayoutBorder.
31396
+ */
31397
+ function convertBorder(border) {
31398
+ if (!border || !border.style) return null;
31399
+ return {
31400
+ width: borderStyleToLineWidth(border.style),
31401
+ color: excelColorToPdf(border.color) ?? DEFAULT_COLORS.black,
31402
+ dashPattern: borderStyleToDashPattern(border.style),
31403
+ isDouble: border.style === "double"
31404
+ };
31405
+ }
31406
+ /**
31407
+ * Convert border data to PDF LayoutBorders.
31408
+ */
31409
+ function excelBordersToPdf(borders) {
31410
+ if (!borders) return {
31411
+ top: null,
31412
+ right: null,
31413
+ bottom: null,
31414
+ left: null
31415
+ };
31416
+ return {
31417
+ top: convertBorder(borders.top),
31418
+ right: convertBorder(borders.right),
31419
+ bottom: convertBorder(borders.bottom),
31420
+ left: convertBorder(borders.left)
31421
+ };
31422
+ }
31423
+ /**
31424
+ * Convert horizontal alignment to PDF alignment.
31425
+ */
31426
+ function excelHAlignToPdf(alignment) {
31427
+ if (!alignment?.horizontal) return "left";
31428
+ switch (alignment.horizontal) {
31429
+ case "center":
31430
+ case "centerContinuous": return "center";
31431
+ case "right": return "right";
31432
+ default: return "left";
31433
+ }
31434
+ }
31435
+ /**
31436
+ * Convert vertical alignment to PDF alignment.
31437
+ */
31438
+ function excelVAlignToPdf(alignment) {
31439
+ if (!alignment?.vertical) return "bottom";
31440
+ switch (alignment.vertical) {
31441
+ case "top": return "top";
31442
+ case "middle": return "middle";
31443
+ default: return "bottom";
31444
+ }
31445
+ }
31446
+ var INDEXED_COLORS, DEFAULT_COLORS;
31447
+ var init_style_converter = __esmMin((() => {
31448
+ init_theme_colors();
31449
+ INDEXED_COLORS = [
31450
+ "000000",
31451
+ "FFFFFF",
31452
+ "FF0000",
31453
+ "00FF00",
31454
+ "0000FF",
31455
+ "FFFF00",
31456
+ "FF00FF",
31457
+ "00FFFF",
31458
+ "000000",
31459
+ "FFFFFF",
31460
+ "FF0000",
31461
+ "00FF00",
31462
+ "0000FF",
31463
+ "FFFF00",
31464
+ "FF00FF",
31465
+ "00FFFF",
31466
+ "800000",
31467
+ "008000",
31468
+ "000080",
31469
+ "808000",
31470
+ "800080",
31471
+ "008080",
31472
+ "C0C0C0",
31473
+ "808080",
31474
+ "9999FF",
31475
+ "993366",
31476
+ "FFFFCC",
31477
+ "CCFFFF",
31478
+ "660066",
31479
+ "FF8080",
31480
+ "0066CC",
31481
+ "CCCCFF",
31482
+ "000080",
31483
+ "FF00FF",
31484
+ "FFFF00",
31485
+ "00FFFF",
31486
+ "800080",
31487
+ "800000",
31488
+ "008080",
31489
+ "0000FF",
31490
+ "00CCFF",
31491
+ "CCFFFF",
31492
+ "CCFFCC",
31493
+ "FFFF99",
31494
+ "99CCFF",
31495
+ "FF99CC",
31496
+ "CC99FF",
31497
+ "FFCC99",
31498
+ "3366FF",
31499
+ "33CCCC",
31500
+ "99CC00",
31501
+ "FFCC00",
31502
+ "FF9900",
31503
+ "FF6600",
31504
+ "666699",
31505
+ "969696",
31506
+ "003366",
31507
+ "339966",
31508
+ "003300",
31509
+ "333300",
31510
+ "993300",
31511
+ "993366",
31512
+ "333399",
31513
+ "333333"
31514
+ ];
31515
+ DEFAULT_COLORS = {
31516
+ black: {
31517
+ r: 0,
31518
+ g: 0,
31519
+ b: 0
31520
+ },
31521
+ white: {
31522
+ r: 1,
31523
+ g: 1,
31524
+ b: 1
31525
+ },
31526
+ gridLine: {
31527
+ r: .816,
31528
+ g: .816,
31529
+ b: .816
31530
+ }
31531
+ };
31027
31532
  }));
31028
31533
  //#endregion
31029
31534
  //#region src/modules/pdf/render/page-renderer.ts
@@ -31086,6 +31591,8 @@ this.Documonster = this.Documonster || {};
31086
31591
  for (const cell of page.cells) drawCellBorders(stream, cell);
31087
31592
  const sf = page.scaleFactor;
31088
31593
  for (const cell of page.cells) if (cell.text || cell.richText) drawCellText(stream, cell, fontManager, alphaValues, sf);
31594
+ if (options.showRowColHeaders) drawRowColHeadings(stream, page, options, fontManager);
31595
+ if (page.commentBoxes?.length) drawCommentBoxes(stream, page, options, fontManager);
31089
31596
  const excelHeader = selectHeaderFooter(page, "header");
31090
31597
  const excelFooter = selectHeaderFooter(page, "footer");
31091
31598
  if (excelHeader !== void 0) {
@@ -31987,11 +32494,13 @@ this.Documonster = this.Documonster || {};
31987
32494
  for (const item of resolved) {
31988
32495
  const { run, text, resourceName, fontSize, width } = item;
31989
32496
  const shift = run.superscript ? fontSize * HEADER_FOOTER_METRICS.SUPERSCRIPT_RISE : run.subscript ? -fontSize * HEADER_FOOTER_METRICS.SUBSCRIPT_DROP : 0;
31990
- const color = run.color ?? {
32497
+ const bw = page.options.blackAndWhite;
32498
+ const rawColor = run.color ?? {
31991
32499
  r: 0,
31992
32500
  g: 0,
31993
32501
  b: 0
31994
32502
  };
32503
+ const color = bw ? toGrayscale(rawColor) : rawColor;
31995
32504
  stream.save();
31996
32505
  stream.setFillColor(color);
31997
32506
  stream.setStrokeColor(color);
@@ -32055,6 +32564,169 @@ this.Documonster = this.Documonster || {};
32055
32564
  };
32056
32565
  });
32057
32566
  }
32567
+ /** Convert a 1-based column number to its Excel letters (1 → "A", 27 → "AA"). */
32568
+ function columnNumberToLetters(col) {
32569
+ let n = col;
32570
+ let out = "";
32571
+ while (n > 0) {
32572
+ const rem = (n - 1) % 26;
32573
+ out = String.fromCharCode(65 + rem) + out;
32574
+ n = Math.floor((n - 1) / 26);
32575
+ }
32576
+ return out;
32577
+ }
32578
+ /**
32579
+ * Draw Excel's printed row numbers and column letters around the grid.
32580
+ *
32581
+ * The bands occupy space reserved by the layout engine (see
32582
+ * `computeHeadingMetrics`), so the grid origin already accounts for them and
32583
+ * nothing here can push content off the page.
32584
+ */
32585
+ function drawRowColHeadings(stream, page, options, fontManager) {
32586
+ const headings = page.headings;
32587
+ if (!headings || page.columnWidths.length === 0 || page.rowHeights.length === 0) return;
32588
+ const { gutterWidth, bandHeight, fontSize } = headings;
32589
+ const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(options.defaultFontFamily, false, false));
32590
+ const fill = HEADING_FILL;
32591
+ const rule = HEADING_RULE;
32592
+ const textColor = HEADING_TEXT;
32593
+ const gridLeft = page.columnOffsets[0];
32594
+ const lastCol = page.columnWidths.length - 1;
32595
+ const gridRight = page.columnOffsets[lastCol] + page.columnWidths[lastCol];
32596
+ const gridTop = page.rowYPositions[0];
32597
+ const lastRow = page.rowHeights.length - 1;
32598
+ const gridBottom = page.rowYPositions[lastRow] - page.rowHeights[lastRow];
32599
+ stream.save();
32600
+ stream.fillRect(gridLeft, gridTop, gridRight - gridLeft, bandHeight, fill);
32601
+ stream.fillRect(gridLeft - gutterWidth, gridBottom, gutterWidth, gridTop - gridBottom, fill);
32602
+ stream.fillRect(gridLeft - gutterWidth, gridTop, gutterWidth, bandHeight, fill);
32603
+ stream.setStrokeColor(rule);
32604
+ stream.setLineWidth(.25);
32605
+ for (let i = 0; i <= page.columnWidths.length; i++) {
32606
+ const x = i < page.columnWidths.length ? page.columnOffsets[i] : page.columnOffsets[i - 1] + page.columnWidths[i - 1];
32607
+ stream.moveTo(x, gridTop);
32608
+ stream.lineTo(x, gridTop + bandHeight);
32609
+ }
32610
+ for (let i = 0; i <= page.rowYPositions.length; i++) {
32611
+ const y = i < page.rowYPositions.length ? page.rowYPositions[i] : page.rowYPositions[i - 1] - page.rowHeights[i - 1];
32612
+ stream.moveTo(gridLeft - gutterWidth, y);
32613
+ stream.lineTo(gridLeft, y);
32614
+ }
32615
+ stream.moveTo(gridLeft - gutterWidth, gridTop + bandHeight);
32616
+ stream.lineTo(gridRight, gridTop + bandHeight);
32617
+ stream.moveTo(gridLeft - gutterWidth, gridBottom);
32618
+ stream.lineTo(gridLeft - gutterWidth, gridTop + bandHeight);
32619
+ stream.stroke();
32620
+ stream.setFillColor(textColor);
32621
+ const baselineOffset = (bandHeight - fontSize) / 2 + fontSize * .2;
32622
+ for (let i = 0; i < page.sheetCols.length; i++) {
32623
+ const label = columnNumberToLetters(page.sheetCols[i]);
32624
+ fontManager.trackText(label);
32625
+ const w = fontManager.measureText(label, resourceName, fontSize);
32626
+ emitTextWithMatrix(stream, fontManager, {
32627
+ text: label,
32628
+ matrix: [
32629
+ 1,
32630
+ 0,
32631
+ 0,
32632
+ 1,
32633
+ page.columnOffsets[i] + (page.columnWidths[i] - w) / 2,
32634
+ gridTop + baselineOffset
32635
+ ],
32636
+ resourceName,
32637
+ fontSize
32638
+ });
32639
+ }
32640
+ for (let i = 0; i < page.sheetRows.length; i++) {
32641
+ const label = String(page.sheetRows[i]);
32642
+ fontManager.trackText(label);
32643
+ const w = fontManager.measureText(label, resourceName, fontSize);
32644
+ const rowH = page.rowHeights[i];
32645
+ emitTextWithMatrix(stream, fontManager, {
32646
+ text: label,
32647
+ matrix: [
32648
+ 1,
32649
+ 0,
32650
+ 0,
32651
+ 1,
32652
+ gridLeft - gutterWidth + (gutterWidth - w) / 2,
32653
+ page.rowYPositions[i] - rowH + (rowH - fontSize) / 2 + fontSize * .2
32654
+ ],
32655
+ resourceName,
32656
+ fontSize
32657
+ });
32658
+ }
32659
+ stream.restore();
32660
+ }
32661
+ /**
32662
+ * Draw Excel's on-sheet comment boxes and their cell corner markers.
32663
+ *
32664
+ * Positioned by the layout engine, so this only has to paint: a pale note-yellow
32665
+ * box with a thin border, the wrapped body inside it, and the small red triangle
32666
+ * Excel puts in the commented cell's top-right corner.
32667
+ */
32668
+ function drawCommentBoxes(stream, page, options, fontManager) {
32669
+ const boxes = page.commentBoxes;
32670
+ if (!boxes?.length) return;
32671
+ const bw = options.blackAndWhite;
32672
+ const fill = bw ? toGrayscale(COMMENT_FILL) : COMMENT_FILL;
32673
+ const border = bw ? toGrayscale(COMMENT_BORDER) : COMMENT_BORDER;
32674
+ const markerColor = bw ? toGrayscale(COMMENT_MARKER_COLOR) : COMMENT_MARKER_COLOR;
32675
+ const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(options.defaultFontFamily, false, false));
32676
+ for (const box of boxes) {
32677
+ const { rect } = box;
32678
+ stream.save();
32679
+ stream.fillRect(rect.x, rect.y, rect.width, rect.height, fill);
32680
+ stream.setStrokeColor(border);
32681
+ stream.setLineWidth(.5);
32682
+ stream.rect(rect.x, rect.y, rect.width, rect.height);
32683
+ stream.stroke();
32684
+ const lineHeight = box.fontSize * LINE_HEIGHT_FACTOR;
32685
+ const innerWidth = Math.max(rect.width - 6, 1);
32686
+ const measure = (t) => fontManager.measureText(t, resourceName, box.fontSize);
32687
+ const lines = box.text.split("\n").flatMap((part) => wrapTextLines(part, measure, innerWidth));
32688
+ stream.setFillColor(bw ? toGrayscale({
32689
+ r: 0,
32690
+ g: 0,
32691
+ b: 0
32692
+ }) : {
32693
+ r: 0,
32694
+ g: 0,
32695
+ b: 0
32696
+ });
32697
+ let baseline = rect.y + rect.height - 2 - box.fontSize;
32698
+ for (const line of lines) {
32699
+ if (baseline < rect.y + 2) break;
32700
+ fontManager.trackText(line);
32701
+ emitTextWithMatrix(stream, fontManager, {
32702
+ text: line,
32703
+ matrix: [
32704
+ 1,
32705
+ 0,
32706
+ 0,
32707
+ 1,
32708
+ rect.x + 3,
32709
+ baseline
32710
+ ],
32711
+ resourceName,
32712
+ fontSize: box.fontSize
32713
+ });
32714
+ baseline -= lineHeight;
32715
+ }
32716
+ stream.restore();
32717
+ if (box.marker) {
32718
+ const { x, y, size } = box.marker;
32719
+ stream.save();
32720
+ stream.setFillColor(markerColor);
32721
+ stream.moveTo(x - size, y);
32722
+ stream.lineTo(x, y);
32723
+ stream.lineTo(x, y - size);
32724
+ stream.closePath();
32725
+ stream.fill();
32726
+ stream.restore();
32727
+ }
32728
+ }
32729
+ }
32058
32730
  function drawPageHeader(stream, page, options, fontManager) {
32059
32731
  const headerFontSize = 10;
32060
32732
  const headerText = page.sheetName;
@@ -32143,7 +32815,8 @@ this.Documonster = this.Documonster || {};
32143
32815
  */
32144
32816
  function renderTextWatermark(stream, page, watermark, fontManager) {
32145
32817
  const fontSize = watermark.fontSize ?? TEXT_WM_DEFAULTS.fontSize;
32146
- const color = watermark.color ?? TEXT_WM_DEFAULTS.color;
32818
+ const rawColor = watermark.color ?? TEXT_WM_DEFAULTS.color;
32819
+ const color = page.options.blackAndWhite ? toGrayscale(rawColor) : rawColor;
32147
32820
  const opacity = watermark.opacity ?? TEXT_WM_DEFAULTS.opacity;
32148
32821
  const rotation = watermark.rotation ?? TEXT_WM_DEFAULTS.rotation;
32149
32822
  const fontFamily = watermark.fontFamily ?? TEXT_WM_DEFAULTS.fontFamily;
@@ -32282,6 +32955,7 @@ this.Documonster = this.Documonster || {};
32282
32955
  init_pdf_stream();
32283
32956
  init_font_manager();
32284
32957
  init_constants();
32958
+ init_style_converter();
32285
32959
  HEADER_FOOTER_DATE_OPTIONS = {
32286
32960
  year: "numeric",
32287
32961
  month: "numeric",
@@ -32337,115 +33011,6 @@ this.Documonster = this.Documonster || {};
32337
33011
  MIN_REPEAT_SPACING = 10;
32338
33012
  }));
32339
33013
  //#endregion
32340
- //#region src/utils/theme-colors.ts
32341
- /**
32342
- * Parse a hex colour string into 0–1 float RGB components.
32343
- *
32344
- * Accepts `RRGGBB` (6 digits), `AARRGGBB` (8 digits, alpha first — the form
32345
- * Excel uses for cell fills), and `RGB` (3 digits, CSS shorthand). A leading
32346
- * `#` is tolerated. Returns `null` for anything else.
32347
- */
32348
- function hexToRgb01(hex) {
32349
- if (!hex) return null;
32350
- let h = hex.startsWith("#") ? hex.slice(1) : hex;
32351
- if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
32352
- let a = 255;
32353
- let rHex;
32354
- let gHex;
32355
- let bHex;
32356
- if (h.length === 8) {
32357
- a = parseInt(h.slice(0, 2), 16);
32358
- rHex = h.slice(2, 4);
32359
- gHex = h.slice(4, 6);
32360
- bHex = h.slice(6, 8);
32361
- } else if (h.length === 6) {
32362
- rHex = h.slice(0, 2);
32363
- gHex = h.slice(2, 4);
32364
- bHex = h.slice(4, 6);
32365
- } else return null;
32366
- const r = parseInt(rHex, 16);
32367
- const g = parseInt(gHex, 16);
32368
- const b = parseInt(bHex, 16);
32369
- if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) || Number.isNaN(a)) return null;
32370
- const alpha = a / 255;
32371
- return alpha < 1 ? {
32372
- r: r / 255,
32373
- g: g / 255,
32374
- b: b / 255,
32375
- a: alpha
32376
- } : {
32377
- r: r / 255,
32378
- g: g / 255,
32379
- b: b / 255
32380
- };
32381
- }
32382
- /**
32383
- * Apply a tint to a 0–1 float RGB colour, per OOXML §18.8.19.
32384
- *
32385
- * Tint ∈ [-1, 1]: positive lightens toward white
32386
- * (`c + (1 - c) * tint`), negative darkens toward black
32387
- * (`c * (1 + tint)`). `tint = 0` returns the colour unchanged. Result
32388
- * components are clamped to [0, 1]; alpha is preserved.
32389
- */
32390
- function applyTintRgb01(color, tint) {
32391
- const apply = (c) => {
32392
- const v = tint < 0 ? c * (1 + tint) : c + (1 - c) * tint;
32393
- return Math.max(0, Math.min(1, v));
32394
- };
32395
- const out = {
32396
- r: apply(color.r),
32397
- g: apply(color.g),
32398
- b: apply(color.b)
32399
- };
32400
- if (color.a !== void 0) out.a = color.a;
32401
- return out;
32402
- }
32403
- var DEFAULT_OFFICE_THEME, CHART_THEME_PALETTE, CELL_THEME_PALETTE;
32404
- var init_theme_colors = __esmMin((() => {
32405
- DEFAULT_OFFICE_THEME = Object.freeze({
32406
- dk1: "000000",
32407
- lt1: "FFFFFF",
32408
- dk2: "44546A",
32409
- lt2: "E7E6E6",
32410
- accent1: "4472C4",
32411
- accent2: "ED7D31",
32412
- accent3: "A5A5A5",
32413
- accent4: "FFC000",
32414
- accent5: "5B9BD5",
32415
- accent6: "70AD47",
32416
- hlink: "0563C1",
32417
- folHlink: "954F72"
32418
- });
32419
- CHART_THEME_PALETTE = Object.freeze([
32420
- DEFAULT_OFFICE_THEME.dk1,
32421
- DEFAULT_OFFICE_THEME.lt1,
32422
- DEFAULT_OFFICE_THEME.dk2,
32423
- DEFAULT_OFFICE_THEME.lt2,
32424
- DEFAULT_OFFICE_THEME.accent1,
32425
- DEFAULT_OFFICE_THEME.accent2,
32426
- DEFAULT_OFFICE_THEME.accent3,
32427
- DEFAULT_OFFICE_THEME.accent4,
32428
- DEFAULT_OFFICE_THEME.accent5,
32429
- DEFAULT_OFFICE_THEME.accent6,
32430
- DEFAULT_OFFICE_THEME.hlink,
32431
- DEFAULT_OFFICE_THEME.folHlink
32432
- ]);
32433
- CELL_THEME_PALETTE = Object.freeze([
32434
- DEFAULT_OFFICE_THEME.lt1,
32435
- DEFAULT_OFFICE_THEME.dk1,
32436
- DEFAULT_OFFICE_THEME.lt2,
32437
- DEFAULT_OFFICE_THEME.dk2,
32438
- DEFAULT_OFFICE_THEME.accent1,
32439
- DEFAULT_OFFICE_THEME.accent2,
32440
- DEFAULT_OFFICE_THEME.accent3,
32441
- DEFAULT_OFFICE_THEME.accent4,
32442
- DEFAULT_OFFICE_THEME.accent5,
32443
- DEFAULT_OFFICE_THEME.accent6,
32444
- DEFAULT_OFFICE_THEME.hlink,
32445
- DEFAULT_OFFICE_THEME.folHlink
32446
- ]);
32447
- }));
32448
- //#endregion
32449
33014
  //#region src/modules/pdf/core/digital-signature.ts
32450
33015
  var digital_signature_exports = /* @__PURE__ */ __exportAll({
32451
33016
  asn1Parse: () => asn1Parse,
@@ -34488,29 +35053,66 @@ this.Documonster = this.Documonster || {};
34488
35053
  * @param alphaValues Shared set that accumulates transparency values for
34489
35054
  * later `ExtGState` registration. The surface adds any
34490
35055
  * `color.a` values it observes to this set.
34491
- */
34492
- function createChartSurface(stream, fontManager, alphaValues) {
35056
+ * @param grayscale When true, every color the chart renderer emits is
35057
+ * converted to grayscale. Charts bypass the cell style
35058
+ * pipeline entirely — the chart engine pushes colors
35059
+ * straight into this surface — so Excel's "Black and
35060
+ * white" print option has to be applied right here.
35061
+ */
35062
+ function createChartSurface(stream, fontManager, alphaValues, grayscale = false) {
35063
+ /** Single choke point for every color entering the surface. */
35064
+ const mapColor = (color) => grayscale && color ? toGrayscale(color) : color;
34493
35065
  const applyAlpha = (color) => {
34494
35066
  if (color?.a !== void 0 && color.a < 1) {
34495
35067
  alphaValues.add(color.a);
34496
35068
  stream.setGraphicsState(alphaGsName(color.a));
34497
35069
  }
34498
35070
  };
34499
- const paintFillStroke = (options) => {
34500
- const { fill, stroke, lineWidth } = options;
35071
+ /**
35072
+ * Fill and/or stroke the path built by `buildPath`.
35073
+ *
35074
+ * `buildPath` is a closure rather than a pre-built path because a fill and a
35075
+ * stroke with *different* opacities cannot share one graphics state: the
35076
+ * alpha `ExtGState` sets `/ca` and `/CA` together, and `fillAndStroke()`
35077
+ * paints both under whichever was applied last. In that case we paint in two
35078
+ * passes and rebuild the path for the second one. (Within a single pass the
35079
+ * unused half of the ExtGState is harmless — a fill-only pass never strokes.)
35080
+ */
35081
+ const paintFillStroke = (buildPath, options) => {
35082
+ const { lineWidth, dashPattern } = options;
35083
+ const fill = mapColor(options.fill);
35084
+ const stroke = mapColor(options.stroke);
35085
+ const applyDash = () => {
35086
+ if (dashPattern && dashPattern.length > 0) stream.setDashPattern(dashPattern);
35087
+ };
35088
+ if (fill && stroke) {
35089
+ if ((fill.a ?? 1) !== (stroke.a ?? 1)) {
35090
+ stream.save();
35091
+ buildPath();
35092
+ stream.setFillColor(fill);
35093
+ applyAlpha(fill);
35094
+ stream.fill();
35095
+ stream.restore();
35096
+ stream.save();
35097
+ buildPath();
35098
+ stream.setStrokeColor(stroke);
35099
+ applyAlpha(stroke);
35100
+ applyDash();
35101
+ if (lineWidth !== void 0) stream.setLineWidth(lineWidth);
35102
+ stream.stroke();
35103
+ stream.restore();
35104
+ return;
35105
+ }
35106
+ }
35107
+ buildPath();
35108
+ applyDash();
34501
35109
  if (fill) {
34502
35110
  stream.setFillColor(fill);
34503
35111
  applyAlpha(fill);
34504
35112
  }
34505
35113
  if (stroke) {
34506
35114
  stream.setStrokeColor(stroke);
34507
- const strokeNeedsAlpha = stroke.a !== void 0 && stroke.a < 1;
34508
- const fillAppliedAlpha = fill?.a !== void 0 && fill.a < 1;
34509
- if (strokeNeedsAlpha) applyAlpha(stroke);
34510
- else if (fillAppliedAlpha) {
34511
- alphaValues.add(1);
34512
- stream.setGraphicsState(alphaGsName(1));
34513
- }
35115
+ if (!fill) applyAlpha(stroke);
34514
35116
  if (lineWidth !== void 0) stream.setLineWidth(lineWidth);
34515
35117
  }
34516
35118
  if (fill && stroke) stream.fillAndStroke();
@@ -34522,8 +35124,7 @@ this.Documonster = this.Documonster || {};
34522
35124
  drawRect(options) {
34523
35125
  const { x, y, width, height, fill, stroke, lineWidth } = options;
34524
35126
  stream.save();
34525
- stream.rect(x, y, width, height);
34526
- paintFillStroke({
35127
+ paintFillStroke(() => stream.rect(x, y, width, height), {
34527
35128
  fill,
34528
35129
  stroke,
34529
35130
  lineWidth
@@ -34532,7 +35133,8 @@ this.Documonster = this.Documonster || {};
34532
35133
  return this;
34533
35134
  },
34534
35135
  drawLine(options) {
34535
- const { x1, y1, x2, y2, color, lineWidth, dashPattern } = options;
35136
+ const { x1, y1, x2, y2, lineWidth, dashPattern } = options;
35137
+ const color = mapColor(options.color);
34536
35138
  stream.save();
34537
35139
  if (color) {
34538
35140
  stream.setStrokeColor(color);
@@ -34547,11 +35149,11 @@ this.Documonster = this.Documonster || {};
34547
35149
  drawText(text, options) {
34548
35150
  if (!text) return this;
34549
35151
  const fontSize = options.fontSize ?? 10;
34550
- const color = options.color ?? {
35152
+ const color = mapColor(options.color ?? {
34551
35153
  r: 0,
34552
35154
  g: 0,
34553
35155
  b: 0
34554
- };
35156
+ });
34555
35157
  const bold = options.bold ?? false;
34556
35158
  const italic = options.italic ?? false;
34557
35159
  const fontFamily = options.fontFamily ?? "Helvetica";
@@ -34578,8 +35180,7 @@ this.Documonster = this.Documonster || {};
34578
35180
  drawCircle(options) {
34579
35181
  const { cx, cy, r, fill, stroke, lineWidth } = options;
34580
35182
  stream.save();
34581
- stream.circle(cx, cy, r);
34582
- paintFillStroke({
35183
+ paintFillStroke(() => stream.circle(cx, cy, r), {
34583
35184
  fill,
34584
35185
  stroke,
34585
35186
  lineWidth
@@ -34588,25 +35189,27 @@ this.Documonster = this.Documonster || {};
34588
35189
  return this;
34589
35190
  },
34590
35191
  drawPath(ops, options) {
35192
+ const buildPath = () => {
35193
+ for (const op of ops) switch (op.op) {
35194
+ case "move":
35195
+ stream.moveTo(op.x, op.y);
35196
+ break;
35197
+ case "line":
35198
+ stream.lineTo(op.x, op.y);
35199
+ break;
35200
+ case "curve":
35201
+ stream.curveTo(op.x1, op.y1, op.x2, op.y2, op.x3, op.y3);
35202
+ break;
35203
+ case "close": stream.closePath();
35204
+ }
35205
+ if (options?.closePath) stream.closePath();
35206
+ };
34591
35207
  stream.save();
34592
- for (const op of ops) switch (op.op) {
34593
- case "move":
34594
- stream.moveTo(op.x, op.y);
34595
- break;
34596
- case "line":
34597
- stream.lineTo(op.x, op.y);
34598
- break;
34599
- case "curve":
34600
- stream.curveTo(op.x1, op.y1, op.x2, op.y2, op.x3, op.y3);
34601
- break;
34602
- case "close": stream.closePath();
34603
- }
34604
- if (options?.closePath) stream.closePath();
34605
- if (options?.dashPattern && options.dashPattern.length > 0) stream.setDashPattern(options.dashPattern);
34606
- paintFillStroke({
35208
+ paintFillStroke(buildPath, {
34607
35209
  fill: options?.fill,
34608
35210
  stroke: options?.stroke,
34609
- lineWidth: options?.lineWidth
35211
+ lineWidth: options?.lineWidth,
35212
+ dashPattern: options?.dashPattern
34610
35213
  });
34611
35214
  stream.restore();
34612
35215
  return this;
@@ -34620,309 +35223,7 @@ this.Documonster = this.Documonster || {};
34620
35223
  var init_chart_surface = __esmMin((() => {
34621
35224
  init_font_manager();
34622
35225
  init_page_renderer();
34623
- }));
34624
- //#endregion
34625
- //#region src/modules/pdf/render/style-converter.ts
34626
- /**
34627
- * Convert an ARGB color string to PDF RGB color.
34628
- * Handles "AARRGGBB" (8-char) and "RRGGBB" (6-char) formats.
34629
- * PDF uses 0-1 floats for each component.
34630
- */
34631
- function argbToPdfColor(argb) {
34632
- if (!argb || argb.length < 6) return null;
34633
- return hexToRgb01(argb);
34634
- }
34635
- /**
34636
- * Convert a color data object to PDF color.
34637
- * Handles ARGB, theme-based, and indexed colors.
34638
- */
34639
- function excelColorToPdf(color) {
34640
- if (!color) return null;
34641
- if (color.argb) return argbToPdfColor(color.argb);
34642
- if (color.theme !== void 0) {
34643
- const base = themeColorToPdf(color.theme);
34644
- if (!base) return null;
34645
- const tint = color.tint;
34646
- if (tint !== void 0 && tint !== 0) return applyTint(base, tint);
34647
- return base;
34648
- }
34649
- if (color.indexed !== void 0) return indexedColorToPdf(color.indexed);
34650
- return null;
34651
- }
34652
- /**
34653
- * Map a cell-colour theme index to a PDF colour using the default Office
34654
- * palette in SpreadsheetML cell order (0=lt1, 1=dk1, 2=lt2, 3=dk2, …).
34655
- */
34656
- function themeColorToPdf(themeIndex) {
34657
- if (themeIndex >= 0 && themeIndex < CELL_THEME_PALETTE.length) return hexToRgb01(CELL_THEME_PALETTE[themeIndex]);
34658
- return null;
34659
- }
34660
- /**
34661
- * Convert an indexed color to PDF color.
34662
- * Index 64 = system foreground (black), 65 = system background (white).
34663
- */
34664
- function indexedColorToPdf(index) {
34665
- if (index === 64) return {
34666
- r: 0,
34667
- g: 0,
34668
- b: 0
34669
- };
34670
- if (index === 65) return {
34671
- r: 1,
34672
- g: 1,
34673
- b: 1
34674
- };
34675
- if (index >= 0 && index < INDEXED_COLORS.length) return argbToPdfColor(INDEXED_COLORS[index]) ?? null;
34676
- return null;
34677
- }
34678
- /**
34679
- * Apply a tint value to a color.
34680
- * Tint range: -1.0 (fully dark) to +1.0 (fully light).
34681
- * Negative tint darkens, positive tint lightens.
34682
- *
34683
- * @see OOXML §18.8.19 - tint formula
34684
- */
34685
- function applyTint(color, tint) {
34686
- return applyTintRgb01(color, tint);
34687
- }
34688
- /**
34689
- * Extract font properties for PDF rendering.
34690
- */
34691
- function extractFontProperties(font, defaultFamily, defaultSize) {
34692
- return {
34693
- fontFamily: font?.name ?? defaultFamily,
34694
- fontSize: font?.size ?? defaultSize,
34695
- bold: font?.bold ?? false,
34696
- italic: font?.italic ?? false,
34697
- strike: font?.strike ?? false,
34698
- underline: !!(font?.underline && font.underline !== "none"),
34699
- textColor: excelColorToPdf(font?.color) ?? DEFAULT_COLORS.black
34700
- };
34701
- }
34702
- /**
34703
- * Convert a fill to a PDF background color.
34704
- * Only pattern fills with "solid" pattern are supported as PDF backgrounds.
34705
- * Other patterns are approximated or ignored.
34706
- */
34707
- function excelFillToPdfColor(fill) {
34708
- if (!fill) return null;
34709
- if (fill.type === "pattern") {
34710
- if (fill.pattern === "solid" && fill.fgColor) return excelColorToPdf(fill.fgColor);
34711
- if (fill.pattern === "none") return null;
34712
- if (fill.fgColor) return excelColorToPdf(fill.fgColor);
34713
- }
34714
- if (fill.type === "gradient") {
34715
- if (fill.stops && fill.stops.length > 0) return excelColorToPdf(fill.stops[0].color);
34716
- }
34717
- return null;
34718
- }
34719
- /**
34720
- * Map border styles to PDF line widths (in points).
34721
- *
34722
- * Values match Excel's actual border weights as used historically by this
34723
- * library. An earlier change doubled every width (0.25 → 0.5, 0.5 → 1,
34724
- * 1 → 2) to make `thin` and `medium` more visually distinct in PDF
34725
- * viewers, but that change made all borders heavier than Excel itself.
34726
- * The 2× ratio between thin/medium and the 4× ratio between
34727
- * thin/thick are preserved with the lighter values, so styles remain
34728
- * distinguishable while matching Excel.
34729
- *
34730
- * hair = 0.1 pt
34731
- * thin = 0.25 pt (also dotted, dashed, dashDot, dashDotDot, slantDashDot, double)
34732
- * medium = 0.5 pt (also mediumDashed, mediumDashDot, mediumDashDotDot)
34733
- * thick = 1 pt
34734
- */
34735
- function borderStyleToLineWidth(style) {
34736
- switch (style) {
34737
- case "hair": return .1;
34738
- case "thin": return .25;
34739
- case "medium": return .5;
34740
- case "thick": return 1;
34741
- case "double": return .25;
34742
- case "dotted": return .25;
34743
- case "dashed": return .25;
34744
- case "dashDot": return .25;
34745
- case "dashDotDot": return .25;
34746
- case "slantDashDot": return .25;
34747
- case "mediumDashed": return .5;
34748
- case "mediumDashDot": return .5;
34749
- case "mediumDashDotDot": return .5;
34750
- default: return .25;
34751
- }
34752
- }
34753
- /**
34754
- * Map border styles to PDF dash patterns.
34755
- * An empty array means a solid line.
34756
- */
34757
- function borderStyleToDashPattern(style) {
34758
- switch (style) {
34759
- case "dotted": return [1, 1];
34760
- case "dashed":
34761
- case "mediumDashed": return [3, 2];
34762
- case "dashDot":
34763
- case "mediumDashDot": return [
34764
- 3,
34765
- 1,
34766
- 1,
34767
- 1
34768
- ];
34769
- case "dashDotDot":
34770
- case "mediumDashDotDot": return [
34771
- 3,
34772
- 1,
34773
- 1,
34774
- 1,
34775
- 1,
34776
- 1
34777
- ];
34778
- case "slantDashDot": return [
34779
- 4,
34780
- 2,
34781
- 1,
34782
- 2
34783
- ];
34784
- case "hair": return [.5, .5];
34785
- default: return [];
34786
- }
34787
- }
34788
- /**
34789
- * Convert a single border side to a PDF LayoutBorder.
34790
- */
34791
- function convertBorder(border) {
34792
- if (!border || !border.style) return null;
34793
- return {
34794
- width: borderStyleToLineWidth(border.style),
34795
- color: excelColorToPdf(border.color) ?? DEFAULT_COLORS.black,
34796
- dashPattern: borderStyleToDashPattern(border.style),
34797
- isDouble: border.style === "double"
34798
- };
34799
- }
34800
- /**
34801
- * Convert border data to PDF LayoutBorders.
34802
- */
34803
- function excelBordersToPdf(borders) {
34804
- if (!borders) return {
34805
- top: null,
34806
- right: null,
34807
- bottom: null,
34808
- left: null
34809
- };
34810
- return {
34811
- top: convertBorder(borders.top),
34812
- right: convertBorder(borders.right),
34813
- bottom: convertBorder(borders.bottom),
34814
- left: convertBorder(borders.left)
34815
- };
34816
- }
34817
- /**
34818
- * Convert horizontal alignment to PDF alignment.
34819
- */
34820
- function excelHAlignToPdf(alignment) {
34821
- if (!alignment?.horizontal) return "left";
34822
- switch (alignment.horizontal) {
34823
- case "center":
34824
- case "centerContinuous": return "center";
34825
- case "right": return "right";
34826
- default: return "left";
34827
- }
34828
- }
34829
- /**
34830
- * Convert vertical alignment to PDF alignment.
34831
- */
34832
- function excelVAlignToPdf(alignment) {
34833
- if (!alignment?.vertical) return "bottom";
34834
- switch (alignment.vertical) {
34835
- case "top": return "top";
34836
- case "middle": return "middle";
34837
- default: return "bottom";
34838
- }
34839
- }
34840
- var INDEXED_COLORS, DEFAULT_COLORS;
34841
- var init_style_converter = __esmMin((() => {
34842
- init_theme_colors();
34843
- INDEXED_COLORS = [
34844
- "000000",
34845
- "FFFFFF",
34846
- "FF0000",
34847
- "00FF00",
34848
- "0000FF",
34849
- "FFFF00",
34850
- "FF00FF",
34851
- "00FFFF",
34852
- "000000",
34853
- "FFFFFF",
34854
- "FF0000",
34855
- "00FF00",
34856
- "0000FF",
34857
- "FFFF00",
34858
- "FF00FF",
34859
- "00FFFF",
34860
- "800000",
34861
- "008000",
34862
- "000080",
34863
- "808000",
34864
- "800080",
34865
- "008080",
34866
- "C0C0C0",
34867
- "808080",
34868
- "9999FF",
34869
- "993366",
34870
- "FFFFCC",
34871
- "CCFFFF",
34872
- "660066",
34873
- "FF8080",
34874
- "0066CC",
34875
- "CCCCFF",
34876
- "000080",
34877
- "FF00FF",
34878
- "FFFF00",
34879
- "00FFFF",
34880
- "800080",
34881
- "800000",
34882
- "008080",
34883
- "0000FF",
34884
- "00CCFF",
34885
- "CCFFFF",
34886
- "CCFFCC",
34887
- "FFFF99",
34888
- "99CCFF",
34889
- "FF99CC",
34890
- "CC99FF",
34891
- "FFCC99",
34892
- "3366FF",
34893
- "33CCCC",
34894
- "99CC00",
34895
- "FFCC00",
34896
- "FF9900",
34897
- "FF6600",
34898
- "666699",
34899
- "969696",
34900
- "003366",
34901
- "339966",
34902
- "003300",
34903
- "333300",
34904
- "993300",
34905
- "993366",
34906
- "333399",
34907
- "333333"
34908
- ];
34909
- DEFAULT_COLORS = {
34910
- black: {
34911
- r: 0,
34912
- g: 0,
34913
- b: 0
34914
- },
34915
- white: {
34916
- r: 1,
34917
- g: 1,
34918
- b: 1
34919
- },
34920
- gridLine: {
34921
- r: .816,
34922
- g: .816,
34923
- b: .816
34924
- }
34925
- };
35226
+ init_style_converter();
34926
35227
  }));
34927
35228
  //#endregion
34928
35229
  //#region src/modules/pdf/types.ts
@@ -35130,18 +35431,19 @@ this.Documonster = this.Documonster || {};
35130
35431
  if (!ctx) return [createEmptyPage(sheet, options)];
35131
35432
  const layoutPages = [];
35132
35433
  const totalOutputPages = ctx.rowPages.length * ctx.colGroups.length;
35133
- for (const rowPage of ctx.rowPages) for (const colGroup of ctx.colGroups) {
35434
+ const overThenDown = options.pageOrder === "overThenDown";
35435
+ const outer = overThenDown ? ctx.rowPages : ctx.colGroups;
35436
+ const inner = overThenDown ? ctx.colGroups : ctx.rowPages;
35437
+ for (const outerTracks of outer) for (const innerTracks of inner) {
35438
+ const rowPage = overThenDown ? outerTracks : innerTracks;
35439
+ const colGroup = overThenDown ? innerTracks : outerTracks;
35134
35440
  layoutPages.push(buildPageLayout(ctx, rowPage, colGroup, layoutPages.length, sheet, options, fontManager));
35135
35441
  if (layoutPages.length < totalOutputPages) await yieldToEventLoop();
35136
35442
  }
35137
- if (layoutPages.length > 0 && sheet.images) assignImagesToPages(sheet.images, layoutPages, ctx.scaleFactor);
35138
- if (layoutPages.length > 0 && sheet.charts) assignChartsToPages(sheet.charts, layoutPages, ctx.scaleFactor);
35139
- const firstPageNumber = sheet.pageSetup?.firstPageNumber ?? 1;
35140
- for (let i = 0; i < layoutPages.length; i++) {
35141
- layoutPages[i].sheetPageNumber = firstPageNumber + i;
35142
- layoutPages[i].sheetPageIndex = i + 1;
35143
- layoutPages[i].sheetPageCount = layoutPages.length;
35144
- }
35443
+ if (layoutPages.length > 0 && sheet.images && !options.draft) assignImagesToPages(sheet.images, layoutPages, ctx.scaleFactor);
35444
+ if (layoutPages.length > 0 && sheet.charts && !options.draft) assignChartsToPages(sheet.charts, layoutPages, ctx.scaleFactor);
35445
+ if (options.cellComments === "atEnd" && sheet.comments?.length) layoutPages.push(...buildCommentPages(sheet, sheet.comments, options, fontManager));
35446
+ for (let i = 0; i < layoutPages.length; i++) layoutPages[i].sheetPageIndex = i + 1;
35145
35447
  return layoutPages;
35146
35448
  }
35147
35449
  /**
@@ -35170,9 +35472,7 @@ this.Documonster = this.Documonster || {};
35170
35472
  ...documentOptions,
35171
35473
  orientation
35172
35474
  };
35173
- let pageWidth = options.pageSize.width;
35174
- let pageHeight = options.pageSize.height;
35175
- if (options.orientation === "landscape") [pageWidth, pageHeight] = [pageHeight, pageWidth];
35475
+ const { width: pageWidth, height: pageHeight } = pageDimensions(options);
35176
35476
  const margins = options.margins;
35177
35477
  const headerHeight = options.showSheetNames ? 20 : 0;
35178
35478
  const footerHeight = options.showPageNumbers ? 20 : 0;
@@ -35191,6 +35491,35 @@ this.Documonster = this.Documonster || {};
35191
35491
  raster: sheet.chart.raster
35192
35492
  };
35193
35493
  return [{
35494
+ ...blankPage(sheet, options),
35495
+ charts: options.draft ? [] : [chart]
35496
+ }];
35497
+ }
35498
+ /**
35499
+ * Page dimensions for the resolved options, with landscape applied.
35500
+ *
35501
+ * `pageSize` is always stored portrait-wise, so every consumer has to swap the
35502
+ * axes itself; centralised here so the four layout entry points cannot disagree.
35503
+ */
35504
+ function pageDimensions(options) {
35505
+ const { width, height } = options.pageSize;
35506
+ return options.orientation === "landscape" ? {
35507
+ width: height,
35508
+ height: width
35509
+ } : {
35510
+ width,
35511
+ height
35512
+ };
35513
+ }
35514
+ /**
35515
+ * A `LayoutPage` with every field at its empty value.
35516
+ *
35517
+ * The shape has ~20 members and four construction sites; building them from one
35518
+ * skeleton means a new field cannot be forgotten in three of them.
35519
+ */
35520
+ function blankPage(sheet, options) {
35521
+ const { width, height } = pageDimensions(options);
35522
+ return {
35194
35523
  pageNumber: 1,
35195
35524
  sheetPageNumber: sheet.pageSetup?.firstPageNumber ?? 1,
35196
35525
  sheetPageIndex: 1,
@@ -35198,8 +35527,8 @@ this.Documonster = this.Documonster || {};
35198
35527
  firstPageNumber: sheet.pageSetup?.firstPageNumber,
35199
35528
  options,
35200
35529
  cells: [],
35201
- width: pageWidth,
35202
- height: pageHeight,
35530
+ width,
35531
+ height,
35203
35532
  sheetName: sheet.name,
35204
35533
  sheetCols: [],
35205
35534
  columnOffsets: [],
@@ -35208,10 +35537,10 @@ this.Documonster = this.Documonster || {};
35208
35537
  rowYPositions: [],
35209
35538
  rowHeights: [],
35210
35539
  images: [],
35211
- charts: [chart],
35540
+ charts: [],
35212
35541
  scaleFactor: 1,
35213
35542
  headerFooter: options.includeHeadersFooters ? sheet.headerFooter : void 0
35214
- }];
35543
+ };
35215
35544
  }
35216
35545
  /**
35217
35546
  * Steps 1–5: compute columns, scale, rows, merges, pagination.
@@ -35219,32 +35548,54 @@ this.Documonster = this.Documonster || {};
35219
35548
  */
35220
35549
  function prepareLayout(sheet, options, fontManager) {
35221
35550
  const { margins } = options;
35222
- let pageWidth = options.pageSize.width;
35223
- let pageHeight = options.pageSize.height;
35224
- if (options.orientation === "landscape") [pageWidth, pageHeight] = [pageHeight, pageWidth];
35225
- const contentWidth = pageWidth - margins.left - margins.right;
35226
- const contentHeight = pageHeight - margins.top - margins.bottom;
35551
+ const { width: pageWidth, height: pageHeight } = pageDimensions(options);
35227
35552
  const headerHeight = options.showSheetNames ? 20 : 0;
35228
35553
  const printRange = getPrintRange(sheet, options);
35229
- const { columnWidths, visibleCols } = computeColumnWidths(sheet, printRange);
35554
+ const headings = options.showRowColHeaders ? computeHeadingMetrics(sheet, printRange, fontManager, options) : void 0;
35555
+ const gutterWidth = headings?.gutterWidth ?? 0;
35556
+ const bandHeight = headings?.bandHeight ?? 0;
35557
+ const contentWidth = pageWidth - margins.left - margins.right - gutterWidth;
35558
+ const contentHeight = pageHeight - margins.top - margins.bottom - bandHeight;
35559
+ const { columnWidths, visibleCols, repeatColIndices } = computeColumnWidths(sheet, printRange, options.repeatCols);
35230
35560
  if (visibleCols.length === 0) return null;
35561
+ const natural = computeRowHeights(sheet, printRange, fontManager, options, options.repeatRows);
35562
+ const rowBreaks = buildBreakSet(sheet.rowBreaks ?? [], natural.visibleRows);
35563
+ const colBreaks = buildBreakSet(sheet.colBreaks ?? [], visibleCols);
35231
35564
  const totalTableWidth = columnWidths.reduce((sum, w) => sum + w, 0);
35565
+ const footerHeight = options.showPageNumbers ? 20 : 0;
35566
+ const availableHeight = contentHeight - headerHeight - footerHeight;
35232
35567
  let scaleFactor = options.scale;
35233
- if (options.fitToPage && totalTableWidth > 0) {
35234
- const fitScale = contentWidth / totalTableWidth;
35568
+ if (options.fitToWidth > 0 || options.fitToHeight > 0) {
35569
+ let fit = 1;
35570
+ const minFit = Math.min(1, FIT_MIN_SCALE / scaleFactor);
35571
+ if (options.fitToWidth > 0 && totalTableWidth > 0) {
35572
+ fit = Math.min(fit, contentWidth * options.fitToWidth / (totalTableWidth * scaleFactor));
35573
+ fit = tightenToPageCount((candidate) => paginateTracks(columnWidths.map((w) => w * scaleFactor * candidate), contentWidth, repeatColIndices, colBreaks, COLUMN_FIT_EPSILON).length, options.fitToWidth, fit, minFit);
35574
+ }
35575
+ if (options.fitToHeight > 0 && availableHeight > 0) {
35576
+ const totalTableHeight = natural.rowHeights.reduce((sum, h) => sum + h, 0);
35577
+ if (totalTableHeight > 0) {
35578
+ let heightFit = Math.min(fit, availableHeight * options.fitToHeight / (totalTableHeight * scaleFactor));
35579
+ heightFit = tightenToPageCount((candidate) => paginateTracks(natural.rowHeights.map((h) => h * scaleFactor * candidate), availableHeight, natural.repeatRowIndices, rowBreaks).length, options.fitToHeight, heightFit, minFit);
35580
+ fit = Math.min(fit, heightFit);
35581
+ }
35582
+ }
35583
+ if (fit < 1) scaleFactor = Math.max(scaleFactor * fit, FIT_MIN_SCALE);
35584
+ } else if (options.fitToPage && totalTableWidth > 0) {
35585
+ const fitScale = contentWidth / (totalTableWidth * scaleFactor);
35235
35586
  if (fitScale < 1) scaleFactor *= fitScale;
35236
35587
  }
35237
35588
  const scaledColumnWidths = columnWidths.map((w) => w * scaleFactor);
35238
- const footerHeight = options.showPageNumbers ? 20 : 0;
35239
- const availableHeight = contentHeight - headerHeight - footerHeight;
35240
- const { rowHeights, visibleRows } = computeRowHeights(sheet, scaleFactor, printRange, fontManager, options);
35589
+ const rowHeights = natural.rowHeights.map((h) => h * scaleFactor);
35590
+ const { visibleRows, repeatRowIndices } = natural;
35241
35591
  const mergeMap = buildMergeMap(sheet);
35242
- const rowPages = paginateRows(rowHeights, availableHeight, typeof options.repeatRows === "number" ? options.repeatRows : 0, buildRowBreakSet(sheet, visibleRows));
35243
- const colGroups = paginateColumns(scaledColumnWidths, contentWidth, sheet, visibleCols);
35592
+ const rowPages = paginateTracks(rowHeights, availableHeight, repeatRowIndices, rowBreaks);
35593
+ const colGroups = paginateTracks(scaledColumnWidths, contentWidth, repeatColIndices, colBreaks, COLUMN_FIT_EPSILON);
35244
35594
  return {
35245
35595
  pageWidth,
35246
35596
  pageHeight,
35247
35597
  contentWidth,
35598
+ availableHeight,
35248
35599
  headerHeight,
35249
35600
  scaleFactor,
35250
35601
  scaledColumnWidths,
@@ -35254,27 +35605,173 @@ this.Documonster = this.Documonster || {};
35254
35605
  mergeMap,
35255
35606
  rowPages,
35256
35607
  colGroups,
35257
- margins
35608
+ margins,
35609
+ headings
35258
35610
  };
35259
35611
  }
35260
35612
  /**
35613
+ * Size the row-number gutter and column-letter band for Excel's "Row and
35614
+ * column headings" print option.
35615
+ *
35616
+ * The gutter is sized from the widest row label that can appear in the printed
35617
+ * range, so the grid origin is stable across pages instead of jittering as row
35618
+ * numbers gain digits.
35619
+ */
35620
+ function computeHeadingMetrics(sheet, printRange, fontManager, options) {
35621
+ const fontSize = 8;
35622
+ const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(options.defaultFontFamily, false, false));
35623
+ const lastRow = printRange?.endRow ?? sheet.bounds.bottom;
35624
+ const widestLabel = String(Math.max(1, lastRow));
35625
+ fontManager.trackText(widestLabel);
35626
+ return {
35627
+ gutterWidth: fontManager.measureText(widestLabel, resourceName, fontSize) + 6,
35628
+ bandHeight: 14,
35629
+ fontSize
35630
+ };
35631
+ }
35632
+ /**
35633
+ * Largest scale ≤ `startScale` whose real pagination lands within `target`
35634
+ * pages, found by bisection.
35635
+ *
35636
+ * The caller passes a closure that re-paginates at a candidate scale, so the
35637
+ * answer respects indivisible columns/rows, manual breaks and repeated title
35638
+ * bands instead of trusting a total-size ratio. Returns `startScale` untouched
35639
+ * when it already fits, so nothing is shrunk needlessly.
35640
+ *
35641
+ * `minScale` is the floor the result may not go below. When the target is
35642
+ * unreachable even there — manual page breaks alone can force more pages than
35643
+ * requested — the floor is returned and the sheet simply spans more than
35644
+ * `target` pages, which is preferable to shrinking it into illegibility.
35645
+ */
35646
+ function tightenToPageCount(pageCountAt, target, startScale, minScale) {
35647
+ const start = Math.max(startScale, minScale);
35648
+ if (pageCountAt(start) <= target || start <= minScale) return start;
35649
+ let lo = minScale;
35650
+ let hi = start;
35651
+ if (pageCountAt(lo) > target) return lo;
35652
+ for (let i = 0; i < 24; i++) {
35653
+ const mid = (lo + hi) / 2;
35654
+ if (pageCountAt(mid) <= target) lo = mid;
35655
+ else hi = mid;
35656
+ }
35657
+ return lo;
35658
+ }
35659
+ /**
35660
+ * Lay out Excel's "Comments at end of sheet" as extra pages.
35661
+ *
35662
+ * Each entry becomes an ordinary {@link LayoutCell} spanning the content width,
35663
+ * so the existing page renderer draws them with no special casing — including
35664
+ * word wrapping, which long comment bodies need. Pages are filled top to bottom
35665
+ * and a new one starts when the next entry would overflow.
35666
+ */
35667
+ function buildCommentPages(sheet, comments, options, fontManager) {
35668
+ const { width: pageWidth, height: pageHeight } = pageDimensions(options);
35669
+ const { margins } = options;
35670
+ const headerHeight = options.showSheetNames ? 20 : 0;
35671
+ const footerHeight = options.showPageNumbers ? 20 : 0;
35672
+ const contentWidth = pageWidth - margins.left - margins.right;
35673
+ const top = pageHeight - margins.top - headerHeight;
35674
+ const bottom = margins.bottom + footerHeight;
35675
+ const fontSize = options.defaultFontSize;
35676
+ const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(resolvePdfFontName(options.defaultFontFamily, false, false));
35677
+ const measure = (text) => fontManager.measureText(text, resourceName, fontSize);
35678
+ const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
35679
+ const textColor = {
35680
+ r: 0,
35681
+ g: 0,
35682
+ b: 0
35683
+ };
35684
+ const pages = [];
35685
+ let cells = [];
35686
+ let cursor = top;
35687
+ const flush = () => {
35688
+ if (cells.length > 0) {
35689
+ pages.push({
35690
+ ...blankPage(sheet, options),
35691
+ cells
35692
+ });
35693
+ cells = [];
35694
+ cursor = top;
35695
+ }
35696
+ };
35697
+ const push = (text, bold) => {
35698
+ fontManager.trackText(text);
35699
+ const lines = wrapTextLines(text, measure, Math.max(contentWidth - 6, 1));
35700
+ const height = Math.max(lines.length, 1) * lineHeight + 4;
35701
+ if (cursor - height < bottom) flush();
35702
+ cells.push({
35703
+ text,
35704
+ rect: {
35705
+ x: margins.left,
35706
+ y: cursor - height,
35707
+ width: contentWidth,
35708
+ height
35709
+ },
35710
+ fontFamily: options.defaultFontFamily,
35711
+ fontSize,
35712
+ bold,
35713
+ italic: false,
35714
+ strike: false,
35715
+ underline: false,
35716
+ textColor,
35717
+ fillColor: null,
35718
+ horizontalAlign: "left",
35719
+ verticalAlign: "top",
35720
+ wrapText: true,
35721
+ borders: {
35722
+ top: null,
35723
+ right: null,
35724
+ bottom: null,
35725
+ left: null
35726
+ },
35727
+ borderInsets: {
35728
+ top: 0,
35729
+ right: 0,
35730
+ bottom: 0,
35731
+ left: 0
35732
+ },
35733
+ colSpan: 1,
35734
+ rowSpan: 1,
35735
+ hyperlink: null,
35736
+ richText: null,
35737
+ textRotation: 0,
35738
+ indent: 0,
35739
+ textOverflowWidth: 0
35740
+ });
35741
+ cursor -= height;
35742
+ };
35743
+ push(`${sheet.name} — comments`, true);
35744
+ for (const comment of comments) {
35745
+ const author = comment.author ? ` (${comment.author})` : "";
35746
+ push(`${comment.ref}${author}: ${comment.text}`, false);
35747
+ }
35748
+ flush();
35749
+ return pages;
35750
+ }
35751
+ /**
35261
35752
  * Build the LayoutPage for a single rowPage × colGroup combination.
35262
35753
  */
35263
35754
  function buildPageLayout(ctx, rowPage, colGroup, currentPageCount, sheet, options, fontManager) {
35264
- const { scaledColumnWidths, rowHeights, visibleRows, visibleCols, mergeMap, pageWidth, pageHeight, contentWidth, headerHeight, scaleFactor, margins } = ctx;
35755
+ const { scaledColumnWidths, rowHeights, visibleRows, visibleCols, mergeMap, pageWidth, pageHeight, contentWidth, availableHeight, headerHeight, scaleFactor, margins, headings } = ctx;
35265
35756
  const cells = [];
35757
+ const gridLeft = margins.left + (headings?.gutterWidth ?? 0);
35758
+ const gridTop = pageHeight - margins.top - headerHeight - (headings?.bandHeight ?? 0);
35266
35759
  const groupColWidths = colGroup.map((ci) => scaledColumnWidths[ci]);
35267
35760
  const groupTotalWidth = groupColWidths.reduce((s, w) => s + w, 0);
35268
35761
  const groupColOffsets = [];
35269
- let gx = margins.left;
35270
- if (groupTotalWidth < contentWidth) gx = margins.left + (contentWidth - groupTotalWidth) / 2;
35762
+ let gx = gridLeft;
35763
+ if (options.horizontalCentered && groupTotalWidth < contentWidth) gx = gridLeft + (contentWidth - groupTotalWidth) / 2;
35271
35764
  for (const w of groupColWidths) {
35272
35765
  groupColOffsets.push(gx);
35273
35766
  gx += w;
35274
35767
  }
35275
35768
  const rowYPositions = [];
35276
35769
  const pageRowHeights = [];
35277
- let currentY = pageHeight - margins.top - headerHeight;
35770
+ let currentY = gridTop;
35771
+ if (options.verticalCentered) {
35772
+ const pageTotalHeight = rowPage.reduce((sum, rowIdx) => sum + (rowHeights[rowIdx] ?? DEFAULT_ROW_HEIGHT * scaleFactor), 0);
35773
+ if (pageTotalHeight < availableHeight) currentY -= (availableHeight - pageTotalHeight) / 2;
35774
+ }
35278
35775
  for (const rowIdx of rowPage) {
35279
35776
  const rowH = rowHeights[rowIdx] ?? DEFAULT_ROW_HEIGHT * scaleFactor;
35280
35777
  rowYPositions.push(currentY);
@@ -35314,7 +35811,10 @@ this.Documonster = this.Documonster || {};
35314
35811
  const rectY = cellY - cellHeight;
35315
35812
  cells.push(buildLayoutCell(cell, cellX, rectY, cellWidth, cellHeight, colSpan, rowSpan, options, fontManager, scaleFactor));
35316
35813
  const layoutCell = cells[cells.length - 1];
35317
- if (mergeInfo?.isMaster) propagateMergeBorders(layoutCell, mergeInfo, wsRowNumber, wsColNumber, sheet);
35814
+ if (mergeInfo?.isMaster) {
35815
+ propagateMergeBorders(layoutCell, mergeInfo, wsRowNumber, wsColNumber, sheet);
35816
+ if (options.blackAndWhite) layoutCell.borders = grayscaleBorders(layoutCell.borders);
35817
+ }
35318
35818
  cellGrid.set(`${ri}:${gci}`, layoutCell);
35319
35819
  }
35320
35820
  }
@@ -35340,36 +35840,94 @@ this.Documonster = this.Documonster || {};
35340
35840
  images: [],
35341
35841
  charts: [],
35342
35842
  scaleFactor,
35843
+ headings,
35844
+ commentBoxes: options.cellComments === "asDisplayed" ? placeCommentBoxes(sheet, colGroup.map((ci) => visibleCols[ci]), groupColOffsets, groupColWidths, rowPage.map((ri) => visibleRows[ri]), rowYPositions, pageRowHeights, options, scaleFactor) : void 0,
35343
35845
  headerFooter: options.includeHeadersFooters ? sheet.headerFooter : void 0
35344
35846
  };
35345
35847
  }
35346
- function createEmptyPage(sheet, options) {
35347
- let pageWidth = options.pageSize.width;
35348
- let pageHeight = options.pageSize.height;
35349
- if (options.orientation === "landscape") [pageWidth, pageHeight] = [pageHeight, pageWidth];
35848
+ /**
35849
+ * Position each comment box on a page, for `cellComments: "asDisplayed"`.
35850
+ *
35851
+ * A comment is placed when its box overlaps the page's tracks. Fractional VML
35852
+ * coordinates are interpolated against the page's own column offsets and row
35853
+ * positions, so a box lands correctly even when the scale, hidden tracks or
35854
+ * repeated title bands have moved the grid around. Comments whose box falls
35855
+ * entirely outside the page are skipped, which keeps each one on a single page
35856
+ * rather than slicing it across the seam.
35857
+ */
35858
+ function placeCommentBoxes(sheet, sheetCols, columnOffsets, columnWidths, sheetRows, rowYPositions, rowHeights, options, scaleFactor) {
35859
+ const comments = sheet.comments;
35860
+ if (!comments?.length || sheetCols.length === 0 || sheetRows.length === 0) return;
35861
+ const xAt = (col) => {
35862
+ const index = sheetCols.indexOf(Math.floor(col) + 1);
35863
+ if (index < 0) return;
35864
+ return columnOffsets[index] + (col - Math.floor(col)) * columnWidths[index];
35865
+ };
35866
+ const yAt = (row) => {
35867
+ const index = sheetRows.indexOf(Math.floor(row) + 1);
35868
+ if (index < 0) return;
35869
+ return rowYPositions[index] - (row - Math.floor(row)) * rowHeights[index];
35870
+ };
35871
+ const boxes = [];
35872
+ for (const comment of comments) {
35873
+ const anchor = comment.anchor ?? defaultCommentAnchor(comment.ref);
35874
+ if (!anchor) continue;
35875
+ const left = xAt(anchor.left);
35876
+ const right = xAt(anchor.right);
35877
+ const top = yAt(anchor.top);
35878
+ const bottom = yAt(anchor.bottom);
35879
+ if (left === void 0 || right === void 0 || top === void 0 || bottom === void 0) continue;
35880
+ const width = right - left;
35881
+ const height = top - bottom;
35882
+ if (width <= 0 || height <= 0) continue;
35883
+ const cell = parseCellRef(comment.ref);
35884
+ const markerCol = sheetCols.indexOf(cell.c + 1);
35885
+ const markerRow = sheetRows.indexOf(cell.r + 1);
35886
+ const author = comment.author ? `${comment.author}:\n` : "";
35887
+ boxes.push({
35888
+ rect: {
35889
+ x: left,
35890
+ y: bottom,
35891
+ width,
35892
+ height
35893
+ },
35894
+ text: `${author}${comment.text}`,
35895
+ fontSize: options.defaultFontSize * scaleFactor,
35896
+ marker: markerCol >= 0 && markerRow >= 0 ? {
35897
+ x: columnOffsets[markerCol] + columnWidths[markerCol],
35898
+ y: rowYPositions[markerRow],
35899
+ size: 4 * scaleFactor
35900
+ } : void 0
35901
+ });
35902
+ }
35903
+ return boxes.length > 0 ? boxes : void 0;
35904
+ }
35905
+ /**
35906
+ * Excel's default comment placement, used when the note carries no VML anchor.
35907
+ *
35908
+ * Mirrors the geometry Excel writes for a fresh comment: the box starts at the
35909
+ * commented cell's column, two rows above it, and spans two columns by four
35910
+ * rows.
35911
+ */
35912
+ function defaultCommentAnchor(ref) {
35913
+ let cell;
35914
+ try {
35915
+ cell = parseCellRef(ref);
35916
+ } catch {
35917
+ return;
35918
+ }
35919
+ const left = cell.c + 6 / 68;
35920
+ const top = Math.max(cell.r - 2, 0) + 14 / 18;
35350
35921
  return {
35351
- pageNumber: 1,
35352
- sheetPageNumber: sheet.pageSetup?.firstPageNumber ?? 1,
35353
- sheetPageIndex: 1,
35354
- sheetPageCount: 1,
35355
- firstPageNumber: sheet.pageSetup?.firstPageNumber,
35356
- options,
35357
- cells: [],
35358
- width: pageWidth,
35359
- height: pageHeight,
35360
- sheetName: sheet.name,
35361
- sheetCols: [],
35362
- columnOffsets: [],
35363
- columnWidths: [],
35364
- sheetRows: [],
35365
- rowYPositions: [],
35366
- rowHeights: [],
35367
- images: [],
35368
- charts: [],
35369
- scaleFactor: 1,
35370
- headerFooter: options.includeHeadersFooters ? sheet.headerFooter : void 0
35922
+ left,
35923
+ top,
35924
+ right: left + 2,
35925
+ bottom: top + 4
35371
35926
  };
35372
35927
  }
35928
+ function createEmptyPage(sheet, options) {
35929
+ return blankPage(sheet, options);
35930
+ }
35373
35931
  /**
35374
35932
  * Parse a cell reference like "A1" into 0-indexed { c, r }.
35375
35933
  */
@@ -35426,63 +35984,87 @@ this.Documonster = this.Documonster || {};
35426
35984
  return null;
35427
35985
  }
35428
35986
  }
35429
- function computeColumnWidths(sheet, printRange) {
35987
+ function computeColumnWidths(sheet, printRange, titleBand) {
35430
35988
  const bounds = sheet.bounds;
35431
35989
  if (!(bounds.top > 0 && bounds.left > 0)) return {
35432
35990
  columnWidths: [],
35433
- visibleCols: []
35991
+ visibleCols: [],
35992
+ repeatColIndices: []
35434
35993
  };
35435
35994
  const startCol = printRange?.startCol ?? bounds.left;
35436
35995
  const endCol = printRange?.endCol ?? bounds.right;
35437
35996
  const columnWidths = [];
35438
35997
  const visibleCols = [];
35439
- for (let c = startCol; c <= endCol; c++) {
35998
+ const emitted = /* @__PURE__ */ new Set();
35999
+ const push = (c) => {
36000
+ if (emitted.has(c)) return;
35440
36001
  const col = sheet.columns.get(c);
35441
- if (col?.hidden) continue;
36002
+ if (col?.hidden) return;
36003
+ emitted.add(c);
35442
36004
  const pixelWidth = charWidthToPixel(col?.width ?? DEFAULT_COLUMN_WIDTH$1, 7);
35443
36005
  const pointWidth = Math.max(pixelWidth * PX_TO_PT, MIN_COLUMN_WIDTH);
35444
36006
  columnWidths.push(pointWidth);
35445
36007
  visibleCols.push(c);
35446
- }
36008
+ };
36009
+ const titleFullyInside = titleBand && titleBand.first >= startCol && titleBand.last <= endCol;
36010
+ if (titleBand && !titleFullyInside) for (let c = titleBand.first; c <= titleBand.last; c++) push(c);
36011
+ for (let c = startCol; c <= endCol; c++) push(c);
35447
36012
  return {
35448
36013
  columnWidths,
35449
- visibleCols
36014
+ visibleCols,
36015
+ repeatColIndices: titleBand ? visibleCols.flatMap((c, i) => c >= titleBand.first && c <= titleBand.last ? [i] : []) : []
35450
36016
  };
35451
36017
  }
35452
- function computeRowHeights(sheet, scaleFactor, printRange, fontManager, options) {
36018
+ /**
36019
+ * Measure every printable row at 100% scale.
36020
+ *
36021
+ * Heights are deliberately unscaled: `countWrapLines` derives the wrapped line
36022
+ * count from ratios that are independent of the print scale, so a scaled height
36023
+ * is exactly `unscaled * scale`. Measuring once and multiplying avoids re-running
36024
+ * the most expensive step of layout for every probe of the fit solver.
36025
+ */
36026
+ function computeRowHeights(sheet, printRange, fontManager, options, titleBand) {
35453
36027
  const bounds = sheet.bounds;
35454
36028
  if (bounds.top <= 0) return {
35455
36029
  rowHeights: [],
35456
- visibleRows: []
36030
+ visibleRows: [],
36031
+ repeatRowIndices: []
35457
36032
  };
35458
36033
  const startRow = printRange?.startRow ?? bounds.top;
35459
36034
  const endRow = printRange?.endRow ?? bounds.bottom;
35460
36035
  const rowHeights = [];
35461
36036
  const visibleRows = [];
35462
- for (let r = startRow; r <= endRow; r++) {
36037
+ const emitted = /* @__PURE__ */ new Set();
36038
+ const push = (r) => {
36039
+ if (emitted.has(r)) return;
35463
36040
  const row = sheet.rows.get(r);
35464
- if (row?.hidden) continue;
36041
+ if (row?.hidden) return;
36042
+ emitted.add(r);
35465
36043
  let height;
35466
36044
  if (row?.height && row.customHeight) height = row.height;
35467
- else if (row?.height) height = Math.max(row.height, autoRowHeight(row, scaleFactor, sheet, fontManager, options));
35468
- else height = autoRowHeight(row, scaleFactor, sheet, fontManager, options);
35469
- rowHeights.push(height * scaleFactor);
36045
+ else if (row?.height) height = Math.max(row.height, autoRowHeight(row, sheet, fontManager, options));
36046
+ else height = autoRowHeight(row, sheet, fontManager, options);
36047
+ rowHeights.push(height);
35470
36048
  visibleRows.push(r);
35471
- }
36049
+ };
36050
+ const titleFullyInside = titleBand && titleBand.first >= startRow && titleBand.last <= endRow;
36051
+ if (titleBand && !titleFullyInside) for (let r = titleBand.first; r <= titleBand.last; r++) push(r);
36052
+ for (let r = startRow; r <= endRow; r++) push(r);
35472
36053
  return {
35473
36054
  rowHeights,
35474
- visibleRows
36055
+ visibleRows,
36056
+ repeatRowIndices: titleBand ? visibleRows.flatMap((r, i) => r >= titleBand.first && r <= titleBand.last ? [i] : []) : []
35475
36057
  };
35476
36058
  }
35477
36059
  /**
35478
36060
  * Compute the minimum row height required to display wrapped cell content.
35479
36061
  * Returns at least `DEFAULT_ROW_HEIGHT`.
35480
36062
  */
35481
- function autoRowHeight(row, scaleFactor, sheet, fontManager, options) {
36063
+ function autoRowHeight(row, sheet, fontManager, options) {
35482
36064
  let height = DEFAULT_ROW_HEIGHT;
35483
36065
  if (row) for (const cell of row.cells.values()) {
35484
36066
  const fontSize = getCellFontSize(cell);
35485
- const wrapLineCount = countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options);
36067
+ const wrapLineCount = countWrapLines(cell, fontSize, sheet, fontManager, options);
35486
36068
  const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
35487
36069
  const borderTop = cell.style?.border?.top?.style ? borderStyleToLineWidth(cell.style.border.top.style) / 2 : 0;
35488
36070
  const borderBottom = cell.style?.border?.bottom?.style ? borderStyleToLineWidth(cell.style.border.bottom.style) / 2 : 0;
@@ -35512,31 +36094,30 @@ this.Documonster = this.Documonster || {};
35512
36094
  * Count the wrap-line count for a cell, using actual font measurements
35513
36095
  * so row heights match the page renderer exactly.
35514
36096
  */
35515
- function countWrapLines(cell, fontSize, scaleFactor, sheet, fontManager, options) {
36097
+ function countWrapLines(cell, fontSize, sheet, fontManager, options) {
35516
36098
  const text = typeof cell.text === "string" ? cell.text : String(cell.text ?? "");
35517
36099
  const lineCount = Math.max(1, (text.match(/\n/g) ?? []).length + 1);
35518
36100
  if (!cell.style?.alignment?.wrapText || text.length === 0) return lineCount;
35519
- const scaledColPts = charWidthToPixel(sheet.columns.get(cell.col)?.width ?? DEFAULT_COLUMN_WIDTH$1, 7) * PX_TO_PT * scaleFactor;
36101
+ const colPts = charWidthToPixel(sheet.columns.get(cell.col)?.width ?? DEFAULT_COLUMN_WIDTH$1, 7) * PX_TO_PT;
35520
36102
  const indent = cell.style.alignment.indent ?? 0;
35521
36103
  const borderLeft = cell.style?.border?.left?.style ? borderStyleToLineWidth(cell.style.border.left.style) / 2 : 0;
35522
36104
  const borderRight = cell.style?.border?.right?.style ? borderStyleToLineWidth(cell.style.border.right.style) / 2 : 0;
35523
36105
  const padding = 3 + borderLeft + (3 + borderRight) + indent * 10;
35524
- const effectiveWidth = Math.max(scaledColPts - padding, 1);
36106
+ const effectiveWidth = Math.max(colPts - padding, 1);
35525
36107
  if (cell.type === PdfCellType.RichText) {
35526
36108
  const value = cell.value;
35527
36109
  if (value && typeof value === "object" && "richText" in value) {
35528
36110
  const runs = value.richText;
35529
36111
  if (runs.length > 0) {
35530
- const wrappedCount = countRichTextWrapLines(text, runs, scaleFactor, effectiveWidth, fontManager, options, cell.style?.font);
36112
+ const wrappedCount = countRichTextWrapLines(text, runs, effectiveWidth, fontManager, options, cell.style?.font);
35531
36113
  return Math.max(lineCount, wrappedCount);
35532
36114
  }
35533
36115
  }
35534
36116
  }
35535
- const scaledFontSize = fontSize * scaleFactor;
35536
36117
  const fontProps = extractFontProperties(cell.style.font, options.defaultFontFamily, options.defaultFontSize);
35537
36118
  const pdfFontName = resolvePdfFontName(fontProps.fontFamily, fontProps.bold, fontProps.italic);
35538
36119
  const resourceName = fontManager.hasEmbeddedFont() ? fontManager.getEmbeddedResourceName() : fontManager.ensureFont(pdfFontName);
35539
- const measure = (s) => fontManager.measureText(s, resourceName, scaledFontSize);
36120
+ const measure = (s) => fontManager.measureText(s, resourceName, fontSize);
35540
36121
  const wrappedLines = wrapTextLines(text, measure, effectiveWidth);
35541
36122
  return Math.max(lineCount, wrappedLines.length);
35542
36123
  }
@@ -35545,7 +36126,7 @@ this.Documonster = this.Documonster || {};
35545
36126
  * This mirrors the logic in wrapRichTextLines (page-renderer) so that
35546
36127
  * the row height calculation matches the actual rendering.
35547
36128
  */
35548
- function countRichTextWrapLines(text, runs, scaleFactor, effectiveWidth, fontManager, options, cellFont) {
36129
+ function countRichTextWrapLines(text, runs, effectiveWidth, fontManager, options, cellFont) {
35549
36130
  const defaultFamily = cellFont?.name ?? options.defaultFontFamily;
35550
36131
  const defaultSize = cellFont?.size ?? options.defaultFontSize;
35551
36132
  const runForChar = [];
@@ -35569,7 +36150,7 @@ this.Documonster = this.Documonster || {};
35569
36150
  size: run.font.size ?? cellFont?.size,
35570
36151
  bold: run.font.bold ?? false,
35571
36152
  italic: run.font.italic ?? false
35572
- } : cellFont, defaultFamily, defaultSize).fontSize * scaleFactor;
36153
+ } : cellFont, defaultFamily, defaultSize).fontSize;
35573
36154
  });
35574
36155
  const measureRange = (start, end) => {
35575
36156
  let width = 0;
@@ -35638,17 +36219,21 @@ this.Documonster = this.Documonster || {};
35638
36219
  return Math.max(1, totalLines);
35639
36220
  }
35640
36221
  /**
35641
- * Build a set of visible-row indices where manual page breaks occur.
36222
+ * Translate manual break positions from sheet track numbers into indices of the
36223
+ * printed track list.
36224
+ *
36225
+ * A break sits *after* its track, so the following index starts a new page.
36226
+ * Shared by both axes; computed once per sheet because the fit solver paginates
36227
+ * many times and must not rebuild the lookup on every probe.
35642
36228
  */
35643
- function buildRowBreakSet(sheet, visibleRows) {
36229
+ function buildBreakSet(breakTracks, visibleTracks) {
35644
36230
  const breaks = /* @__PURE__ */ new Set();
35645
- const rowBreaks = sheet.rowBreaks ?? [];
35646
- if (rowBreaks.length === 0) return breaks;
35647
- const rowToIndex = /* @__PURE__ */ new Map();
35648
- for (let i = 0; i < visibleRows.length; i++) rowToIndex.set(visibleRows[i], i);
35649
- for (const brk of rowBreaks) {
35650
- const idx = rowToIndex.get(brk);
35651
- if (idx !== void 0) breaks.add(idx + 1);
36231
+ if (breakTracks.length === 0) return breaks;
36232
+ const trackToIndex = /* @__PURE__ */ new Map();
36233
+ for (let i = 0; i < visibleTracks.length; i++) trackToIndex.set(visibleTracks[i], i);
36234
+ for (const track of breakTracks) {
36235
+ const index = trackToIndex.get(track);
36236
+ if (index !== void 0) breaks.add(index + 1);
35652
36237
  }
35653
36238
  return breaks;
35654
36239
  }
@@ -35676,89 +36261,73 @@ this.Documonster = this.Documonster || {};
35676
36261
  }
35677
36262
  return map;
35678
36263
  }
35679
- function paginateRows(rowHeights, availableHeight, repeatRowCount, rowBreaks) {
35680
- if (rowHeights.length === 0) return [[]];
36264
+ /**
36265
+ * Split a track list (rows or columns) into pages.
36266
+ *
36267
+ * One implementation serves both axes: heights against the available page
36268
+ * height, widths against the content width. The axes differ only in the overflow
36269
+ * tolerance, so `epsilon` is the sole parameter that distinguishes them —
36270
+ * columns need a small slack because scaled point widths accumulate rounding.
36271
+ *
36272
+ * `repeatIndices` are the tracks of a print-title band. They are re-emitted at
36273
+ * the start of every page after the first, which is why they are absolute
36274
+ * indices rather than a count: a band may sit in the middle of the printed range
36275
+ * (Excel allows `printTitlesColumn = "C:D"`).
36276
+ *
36277
+ * Manual breaks are honoured via `breaks`, holding the index that must start a
36278
+ * new page.
36279
+ */
36280
+ function paginateTracks(sizes, available, repeatIndices, breaks, epsilon = 0) {
36281
+ if (sizes.length === 0) return [[]];
35681
36282
  const pages = [];
35682
- let currentPage = [];
35683
- let currentPageHeight = 0;
36283
+ let current = [];
36284
+ let used = 0;
35684
36285
  let isFirstPage = true;
35685
36286
  let repeatedPrefixCount = 0;
35686
- const addRepeatRows = () => {
36287
+ const repeatSet = new Set(repeatIndices);
36288
+ const addRepeats = () => {
35687
36289
  repeatedPrefixCount = 0;
35688
- for (let h = 0; h < repeatRowCount && h < rowHeights.length; h++) {
35689
- if (currentPageHeight + rowHeights[h] > availableHeight && currentPage.length > 0) break;
35690
- currentPage.push(h);
35691
- currentPageHeight += rowHeights[h];
36290
+ for (const index of repeatIndices) {
36291
+ if (index >= sizes.length) continue;
36292
+ if (used + sizes[index] > available + epsilon && current.length > 0) break;
36293
+ current.push(index);
36294
+ used += sizes[index];
35692
36295
  repeatedPrefixCount++;
35693
36296
  }
35694
36297
  };
35695
- for (let i = 0; i < rowHeights.length; i++) {
35696
- const rowHeight = rowHeights[i];
35697
- const pageAvailable = availableHeight;
35698
- let skipRepeatedRow = false;
35699
- while (true) {
35700
- const forceBreak = rowBreaks.has(i) && currentPage.length > 0;
35701
- if ((forceBreak || currentPageHeight + rowHeight > pageAvailable) && currentPage.length > 0) {
35702
- if (!forceBreak && !isFirstPage && currentPage.length > 0 && currentPage.length === repeatedPrefixCount) {
35703
- currentPage = [];
35704
- currentPageHeight = 0;
36298
+ for (let i = 0; i < sizes.length; i++) {
36299
+ const size = sizes[i];
36300
+ let breakConsumed = false;
36301
+ for (;;) {
36302
+ const forceBreak = !breakConsumed && breaks.has(i) && current.length > 0;
36303
+ if ((forceBreak || used + size > available + epsilon) && current.length > 0) {
36304
+ if (forceBreak) breakConsumed = true;
36305
+ if (!forceBreak && !isFirstPage && current.length === repeatedPrefixCount) {
36306
+ current = [];
36307
+ used = 0;
35705
36308
  repeatedPrefixCount = 0;
35706
36309
  continue;
35707
36310
  }
35708
- pages.push(currentPage);
35709
- currentPage = [];
35710
- currentPageHeight = 0;
36311
+ pages.push(current);
36312
+ current = [];
36313
+ used = 0;
35711
36314
  repeatedPrefixCount = 0;
35712
36315
  isFirstPage = false;
35713
- addRepeatRows();
36316
+ addRepeats();
35714
36317
  continue;
35715
36318
  }
35716
- if (!isFirstPage && i < repeatRowCount && currentPage.includes(i)) {
35717
- skipRepeatedRow = true;
35718
- break;
36319
+ if (isFirstPage || !repeatSet.has(i) || !current.includes(i)) {
36320
+ current.push(i);
36321
+ used += size;
35719
36322
  }
35720
- currentPage.push(i);
35721
- currentPageHeight += rowHeight;
35722
36323
  break;
35723
36324
  }
35724
- if (skipRepeatedRow) continue;
35725
- }
35726
- if (currentPage.length > 0) pages.push(currentPage);
35727
- return pages.length > 0 ? pages : [[]];
35728
- }
35729
- /**
35730
- * Split columns into groups for horizontal pagination.
35731
- */
35732
- function paginateColumns(columnWidths, contentWidth, sheet, visibleCols) {
35733
- if (columnWidths.length === 0) return [[]];
35734
- const colBreaks = /* @__PURE__ */ new Set();
35735
- const wsColBreaks = sheet.colBreaks ?? [];
35736
- if (wsColBreaks.length > 0) {
35737
- const colToIndex = /* @__PURE__ */ new Map();
35738
- for (let i = 0; i < visibleCols.length; i++) colToIndex.set(visibleCols[i], i);
35739
- for (const brk of wsColBreaks) {
35740
- const idx = colToIndex.get(brk);
35741
- if (idx !== void 0) colBreaks.add(idx + 1);
35742
- }
35743
- }
35744
- const groups = [];
35745
- let currentGroup = [];
35746
- let currentWidth = 0;
35747
- for (let i = 0; i < columnWidths.length; i++) {
35748
- const colWidth = columnWidths[i];
35749
- if ((colBreaks.has(i) && currentGroup.length > 0 || currentWidth + colWidth > contentWidth + .01) && currentGroup.length > 0) {
35750
- groups.push(currentGroup);
35751
- currentGroup = [];
35752
- currentWidth = 0;
35753
- }
35754
- currentGroup.push(i);
35755
- currentWidth += colWidth;
35756
36325
  }
35757
- if (currentGroup.length > 0) groups.push(currentGroup);
35758
- return groups.length > 0 ? groups : [Array.from({ length: columnWidths.length }, (_, i) => i)];
36326
+ if (current.length > 0) pages.push(current);
36327
+ return pages;
35759
36328
  }
35760
36329
  function buildLayoutCell(cell, x, y, width, height, colSpan, rowSpan, options, fontManager, scaleFactor) {
35761
- const text = cell?.text ?? "";
36330
+ const text = resolveErrorText(cell, options.errors);
35762
36331
  const style = cell?.style ?? {};
35763
36332
  const fontProps = extractFontProperties(style.font, options.defaultFontFamily, options.defaultFontSize);
35764
36333
  const scaledFontSize = fontProps.fontSize * scaleFactor;
@@ -35769,7 +36338,9 @@ this.Documonster = this.Documonster || {};
35769
36338
  fontManager.trackText(text);
35770
36339
  }
35771
36340
  const richText = buildRichTextRuns(cell, options, fontManager, scaleFactor, style.font);
35772
- const borders = excelBordersToPdf(style.border);
36341
+ const rawBorders = excelBordersToPdf(style.border);
36342
+ const borders = options.blackAndWhite ? grayscaleBorders(rawBorders) : rawBorders;
36343
+ const rawFill = excelFillToPdfColor(style.fill);
35773
36344
  return {
35774
36345
  text,
35775
36346
  rect: {
@@ -35784,8 +36355,8 @@ this.Documonster = this.Documonster || {};
35784
36355
  italic: fontProps.italic,
35785
36356
  strike: fontProps.strike,
35786
36357
  underline: fontProps.underline,
35787
- textColor: fontProps.textColor,
35788
- fillColor: excelFillToPdfColor(style.fill),
36358
+ textColor: options.blackAndWhite ? toGrayscale(fontProps.textColor) : fontProps.textColor,
36359
+ fillColor: options.blackAndWhite && rawFill !== null ? toGrayscale(rawFill) : rawFill,
35789
36360
  horizontalAlign: resolveHorizontalAlign(style.alignment, cell?.type, cell?.result),
35790
36361
  verticalAlign: excelVAlignToPdf(style.alignment),
35791
36362
  wrapText: style.alignment?.wrapText ?? false,
@@ -35873,7 +36444,7 @@ this.Documonster = this.Documonster || {};
35873
36444
  * - `br` — when present — locates the opposite corner, so the rect size is
35874
36445
  * the difference.
35875
36446
  * - `ext` — when present — overrides the size directly. Images use pixels
35876
- * (px × 0.75 = pt); charts use EMU (EMU / 9525 = pt). The `extUnit`
36447
+ * (px × 0.75 = pt); charts use EMU (EMU / 12700 = pt). The `extUnit`
35877
36448
  * field disambiguates. Historical callers that omit `extUnit` keep
35878
36449
  * the legacy px behaviour.
35879
36450
  *
@@ -35899,8 +36470,8 @@ this.Documonster = this.Documonster || {};
35899
36470
  const extUnit = range.extUnit ?? "px";
35900
36471
  if (range.ext) {
35901
36472
  if (extUnit === "emu") {
35902
- width = emuToPx$1(range.ext.width) * scaleFactor;
35903
- height = emuToPx$1(range.ext.height) * scaleFactor;
36473
+ width = emuToPt$1(range.ext.width) * scaleFactor;
36474
+ height = emuToPt$1(range.ext.height) * scaleFactor;
35904
36475
  } else {
35905
36476
  width = range.ext.width * .75 * scaleFactor;
35906
36477
  height = range.ext.height * .75 * scaleFactor;
@@ -36006,13 +36577,16 @@ this.Documonster = this.Documonster || {};
36006
36577
  continue;
36007
36578
  }
36008
36579
  const tl = chart.range.tl;
36580
+ const tlCol = (tl.nativeCol ?? tl.col ?? 0) + 1;
36009
36581
  const tlRow = (tl.nativeRow ?? tl.row ?? 0) + 1;
36582
+ const band = layoutPages.filter((page) => page.sheetCols.includes(tlCol));
36583
+ const ordered = (band.length > 0 ? band : [...layoutPages]).sort((a, b) => (a.sheetRows[0] ?? 0) - (b.sheetRows[0] ?? 0));
36010
36584
  let targetPage;
36011
- for (let pi = 0; pi < layoutPages.length; pi++) {
36012
- const page = layoutPages[pi];
36585
+ for (let pi = 0; pi < ordered.length; pi++) {
36586
+ const page = ordered[pi];
36013
36587
  const lastPageRow = page.sheetRows[page.sheetRows.length - 1] ?? 0;
36014
- if (lastPageRow >= tlRow - 1 && pi + 1 < layoutPages.length) {
36015
- targetPage = layoutPages[pi + 1];
36588
+ if (lastPageRow >= tlRow - 1 && pi + 1 < ordered.length) {
36589
+ targetPage = ordered[pi + 1];
36016
36590
  break;
36017
36591
  }
36018
36592
  if (lastPageRow >= tlRow) {
@@ -36020,7 +36594,7 @@ this.Documonster = this.Documonster || {};
36020
36594
  break;
36021
36595
  }
36022
36596
  }
36023
- if (!targetPage) targetPage = layoutPages[layoutPages.length - 1];
36597
+ if (!targetPage) targetPage = ordered[ordered.length - 1];
36024
36598
  if (targetPage) {
36025
36599
  const margins = targetPage.options.margins;
36026
36600
  const headerH = targetPage.options.showSheetNames ? 20 : 0;
@@ -36155,10 +36729,28 @@ this.Documonster = this.Documonster || {};
36155
36729
  italic: fontProps.italic,
36156
36730
  strike: fontProps.strike,
36157
36731
  underline: fontProps.underline,
36158
- textColor: fontProps.textColor
36732
+ textColor: options.blackAndWhite ? toGrayscale(fontProps.textColor) : fontProps.textColor
36159
36733
  };
36160
36734
  });
36161
36735
  }
36736
+ /**
36737
+ * Apply Excel's "Cell errors as" print option to a cell's display text.
36738
+ *
36739
+ * Error values reach the PDF model in two shapes: a plain error cell
36740
+ * (`PdfCellType.Error`) and a formula whose computed result is an error
36741
+ * (`PdfCellType.Formula` with an `{ error }` result). Both must be substituted.
36742
+ */
36743
+ function resolveErrorText(cell, mode) {
36744
+ const text = cell?.text ?? "";
36745
+ if (mode === "displayed" || !cell) return text;
36746
+ if (!(cell.type === PdfCellType.Error || cell.type === PdfCellType.Formula && typeof cell.result === "object" && cell.result !== null && "error" in cell.result)) return text;
36747
+ switch (mode) {
36748
+ case "blank": return "";
36749
+ case "dash": return "--";
36750
+ case "NA": return "#N/A";
36751
+ default: return text;
36752
+ }
36753
+ }
36162
36754
  var DEFAULT_COLUMN_WIDTH$1, DEFAULT_ROW_HEIGHT, MIN_COLUMN_WIDTH;
36163
36755
  var init_layout_engine = __esmMin((() => {
36164
36756
  init_font_manager();
@@ -36269,7 +36861,7 @@ this.Documonster = this.Documonster || {};
36269
36861
  if (!chart.drawVector) continue;
36270
36862
  const stream = new PdfContentStream();
36271
36863
  const alphaValues = /* @__PURE__ */ new Set();
36272
- const surface = createChartSurface(stream, fontManager, alphaValues);
36864
+ const surface = createChartSurface(stream, fontManager, alphaValues, page.options.blackAndWhite);
36273
36865
  chart.drawVector(surface, constrainChartAspectRatio(chart.rect));
36274
36866
  cache.set(chart, {
36275
36867
  stream,
@@ -36363,6 +36955,7 @@ this.Documonster = this.Documonster || {};
36363
36955
  try {
36364
36956
  const { stream: contentStream, alphaValues } = renderPage(page, page.options, fontManager, totalPages);
36365
36957
  const imageXObjects = /* @__PURE__ */ new Map();
36958
+ const grayscaleRaster = page.options.blackAndWhite;
36366
36959
  let headerImageContentObjNum;
36367
36960
  const headerContent = selectHeaderFooter(page, "header");
36368
36961
  const footerContent = selectHeaderFooter(page, "footer");
@@ -36373,15 +36966,18 @@ this.Documonster = this.Documonster || {};
36373
36966
  const alignWithMargins = page.headerFooter?.alignWithMargins !== false;
36374
36967
  const leftEdge = alignWithMargins ? page.options.margins.left : 18;
36375
36968
  const rightEdge = alignWithMargins ? page.width - page.options.margins.right : page.width - 18;
36969
+ const hfScale = page.headerFooter?.scaleWithDoc === false ? 1 : page.scaleFactor;
36376
36970
  for (let index = 0; index < headerImages.length; index++) {
36377
36971
  const headerImage = headerImages[index];
36378
36972
  if (!activePositions.has(headerImage.position)) continue;
36379
36973
  const imageName = `HfIm${index + 1}`;
36380
- imageXObjects.set(imageName, getOrWriteImageObject(writer, imageObjectCache, headerImage.data, headerImage.format));
36974
+ imageXObjects.set(imageName, getOrWriteImageObject(writer, imageObjectCache, headerImage.data, headerImage.format, grayscaleRaster));
36975
+ const drawWidth = headerImage.width * hfScale;
36976
+ const drawHeight = headerImage.height * hfScale;
36381
36977
  const placement = headerFooterImagePlacement(headerImage.position);
36382
- const imageX = placement.section === "left" ? leftEdge : placement.section === "right" ? rightEdge - headerImage.width : (page.width - headerImage.width) / 2;
36383
- const imageY = placement.kind === "header" ? page.height - page.options.headerMargin - headerImage.height : page.options.footerMargin;
36384
- imageStream.drawImage(imageName, imageX, imageY, headerImage.width, headerImage.height);
36978
+ const imageX = placement.section === "left" ? leftEdge : placement.section === "right" ? rightEdge - drawWidth : (page.width - drawWidth) / 2;
36979
+ const imageY = placement.kind === "header" ? page.height - page.options.headerMargin - drawHeight : page.options.footerMargin;
36980
+ imageStream.drawImage(imageName, imageX, imageY, drawWidth, drawHeight);
36385
36981
  }
36386
36982
  headerImageContentObjNum = writer.allocObject();
36387
36983
  writer.addStreamObject(headerImageContentObjNum, new PdfDict(), imageStream);
@@ -36389,7 +36985,7 @@ this.Documonster = this.Documonster || {};
36389
36985
  if (page.images.length > 0) for (let imgIdx = 0; imgIdx < page.images.length; imgIdx++) {
36390
36986
  const img = page.images[imgIdx];
36391
36987
  const imgName = `Im${imgIdx + 1}`;
36392
- const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, img.data, img.format);
36988
+ const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, img.data, img.format, grayscaleRaster);
36393
36989
  imageXObjects.set(imgName, imgObjNum);
36394
36990
  contentStream.drawImage(imgName, img.rect.x, img.rect.y, img.rect.width, img.rect.height);
36395
36991
  }
@@ -36405,7 +37001,7 @@ this.Documonster = this.Documonster || {};
36405
37001
  }
36406
37002
  if (chart.raster) {
36407
37003
  const imgName = `Im${++rasterCounter}`;
36408
- const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, chart.raster.data, chart.raster.format);
37004
+ const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, chart.raster.data, chart.raster.format, grayscaleRaster);
36409
37005
  imageXObjects.set(imgName, imgObjNum);
36410
37006
  contentStream.drawImage(imgName, chart.rect.x, chart.rect.y, chart.rect.width, chart.rect.height);
36411
37007
  }
@@ -36417,7 +37013,7 @@ this.Documonster = this.Documonster || {};
36417
37013
  const wmResult = renderWatermark(wmContentStream, page, watermark, fontManager);
36418
37014
  for (const alpha of wmResult.alphaValues) alphaValues.add(alpha);
36419
37015
  for (const wmImg of wmResult.imageXObjects) {
36420
- const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, wmImg.data, wmImg.format);
37016
+ const imgObjNum = getOrWriteImageObject(writer, imageObjectCache, wmImg.data, wmImg.format, grayscaleRaster);
36421
37017
  imageXObjects.set(wmImg.name, imgObjNum);
36422
37018
  }
36423
37019
  watermarkContentObjNum = writer.allocObject();
@@ -36469,6 +37065,7 @@ this.Documonster = this.Documonster || {};
36469
37065
  height: page.height,
36470
37066
  contentsRef,
36471
37067
  resourcesRef: resourcesObjNum,
37068
+ transparencyGroup: alphaValues.size > 0,
36472
37069
  annotRefs: annotRefs.length > 0 ? annotRefs : void 0
36473
37070
  });
36474
37071
  pageObjNums.push(pageObjNum);
@@ -36477,11 +37074,16 @@ this.Documonster = this.Documonster || {};
36477
37074
  throw new PdfRenderError(`Failed to render page ${page.pageNumber} of "${page.sheetName}"`, { cause: err });
36478
37075
  }
36479
37076
  }
36480
- function getOrWriteImageObject(writer, cache, data, format) {
36481
- const existing = cache.get(data);
37077
+ function getOrWriteImageObject(writer, cache, data, format, grayscale = false) {
37078
+ let variants = cache.get(data);
37079
+ if (variants === void 0) {
37080
+ variants = /* @__PURE__ */ new Map();
37081
+ cache.set(data, variants);
37082
+ }
37083
+ const existing = variants.get(grayscale);
36482
37084
  if (existing !== void 0) return existing;
36483
- const objectNumber = writeImageXObject(writer, data, format);
36484
- cache.set(data, objectNumber);
37085
+ const objectNumber = writeImageXObject(writer, data, format, grayscale);
37086
+ variants.set(grayscale, objectNumber);
36485
37087
  return objectNumber;
36486
37088
  }
36487
37089
  function headerFooterImagePositions(content, kind) {
@@ -36536,6 +37138,97 @@ this.Documonster = this.Documonster || {};
36536
37138
  return result;
36537
37139
  }
36538
37140
  /**
37141
+ * Parse an Excel "Columns to repeat at left" reference (`printTitlesColumn`)
37142
+ * into an absolute column band, e.g. `"A:B"` → `{ first: 1, last: 2 }`.
37143
+ *
37144
+ * Returns `null` for anything that is not a column range.
37145
+ */
37146
+ function parseTitlesColumnBand(ref) {
37147
+ const match = ref.replace(/\$/g, "").toUpperCase().match(/^([A-Z]+)(?::([A-Z]+))?$/);
37148
+ if (!match) return null;
37149
+ const first = columnLettersToNumber(match[1]);
37150
+ const last = match[2] ? columnLettersToNumber(match[2]) : first;
37151
+ if (last < first) return null;
37152
+ return {
37153
+ first,
37154
+ last
37155
+ };
37156
+ }
37157
+ /**
37158
+ * Parse an Excel "Rows to repeat at top" reference (`printTitlesRow`) into an
37159
+ * absolute row band, e.g. `"1:3"` → `{ first: 1, last: 3 }`.
37160
+ */
37161
+ function parseTitlesRowBand(ref) {
37162
+ const match = ref.replace(/\$/g, "").match(/^(\d+)(?::(\d+))?$/);
37163
+ if (!match) return null;
37164
+ const first = parseInt(match[1], 10);
37165
+ const last = match[2] ? parseInt(match[2], 10) : first;
37166
+ if (first < 1 || last < first) return null;
37167
+ return {
37168
+ first,
37169
+ last
37170
+ };
37171
+ }
37172
+ function columnLettersToNumber(letters) {
37173
+ let n = 0;
37174
+ for (let i = 0; i < letters.length; i++) n = n * 26 + (letters.charCodeAt(i) - 64);
37175
+ return n;
37176
+ }
37177
+ function normalizePageOrder(value) {
37178
+ return value === "overThenDown" ? "overThenDown" : "downThenOver";
37179
+ }
37180
+ /** Excel writes `None` / `asDisplayed` / `atEnd`. */
37181
+ function normalizeCommentMode(value) {
37182
+ if (value === "atEnd" || value === "asDisplayed") return value;
37183
+ return "none";
37184
+ }
37185
+ function normalizeErrorMode(value) {
37186
+ switch (value) {
37187
+ case "blank":
37188
+ case "dash":
37189
+ case "NA": return value;
37190
+ default: return "displayed";
37191
+ }
37192
+ }
37193
+ /**
37194
+ * Reconcile documonster's scaling options with Excel's two mutually exclusive
37195
+ * scaling modes (Page Setup → Scaling):
37196
+ *
37197
+ * - **"Adjust to N %"** — `pageSetup.scale`, with `pageSetup.fitToPage` off.
37198
+ * - **"Fit to N pages wide by M tall"** — `pageSetup.fitToWidth` /
37199
+ * `fitToHeight`, with `pageSetup.fitToPage` on. Excel ignores `scale` here.
37200
+ *
37201
+ * Neither mode ever enlarges: Excel caps fit-to-page scaling at 100%.
37202
+ *
37203
+ * `PdfExportOptions.fitToPage` is documonster's own convenience default
37204
+ * ("shrink to one page wide"). It only applies when neither the caller nor the
37205
+ * sheet expresses a scaling intent, so a sheet that asks for 80% is not
37206
+ * shrunk twice.
37207
+ */
37208
+ function resolveScaling(options, ps) {
37209
+ const clampPages = (n) => n === void 0 || !Number.isFinite(n) || n <= 0 ? 0 : Math.floor(n);
37210
+ const callerFit = options?.fitToWidth !== void 0 || options?.fitToHeight !== void 0;
37211
+ const sheetFitMode = ps?.fitToPage === true;
37212
+ let fitToWidth = 0;
37213
+ let fitToHeight = 0;
37214
+ if (callerFit) {
37215
+ fitToWidth = clampPages(options?.fitToWidth);
37216
+ fitToHeight = clampPages(options?.fitToHeight);
37217
+ } else if (sheetFitMode && options?.scale === void 0 && options?.fitToPage !== false) {
37218
+ fitToWidth = clampPages(ps?.fitToWidth);
37219
+ fitToHeight = clampPages(ps?.fitToHeight);
37220
+ }
37221
+ const sheetScalePercent = !sheetFitMode && ps?.scale !== void 0 && ps.scale !== 100 ? ps.scale / 100 : void 0;
37222
+ const scale = Math.max(.1, Math.min(4, options?.scale ?? sheetScalePercent ?? 1));
37223
+ const hasFitConstraint = fitToWidth > 0 || fitToHeight > 0;
37224
+ return {
37225
+ fitToPage: options?.fitToPage ?? !(hasFitConstraint || sheetScalePercent !== void 0 || callerFit),
37226
+ scale,
37227
+ fitToWidth,
37228
+ fitToHeight
37229
+ };
37230
+ }
37231
+ /**
36539
37232
  * Resolve user options with defaults.
36540
37233
  */
36541
37234
  function resolveOptions(options, sheet) {
@@ -36544,26 +37237,46 @@ this.Documonster = this.Documonster || {};
36544
37237
  const chartsheetOrientation = sheet && isPdfChartsheet(sheet) ? sheet.orientation : void 0;
36545
37238
  const orientation = options?.orientation ?? chartsheetOrientation ?? (ps?.orientation === "landscape" ? "landscape" : "portrait");
36546
37239
  const margins = resolveMargins(options?.margins, ps?.margins);
36547
- const gridLineColor = argbToPdfColor(options?.gridLineColor ?? "FFD0D0D0") ?? {
37240
+ const blackAndWhite = options?.blackAndWhite ?? ps?.blackAndWhite ?? false;
37241
+ const gridLineColorRaw = argbToPdfColor(options?.gridLineColor ?? "FFD0D0D0") ?? {
36548
37242
  r: .816,
36549
37243
  g: .816,
36550
37244
  b: .816
36551
37245
  };
36552
- let repeatRows = options?.repeatRows ?? false;
36553
- if (repeatRows === false && sheet && !isPdfChartsheet(sheet) && ps?.printTitlesRow) {
36554
- const match = ps.printTitlesRow.match(/^(\d+)(?::(\d+))?$/);
36555
- if (match) repeatRows = parseInt(match[2] ?? match[1], 10);
36556
- }
37246
+ const gridLineColor = blackAndWhite ? toGrayscale(gridLineColorRaw) : gridLineColorRaw;
37247
+ const isWorksheet = Boolean(sheet) && !isPdfChartsheet(sheet);
37248
+ const countToBand = (n) => typeof n === "number" && n > 0 ? {
37249
+ first: 1,
37250
+ last: n
37251
+ } : false;
37252
+ let repeatRows = false;
37253
+ if (options?.repeatRows !== void 0) repeatRows = countToBand(options.repeatRows);
37254
+ else if (isWorksheet && ps?.printTitlesRow) repeatRows = parseTitlesRowBand(ps.printTitlesRow) ?? false;
37255
+ let repeatCols = false;
37256
+ if (options?.repeatCols !== void 0) repeatCols = countToBand(options.repeatCols);
37257
+ else if (isWorksheet && ps?.printTitlesColumn) repeatCols = parseTitlesColumnBand(ps.printTitlesColumn) ?? false;
37258
+ const { fitToPage, scale, fitToWidth, fitToHeight } = resolveScaling(options, ps);
36557
37259
  return {
36558
37260
  pageSize,
36559
37261
  orientation,
36560
37262
  margins,
36561
37263
  ignorePrintArea: options?.ignorePrintArea ?? false,
36562
- fitToPage: options?.fitToPage !== void 0 ? options.fitToPage : true,
36563
- scale: Math.max(.1, Math.min(3, options?.scale ?? ((options?.fitToPage !== void 0 ? options.fitToPage : true) ? 1 : ps?.scale ? ps.scale / 100 : 1))),
37264
+ fitToPage,
37265
+ scale,
37266
+ fitToWidth,
37267
+ fitToHeight,
36564
37268
  showGridLines: options?.showGridLines ?? ps?.showGridLines ?? false,
36565
37269
  gridLineColor,
37270
+ showRowColHeaders: options?.showRowColHeaders ?? ps?.showRowColHeaders ?? false,
37271
+ horizontalCentered: options?.horizontalCentered ?? ps?.horizontalCentered ?? false,
37272
+ verticalCentered: options?.verticalCentered ?? ps?.verticalCentered ?? false,
37273
+ pageOrder: options?.pageOrder ?? normalizePageOrder(ps?.pageOrder),
37274
+ blackAndWhite,
37275
+ draft: options?.draft ?? ps?.draft ?? false,
37276
+ errors: options?.errors ?? normalizeErrorMode(ps?.errors),
37277
+ cellComments: options?.cellComments ?? normalizeCommentMode(ps?.cellComments),
36566
37278
  repeatRows,
37279
+ repeatCols,
36567
37280
  defaultFontFamily: options?.defaultFontFamily ?? "Helvetica",
36568
37281
  defaultFontSize: options?.defaultFontSize ?? 11,
36569
37282
  showSheetNames: options?.showSheetNames ?? false,
@@ -45944,6 +46657,9 @@ this.Documonster = this.Documonster || {};
45944
46657
  function cellHyperlink(c) {
45945
46658
  return c._value.hyperlink;
45946
46659
  }
46660
+ function cellComment(c) {
46661
+ return c._comment;
46662
+ }
45947
46663
  function cellText(c) {
45948
46664
  return c._value.toString();
45949
46665
  }
@@ -63776,7 +64492,7 @@ this.Documonster = this.Documonster || {};
63776
64492
  */
63777
64493
  async function excelToPdf(workbook, options) {
63778
64494
  options?.recalculate?.(workbook);
63779
- return exportPdf(await excelWorkbookToPdf(workbook), options);
64495
+ return exportPdf(await excelWorkbookToPdf(workbook, options), options);
63780
64496
  }
63781
64497
  /**
63782
64498
  * Render a single {@link Chart} to a standalone one-page PDF.
@@ -63879,9 +64595,9 @@ this.Documonster = this.Documonster || {};
63879
64595
  * Chartsheets without an orderNo fall to the end, mirroring how Excel
63880
64596
  * treats sheets with missing tab positions.
63881
64597
  */
63882
- async function excelWorkbookToPdf(workbook) {
63883
- const worksheetResults = await Promise.all(getWorksheets(workbook).map((ws) => convertSheet(ws, workbook)));
63884
- const chartsheetResults = await Promise.all(getChartsheets(workbook).map((cs) => convertChartsheet(cs, workbook)));
64598
+ async function excelWorkbookToPdf(workbook, options) {
64599
+ const worksheetResults = await Promise.all(getWorksheets(workbook).map((ws) => convertSheet(ws, workbook, options)));
64600
+ const chartsheetResults = await Promise.all(getChartsheets(workbook).map((cs) => convertChartsheet(cs, workbook, options)));
63885
64601
  const combined = [...worksheetResults, ...chartsheetResults];
63886
64602
  combined.sort((a, b) => (a.orderNo ?? Number.POSITIVE_INFINITY) - (b.orderNo ?? Number.POSITIVE_INFINITY));
63887
64603
  return {
@@ -63894,7 +64610,7 @@ this.Documonster = this.Documonster || {};
63894
64610
  sheets: combined
63895
64611
  };
63896
64612
  }
63897
- async function convertSheet(ws, workbook) {
64613
+ async function convertSheet(ws, workbook, options) {
63898
64614
  const dimensions = getSheetDimensions(ws);
63899
64615
  const hasData = dimensions && dimensions.top > 0 && dimensions.left > 0;
63900
64616
  const bounds = hasData ? {
@@ -63971,16 +64687,30 @@ this.Documonster = this.Documonster || {};
63971
64687
  footer: ps.margins.footer
63972
64688
  } : void 0,
63973
64689
  scale: ps.scale,
64690
+ fitToPage: ps.fitToPage,
64691
+ fitToWidth: ps.fitToWidth,
64692
+ fitToHeight: ps.fitToHeight,
63974
64693
  printTitlesRow: ps.printTitlesRow,
64694
+ printTitlesColumn: ps.printTitlesColumn,
63975
64695
  showGridLines: ps.showGridLines,
64696
+ showRowColHeaders: ps.showRowColHeaders,
63976
64697
  printArea: ps.printArea,
63977
- firstPageNumber: ps.useFirstPageNumber === false ? void 0 : ps.firstPageNumber
64698
+ firstPageNumber: ps.useFirstPageNumber === false ? void 0 : ps.firstPageNumber,
64699
+ pageOrder: ps.pageOrder,
64700
+ blackAndWhite: ps.blackAndWhite,
64701
+ draft: ps.draft,
64702
+ errors: ps.errors,
64703
+ cellComments: ps.cellComments,
64704
+ horizontalCentered: ps.horizontalCentered,
64705
+ verticalCentered: ps.verticalCentered
63978
64706
  } : void 0;
63979
64707
  const rowBreaks = ws.rowBreaks?.map((b) => b.id);
63980
64708
  const colBreaks = ws.colBreaks?.map((b) => b.id);
63981
- const images = collectImages(ws, workbook);
63982
- const charts = await collectCharts(ws);
63983
- const sparklineCharts = collectSparklineCharts(ws);
64709
+ const comments = collectComments(ws);
64710
+ const draftQuality = options?.draft ?? ws.pageSetup?.draft ?? false;
64711
+ const images = draftQuality ? void 0 : collectImages(ws, workbook);
64712
+ const charts = draftQuality ? void 0 : await collectCharts(ws);
64713
+ const sparklineCharts = draftQuality ? void 0 : collectSparklineCharts(ws);
63984
64714
  const allCharts = charts ? sparklineCharts ? [...charts, ...sparklineCharts] : charts : sparklineCharts || void 0;
63985
64715
  const anchoredRanges = [];
63986
64716
  if (images) for (const img of images) anchoredRanges.push(img.range);
@@ -64023,14 +64753,74 @@ this.Documonster = this.Documonster || {};
64023
64753
  rows,
64024
64754
  merges,
64025
64755
  pageSetup,
64026
- headerFooter: convertHeaderFooter(ws, workbook),
64756
+ headerFooter: convertHeaderFooter(ws, workbook, draftQuality),
64027
64757
  rowBreaks,
64028
64758
  colBreaks,
64029
64759
  images,
64030
- charts: allCharts
64760
+ charts: allCharts,
64761
+ comments
64031
64762
  };
64032
64763
  }
64033
- function convertHeaderFooter(ws, workbook) {
64764
+ /**
64765
+ * Collect every comment on a worksheet, in row-major order.
64766
+ *
64767
+ * Two sources are merged: classic notes stored on the cell (`_comment`, the
64768
+ * VML-backed kind) and Office 365 threaded comments held on the sheet. Only the
64769
+ * text and author are needed — the printed form is an end-of-sheet list, not a
64770
+ * positioned box — so the VML geometry is deliberately ignored.
64771
+ */
64772
+ function collectComments(ws) {
64773
+ const collected = [];
64774
+ for (const row of ws._rows ?? []) {
64775
+ if (!row) continue;
64776
+ for (const cell of row.cells ?? []) {
64777
+ if (!cell) continue;
64778
+ const note = cellComment(cell);
64779
+ const text = noteText(note?.note);
64780
+ if (text) collected.push({
64781
+ ref: cell.address,
64782
+ text,
64783
+ author: note?.author,
64784
+ anchor: parseVmlAnchor(note?.note)
64785
+ });
64786
+ }
64787
+ }
64788
+ for (const entry of ws.threadedComments ?? []) {
64789
+ const text = entry.comment.text?.trim();
64790
+ if (text) collected.push({
64791
+ ref: entry.ref,
64792
+ text,
64793
+ author: entry.comment.personId
64794
+ });
64795
+ }
64796
+ return collected.length > 0 ? collected : void 0;
64797
+ }
64798
+ /**
64799
+ * Decode a VML comment anchor into fractional sheet coordinates.
64800
+ *
64801
+ * `<x:Anchor>` holds eight integers: for each edge a column/row index followed
64802
+ * by an offset into it, measured in 1/68 of a column and 1/18 of a row (the
64803
+ * reciprocals Excel uses when it writes the value). Returns `undefined` when the
64804
+ * note has no anchor, so callers fall back to Excel's default placement.
64805
+ */
64806
+ function parseVmlAnchor(note) {
64807
+ if (!note || typeof note === "string" || typeof note.anchor !== "string") return;
64808
+ const parts = note.anchor.split(",").map((p) => Number(p.trim()));
64809
+ if (parts.length !== 8 || parts.some((n) => !Number.isFinite(n))) return;
64810
+ const [l, lf, t, tf, r, rf, b, bf] = parts;
64811
+ return {
64812
+ left: l + lf / 68,
64813
+ top: t + tf / 18,
64814
+ right: r + rf / 68,
64815
+ bottom: b + bf / 18
64816
+ };
64817
+ }
64818
+ /** Flatten a note body, which may be plain text or a rich `NoteConfig`. */
64819
+ function noteText(note) {
64820
+ if (typeof note === "string") return note.trim() || void 0;
64821
+ if (note && typeof note === "object" && Array.isArray(note.texts)) return note.texts.map((t) => typeof t === "string" ? t : t.text ?? "").join("").trim() || void 0;
64822
+ }
64823
+ function convertHeaderFooter(ws, workbook, draftQuality = false) {
64034
64824
  const source = ws.headerFooter;
64035
64825
  const fields = [
64036
64826
  source.oddHeader,
@@ -64040,7 +64830,7 @@ this.Documonster = this.Documonster || {};
64040
64830
  source.firstHeader,
64041
64831
  source.firstFooter
64042
64832
  ];
64043
- const images = convertHeaderFooterImages(ws, workbook);
64833
+ const images = draftQuality ? [] : convertHeaderFooterImages(ws, workbook);
64044
64834
  if (!fields.some(Boolean) && images.length === 0) return;
64045
64835
  return {
64046
64836
  differentFirst: source.differentFirst,
@@ -64797,11 +65587,12 @@ this.Documonster = this.Documonster || {};
64797
65587
  * (matches what Excel prints for a chartsheet whose chart was deleted
64798
65588
  * but the sheet kept).
64799
65589
  */
64800
- async function convertChartsheet(cs, workbook) {
65590
+ async function convertChartsheet(cs, workbook, options) {
64801
65591
  const classicModel = chartsheetChartModel(cs);
64802
65592
  const chartExModel = chartsheetChartExModel(cs);
65593
+ const draftQuality = options?.draft ?? chartsheetPageSetup(cs)?.draft ?? false;
64803
65594
  let chart = {};
64804
- if (classicModel) {
65595
+ if (!draftQuality && classicModel) {
64805
65596
  const model = classicModel;
64806
65597
  chart = { drawVector: (surface, rect) => {
64807
65598
  drawChartPdf(surface, model, {
@@ -64811,7 +65602,7 @@ this.Documonster = this.Documonster || {};
64811
65602
  height: rect.height
64812
65603
  });
64813
65604
  } };
64814
- } else if (chartExModel) {
65605
+ } else if (!draftQuality && chartExModel) {
64815
65606
  if (canRenderChartExAsVectorPdf(chartExModel)) {
64816
65607
  const model = chartExModel;
64817
65608
  chart = { drawVector: (surface, rect) => {
@@ -64839,6 +65630,8 @@ this.Documonster = this.Documonster || {};
64839
65630
  orientation: ps?.orientation,
64840
65631
  paperSize: ps?.paperSize,
64841
65632
  showGridLines: false,
65633
+ blackAndWhite: ps?.blackAndWhite,
65634
+ draft: ps?.draft,
64842
65635
  firstPageNumber: ps?.useFirstPageNumber === false ? void 0 : ps?.firstPageNumber,
64843
65636
  margins: margins ? {
64844
65637
  left: margins.left ?? margins.l ?? .7,
@@ -64857,7 +65650,7 @@ this.Documonster = this.Documonster || {};
64857
65650
  orientation,
64858
65651
  chart,
64859
65652
  pageSetup,
64860
- headerFooter: convertHeaderFooterModel(chartsheetModel(cs).headerFooter, convertChartsheetHeaderImages(chartsheetModel(cs).headerImages, workbook))
65653
+ headerFooter: convertHeaderFooterModel(chartsheetModel(cs).headerFooter, draftQuality ? [] : convertChartsheetHeaderImages(chartsheetModel(cs).headerImages, workbook))
64861
65654
  };
64862
65655
  }
64863
65656
  function convertChartsheetHeaderImages(images, workbook) {