@cj-tech-master/excelts 9.0.0 → 9.1.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 (96) hide show
  1. package/dist/browser/index.browser.d.ts +2 -0
  2. package/dist/browser/index.browser.js +2 -0
  3. package/dist/browser/index.d.ts +2 -0
  4. package/dist/browser/index.js +2 -0
  5. package/dist/browser/modules/excel/image.d.ts +27 -2
  6. package/dist/browser/modules/excel/image.js +23 -1
  7. package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +16 -1
  8. package/dist/browser/modules/excel/stream/worksheet-writer.js +68 -0
  9. package/dist/browser/modules/excel/types.d.ts +72 -0
  10. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +4 -0
  11. package/dist/browser/modules/excel/utils/drawing-utils.js +5 -0
  12. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +4 -0
  13. package/dist/browser/modules/excel/utils/ooxml-paths.js +15 -0
  14. package/dist/browser/modules/excel/utils/watermark-image.d.ts +67 -0
  15. package/dist/browser/modules/excel/utils/watermark-image.js +383 -0
  16. package/dist/browser/modules/excel/worksheet.d.ts +39 -1
  17. package/dist/browser/modules/excel/worksheet.js +99 -0
  18. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
  19. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
  20. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
  21. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
  22. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
  23. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
  24. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
  25. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
  26. package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
  27. package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
  28. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
  29. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
  30. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +53 -1
  31. package/dist/browser/modules/pdf/core/pdf-writer.d.ts +1 -1
  32. package/dist/browser/modules/pdf/core/pdf-writer.js +2 -1
  33. package/dist/browser/modules/pdf/index.d.ts +1 -1
  34. package/dist/browser/modules/pdf/render/page-renderer.d.ts +29 -1
  35. package/dist/browser/modules/pdf/render/page-renderer.js +394 -25
  36. package/dist/browser/modules/pdf/render/pdf-exporter.js +84 -47
  37. package/dist/browser/modules/pdf/types.d.ts +235 -0
  38. package/dist/cjs/index.js +5 -2
  39. package/dist/cjs/modules/excel/image.js +23 -1
  40. package/dist/cjs/modules/excel/stream/worksheet-writer.js +68 -0
  41. package/dist/cjs/modules/excel/utils/drawing-utils.js +5 -0
  42. package/dist/cjs/modules/excel/utils/ooxml-paths.js +19 -0
  43. package/dist/cjs/modules/excel/utils/watermark-image.js +386 -0
  44. package/dist/cjs/modules/excel/worksheet.js +99 -0
  45. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
  46. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
  47. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
  48. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
  49. package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
  50. package/dist/cjs/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
  51. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +134 -7
  52. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +52 -0
  53. package/dist/cjs/modules/pdf/core/pdf-writer.js +2 -1
  54. package/dist/cjs/modules/pdf/render/page-renderer.js +396 -25
  55. package/dist/cjs/modules/pdf/render/pdf-exporter.js +83 -46
  56. package/dist/esm/index.browser.js +2 -0
  57. package/dist/esm/index.js +2 -0
  58. package/dist/esm/modules/excel/image.js +23 -1
  59. package/dist/esm/modules/excel/stream/worksheet-writer.js +68 -0
  60. package/dist/esm/modules/excel/utils/drawing-utils.js +5 -0
  61. package/dist/esm/modules/excel/utils/ooxml-paths.js +15 -0
  62. package/dist/esm/modules/excel/utils/watermark-image.js +383 -0
  63. package/dist/esm/modules/excel/worksheet.js +99 -0
  64. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
  65. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
  66. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
  67. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
  68. package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
  69. package/dist/esm/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
  70. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
  71. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +53 -1
  72. package/dist/esm/modules/pdf/core/pdf-writer.js +2 -1
  73. package/dist/esm/modules/pdf/render/page-renderer.js +394 -25
  74. package/dist/esm/modules/pdf/render/pdf-exporter.js +84 -47
  75. package/dist/iife/excelts.iife.js +2390 -469
  76. package/dist/iife/excelts.iife.js.map +1 -1
  77. package/dist/iife/excelts.iife.min.js +47 -47
  78. package/dist/types/index.browser.d.ts +2 -0
  79. package/dist/types/index.d.ts +2 -0
  80. package/dist/types/modules/excel/image.d.ts +27 -2
  81. package/dist/types/modules/excel/stream/worksheet-writer.d.ts +16 -1
  82. package/dist/types/modules/excel/types.d.ts +72 -0
  83. package/dist/types/modules/excel/utils/drawing-utils.d.ts +4 -0
  84. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +4 -0
  85. package/dist/types/modules/excel/utils/watermark-image.d.ts +67 -0
  86. package/dist/types/modules/excel/worksheet.d.ts +39 -1
  87. package/dist/types/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
  88. package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
  89. package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
  90. package/dist/types/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
  91. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
  92. package/dist/types/modules/pdf/core/pdf-writer.d.ts +1 -1
  93. package/dist/types/modules/pdf/index.d.ts +1 -1
  94. package/dist/types/modules/pdf/render/page-renderer.d.ts +29 -1
  95. package/dist/types/modules/pdf/types.d.ts +235 -0
  96. package/package.json +1 -1
