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
@@ -122,11 +122,52 @@ export interface PdfPageSetupData {
122
122
  header?: number;
123
123
  footer?: number;
124
124
  };
125
+ /** Excel's scaling percentage (10–400, where 100 = actual size). */
125
126
  scale?: number;
127
+ /**
128
+ * Whether Excel's "Fit to N page(s) wide by M tall" scaling mode is active
129
+ * (`<sheetPr><pageSetUpPr fitToPage="1"/></sheetPr>`). When true,
130
+ * {@link fitToWidth} / {@link fitToHeight} drive the scale and
131
+ * {@link scale} is ignored — the two are mutually exclusive in Excel's UI.
132
+ */
133
+ fitToPage?: boolean;
134
+ /** Pages wide to fit into. `0` means "unlimited" (no width constraint). */
135
+ fitToWidth?: number;
136
+ /** Pages tall to fit into. `0` means "unlimited" (no height constraint). */
137
+ fitToHeight?: number;
126
138
  printTitlesRow?: string;
139
+ /** Repeated columns, e.g. `"A:B"` or `"A"` (Excel's "Columns to repeat at left"). */
140
+ printTitlesColumn?: string;
127
141
  showGridLines?: boolean;
142
+ /** Print row numbers and column letters (`<printOptions headings="1"/>`). */
143
+ showRowColHeaders?: boolean;
128
144
  printArea?: string;
129
145
  firstPageNumber?: number;
146
+ /**
147
+ * Order in which pages of a multi-page sheet are emitted.
148
+ * Excel's default is `"downThenOver"`.
149
+ */
150
+ pageOrder?: string;
151
+ /** Render the sheet without color (Excel's "Black and white" print option). */
152
+ blackAndWhite?: boolean;
153
+ /** Draft quality — skips graphics (images and charts). */
154
+ draft?: boolean;
155
+ /** How error values are printed: `displayed` | `blank` | `dash` | `NA`. */
156
+ errors?: string;
157
+ /** How comments print: `None` | `asDisplayed` | `atEnd`. */
158
+ cellComments?: string;
159
+ /**
160
+ * Excel's "Center on page → Horizontally" print option
161
+ * (`<printOptions horizontalCentered="1"/>`). When false/absent the
162
+ * printed grid starts at the left margin.
163
+ */
164
+ horizontalCentered?: boolean;
165
+ /**
166
+ * Excel's "Center on page → Vertically" print option
167
+ * (`<printOptions verticalCentered="1"/>`). When false/absent the
168
+ * printed grid starts at the top margin.
169
+ */
170
+ verticalCentered?: boolean;
130
171
  }
131
172
  export type PdfHeaderFooterField = "pageNumber" | "pageCount" | "sheetName" | "fileName" | "filePath" | "date" | "time" | "image";
