@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,6 +8,8 @@ const form_control_1 = require("../../../form-control.js");
8
8
  class VmlDrawingXform extends base_xform_1.BaseXform {
9
9
  constructor() {
10
10
  super();
11
+ // Internal state for parsing header image shapes
12
+ this._parsingHeaderImage = false;
11
13
  this.map = {
12
14
  "v:shape": new vml_shape_xform_1.VmlShapeXform()
13
15
  };
@@ -23,8 +25,10 @@ class VmlDrawingXform extends base_xform_1.BaseXform {
23
25
  const renderModel = (model || this.model);
24
26
  const comments = renderModel.comments;
25
27
  const formControls = renderModel.formControls;
28
+ const headerImage = renderModel.headerImage;
26
29
  const hasComments = comments && comments.length > 0;
27
30
  const hasFormControls = formControls && formControls.length > 0;
31
+ const hasHeaderImage = !!headerImage;
28
32
  xmlStream.openXml(writer_1.StdDocAttributes);
29
33
  xmlStream.openNode(this.tag, VmlDrawingXform.DRAWING_ATTRIBUTES);
30
34
  // Shape layout - shared by both notes and form controls
@@ -62,6 +66,40 @@ class VmlDrawingXform extends base_xform_1.BaseXform {
62
66
  xmlStream.leafNode("o:lock", { "v:ext": "edit", shapetype: "t" });
63
67
  xmlStream.closeNode();
64
68
  }
69
+ // Shapetype 75 for header/footer image (watermark)
70
+ if (hasHeaderImage) {
71
+ xmlStream.openNode("v:shapetype", {
72
+ id: "_x0000_t75",
73
+ coordsize: "21600,21600",
74
+ "o:spt": "75",
75
+ "o:preferrelative": "t",
76
+ path: "m@4@5l@4@11@9@11@9@5xe",
77
+ filled: "f",
78
+ stroked: "f"
79
+ });
80
+ xmlStream.leafNode("v:stroke", { joinstyle: "miter" });
81
+ xmlStream.openNode("v:formulas");
82
+ xmlStream.leafNode("v:f", { eqn: "if lineDrawn pixelLineWidth 0" });
83
+ xmlStream.leafNode("v:f", { eqn: "sum @0 1 0" });
84
+ xmlStream.leafNode("v:f", { eqn: "sum 0 0 @1" });
85
+ xmlStream.leafNode("v:f", { eqn: "prod @2 1 2" });
86
+ xmlStream.leafNode("v:f", { eqn: "prod @3 21600 pixelWidth" });
87
+ xmlStream.leafNode("v:f", { eqn: "prod @3 21600 pixelHeight" });
88
+ xmlStream.leafNode("v:f", { eqn: "sum @0 0 1" });
89
+ xmlStream.leafNode("v:f", { eqn: "prod @6 1 2" });
90
+ xmlStream.leafNode("v:f", { eqn: "prod @7 21600 pixelWidth" });
91
+ xmlStream.leafNode("v:f", { eqn: "sum @8 21600 0" });
92
+ xmlStream.leafNode("v:f", { eqn: "prod @7 21600 pixelHeight" });
93
+ xmlStream.leafNode("v:f", { eqn: "sum @10 21600 0" });
94
+ xmlStream.closeNode(); // v:formulas
95
+ xmlStream.leafNode("v:path", {
96
+ "o:extrusionok": "f",
97
+ gradientshapeok: "t",
98
+ "o:connecttype": "rect"
99
+ });
100
+ xmlStream.leafNode("o:lock", { "v:ext": "edit", aspectratio: "t" });
101
+ xmlStream.closeNode(); // v:shapetype
102
+ }
65
103
  // Render comment shapes
66
104
  if (hasComments) {
67
105
  for (let i = 0; i < comments.length; i++) {
@@ -74,8 +112,32 @@ class VmlDrawingXform extends base_xform_1.BaseXform {
74
112
  this._renderCheckboxShape(xmlStream, control);
75
113
  }
76
114
  }
115
+ // Render header/footer image shape
116
+ if (hasHeaderImage) {
117
+ this._renderHeaderImageShape(xmlStream, headerImage);
118
+ }
77
119
  xmlStream.closeNode();
78
120
  }
121
+ /**
122
+ * Render a header/footer image shape for watermark
123
+ */
124
+ _renderHeaderImageShape(xmlStream, headerImage) {
125
+ const width = headerImage.width ?? 467.25;
126
+ const height = headerImage.height ?? 311.25;
127
+ // CH = Center Header, used by Excel for center-positioned header images
128
+ xmlStream.openNode("v:shape", {
129
+ id: "CH",
130
+ "o:spid": "_x0000_s2049",
131
+ type: "#_x0000_t75",
132
+ style: `position:absolute;margin-left:0;margin-top:0;width:${width}pt;height:${height}pt;z-index:1`
133
+ });
134
+ xmlStream.leafNode("v:imagedata", {
135
+ "o:relid": headerImage.imageRelId,
136
+ "o:title": "watermark"
137
+ });
138
+ xmlStream.leafNode("o:lock", { "v:ext": "edit", rotation: "t" });
139
+ xmlStream.closeNode(); // v:shape
140
+ }
79
141
  /**
80
142
  * Render a checkbox form control shape
81
143
  */
@@ -144,7 +206,7 @@ class VmlDrawingXform extends base_xform_1.BaseXform {
144
206
  xmlStream.closeNode(); // x:ClientData
145
207
  xmlStream.closeNode(); // v:shape
146
208
  }
147
- // Parsing - delegate to VmlShapeXform for notes
209
+ // Parsing - delegate to VmlShapeXform for notes, handle header images directly
148
210
  parseOpen(node) {
149
211
  if (this.parser) {
150
212
  this.parser.parseOpen(node);
@@ -158,10 +220,34 @@ class VmlDrawingXform extends base_xform_1.BaseXform {
158
220
  formControls: []
159
221
  };
160
222
  break;
223
+ case "v:shape":
224
+ // Check if this is a header image shape (type="#_x0000_t75")
225
+ if (node.attributes.type === "#_x0000_t75") {
226
+ this._parsingHeaderImage = true;
227
+ // Extract width/height from style
228
+ const style = node.attributes.style || "";
229
+ const widthMatch = /width:([0-9.]+)pt/.exec(style);
230
+ const heightMatch = /height:([0-9.]+)pt/.exec(style);
231
+ this._headerImageWidth = widthMatch ? parseFloat(widthMatch[1]) : undefined;
232
+ this._headerImageHeight = heightMatch ? parseFloat(heightMatch[1]) : undefined;
233
+ }
234
+ else {
235
+ // Regular shape — delegate to VmlShapeXform (comments)
236
+ this.parser = this.map[node.name];
237
+ if (this.parser) {
238
+ this.parser.parseOpen(node);
239
+ }
240
+ }
241
+ break;
161
242
  default:
162
- this.parser = this.map[node.name];
163
- if (this.parser) {
164
- this.parser.parseOpen(node);
243
+ if (this._parsingHeaderImage && node.name === "v:imagedata") {
244
+ this._headerImageRelId = node.attributes["o:relid"];
245
+ }
246
+ else {
247
+ this.parser = this.map[node.name];
248
+ if (this.parser) {
249
+ this.parser.parseOpen(node);
250
+ }
165
251
  }
166
252
  break;
167
253
  }
@@ -181,6 +267,19 @@ class VmlDrawingXform extends base_xform_1.BaseXform {
181
267
  return true;
182
268
  }
183
269
  switch (name) {
270
+ case "v:shape":
271
+ if (this._parsingHeaderImage && this._headerImageRelId) {
272
+ this.model.headerImage = {
273
+ imageRelId: this._headerImageRelId,
274
+ width: this._headerImageWidth,
275
+ height: this._headerImageHeight
276
+ };
277
+ }
278
+ this._parsingHeaderImage = false;
279
+ this._headerImageRelId = undefined;
280
+ this._headerImageWidth = undefined;
281
+ this._headerImageHeight = undefined;
282
+ return true;
184
283
  case this.tag:
185
284
  return false;
186
285
  default:
@@ -235,6 +235,8 @@ class WorkSheetXform extends base_xform_1.BaseXform {
235
235
  // Process background and image media entries
236
236
  const backgroundMedia = [];
237
237
  const imageMedia = [];
238
+ const watermarkMedia = [];
239
+ const headerImageMedia = [];
238
240
  model.media.forEach(medium => {
239
241
  if (medium.type === "background") {
240
242
  backgroundMedia.push(medium);
@@ -242,6 +244,12 @@ class WorkSheetXform extends base_xform_1.BaseXform {
242
244
  else if (medium.type === "image") {
243
245
  imageMedia.push(medium);
244
246
  }
247
+ else if (medium.type === "watermark") {
248
+ watermarkMedia.push(medium);
249
+ }
250
+ else if (medium.type === "headerImage") {
251
+ headerImageMedia.push(medium);
252
+ }
245
253
  });
246
254
  // Handle background images
247
255
  backgroundMedia.forEach(medium => {
@@ -279,6 +287,107 @@ class WorkSheetXform extends base_xform_1.BaseXform {
279
287
  drawing.anchors.push(...result.anchors);
280
288
  drawing.rels = result.rels;
281
289
  }
290
+ // Handle watermark overlay images — placed as a full-sheet drawing with transparency
291
+ if (watermarkMedia.length > 0) {
292
+ let { drawing } = model;
293
+ if (!drawing) {
294
+ drawing = model.drawing = {
295
+ rId: nextRid(rels),
296
+ name: `drawing${++options.drawingsCount}`,
297
+ anchors: [],
298
+ rels: []
299
+ };
300
+ options.drawings.push(drawing);
301
+ rels.push({
302
+ Id: drawing.rId,
303
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
304
+ Target: (0, ooxml_paths_1.drawingRelTargetFromWorksheet)(drawing.name)
305
+ });
306
+ }
307
+ for (const medium of watermarkMedia) {
308
+ const bookImage = options.media[medium.imageId];
309
+ if (!bookImage) {
310
+ continue;
311
+ }
312
+ const rIdImage = nextRid(drawing.rels);
313
+ drawing.rels.push({
314
+ Id: rIdImage,
315
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
316
+ Target: (0, drawing_utils_1.resolveMediaTarget)(bookImage)
317
+ });
318
+ // Convert opacity (0-1) to OOXML percentage (0-100000), clamped
319
+ const rawOpacity = medium.opacity !== undefined ? medium.opacity : 0.15;
320
+ const clampedOpacity = Math.max(0, Math.min(1, rawOpacity));
321
+ const alphaModFix = Math.round(clampedOpacity * 100000);
322
+ // Compute coverage based on actual worksheet dimensions.
323
+ // Use the model's dimensions if available, otherwise use generous defaults.
324
+ const dims = model.dimensions;
325
+ const maxCol = dims ? Math.max(dims.model?.right ?? 100, 100) : 100;
326
+ const maxRow = dims ? Math.max(dims.model?.bottom ?? 200, 200) : 200;
327
+ drawing.anchors.push({
328
+ picture: {
329
+ rId: rIdImage,
330
+ alphaModFix
331
+ },
332
+ // Cover the full data area with extra margin
333
+ range: {
334
+ editAs: "absolute",
335
+ tl: { nativeCol: 0, nativeColOff: 0, nativeRow: 0, nativeRowOff: 0 },
336
+ br: { nativeCol: maxCol, nativeColOff: 0, nativeRow: maxRow, nativeRowOff: 0 }
337
+ }
338
+ });
339
+ }
340
+ }
341
+ // Handle header watermark images — VML header/footer image
342
+ if (headerImageMedia.length > 0) {
343
+ const medium = headerImageMedia[0]; // Only one header image per sheet
344
+ const bookImage = options.media[medium.imageId];
345
+ if (bookImage) {
346
+ const rIdVml = nextRid(rels);
347
+ rels.push({
348
+ Id: rIdVml,
349
+ Type: rel_type_1.RelType.VmlDrawing,
350
+ Target: (0, ooxml_paths_1.vmlDrawingHFRelTargetFromWorksheet)(fileIndex)
351
+ });
352
+ // Store header image info on the model for the VML writer and worksheet render
353
+ model.headerImage = {
354
+ vmlRelId: rIdVml,
355
+ imageId: medium.imageId,
356
+ bookImage,
357
+ headerWidth: medium.headerWidth,
358
+ headerHeight: medium.headerHeight
359
+ };
360
+ // Flag for content-types registration
361
+ options.hasHeaderWatermark = true;
362
+ // Update headerFooter to include &G placeholder.
363
+ // Respects the applyTo option: "all" (default), "odd", "even", "first".
364
+ if (!model.headerFooter) {
365
+ model.headerFooter = {};
366
+ }
367
+ const applyTo = medium.applyTo || "all";
368
+ const insertG = (field) => {
369
+ const existing = model.headerFooter[field] || "";
370
+ if (existing.includes("&G")) {
371
+ return existing;
372
+ }
373
+ if (existing.includes("&C")) {
374
+ return existing.replace("&C", "&C&G");
375
+ }
376
+ return existing + "&C&G";
377
+ };
378
+ if (applyTo === "all" || applyTo === "odd") {
379
+ model.headerFooter.oddHeader = insertG("oddHeader");
380
+ }
381
+ if (applyTo === "all" || applyTo === "even") {
382
+ model.headerFooter.evenHeader = insertG("evenHeader");
383
+ model.headerFooter.differentOddEven = true;
384
+ }
385
+ if (applyTo === "all" || applyTo === "first") {
386
+ model.headerFooter.firstHeader = insertG("firstHeader");
387
+ model.headerFooter.differentFirst = true;
388
+ }
389
+ }
390
+ }
282
391
  // prepare tables
283
392
  model.tables.forEach(table => {
284
393
  // relationships
@@ -439,10 +548,18 @@ class WorkSheetXform extends base_xform_1.BaseXform {
439
548
  // NOTE: Excel is picky about worksheet child element order; legacyDrawing must come before controls.
440
549
  model.rels.forEach(rel => {
441
550
  if (rel.Type === rel_type_1.RelType.VmlDrawing) {
551
+ // Skip VML rels that are for header images (they use legacyDrawingHF instead)
552
+ if (model.headerImage && rel.Id === model.headerImage.vmlRelId) {
553
+ return;
554
+ }
442
555
  xmlStream.leafNode("legacyDrawing", { "r:id": rel.Id });
443
556
  }
444
557
  });
445
558
  }
559
+ // legacyDrawingHF — VML drawing for header/footer images (watermark in header mode)
560
+ if (model.headerImage) {
561
+ xmlStream.leafNode("legacyDrawingHF", { "r:id": model.headerImage.vmlRelId });
562
+ }
446
563
  // Controls section for legacy form controls (checkboxes, etc.)
447
564
  // Excel expects <controls> entries that reference ctrlProp relationships.
448
565
  if (model.formControls && model.formControls.length > 0) {
@@ -658,13 +775,23 @@ class WorkSheetXform extends base_xform_1.BaseXform {
658
775
  // Also extract images to model.media for backward compatibility
659
776
  drawing.anchors.forEach(anchor => {
660
777
  if (anchor.medium) {
661
- const image = {
662
- type: "image",
663
- imageId: anchor.medium.index,
664
- range: anchor.range,
665
- hyperlinks: anchor.picture.hyperlinks
666
- };
667
- model.media.push(image);
778
+ // Detect overlay watermarks: drawings that carry alphaModFix
779
+ const hasAlpha = anchor.medium.alphaModFix !== undefined && anchor.medium.alphaModFix < 100000;
780
+ if (hasAlpha) {
781
+ model.media.push({
782
+ type: "watermark",
783
+ imageId: anchor.medium.index,
784
+ opacity: anchor.medium.alphaModFix / 100000
785
+ });
786
+ }
787
+ else {
788
+ model.media.push({
789
+ type: "image",
790
+ imageId: anchor.medium.index,
791
+ range: anchor.range,
792
+ hyperlinks: anchor.picture.hyperlinks
793
+ });
794
+ }
668
795
  }
669
796
  });
670
797
  }
@@ -909,6 +909,17 @@ class XLSX {
909
909
  const vmlDrawing = await xform.parseStream(entry);
910
910
  model.vmlDrawings[(0, ooxml_paths_1.vmlDrawingRelTargetFromWorksheetName)(name)] = vmlDrawing;
911
911
  }
912
+ async _processVmlDrawingHFEntry(entry, model, _name) {
913
+ const xform = new vml_drawing_xform_1.VmlDrawingXform();
914
+ const vmlDrawing = await xform.parseStream(entry);
915
+ // Store parsed header image info for reconciliation
916
+ if (vmlDrawing && vmlDrawing.headerImage) {
917
+ if (!model.vmlDrawingHF) {
918
+ model.vmlDrawingHF = {};
919
+ }
920
+ model.vmlDrawingHF[_name] = vmlDrawing.headerImage;
921
+ }
922
+ }
912
923
  async _processThemeEntry(stream, model, name) {
913
924
  await new Promise((resolve, reject) => {
914
925
  const streamBuf = this.createStreamBuf();
@@ -1023,6 +1034,13 @@ class XLSX {
1023
1034
  await this._processVmlDrawingEntry(stream, model, vmlDrawingName);
1024
1035
  return true;
1025
1036
  }
1037
+ // VML header/footer drawings (watermark in header mode).
1038
+ // Parse to extract header image info for round-trip preservation.
1039
+ const vmlHFName = (0, ooxml_paths_1.getVmlDrawingHFNameFromPath)(entryName);
1040
+ if (vmlHFName) {
1041
+ await this._processVmlDrawingHFEntry(stream, model, vmlHFName);
1042
+ return true;
1043
+ }
1026
1044
  const commentsIndex = (0, ooxml_paths_1.getCommentsIndexFromPath)(entryName);
1027
1045
  if (commentsIndex) {
1028
1046
  await this._processCommentEntry(stream, model, `comments${commentsIndex}`);
@@ -1229,6 +1247,35 @@ class XLSX {
1229
1247
  formControls: hasFormControls ? worksheet.formControls : []
1230
1248
  });
1231
1249
  }
1250
+ // Generate VML drawing for header/footer images (watermark in header mode)
1251
+ if (worksheet.headerImage) {
1252
+ const hdrImage = worksheet.headerImage;
1253
+ const bookImage = hdrImage.bookImage;
1254
+ const imageFileName = bookImage.name &&
1255
+ bookImage.extension &&
1256
+ bookImage.name.endsWith(`.${bookImage.extension}`)
1257
+ ? bookImage.name
1258
+ : `${bookImage.name}.${bookImage.extension}`;
1259
+ const imageRelTarget = `../media/${imageFileName}`;
1260
+ // Write the VML file for the header image
1261
+ await this._renderToZip(zip, (0, ooxml_paths_1.vmlDrawingHFPath)(fileIndex), vmlDrawingXform, {
1262
+ comments: [],
1263
+ formControls: [],
1264
+ headerImage: {
1265
+ imageRelId: "rId1",
1266
+ width: hdrImage.headerWidth,
1267
+ height: hdrImage.headerHeight
1268
+ }
1269
+ });
1270
+ // Write the VML rels file referencing the image
1271
+ await this._renderToZip(zip, (0, ooxml_paths_1.vmlDrawingHFRelsPath)(fileIndex), relationshipsXform, [
1272
+ {
1273
+ Id: "rId1",
1274
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
1275
+ Target: imageRelTarget
1276
+ }
1277
+ ]);
1278
+ }
1232
1279
  // Generate ctrlProp files for form controls
1233
1280
  if (hasFormControls) {
1234
1281
  for (const control of worksheet.formControls) {
@@ -1375,6 +1422,7 @@ class XLSX {
1375
1422
  worksheetOptions.drawings = model.drawings = [];
1376
1423
  worksheetOptions.commentRefs = model.commentRefs = [];
1377
1424
  worksheetOptions.formControlRefs = model.formControlRefs = [];
1425
+ model.hasHeaderWatermark = false;
1378
1426
  let tableCount = 0;
1379
1427
  model.tables = [];
1380
1428
  model.worksheets.forEach((worksheet, index) => {
@@ -1393,6 +1441,10 @@ class XLSX {
1393
1441
  });
1394
1442
  // ContentTypesXform expects this flag
1395
1443
  model.hasCheckboxes = model.styles.hasCheckboxes;
1444
+ // Propagate header watermark flag from worksheet prepare options
1445
+ if (worksheetOptions.hasHeaderWatermark) {
1446
+ model.hasHeaderWatermark = true;
1447
+ }
1396
1448
  // Build passthroughContentTypes for ContentTypesXform using PassthroughManager
1397
1449
  const passthrough = model.passthrough || {};
1398
1450
  const passthroughManager = new passthrough_manager_1.PassthroughManager();
@@ -132,11 +132,12 @@ class PdfWriter {
132
132
  addPage(options) {
133
133
  const objNum = this.allocObject();
134
134
  const mediaBox = `[0 0 ${(0, pdf_object_1.pdfNumber)(options.width)} ${(0, pdf_object_1.pdfNumber)(options.height)}]`;
135
+ const contentsValue = typeof options.contentsRef === "string" ? options.contentsRef : (0, pdf_object_1.pdfRef)(options.contentsRef);
135
136
  const dict = new pdf_object_1.PdfDict()
136
137
  .set("Type", "/Page")
137
138
  .set("Parent", (0, pdf_object_1.pdfRef)(options.parentRef))
138
139
  .set("MediaBox", mediaBox)
139
- .set("Contents", (0, pdf_object_1.pdfRef)(options.contentsRef))
140
+ .set("Contents", contentsValue)
140
141
  .set("Resources", (0, pdf_object_1.pdfRef)(options.resourcesRef));
141
142
  if (options.annotRefs && options.annotRefs.length > 0) {
142
143
  dict.set("Annots", "[" + options.annotRefs.map(r => (0, pdf_object_1.pdfRef)(r)).join(" ") + "]");