@@ -8,12 +8,23 @@ class BlipXform extends BaseXform {
8
8
  return "a:blip";
9
9
  }
10
10
  render(xmlStream, model) {
11
- xmlStream.leafNode(this.tag, {
12
- "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
13
- "r:embed": model.rId,
14
- cstate: "print"
15
- });
16
- // TODO: handle children (e.g. a:extLst=>a:ext=>a14:useLocalDpi
11
+ if (model.alphaModFix !== undefined && model.alphaModFix < 100000) {
12
+ // Render as open/close node with a:alphaModFix child
13
+ xmlStream.openNode(this.tag, {
14
+ "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
15
+ "r:embed": model.rId,
16
+ cstate: "print"
17
+ });
18
+ xmlStream.leafNode("a:alphaModFix", { amt: String(model.alphaModFix) });
19
+ xmlStream.closeNode();
20
+ }
21
+ else {
22
+ xmlStream.leafNode(this.tag, {
23
+ "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
24
+ "r:embed": model.rId,
25
+ cstate: "print"
26
+ });
27
+ }
17
28
  }
18
29
  parseOpen(node) {
19
30
  switch (node.name) {
@@ -22,6 +33,11 @@ class BlipXform extends BaseXform {
22
33
  rId: node.attributes["r:embed"]
23
34
  };
24
35
  return true;
36
+ case "a:alphaModFix":
37
+ if (node.attributes.amt) {
38
+ this.model.alphaModFix = parseInt(node.attributes.amt, 10);
39
+ }
40
+ return true;
25
41
  default:
26
42
  return true;
27
43
  }
@@ -1,6 +1,9 @@
1
1
  import { BaseXform } from "../base-xform.js";
2
2
  interface PicModel {
3
3
  index?: number;
4
+ rId?: string;
5
+ /** Alpha modulation for transparency (OOXML percentage, e.g. 15000 = 15%). */
6
+ alphaModFix?: number;
4
7
  [key: string]: any;
5
8
  }
6
9
  declare class PicXform extends BaseXform {
@@ -21,7 +21,11 @@ class PicXform extends BaseXform {
21
21
  render(xmlStream, model) {
22
22
  xmlStream.openNode(this.tag);
23
23
  this.map["xdr:nvPicPr"].render(xmlStream, model);
24
- this.map["xdr:blipFill"].render(xmlStream, model);
24
+ // Pass alphaModFix through to blipFill → blip
25
+ this.map["xdr:blipFill"].render(xmlStream, {
26
+ rId: model.rId,
27
+ alphaModFix: model.alphaModFix
28
+ });
25
29
  this.map["xdr:spPr"].render(xmlStream, model);
26
30
  xmlStream.closeNode();
27
31
  }
@@ -9,11 +9,22 @@ import { type FormCheckboxModel } from "../../../form-control.js";
9
9
  *
10
10
  * This unified xform renders both into a single VML file.
11
11
  */
12
+ /** Header/footer image model for VML rendering. */
13
+ interface VmlHeaderImageModel {
14
+ /** rId referencing the image in the VML drawing's .rels */
15
+ imageRelId: string;
16
+ /** Image width in points */
17
+ width?: number;
18
+ /** Image height in points */
19
+ height?: number;
20
+ }
12
21
  interface VmlDrawingModel {
13
22
  /** Comment/note shapes */
14
23
  comments?: any[];
15
24
  /** Form control checkboxes */
16
25
  formControls?: FormCheckboxModel[];
26
+ /** Header/footer image (for watermark in header mode) */
27
+ headerImage?: VmlHeaderImageModel;
17
28
  }
18
29
  declare class VmlDrawingXform extends BaseXform<VmlDrawingModel> {
19
30
  map: {
@@ -26,6 +37,10 @@ declare class VmlDrawingXform extends BaseXform<VmlDrawingModel> {
26
37
  * Render VML drawing containing both notes and form controls
27
38
  */
28
39
  render(xmlStream: any, model?: VmlDrawingModel): void;
40
+ /**
41
+ * Render a header/footer image shape for watermark
42
+ */
43
+ private _renderHeaderImageShape;
29
44
  /**
30
45
  * Render a checkbox form control shape
31
46
  */
@@ -33,6 +48,10 @@ declare class VmlDrawingXform extends BaseXform<VmlDrawingModel> {
33
48
  parseOpen(node: any): boolean;
34
49
  parseText(text: string): void;
35
50
  parseClose(name: string): boolean;
51
+ private _parsingHeaderImage;
52
+ private _headerImageRelId?;
53
+ private _headerImageWidth?;
54
+ private _headerImageHeight?;
36
55
  static DRAWING_ATTRIBUTES: {
37
56
  "xmlns:v": string;
38
57
  "xmlns:o": string;
@@ -5,6 +5,8 @@ import { FormCheckbox } from "../../../form-control.js";
5
5
  class VmlDrawingXform extends BaseXform {
6
6
  constructor() {
7
7
  super();
8
+ // Internal state for parsing header image shapes
9
+ this._parsingHeaderImage = false;
8
10
  this.map = {
9
11
  "v:shape": new VmlShapeXform()
10
12
  };
@@ -20,8 +22,10 @@ class VmlDrawingXform extends BaseXform {
20
22
  const renderModel = (model || this.model);
21
23
  const comments = renderModel.comments;
22
24
  const formControls = renderModel.formControls;
25
+ const headerImage = renderModel.headerImage;
23
26
  const hasComments = comments && comments.length > 0;
24
27
  const hasFormControls = formControls && formControls.length > 0;
28
+ const hasHeaderImage = !!headerImage;
25
29
  xmlStream.openXml(StdDocAttributes);
26
30
  xmlStream.openNode(this.tag, VmlDrawingXform.DRAWING_ATTRIBUTES);
27
31
  // Shape layout - shared by both notes and form controls
@@ -59,6 +63,40 @@ class VmlDrawingXform extends BaseXform {
59
63
  xmlStream.leafNode("o:lock", { "v:ext": "edit", shapetype: "t" });
60
64
  xmlStream.closeNode();
61
65
  }
66
+ // Shapetype 75 for header/footer image (watermark)
67
+ if (hasHeaderImage) {
68
+ xmlStream.openNode("v:shapetype", {
69
+ id: "_x0000_t75",
70
+ coordsize: "21600,21600",
71
+ "o:spt": "75",
72
+ "o:preferrelative": "t",
73
+ path: "m@4@5l@4@11@9@11@9@5xe",
74
+ filled: "f",
75
+ stroked: "f"
76
+ });
77
+ xmlStream.leafNode("v:stroke", { joinstyle: "miter" });
78
+ xmlStream.openNode("v:formulas");
79
+ xmlStream.leafNode("v:f", { eqn: "if lineDrawn pixelLineWidth 0" });
80
+ xmlStream.leafNode("v:f", { eqn: "sum @0 1 0" });
81
+ xmlStream.leafNode("v:f", { eqn: "sum 0 0 @1" });
82
+ xmlStream.leafNode("v:f", { eqn: "prod @2 1 2" });
83
+ xmlStream.leafNode("v:f", { eqn: "prod @3 21600 pixelWidth" });
84
+ xmlStream.leafNode("v:f", { eqn: "prod @3 21600 pixelHeight" });
85
+ xmlStream.leafNode("v:f", { eqn: "sum @0 0 1" });
86
+ xmlStream.leafNode("v:f", { eqn: "prod @6 1 2" });
87
+ xmlStream.leafNode("v:f", { eqn: "prod @7 21600 pixelWidth" });
88
+ xmlStream.leafNode("v:f", { eqn: "sum @8 21600 0" });
89
+ xmlStream.leafNode("v:f", { eqn: "prod @7 21600 pixelHeight" });
90
+ xmlStream.leafNode("v:f", { eqn: "sum @10 21600 0" });
91
+ xmlStream.closeNode(); // v:formulas
92
+ xmlStream.leafNode("v:path", {
93
+ "o:extrusionok": "f",
94
+ gradientshapeok: "t",
95
+ "o:connecttype": "rect"
96
+ });
97
+ xmlStream.leafNode("o:lock", { "v:ext": "edit", aspectratio: "t" });
98
+ xmlStream.closeNode(); // v:shapetype
99
+ }
62
100
  // Render comment shapes
63
101
  if (hasComments) {
64
102
  for (let i = 0; i < comments.length; i++) {
@@ -71,8 +109,32 @@ class VmlDrawingXform extends BaseXform {
71
109
  this._renderCheckboxShape(xmlStream, control);
72
110
  }
73
111
  }
112
+ // Render header/footer image shape
113
+ if (hasHeaderImage) {
114
+ this._renderHeaderImageShape(xmlStream, headerImage);
115
+ }
74
116
  xmlStream.closeNode();
75
117
  }
118
+ /**
119
+ * Render a header/footer image shape for watermark
120
+ */
121
+ _renderHeaderImageShape(xmlStream, headerImage) {
122
+ const width = headerImage.width ?? 467.25;
123
+ const height = headerImage.height ?? 311.25;
124
+ // CH = Center Header, used by Excel for center-positioned header images
125
+ xmlStream.openNode("v:shape", {
126
+ id: "CH",
127
+ "o:spid": "_x0000_s2049",
128
+ type: "#_x0000_t75",
129
+ style: `position:absolute;margin-left:0;margin-top:0;width:${width}pt;height:${height}pt;z-index:1`
130
+ });
131
+ xmlStream.leafNode("v:imagedata", {
132
+ "o:relid": headerImage.imageRelId,
133
+ "o:title": "watermark"
134
+ });
135
+ xmlStream.leafNode("o:lock", { "v:ext": "edit", rotation: "t" });
136
+ xmlStream.closeNode(); // v:shape
137
+ }
76
138
  /**
77
139
  * Render a checkbox form control shape
78
140
  */
@@ -141,7 +203,7 @@ class VmlDrawingXform extends BaseXform {
141
203
  xmlStream.closeNode(); // x:ClientData
142
204
  xmlStream.closeNode(); // v:shape
143
205
  }
144
- // Parsing - delegate to VmlShapeXform for notes
206
+ // Parsing - delegate to VmlShapeXform for notes, handle header images directly
145
207
  parseOpen(node) {
146
208
  if (this.parser) {
147
209
  this.parser.parseOpen(node);
@@ -155,10 +217,34 @@ class VmlDrawingXform extends BaseXform {
155
217
  formControls: []
156
218
  };
157
219
  break;
220
+ case "v:shape":
221
+ // Check if this is a header image shape (type="#_x0000_t75")
222
+ if (node.attributes.type === "#_x0000_t75") {
223
+ this._parsingHeaderImage = true;
224
+ // Extract width/height from style
225
+ const style = node.attributes.style || "";
226
+ const widthMatch = /width:([0-9.]+)pt/.exec(style);
227
+ const heightMatch = /height:([0-9.]+)pt/.exec(style);
228
+ this._headerImageWidth = widthMatch ? parseFloat(widthMatch[1]) : undefined;
229
+ this._headerImageHeight = heightMatch ? parseFloat(heightMatch[1]) : undefined;
230
+ }
231
+ else {
232
+ // Regular shape — delegate to VmlShapeXform (comments)
233
+ this.parser = this.map[node.name];
234
+ if (this.parser) {
235
+ this.parser.parseOpen(node);
236
+ }
237
+ }
238
+ break;
158
239
  default:
159
- this.parser = this.map[node.name];
160
- if (this.parser) {
161
- this.parser.parseOpen(node);
240
+ if (this._parsingHeaderImage && node.name === "v:imagedata") {
241
+ this._headerImageRelId = node.attributes["o:relid"];
242
+ }
243
+ else {
244
+ this.parser = this.map[node.name];
245
+ if (this.parser) {
246
+ this.parser.parseOpen(node);
247
+ }
162
248
  }
163
249
  break;
164
250
  }
@@ -178,6 +264,19 @@ class VmlDrawingXform extends BaseXform {
178
264
  return true;
179
265
  }
180
266
  switch (name) {
267
+ case "v:shape":
268
+ if (this._parsingHeaderImage && this._headerImageRelId) {
269
+ this.model.headerImage = {
270
+ imageRelId: this._headerImageRelId,
271
+ width: this._headerImageWidth,
272
+ height: this._headerImageHeight
273
+ };
274
+ }
275
+ this._parsingHeaderImage = false;
276
+ this._headerImageRelId = undefined;
277
+ this._headerImageWidth = undefined;
278
+ this._headerImageHeight = undefined;
279
+ return true;
181
280
  case this.tag:
182
281
  return false;
183
282
  default:
@@ -26,7 +26,7 @@ import { ColBreaksXform } from "./col-breaks-xform.js";
26
26
  import { HeaderFooterXform } from "./header-footer-xform.js";
27
27
  import { ConditionalFormattingsXform } from "./cf/conditional-formattings-xform.js";
28
28
  import { ExtLstXform } from "./ext-lst-xform.js";
29
- import { commentsRelTargetFromWorksheet, ctrlPropRelTargetFromWorksheet, drawingRelTargetFromWorksheet, pivotTableRelTargetFromWorksheet, tableRelTargetFromWorksheet, vmlDrawingRelTargetFromWorksheet } from "../../../utils/ooxml-paths.js";
29
+ import { commentsRelTargetFromWorksheet, ctrlPropRelTargetFromWorksheet, drawingRelTargetFromWorksheet, pivotTableRelTargetFromWorksheet, tableRelTargetFromWorksheet, vmlDrawingRelTargetFromWorksheet, vmlDrawingHFRelTargetFromWorksheet } from "../../../utils/ooxml-paths.js";
30
30
  import { buildDrawingAnchorsAndRels, resolveMediaTarget } from "../../../utils/drawing-utils.js";
31
31
  const mergeRule = (rule, extRule) => {
32
32
  Object.keys(extRule).forEach(key => {
@@ -232,6 +232,8 @@ class WorkSheetXform extends BaseXform {
232
232
  // Process background and image media entries
233
233
  const backgroundMedia = [];
234
234
  const imageMedia = [];
235
+ const watermarkMedia = [];
236
+ const headerImageMedia = [];
235
237
  model.media.forEach(medium => {
236
238
  if (medium.type === "background") {
237
239
  backgroundMedia.push(medium);
@@ -239,6 +241,12 @@ class WorkSheetXform extends BaseXform {
239
241
  else if (medium.type === "image") {
240
242
  imageMedia.push(medium);
241
243
  }
244
+ else if (medium.type === "watermark") {
245
+ watermarkMedia.push(medium);
246
+ }
247
+ else if (medium.type === "headerImage") {
248
+ headerImageMedia.push(medium);
249
+ }
242
250
  });
243
251
  // Handle background images
244
252
  backgroundMedia.forEach(medium => {
@@ -276,6 +284,107 @@ class WorkSheetXform extends BaseXform {
276
284
  drawing.anchors.push(...result.anchors);
277
285
  drawing.rels = result.rels;
278
286
  }
287
+ // Handle watermark overlay images — placed as a full-sheet drawing with transparency
288
+ if (watermarkMedia.length > 0) {
289
+ let { drawing } = model;
290
+ if (!drawing) {
291
+ drawing = model.drawing = {
292
+ rId: nextRid(rels),
293
+ name: `drawing${++options.drawingsCount}`,
294
+ anchors: [],
295
+ rels: []
296
+ };
297
+ options.drawings.push(drawing);
298
+ rels.push({
299
+ Id: drawing.rId,
300
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
301
+ Target: drawingRelTargetFromWorksheet(drawing.name)
302
+ });
303
+ }
304
+ for (const medium of watermarkMedia) {
305
+ const bookImage = options.media[medium.imageId];
306
+ if (!bookImage) {
307
+ continue;
308
+ }
309
+ const rIdImage = nextRid(drawing.rels);
310
+ drawing.rels.push({
311
+ Id: rIdImage,
312
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
313
+ Target: resolveMediaTarget(bookImage)
314
+ });
315
+ // Convert opacity (0-1) to OOXML percentage (0-100000), clamped
316
+ const rawOpacity = medium.opacity !== undefined ? medium.opacity : 0.15;
317
+ const clampedOpacity = Math.max(0, Math.min(1, rawOpacity));
318
+ const alphaModFix = Math.round(clampedOpacity * 100000);
319
+ // Compute coverage based on actual worksheet dimensions.
320
+ // Use the model's dimensions if available, otherwise use generous defaults.
321
+ const dims = model.dimensions;
322
+ const maxCol = dims ? Math.max(dims.model?.right ?? 100, 100) : 100;
323
+ const maxRow = dims ? Math.max(dims.model?.bottom ?? 200, 200) : 200;
324
+ drawing.anchors.push({
325
+ picture: {
326
+ rId: rIdImage,
327
+ alphaModFix
328
+ },
329
+ // Cover the full data area with extra margin
330
+ range: {
331
+ editAs: "absolute",
332
+ tl: { nativeCol: 0, nativeColOff: 0, nativeRow: 0, nativeRowOff: 0 },
333
+ br: { nativeCol: maxCol, nativeColOff: 0, nativeRow: maxRow, nativeRowOff: 0 }
334
+ }
335
+ });
336
+ }
337
+ }
338
+ // Handle header watermark images — VML header/footer image
339
+ if (headerImageMedia.length > 0) {
340
+ const medium = headerImageMedia[0]; // Only one header image per sheet
341
+ const bookImage = options.media[medium.imageId];
342
+ if (bookImage) {
343
+ const rIdVml = nextRid(rels);
344
+ rels.push({
345
+ Id: rIdVml,
346
+ Type: RelType.VmlDrawing,
347
+ Target: vmlDrawingHFRelTargetFromWorksheet(fileIndex)
348
+ });
349
+ // Store header image info on the model for the VML writer and worksheet render
350
+ model.headerImage = {
351
+ vmlRelId: rIdVml,
352
+ imageId: medium.imageId,
353
+ bookImage,
354
+ headerWidth: medium.headerWidth,
355
+ headerHeight: medium.headerHeight
356
+ };
357
+ // Flag for content-types registration
358
+ options.hasHeaderWatermark = true;
359
+ // Update headerFooter to include &G placeholder.
360
+ // Respects the applyTo option: "all" (default), "odd", "even", "first".
361
+ if (!model.headerFooter) {
362
+ model.headerFooter = {};
363
+ }
364
+ const applyTo = medium.applyTo || "all";
365
+ const insertG = (field) => {
366
+ const existing = model.headerFooter[field] || "";
367
+ if (existing.includes("&G")) {
368
+ return existing;
369
+ }
370
+ if (existing.includes("&C")) {
371
+ return existing.replace("&C", "&C&G");
372
+ }
373
+ return existing + "&C&G";
374
+ };
375
+ if (applyTo === "all" || applyTo === "odd") {
376
+ model.headerFooter.oddHeader = insertG("oddHeader");
377
+ }
378
+ if (applyTo === "all" || applyTo === "even") {
379
+ model.headerFooter.evenHeader = insertG("evenHeader");
380
+ model.headerFooter.differentOddEven = true;
381
+ }
382
+ if (applyTo === "all" || applyTo === "first") {
383
+ model.headerFooter.firstHeader = insertG("firstHeader");
384
+ model.headerFooter.differentFirst = true;
385
+ }
386
+ }
387
+ }
279
388
  // prepare tables
280
389
  model.tables.forEach(table => {
281
390
  // relationships
@@ -436,10 +545,18 @@ class WorkSheetXform extends BaseXform {
436
545
  // NOTE: Excel is picky about worksheet child element order; legacyDrawing must come before controls.
437
546
  model.rels.forEach(rel => {
438
547
  if (rel.Type === RelType.VmlDrawing) {
548
+ // Skip VML rels that are for header images (they use legacyDrawingHF instead)
549
+ if (model.headerImage && rel.Id === model.headerImage.vmlRelId) {
550
+ return;
551
+ }
439
552
  xmlStream.leafNode("legacyDrawing", { "r:id": rel.Id });
440
553
  }
441
554
  });
442
555
  }
556
+ // legacyDrawingHF — VML drawing for header/footer images (watermark in header mode)
557
+ if (model.headerImage) {
558
+ xmlStream.leafNode("legacyDrawingHF", { "r:id": model.headerImage.vmlRelId });
559
+ }
443
560
  // Controls section for legacy form controls (checkboxes, etc.)
444
561
  // Excel expects <controls> entries that reference ctrlProp relationships.
445
562
  if (model.formControls && model.formControls.length > 0) {
@@ -655,13 +772,23 @@ class WorkSheetXform extends BaseXform {
655
772
  // Also extract images to model.media for backward compatibility
656
773
  drawing.anchors.forEach(anchor => {
657
774
  if (anchor.medium) {
658
- const image = {
659
- type: "image",
660
- imageId: anchor.medium.index,
661
- range: anchor.range,
662
- hyperlinks: anchor.picture.hyperlinks
663
- };
664
- model.media.push(image);
775
+ // Detect overlay watermarks: drawings that carry alphaModFix
776
+ const hasAlpha = anchor.medium.alphaModFix !== undefined && anchor.medium.alphaModFix < 100000;
777
+ if (hasAlpha) {
778
+ model.media.push({
779
+ type: "watermark",
780
+ imageId: anchor.medium.index,
781
+ opacity: anchor.medium.alphaModFix / 100000
782
+ });
783
+ }
784
+ else {
785
+ model.media.push({
786
+ type: "image",
787
+ imageId: anchor.medium.index,
788
+ range: anchor.range,
789
+ hyperlinks: anchor.picture.hyperlinks
790
+ });
791
+ }
665
792
  }
666
793
  });
667
794
  }
@@ -223,6 +223,7 @@ declare class XLSX {
223
223
  _processDrawingEntry(stream: IParseStream, model: any, name: string, rawData?: Uint8Array): Promise<void>;
224
224
  _processDrawingRelsEntry(entry: any, model: any, name: string): Promise<void>;
225
225
  _processVmlDrawingEntry(entry: any, model: any, name: string): Promise<void>;
226
+ _processVmlDrawingHFEntry(entry: any, model: any, _name: string): Promise<void>;
226
227
  _processThemeEntry(stream: IParseStream, model: any, name: string): Promise<void>;
227
228
  _processPivotTableEntry(stream: IParseStream, model: any, name: string): Promise<void>;
228
229
  _processPivotTableRelsEntry(stream: IParseStream, model: any, name: string): Promise<void>;
@@ -35,7 +35,7 @@ import { StreamingZip, ZipDeflateFile } from "../../archive/zip/stream.js";
35
35
  import { ZipParser } from "../../archive/unzip/zip-parser.js";
36
36
  import { PassThrough } from "../../stream/index.browser.js";
37
37
  import { concatUint8Arrays } from "../../../utils/binary.js";
38
- import { commentsPath, commentsRelTargetFromWorksheetName, ctrlPropPath, drawingPath, drawingRelsPath, OOXML_REL_TARGETS, pivotTableRelTargetFromWorksheetName, pivotCacheDefinitionRelTargetFromWorkbook, getCommentsIndexFromPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, tableRelTargetFromWorksheetName, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingRelTargetFromWorksheetName, vmlDrawingPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
38
+ import { commentsPath, commentsRelTargetFromWorksheetName, ctrlPropPath, drawingPath, drawingRelsPath, OOXML_REL_TARGETS, pivotTableRelTargetFromWorksheetName, pivotCacheDefinitionRelTargetFromWorkbook, getCommentsIndexFromPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, tableRelTargetFromWorksheetName, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getVmlDrawingHFNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingRelTargetFromWorksheetName, vmlDrawingPath, vmlDrawingHFPath, vmlDrawingHFRelsPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
39
39
  import { filterDrawingAnchors } from "../utils/drawing-utils.js";
40
40
  import { PassthroughManager } from "../utils/passthrough-manager.js";
41
41
  class StreamingZipWriterAdapter {
@@ -906,6 +906,17 @@ class XLSX {
906
906
  const vmlDrawing = await xform.parseStream(entry);
907
907
  model.vmlDrawings[vmlDrawingRelTargetFromWorksheetName(name)] = vmlDrawing;
908
908
  }
909
+ async _processVmlDrawingHFEntry(entry, model, _name) {
910
+ const xform = new VmlDrawingXform();
911
+ const vmlDrawing = await xform.parseStream(entry);
912
+ // Store parsed header image info for reconciliation
913
+ if (vmlDrawing && vmlDrawing.headerImage) {
914
+ if (!model.vmlDrawingHF) {
915
+ model.vmlDrawingHF = {};
916
+ }
917
+ model.vmlDrawingHF[_name] = vmlDrawing.headerImage;
918
+ }
919
+ }
909
920
  async _processThemeEntry(stream, model, name) {
910
921
  await new Promise((resolve, reject) => {
911
922
  const streamBuf = this.createStreamBuf();
@@ -1020,6 +1031,13 @@ class XLSX {
1020
1031
  await this._processVmlDrawingEntry(stream, model, vmlDrawingName);
1021
1032
  return true;
1022
1033
  }
1034
+ // VML header/footer drawings (watermark in header mode).
1035
+ // Parse to extract header image info for round-trip preservation.
1036
+ const vmlHFName = getVmlDrawingHFNameFromPath(entryName);
1037
+ if (vmlHFName) {
1038
+ await this._processVmlDrawingHFEntry(stream, model, vmlHFName);
1039
+ return true;
1040
+ }
1023
1041
  const commentsIndex = getCommentsIndexFromPath(entryName);
1024
1042
  if (commentsIndex) {
1025
1043
  await this._processCommentEntry(stream, model, `comments${commentsIndex}`);
@@ -1226,6 +1244,35 @@ class XLSX {
1226
1244
  formControls: hasFormControls ? worksheet.formControls : []
1227
1245
  });
1228
1246
  }
1247
+ // Generate VML drawing for header/footer images (watermark in header mode)
1248
+ if (worksheet.headerImage) {
1249
+ const hdrImage = worksheet.headerImage;
1250
+ const bookImage = hdrImage.bookImage;
1251
+ const imageFileName = bookImage.name &&
1252
+ bookImage.extension &&
1253
+ bookImage.name.endsWith(`.${bookImage.extension}`)
1254
+ ? bookImage.name
1255
+ : `${bookImage.name}.${bookImage.extension}`;
1256
+ const imageRelTarget = `../media/${imageFileName}`;
1257
+ // Write the VML file for the header image
1258
+ await this._renderToZip(zip, vmlDrawingHFPath(fileIndex), vmlDrawingXform, {
1259
+ comments: [],
1260
+ formControls: [],
1261
+ headerImage: {
1262
+ imageRelId: "rId1",
1263
+ width: hdrImage.headerWidth,
1264
+ height: hdrImage.headerHeight
1265
+ }
1266
+ });
1267
+ // Write the VML rels file referencing the image
1268
+ await this._renderToZip(zip, vmlDrawingHFRelsPath(fileIndex), relationshipsXform, [
1269
+ {
1270
+ Id: "rId1",
1271
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
1272
+ Target: imageRelTarget
1273
+ }
1274
+ ]);
1275
+ }
1229
1276
  // Generate ctrlProp files for form controls
1230
1277
  if (hasFormControls) {
1231
1278
  for (const control of worksheet.formControls) {
@@ -1372,6 +1419,7 @@ class XLSX {
1372
1419
  worksheetOptions.drawings = model.drawings = [];
1373
1420
  worksheetOptions.commentRefs = model.commentRefs = [];
1374
1421
  worksheetOptions.formControlRefs = model.formControlRefs = [];
1422
+ model.hasHeaderWatermark = false;
1375
1423
  let tableCount = 0;
1376
1424
  model.tables = [];
1377
1425
  model.worksheets.forEach((worksheet, index) => {
@@ -1390,6 +1438,10 @@ class XLSX {
1390
1438
  });
1391
1439
  // ContentTypesXform expects this flag
1392
1440
  model.hasCheckboxes = model.styles.hasCheckboxes;
1441
+ // Propagate header watermark flag from worksheet prepare options
1442
+ if (worksheetOptions.hasHeaderWatermark) {
1443
+ model.hasHeaderWatermark = true;
1444
+ }
1393
1445
  // Build passthroughContentTypes for ContentTypesXform using PassthroughManager
1394
1446
  const passthrough = model.passthrough || {};
1395
1447
  const passthroughManager = new PassthroughManager();
@@ -71,7 +71,7 @@ export declare class PdfWriter {
71
71
  parentRef: number;
72
72
  width: number;
73
73
  height: number;
74
- contentsRef: number;
74
+ contentsRef: number | string;
75
75
  resourcesRef: number;
76
76
  annotRefs?: number[];
77
77
  }): number;
@@ -129,11 +129,12 @@ export class PdfWriter {
129
129
  addPage(options) {
130
130
  const objNum = this.allocObject();
131
131
  const mediaBox = `[0 0 ${pdfNumber(options.width)} ${pdfNumber(options.height)}]`;
132
+ const contentsValue = typeof options.contentsRef === "string" ? options.contentsRef : pdfRef(options.contentsRef);
132
133
  const dict = new PdfDict()
133
134
  .set("Type", "/Page")
134
135
  .set("Parent", pdfRef(options.parentRef))
135
136
  .set("MediaBox", mediaBox)
136
- .set("Contents", pdfRef(options.contentsRef))
137
+ .set("Contents", contentsValue)
137
138
  .set("Resources", pdfRef(options.resourcesRef));
138
139
  if (options.annotRefs && options.annotRefs.length > 0) {
139
140
  dict.set("Annots", "[" + options.annotRefs.map(r => pdfRef(r)).join(" ") + "]");
@@ -47,7 +47,7 @@ export { excelToPdf } from "./excel-bridge.js";
47
47
  /** Read a PDF file and extract text, images, and metadata. */
48
48
  export { readPdf } from "./reader/pdf-reader.js";
49
49
  export type { PdfCell, PdfRow, PdfColumn, PdfSheet, PdfBook, PdfImage } from "./pdf.js";
50
- export type { PdfExportOptions, PdfOrientation, PdfPageSize, PdfMargins, PdfColor, PageSizeName } from "./types.js";
50
+ export type { PdfExportOptions, PdfOrientation, PdfPageSize, PdfMargins, PdfColor, PageSizeName, PdfWatermark, PdfTextWatermark, PdfImageWatermark, PdfWatermarkFilter } from "./types.js";
51
51
  export { PageSizes } from "./types.js";
52
52
  export type { ReadPdfOptions, ReadPdfResult, ReadPdfPage } from "./reader/pdf-reader.js";
53
53
  export type { PdfMetadata } from "./reader/metadata-reader.js";
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { PdfContentStream } from "../core/pdf-stream.js";
13
13
  import type { FontManager } from "../font/font-manager.js";
14
- import type { LayoutPage, ResolvedPdfOptions, PdfRect } from "../types.js";
14
+ import type { LayoutPage, ResolvedPdfOptions, PdfRect, PdfWatermark } from "../types.js";
15
15
  /**
16
16
  * Result of rendering a page.
17
17
  */
@@ -40,3 +40,31 @@ export declare function computeTextX(align: "left" | "center" | "right", rect: {
40
40
  * Uses a greedy word-wrap algorithm.
41
41
  */
42
42
  export declare function wrapTextLines(text: string, measure: (s: string) => number, maxWidth: number): string[];
43
+ /**
44
+ * Result of rendering a watermark on a page.
45
+ * Contains any alpha values and image XObjects that need to be registered
46
+ * in the page's resource dictionary.
47
+ */
48
+ export interface WatermarkRenderResult {
49
+ /** Alpha values used by the watermark. */
50
+ alphaValues: number[];
51
+ /** Image XObject entries: name → raw image data + format. */
52
+ imageXObjects: Array<{
53
+ name: string;
54
+ data: Uint8Array;
55
+ format: "jpeg" | "png";
56
+ }>;
57
+ }
58
+ /**
59
+ * Render a watermark onto a PDF content stream.
60
+ * This should be called BEFORE the cell/grid content is rendered so the
61
+ * watermark sits behind everything (under-content).
62
+ */
63
+ export declare function renderWatermark(stream: PdfContentStream, page: LayoutPage, watermark: PdfWatermark, fontManager: FontManager): WatermarkRenderResult;
64
+ /**
65
+ * Parse image dimensions from raw JPEG or PNG data without a full decode.
66
+ */
67
+ export declare function parseImageDimensions(data: Uint8Array, format: "jpeg" | "png"): {
68
+ width: number;
69
+ height: number;
70
+ };