132
173
  export interface PdfHeaderFooterRun {
@@ -359,6 +400,8 @@ export interface PdfSheetData {
359
400
  images?: PdfSheetImage[];
360
401
  /** Embedded charts (classic + ChartEx) */
361
402
  charts?: PdfSheetChart[];
403
+ /** Cell comments and notes, in row-major order. */
404
+ comments?: PdfSheetComment[];
362
405
  }
363
406
  /**
364
407
  * A chartsheet — a single-chart "sheet" with no cell grid.
@@ -440,6 +483,68 @@ export declare const PageSizes: Record<PageSizeName, PdfPageSize>;
440
483
  * Page orientation for PDF export.
441
484
  */
442
485
  export type PdfOrientation = "portrait" | "landscape";
486
+ /**
487
+ * Order in which the pages of a multi-page sheet are emitted, mirroring
488
+ * Excel's "Page order" setting.
489
+ */
490
+ export type PdfPageOrder = "downThenOver" | "overThenDown";
491
+ /**
492
+ * How cells holding an error value are printed, mirroring Excel's
493
+ * "Cell errors as" setting.
494
+ */
495
+ export type PdfCellErrorMode = "displayed" | "blank" | "dash" | "NA";
496
+ /**
497
+ * How cell comments are printed, mirroring Excel's "Comments and notes"
498
+ * print option.
499
+ */
500
+ export type PdfCellCommentMode = "none" | "atEnd" | "asDisplayed";
501
+ /**
502
+ * Comment box placement, in fractional sheet coordinates.
503
+ *
504
+ * Excel stores this as a VML anchor: eight integers giving a column/row plus an
505
+ * offset in 1/68 of a column and 1/18 of a row. Those are pre-divided here so
506
+ * the layout engine only has to interpolate against its own track geometry.
507
+ * All values are 0-based, matching the VML convention.
508
+ */
509
+ export interface PdfCommentAnchor {
510
+ /** Left edge, in columns. `2.5` is halfway across the third column. */
511
+ left: number;
512
+ /** Top edge, in rows. */
513
+ top: number;
514
+ /** Right edge, in columns. */
515
+ right: number;
516
+ /** Bottom edge, in rows. */
517
+ bottom: number;
518
+ }
519
+ /** A single printed cell comment. */
520
+ export interface PdfSheetComment {
521
+ /** Cell address the comment is attached to, e.g. `"B7"`. */
522
+ ref: string;
523
+ /** Plain-text body. */
524
+ text: string;
525
+ /** Author, when the source recorded one. */
526
+ author?: string;
527
+ /**
528
+ * Where the comment box sits on the sheet, when the source recorded a VML
529
+ * anchor. Consumed by `cellComments: "asDisplayed"`; absent comments fall back
530
+ * to Excel's default offset from their cell.
531
+ */
532
+ anchor?: PdfCommentAnchor;
533
+ }
534
+ /**
535
+ * An inclusive band of repeated title rows or columns, in absolute 1-based
536
+ * sheet coordinates.
537
+ *
538
+ * Absolute rather than a count, because Excel's "Rows/Columns to repeat" are
539
+ * independent of the print area: a sheet printing `C5:H50` can still repeat
540
+ * `A:B` at the left of every page.
541
+ */
542
+ export interface PdfRepeatBand {
543
+ /** First row/column of the band (1-based, inclusive). */
544
+ first: number;
545
+ /** Last row/column of the band (1-based, inclusive). */
546
+ last: number;
547
+ }
443
548
  /**
444
549
  * Excel header/footer rendering options.
445
550
  *
@@ -497,16 +602,116 @@ export interface PdfExportOptions {
497
602
  */
498
603
  ignorePrintArea?: boolean;
499
604
  /**
500
- * Whether to auto-fit column widths to page width.
501
- * When true, columns are scaled proportionally to fit the page.
605
+ * Whether to shrink column widths so the grid fits the page width.
606
+ * Never enlarges — content narrower than the page is left at actual size,
607
+ * matching Excel's fit-to-page behaviour.
608
+ *
609
+ * This is the fallback used when neither the caller nor the sheet expresses a
610
+ * fit-to-N or percentage scaling intent, so a sheet asking for 80% is not
611
+ * shrunk twice. Passing it explicitly always wins, including over the sheet's
612
+ * own fit-to-N mode.
613
+ *
614
+ * Note that {@link scale} is a *multiplier applied on top* of this and does
615
+ * not disable it: `{ scale: 0.8 }` on an over-wide grid yields
616
+ * `0.8 × fit-to-width`. Pass `fitToPage: false` alongside it for a plain 80%.
502
617
  * @default true
503
618
  */
504
619
  fitToPage?: boolean;
505
620
  /**
506
- * Scale factor (0.1 to 3.0). Applied after fitToPage.
507
- * @default 1.0
621
+ * Scale factor (0.1 to 4.0), where `1.0` is actual size, multiplied with any
622
+ * fit-to-page shrinking (see {@link fitToPage}). Overrides the sheet's
623
+ * `pageSetup.scale`, which Excel stores as a 10–400 percentage.
624
+ *
625
+ * When omitted, the sheet's own scaling is used; see {@link fitToPage}.
508
626
  */
509
627
  scale?: number;
628
+ /**
629
+ * Shrink the grid so it spans at most this many pages horizontally,
630
+ * mirroring Excel's "Fit to N page(s) wide". `0` removes the constraint.
631
+ *
632
+ * Like Excel, this only ever shrinks. When omitted, the sheet's
633
+ * `pageSetup.fitToWidth` is used if its `fitToPage` mode is on.
634
+ */
635
+ fitToWidth?: number;
636
+ /**
637
+ * Shrink the grid so it spans at most this many pages vertically,
638
+ * mirroring Excel's "Fit to M page(s) tall". `0` removes the constraint.
639
+ *
640
+ * Like Excel, this only ever shrinks. When omitted, the sheet's
641
+ * `pageSetup.fitToHeight` is used if its `fitToPage` mode is on.
642
+ */
643
+ fitToHeight?: number;
644
+ /**
645
+ * Number of leading columns to repeat on every horizontal page, the
646
+ * counterpart of {@link repeatRows}.
647
+ *
648
+ * When omitted, the sheet's `pageSetup.printTitlesColumn` ("Columns to
649
+ * repeat at left", e.g. `"A:B"`) is used.
650
+ * @default false
651
+ */
652
+ repeatCols?: number | false;
653
+ /**
654
+ * Print row numbers and column letters around the grid, mirroring Excel's
655
+ * "Row and column headings" print option.
656
+ *
657
+ * When omitted, the sheet's `pageSetup.showRowColHeaders` is used.
658
+ * @default false
659
+ */
660
+ showRowColHeaders?: boolean;
661
+ /**
662
+ * Order in which the pages of a multi-page sheet are emitted, mirroring
663
+ * Excel's "Page order". `"downThenOver"` finishes each column band top to
664
+ * bottom before moving right; `"overThenDown"` finishes each row band left
665
+ * to right before moving down.
666
+ *
667
+ * When omitted, the sheet's `pageSetup.pageOrder` is used.
668
+ * @default "downThenOver"
669
+ */
670
+ pageOrder?: PdfPageOrder;
671
+ /**
672
+ * Render vector content without color, mirroring Excel's "Black and white"
673
+ * print option. Colors are converted to their luminance-preserving grayscale
674
+ * equivalent, preserving opacity.
675
+ *
676
+ * Covers vector content (cell text, fills and borders, gridlines, the
677
+ * row/column heading bands, chart vectors, `&K`-colored header/footer runs and
678
+ * text watermarks) as well as raster content: PNG samples are converted to a
679
+ * single luma component, and JPEG keeps its DCTDecode data but is reinterpreted
680
+ * through a `/DeviceN` luma color space, so neither needs an overlay and
681
+ * transparency is preserved.
682
+ *
683
+ * When omitted, the sheet's `pageSetup.blackAndWhite` is used.
684
+ * @default false
685
+ */
686
+ blackAndWhite?: boolean;
687
+ /**
688
+ * Draft quality — omit images and charts, mirroring Excel's "Draft quality"
689
+ * print option.
690
+ *
691
+ * When omitted, the sheet's `pageSetup.draft` is used.
692
+ * @default false
693
+ */
694
+ draft?: boolean;
695
+ /**
696
+ * How cells holding an error value are printed, mirroring Excel's
697
+ * "Cell errors as" print option.
698
+ *
699
+ * When omitted, the sheet's `pageSetup.errors` is used.
700
+ * @default "displayed"
701
+ */
702
+ errors?: PdfCellErrorMode;
703
+ /**
704
+ * Whether cell comments and notes are printed, mirroring Excel's
705
+ * "Comments and notes" print option.
706
+ *
707
+ * `"atEnd"` appends a list of every comment after the sheet's pages.
708
+ * `"asDisplayed"` draws each comment as a box where it sits on the sheet,
709
+ * plus the red corner marker Excel puts on the commented cell.
710
+ *
711
+ * When omitted, the sheet's `pageSetup.cellComments` is used.
712
+ * @default "none"
713
+ */
714
+ cellComments?: PdfCellCommentMode;
510
715
  /**
511
716
  * Whether to show grid lines on the page.
512
717
  * @default false
@@ -517,6 +722,24 @@ export interface PdfExportOptions {
517
722
  * @default "FFD0D0D0"
518
723
  */
519
724
  gridLineColor?: string;
725
+ /**
726
+ * Center the printed grid horizontally within the page's content area,
727
+ * mirroring Excel's "Page Setup → Margins → Center on page → Horizontally".
728
+ *
729
+ * When omitted, the source sheet's `pageSetup.horizontalCentered` is used;
730
+ * if that is unset too, content starts at the left margin.
731
+ * @default false
732
+ */
733
+ horizontalCentered?: boolean;
734
+ /**
735
+ * Center the printed grid vertically within the page's content area,
736
+ * mirroring Excel's "Page Setup → Margins → Center on page → Vertically".
737
+ *
738
+ * When omitted, the source sheet's `pageSetup.verticalCentered` is used;
739
+ * if that is unset too, content starts at the top margin.
740
+ * @default false
741
+ */
742
+ verticalCentered?: boolean;
520
743
  /**
521
744
  * Whether to repeat row headers on each page.
522
745
  * Can be a number (row count from top) or false to disable.
@@ -863,9 +1086,22 @@ export interface ResolvedPdfOptions {
863
1086
  ignorePrintArea: boolean;
864
1087
  fitToPage: boolean;
865
1088
  scale: number;
1089
+ /** Pages wide to shrink into; `0` means unconstrained. */
1090
+ fitToWidth: number;
1091
+ /** Pages tall to shrink into; `0` means unconstrained. */
1092
+ fitToHeight: number;
866
1093
  showGridLines: boolean;
867
1094
  gridLineColor: PdfColor;
868
- repeatRows: number | false;
1095
+ showRowColHeaders: boolean;
1096
+ horizontalCentered: boolean;
1097
+ verticalCentered: boolean;
1098
+ pageOrder: PdfPageOrder;
1099
+ blackAndWhite: boolean;
1100
+ draft: boolean;
1101
+ errors: PdfCellErrorMode;
1102
+ cellComments: PdfCellCommentMode;
1103
+ repeatRows: PdfRepeatBand | false;
1104
+ repeatCols: PdfRepeatBand | false;
869
1105
  defaultFontFamily: string;
870
1106
  defaultFontSize: number;
871
1107
  showSheetNames: boolean;
@@ -1040,8 +1276,51 @@ export interface LayoutPage {
1040
1276
  charts: LayoutChart[];
1041
1277
  /** Scale factor applied to this page (for fitToPage) */
1042
1278
  scaleFactor: number;
1279
+ /**
1280
+ * Geometry of the printed row/column heading bands, when
1281
+ * {@link ResolvedPdfOptions.showRowColHeaders} is on. Absent otherwise.
1282
+ */
1283
+ headings?: LayoutHeadings;
1284
+ /**
1285
+ * Comment boxes to draw on this page, for
1286
+ * `cellComments: "asDisplayed"`. Empty otherwise.
1287
+ */
1288
+ commentBoxes?: LayoutCommentBox[];
1043
1289
  headerFooter?: PdfHeaderFooterData;
1044
1290
  }
1291
+ /**
1292
+ * A comment box positioned on the sheet, as Excel's "Comments: as displayed"
1293
+ * prints it, together with the corner marker on the commented cell.
1294
+ */
1295
+ export interface LayoutCommentBox {
1296
+ /** Box outline in page coordinates. */
1297
+ rect: PdfRect;
1298
+ /** Comment body, already prefixed with the author when one is known. */
1299
+ text: string;
1300
+ /** Font size in points. */
1301
+ fontSize: number;
1302
+ /**
1303
+ * Corner marker on the commented cell, when that cell is on this page. Excel
1304
+ * draws a small red triangle in its top-right corner.
1305
+ */
1306
+ marker?: {
1307
+ x: number;
1308
+ y: number;
1309
+ size: number;
1310
+ };
1311
+ }
1312
+ /**
1313
+ * Row-number gutter and column-letter band geometry for a page printed with
1314
+ * Excel's "Row and column headings" option.
1315
+ */
1316
+ export interface LayoutHeadings {
1317
+ /** Width of the row-number gutter to the left of the grid, in points. */
1318
+ gutterWidth: number;
1319
+ /** Height of the column-letter band above the grid, in points. */
1320
+ bandHeight: number;
1321
+ /** Font size used for the heading labels, in points. */
1322
+ fontSize: number;
1323
+ }
1045
1324
  /**
1046
1325
  * A positioned image on a PDF page.
1047
1326
  */
@@ -82,41 +82,90 @@ function parseJpegDimensions(data) {
82
82
  * Write an image XObject (JPEG or PNG) to the writer.
83
83
  * Returns the allocated object number.
84
84
  */
85
- function writeImageXObject(writer, data, format) {
85
+ function writeImageXObject(writer, data, format, grayscale = false) {
86
86
  if (format === "png") {
87
- return writePngImageXObject(writer, data);
87
+ return writePngImageXObject(writer, data, grayscale);
88
88
  }
89
- return writeJpegImageXObject(writer, data);
89
+ return writeJpegImageXObject(writer, data, grayscale);
90
90
  }
91
91
  /**
92
- * Write a JPEG image using DCTDecode (raw JPEG data embedded directly).
92
+ * Write the PostScript calculator function that converts an RGB triple to its
93
+ * Rec. 601 luma, and return its object number.
94
+ *
95
+ * Used as the tint transform of a `/DeviceN` space so a JPEG can be recoloured
96
+ * without decoding it: the samples stay untouched and the consumer applies the
97
+ * transform per pixel. Unlike painting a blend on top, this cannot darken
98
+ * transparent areas, because it *is* the color space rather than an overlay.
93
99
  */
94
- function writeJpegImageXObject(writer, data) {
100
+ function writeLumaTintTransform(writer) {
95
101
  const objNum = writer.allocObject();
102
+ const dict = new pdf_object_1.PdfDict()
103
+ .set("FunctionType", "4")
104
+ .set("Domain", "[0 1 0 1 0 1]")
105
+ .set("Range", "[0 1]");
106
+ // Stack on entry is R G B (B on top):
107
+ // 0.114 mul -> R G 0.114B
108
+ // exch -> R 0.114B G
109
+ // 0.587 mul -> R 0.114B 0.587G
110
+ // add -> R (0.114B + 0.587G)
111
+ // exch -> (0.114B + 0.587G) R
112
+ // 0.299 mul -> (0.114B + 0.587G) 0.299R
113
+ // add -> luma
114
+ const program = "{ 0.114 mul exch 0.587 mul add exch 0.299 mul add }";
115
+ writer.addStreamObject(objNum, dict, new TextEncoder().encode(program));
116
+ return objNum;
117
+ }
118
+ /**
119
+ * Write a JPEG image using DCTDecode (raw JPEG data embedded directly).
120
+ */
121
+ function writeJpegImageXObject(writer, data, grayscale = false) {
96
122
  const dims = parseJpegDimensions(data);
123
+ // Grayscale without a JPEG decoder: keep the DCTDecode samples and reinterpret
124
+ // the three components through a `/DeviceN` space whose tint transform is the
125
+ // luma formula. Component count still matches the JPEG, as PDF requires.
126
+ const tintRef = grayscale ? writeLumaTintTransform(writer) : undefined;
127
+ const objNum = writer.allocObject();
97
128
  const dict = new pdf_object_1.PdfDict()
98
129
  .set("Type", "/XObject")
99
130
  .set("Subtype", "/Image")
100
131
  .set("Width", (0, pdf_object_1.pdfNumber)(dims.width))
101
132
  .set("Height", (0, pdf_object_1.pdfNumber)(dims.height))
102
- .set("ColorSpace", "/DeviceRGB")
133
+ .set("ColorSpace", tintRef === undefined
134
+ ? "/DeviceRGB"
135
+ : `[/DeviceN [/C1 /C2 /C3] /DeviceGray ${(0, pdf_object_1.pdfRef)(tintRef)}]`)
103
136
  .set("BitsPerComponent", "8")
104
137
  .set("Filter", "/DCTDecode");
105
138
  writer.addStreamObject(objNum, dict, data);
106
139
  return objNum;
107
140
  }
141
+ /** Rec. 601 luma of an 8-bit RGB triple. */
142
+ function lumaByte(r, g, b) {
143
+ return Math.round(0.299 * r + 0.587 * g + 0.114 * b);
144
+ }
108
145
  /**
109
146
  * Write a PNG image: decode to raw RGB, create SMask for alpha if needed.
110
147
  */
111
- function writePngImageXObject(writer, data) {
148
+ function writePngImageXObject(writer, data, grayscale = false) {
112
149
  const png = (0, png_decoder_1.decodePng)(data);
113
150
  const objNum = writer.allocObject();
151
+ // PNG is already decoded to raw samples here, so grayscale is a real pixel
152
+ // conversion: collapse RGB to one luma component and switch to DeviceGray.
153
+ // The alpha channel lives in a separate SMask and is untouched, so transparent
154
+ // regions stay transparent.
155
+ let pixels = png.pixels;
156
+ if (grayscale) {
157
+ const gray = new Uint8Array(png.width * png.height);
158
+ for (let i = 0, p = 0; i < gray.length; i++, p += 3) {
159
+ gray[i] = lumaByte(pixels[p], pixels[p + 1], pixels[p + 2]);
160
+ }
161
+ pixels = gray;
162
+ }
114
163
  const dict = new pdf_object_1.PdfDict()
115
164
  .set("Type", "/XObject")
116
165
  .set("Subtype", "/Image")
117
166
  .set("Width", (0, pdf_object_1.pdfNumber)(png.width))
118
167
  .set("Height", (0, pdf_object_1.pdfNumber)(png.height))
119
- .set("ColorSpace", "/DeviceRGB")
168
+ .set("ColorSpace", grayscale ? "/DeviceGray" : "/DeviceRGB")
120
169
  .set("BitsPerComponent", (0, pdf_object_1.pdfNumber)(png.bitsPerComponent));
121
170
  if (png.alpha) {
122
171
  const smaskObjNum = writer.allocObject();
@@ -130,6 +179,6 @@ function writePngImageXObject(writer, data) {
130
179
  writer.addStreamObject(smaskObjNum, smaskDict, png.alpha);
131
180
  dict.set("SMask", (0, pdf_object_1.pdfRef)(smaskObjNum));
132
181
  }
133
- writer.addStreamObject(objNum, dict, png.pixels);
182
+ writer.addStreamObject(objNum, dict, pixels);
134
183
  return objNum;
135
184
  }
@@ -168,6 +168,9 @@ class PdfWriter {
168
168
  .set("MediaBox", mediaBox)
169
169
  .set("Contents", contentsValue)
170
170
  .set("Resources", (0, pdf_object_1.pdfRef)(options.resourcesRef));
171
+ if (options.transparencyGroup) {
172
+ dict.set("Group", "<< /S /Transparency /CS /DeviceRGB /I false /K false >>");
173
+ }
171
174
  if (options.annotRefs && options.annotRefs.length > 0) {
172
175
  dict.set("Annots", "[" + options.annotRefs.map(r => (0, pdf_object_1.pdfRef)(r)).join(" ") + "]");
173
176
  }