@cj-tech-master/excelts 8.1.2 → 9.0.0-canary.20260409073829.a0d72ec

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 (151) hide show
  1. package/README.md +2 -2
  2. package/README_zh.md +2 -2
  3. package/dist/browser/index.browser.d.ts +2 -0
  4. package/dist/browser/index.browser.js +2 -0
  5. package/dist/browser/index.d.ts +2 -0
  6. package/dist/browser/index.js +2 -0
  7. package/dist/browser/modules/excel/cell.js +11 -7
  8. package/dist/browser/modules/excel/column.js +7 -6
  9. package/dist/browser/modules/excel/image.d.ts +27 -2
  10. package/dist/browser/modules/excel/image.js +23 -1
  11. package/dist/browser/modules/excel/row.js +5 -1
  12. package/dist/browser/modules/excel/stream/worksheet-reader.js +3 -2
  13. package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +16 -1
  14. package/dist/browser/modules/excel/stream/worksheet-writer.js +68 -0
  15. package/dist/browser/modules/excel/types.d.ts +72 -0
  16. package/dist/browser/modules/excel/utils/cell-format.js +64 -2
  17. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +4 -0
  18. package/dist/browser/modules/excel/utils/drawing-utils.js +5 -0
  19. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +4 -0
  20. package/dist/browser/modules/excel/utils/ooxml-paths.js +15 -0
  21. package/dist/browser/modules/excel/utils/watermark-image.d.ts +67 -0
  22. package/dist/browser/modules/excel/utils/watermark-image.js +383 -0
  23. package/dist/browser/modules/excel/worksheet.d.ts +39 -1
  24. package/dist/browser/modules/excel/worksheet.js +99 -0
  25. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
  26. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
  27. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
  28. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
  29. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
  30. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
  31. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
  32. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
  33. package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
  34. package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
  35. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
  36. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
  37. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +53 -1
  38. package/dist/browser/modules/pdf/core/pdf-writer.d.ts +1 -1
  39. package/dist/browser/modules/pdf/core/pdf-writer.js +2 -1
  40. package/dist/browser/modules/pdf/excel-bridge.d.ts +4 -3
  41. package/dist/browser/modules/pdf/excel-bridge.js +18 -5
  42. package/dist/browser/modules/pdf/index.d.ts +4 -4
  43. package/dist/browser/modules/pdf/index.js +3 -3
  44. package/dist/browser/modules/pdf/pdf.d.ts +7 -6
  45. package/dist/browser/modules/pdf/pdf.js +7 -6
  46. package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +8 -7
  47. package/dist/browser/modules/pdf/reader/pdf-reader.js +81 -74
  48. package/dist/browser/modules/pdf/render/constants.d.ts +30 -0
  49. package/dist/browser/modules/pdf/render/constants.js +30 -0
  50. package/dist/browser/modules/pdf/render/layout-engine.d.ts +2 -1
  51. package/dist/browser/modules/pdf/render/layout-engine.js +359 -156
  52. package/dist/browser/modules/pdf/render/page-renderer.d.ts +31 -3
  53. package/dist/browser/modules/pdf/render/page-renderer.js +621 -114
  54. package/dist/browser/modules/pdf/render/pdf-exporter.d.ts +3 -2
  55. package/dist/browser/modules/pdf/render/pdf-exporter.js +219 -142
  56. package/dist/browser/modules/pdf/render/style-converter.js +27 -26
  57. package/dist/browser/modules/pdf/types.d.ts +243 -0
  58. package/dist/browser/utils/utils.base.d.ts +5 -0
  59. package/dist/browser/utils/utils.base.js +10 -0
  60. package/dist/cjs/index.js +5 -2
  61. package/dist/cjs/modules/excel/cell.js +11 -7
  62. package/dist/cjs/modules/excel/column.js +7 -6
  63. package/dist/cjs/modules/excel/image.js +23 -1
  64. package/dist/cjs/modules/excel/row.js +5 -1
  65. package/dist/cjs/modules/excel/stream/worksheet-reader.js +3 -2
  66. package/dist/cjs/modules/excel/stream/worksheet-writer.js +68 -0
  67. package/dist/cjs/modules/excel/utils/cell-format.js +64 -2
  68. package/dist/cjs/modules/excel/utils/drawing-utils.js +5 -0
  69. package/dist/cjs/modules/excel/utils/ooxml-paths.js +19 -0
  70. package/dist/cjs/modules/excel/utils/watermark-image.js +386 -0
  71. package/dist/cjs/modules/excel/worksheet.js +99 -0
  72. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
  73. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
  74. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
  75. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
  76. package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
  77. package/dist/cjs/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
  78. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +134 -7
  79. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +52 -0
  80. package/dist/cjs/modules/pdf/core/pdf-writer.js +2 -1
  81. package/dist/cjs/modules/pdf/excel-bridge.js +18 -5
  82. package/dist/cjs/modules/pdf/index.js +3 -3
  83. package/dist/cjs/modules/pdf/pdf.js +7 -6
  84. package/dist/cjs/modules/pdf/reader/pdf-reader.js +81 -74
  85. package/dist/cjs/modules/pdf/render/constants.js +33 -0
  86. package/dist/cjs/modules/pdf/render/layout-engine.js +359 -156
  87. package/dist/cjs/modules/pdf/render/page-renderer.js +623 -114
  88. package/dist/cjs/modules/pdf/render/pdf-exporter.js +218 -141
  89. package/dist/cjs/modules/pdf/render/style-converter.js +27 -26
  90. package/dist/cjs/utils/utils.base.js +11 -0
  91. package/dist/esm/index.browser.js +2 -0
  92. package/dist/esm/index.js +2 -0
  93. package/dist/esm/modules/excel/cell.js +11 -7
  94. package/dist/esm/modules/excel/column.js +7 -6
  95. package/dist/esm/modules/excel/image.js +23 -1
  96. package/dist/esm/modules/excel/row.js +5 -1
  97. package/dist/esm/modules/excel/stream/worksheet-reader.js +3 -2
  98. package/dist/esm/modules/excel/stream/worksheet-writer.js +68 -0
  99. package/dist/esm/modules/excel/utils/cell-format.js +64 -2
  100. package/dist/esm/modules/excel/utils/drawing-utils.js +5 -0
  101. package/dist/esm/modules/excel/utils/ooxml-paths.js +15 -0
  102. package/dist/esm/modules/excel/utils/watermark-image.js +383 -0
  103. package/dist/esm/modules/excel/worksheet.js +99 -0
  104. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
  105. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
  106. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
  107. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
  108. package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
  109. package/dist/esm/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
  110. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
  111. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +53 -1
  112. package/dist/esm/modules/pdf/core/pdf-writer.js +2 -1
  113. package/dist/esm/modules/pdf/excel-bridge.js +18 -5
  114. package/dist/esm/modules/pdf/index.js +3 -3
  115. package/dist/esm/modules/pdf/pdf.js +7 -6
  116. package/dist/esm/modules/pdf/reader/pdf-reader.js +81 -74
  117. package/dist/esm/modules/pdf/render/constants.js +30 -0
  118. package/dist/esm/modules/pdf/render/layout-engine.js +359 -156
  119. package/dist/esm/modules/pdf/render/page-renderer.js +621 -114
  120. package/dist/esm/modules/pdf/render/pdf-exporter.js +219 -142
  121. package/dist/esm/modules/pdf/render/style-converter.js +27 -26
  122. package/dist/esm/utils/utils.base.js +10 -0
  123. package/dist/iife/excelts.iife.js +4243 -1977
  124. package/dist/iife/excelts.iife.js.map +1 -1
  125. package/dist/iife/excelts.iife.min.js +49 -49
  126. package/dist/types/index.browser.d.ts +2 -0
  127. package/dist/types/index.d.ts +2 -0
  128. package/dist/types/modules/excel/image.d.ts +27 -2
  129. package/dist/types/modules/excel/stream/worksheet-writer.d.ts +16 -1
  130. package/dist/types/modules/excel/types.d.ts +72 -0
  131. package/dist/types/modules/excel/utils/drawing-utils.d.ts +4 -0
  132. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +4 -0
  133. package/dist/types/modules/excel/utils/watermark-image.d.ts +67 -0
  134. package/dist/types/modules/excel/worksheet.d.ts +39 -1
  135. package/dist/types/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
  136. package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
  137. package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
  138. package/dist/types/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
  139. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
  140. package/dist/types/modules/pdf/core/pdf-writer.d.ts +1 -1
  141. package/dist/types/modules/pdf/excel-bridge.d.ts +4 -3
  142. package/dist/types/modules/pdf/index.d.ts +4 -4
  143. package/dist/types/modules/pdf/pdf.d.ts +7 -6
  144. package/dist/types/modules/pdf/reader/pdf-reader.d.ts +8 -7
  145. package/dist/types/modules/pdf/render/constants.d.ts +30 -0
  146. package/dist/types/modules/pdf/render/layout-engine.d.ts +2 -1
  147. package/dist/types/modules/pdf/render/page-renderer.d.ts +31 -3
  148. package/dist/types/modules/pdf/render/pdf-exporter.d.ts +3 -2
  149. package/dist/types/modules/pdf/types.d.ts +243 -0
  150. package/dist/types/utils/utils.base.d.ts +5 -0
  151. package/package.json +1 -1
package/README.md CHANGED
@@ -100,14 +100,14 @@ wb.getWorksheet(1).eachRow((row, n) => console.log(n, row.values));
100
100
 
101
101
  // PDF — generate from data, no Workbook needed
102
102
  import { pdf } from "@cj-tech-master/excelts/pdf";
103
- const pdfBytes = pdf([
103
+ const pdfBytes = await pdf([
104
104
  ["Product", "Revenue"],
105
105
  ["Widget", 1000]
106
106
  ]);
107
107
 
108
108
  // PDF — read text, images, and metadata from any PDF
109
109
  import { readPdf } from "@cj-tech-master/excelts/pdf";
110
- const result = readPdf(pdfBytes);
110
+ const result = await readPdf(pdfBytes);
111
111
  console.log(result.text); // extracted text
112
112
  console.log(result.metadata); // title, author, etc.
113
113
 
package/README_zh.md CHANGED
@@ -100,14 +100,14 @@ wb.getWorksheet(1).eachRow((row, n) => console.log(n, row.values));
100
100
 
101
101
  // PDF — 直接从数据生成,无需 Workbook
102
102
  import { pdf } from "@cj-tech-master/excelts/pdf";
103
- const pdfBytes = pdf([
103
+ const pdfBytes = await pdf([
104
104
  ["产品", "收入"],
105
105
  ["小工具", 1000]
106
106
  ]);
107
107
 
108
108
  // PDF — 读取任意 PDF 的文本、图片和元数据
109
109
  import { readPdf } from "@cj-tech-master/excelts/pdf";
110
- const result = readPdf(pdfBytes);
110
+ const result = await readPdf(pdfBytes);
111
111
  console.log(result.text); // 提取的文本
112
112
  console.log(result.metadata); // 标题、作者等
113
113
 
@@ -15,6 +15,8 @@ export { DataValidations } from "./modules/excel/data-validations.js";
15
15
  export { FormCheckbox } from "./modules/excel/form-control.js";
16
16
  export * from "./modules/excel/enums.js";
17
17
  export * from "./modules/excel/types.js";
18
+ export { createTextWatermarkImage } from "./modules/excel/utils/watermark-image.js";
19
+ export type { TextWatermarkImageOptions } from "./modules/excel/utils/watermark-image.js";
18
20
  export type { PivotTable, PivotTableModel, PivotTableValue, PivotTableSource, CacheField, SharedItemValue, DataField, PivotTableSubtotal, RecordValue, ParsedCacheDefinition, ParsedCacheRecords } from "./modules/excel/pivot-table.js";
19
21
  export type { FormCheckboxModel, FormCheckboxOptions, FormControlRange, FormControlAnchor } from "./modules/excel/form-control.js";
20
22
  export { WorkbookWriter } from "./modules/excel/stream/workbook-writer.browser.js";
@@ -25,6 +25,8 @@ export * from "./modules/excel/enums.js";
25
25
  // =============================================================================
26
26
  // Export all type definitions from types.ts
27
27
  export * from "./modules/excel/types.js";
28
+ // Watermark image generator utility
29
+ export { createTextWatermarkImage } from "./modules/excel/utils/watermark-image.js";
28
30
  // =============================================================================
29
31
  // Streaming Writer (Browser-compatible)
30
32
  // Uses cross-platform base implementation without Node.js fs
@@ -15,6 +15,8 @@ export { WorksheetWriter } from "./modules/excel/stream/worksheet-writer.js";
15
15
  export { WorksheetReader } from "./modules/excel/stream/worksheet-reader.js";
16
16
  export * from "./modules/excel/enums.js";
17
17
  export * from "./modules/excel/types.js";
18
+ export { createTextWatermarkImage } from "./modules/excel/utils/watermark-image.js";
19
+ export type { TextWatermarkImageOptions } from "./modules/excel/utils/watermark-image.js";
18
20
  export type { PivotTable, PivotTableModel, PivotTableValue, PivotTableSource, CacheField, SharedItemValue, DataField, PivotTableSubtotal, RecordValue, ParsedCacheDefinition, ParsedCacheRecords } from "./modules/excel/pivot-table.js";
19
21
  export type { FormCheckboxModel, FormCheckboxOptions, FormControlRange, FormControlAnchor } from "./modules/excel/form-control.js";
20
22
  export type { WorkbookReaderOptions, ParseEvent, SharedStringEvent, WorksheetReadyEvent, HyperlinksEvent } from "./modules/excel/stream/workbook-reader.js";
@@ -29,6 +29,8 @@ export * from "./modules/excel/enums.js";
29
29
  // =============================================================================
30
30
  // Export all type definitions from types.ts
31
31
  export * from "./modules/excel/types.js";
32
+ // Watermark image generator utility
33
+ export { createTextWatermarkImage } from "./modules/excel/utils/watermark-image.js";
32
34
  export { CsvParserStream, CsvFormatterStream, createCsvParserStream, createCsvFormatterStream } from "./modules/csv/stream/index.js";
33
35
  // =============================================================================
34
36
  // Additional Classes & Types
@@ -3,6 +3,7 @@ import { Enums } from "./enums.js";
3
3
  import { Note } from "./note.js";
4
4
  import { escapeHtml } from "./utils/under-dash.js";
5
5
  import { slideFormula } from "./utils/shared-formula.js";
6
+ import { copyStyle } from "./utils/copy-style.js";
6
7
  import { ExcelError, InvalidValueTypeError } from "./errors.js";
7
8
  // Returns true if the value is a non-empty object (has at least one own key),
8
9
  // or any truthy non-object value. Returns false for undefined, null, false, 0,
@@ -87,27 +88,27 @@ class Cell {
87
88
  const font = (rowStyle && hasOwnKeys(rowStyle.font) && rowStyle.font) ||
88
89
  (colStyle && hasOwnKeys(colStyle.font) && colStyle.font);
89
90
  if (font) {
90
- style.font = font;
91
+ style.font = structuredClone(font);
91
92
  }
92
93
  const alignment = (rowStyle && hasOwnKeys(rowStyle.alignment) && rowStyle.alignment) ||
93
94
  (colStyle && hasOwnKeys(colStyle.alignment) && colStyle.alignment);
94
95
  if (alignment) {
95
- style.alignment = alignment;
96
+ style.alignment = structuredClone(alignment);
96
97
  }
97
98
  const border = (rowStyle && hasOwnKeys(rowStyle.border) && rowStyle.border) ||
98
99
  (colStyle && hasOwnKeys(colStyle.border) && colStyle.border);
99
100
  if (border) {
100
- style.border = border;
101
+ style.border = structuredClone(border);
101
102
  }
102
103
  const fill = (rowStyle && hasOwnKeys(rowStyle.fill) && rowStyle.fill) ||
103
104
  (colStyle && hasOwnKeys(colStyle.fill) && colStyle.fill);
104
105
  if (fill) {
105
- style.fill = fill;
106
+ style.fill = structuredClone(fill);
106
107
  }
107
108
  const protection = (rowStyle && hasOwnKeys(rowStyle.protection) && rowStyle.protection) ||
108
109
  (colStyle && hasOwnKeys(colStyle.protection) && colStyle.protection);
109
110
  if (protection) {
110
- style.protection = protection;
111
+ style.protection = structuredClone(protection);
111
112
  }
112
113
  return style;
113
114
  }
@@ -151,7 +152,10 @@ class Cell {
151
152
  this._value.release();
152
153
  this._value = Value.create(Cell.Types.Merge, this, master);
153
154
  if (!ignoreStyle) {
154
- this.style = master.style;
155
+ // Deep-copy so each cell has an independent style object.
156
+ // Without this, all cells in the merge share the same reference,
157
+ // and setting a property (e.g. border) on any cell mutates all of them.
158
+ this.style = copyStyle(master.style) ?? {};
155
159
  }
156
160
  }
157
161
  unmerge() {
@@ -318,7 +322,7 @@ class Cell {
318
322
  }
319
323
  }
320
324
  if (value.style) {
321
- this.style = value.style;
325
+ this.style = copyStyle(value.style) ?? {};
322
326
  }
323
327
  else {
324
328
  this.style = {};
@@ -1,5 +1,6 @@
1
1
  import { colCache } from "./utils/col-cache.js";
2
2
  import { isEqual } from "./utils/under-dash.js";
3
+ import { copyStyle } from "./utils/copy-style.js";
3
4
  import { Enums } from "./enums.js";
4
5
  const DEFAULT_COLUMN_WIDTH = 9;
5
6
  /**
@@ -48,7 +49,7 @@ class Column {
48
49
  this.width = value.width !== undefined ? value.width : DEFAULT_COLUMN_WIDTH;
49
50
  this.outlineLevel = value.outlineLevel;
50
51
  if (value.style) {
51
- this.style = value.style;
52
+ this.style = copyStyle(value.style) ?? {};
52
53
  }
53
54
  else {
54
55
  this.style = {};
@@ -259,7 +260,7 @@ class Column {
259
260
  set font(value) {
260
261
  this.style.font = value;
261
262
  this.eachCell(cell => {
262
- cell.font = value;
263
+ cell.font = value ? structuredClone(value) : value;
263
264
  });
264
265
  }
265
266
  get alignment() {
@@ -268,7 +269,7 @@ class Column {
268
269
  set alignment(value) {
269
270
  this.style.alignment = value;
270
271
  this.eachCell(cell => {
271
- cell.alignment = value;
272
+ cell.alignment = value ? structuredClone(value) : value;
272
273
  });
273
274
  }
274
275
  get protection() {
@@ -277,7 +278,7 @@ class Column {
277
278
  set protection(value) {
278
279
  this.style.protection = value;
279
280
  this.eachCell(cell => {
280
- cell.protection = value;
281
+ cell.protection = value ? structuredClone(value) : value;
281
282
  });
282
283
  }
283
284
  get border() {
@@ -286,7 +287,7 @@ class Column {
286
287
  set border(value) {
287
288
  this.style.border = value;
288
289
  this.eachCell(cell => {
289
- cell.border = value;
290
+ cell.border = value ? structuredClone(value) : value;
290
291
  });
291
292
  }
292
293
  get fill() {
@@ -295,7 +296,7 @@ class Column {
295
296
  set fill(value) {
296
297
  this.style.fill = value;
297
298
  this.eachCell(cell => {
298
- cell.fill = value;
299
+ cell.fill = value ? structuredClone(value) : value;
299
300
  });
300
301
  }
301
302
  // =============================================================================
@@ -19,6 +19,19 @@ interface BackgroundModel {
19
19
  type: "background";
20
20
  imageId: string;
21
21
  }
22
+ interface WatermarkModel {
23
+ type: "watermark";
24
+ imageId: string;
25
+ /** Opacity 0-1 for overlay mode */
26
+ opacity?: number;
27
+ }
28
+ interface HeaderImageModel {
29
+ type: "headerImage";
30
+ imageId: string;
31
+ headerWidth?: number;
32
+ headerHeight?: number;
33
+ applyTo?: "all" | "odd" | "even" | "first";
34
+ }
22
35
  interface ImageRangeModel {
23
36
  tl: AnchorModel;
24
37
  br?: AnchorModel;
@@ -31,7 +44,7 @@ interface ImageModel {
31
44
  hyperlinks?: ImageHyperlinks;
32
45
  range: ImageRangeModel;
33
46
  }
34
- type Model = BackgroundModel | ImageModel;
47
+ type Model = BackgroundModel | ImageModel | WatermarkModel | HeaderImageModel;
35
48
  type ImageModelInput = ModelInput;
36
49
  interface RangeInput {
37
50
  tl?: AnchorModel | {
@@ -51,15 +64,27 @@ interface ModelInput {
51
64
  imageId: string;
52
65
  range?: string | RangeInput | ImageRangeModel;
53
66
  hyperlinks?: ImageHyperlinks;
67
+ opacity?: number;
68
+ headerWidth?: number;
69
+ headerHeight?: number;
70
+ applyTo?: "all" | "odd" | "even" | "first";
54
71
  }
55
72
  declare class Image {
56
73
  readonly worksheet: Worksheet;
57
74
  type?: string;
58
75
  imageId?: string;
59
76
  range?: ImageRange;
77
+ /** Opacity for watermark overlay mode (0-1). */
78
+ opacity?: number;
79
+ /** Header image width in points. */
80
+ headerWidth?: number;
81
+ /** Header image height in points. */
82
+ headerHeight?: number;
83
+ /** Header watermark applyTo setting. */
84
+ applyTo?: "all" | "odd" | "even" | "first";
60
85
  constructor(worksheet: Worksheet, model?: ModelInput);
61
86
  get model(): Model;
62
- set model({ type, imageId, range, hyperlinks }: ModelInput);
87
+ set model({ type, imageId, range, hyperlinks, opacity, headerWidth, headerHeight, applyTo }: ModelInput);
63
88
  clone(worksheet?: Worksheet): Image;
64
89
  }
65
90
  export { Image, type Model as ImageModel, type ImageModelInput };
@@ -15,6 +15,20 @@ class Image {
15
15
  type: this.type,
16
16
  imageId: this.imageId ?? ""
17
17
  };
18
+ case "watermark":
19
+ return {
20
+ type: this.type,
21
+ imageId: this.imageId ?? "",
22
+ opacity: this.opacity
23
+ };
24
+ case "headerImage":
25
+ return {
26
+ type: this.type,
27
+ imageId: this.imageId ?? "",
28
+ headerWidth: this.headerWidth,
29
+ headerHeight: this.headerHeight,
30
+ applyTo: this.applyTo
31
+ };
18
32
  case "image": {
19
33
  const range = this.range;
20
34
  if (!range) {
@@ -36,9 +50,13 @@ class Image {
36
50
  throw new ImageError("Invalid Image Type");
37
51
  }
38
52
  }
39
- set model({ type, imageId, range, hyperlinks }) {
53
+ set model({ type, imageId, range, hyperlinks, opacity, headerWidth, headerHeight, applyTo }) {
40
54
  this.type = type;
41
55
  this.imageId = imageId;
56
+ this.opacity = opacity;
57
+ this.headerWidth = headerWidth;
58
+ this.headerHeight = headerHeight;
59
+ this.applyTo = applyTo;
42
60
  if (type === "image") {
43
61
  if (typeof range === "string") {
44
62
  const decoded = colCache.decode(range);
@@ -67,6 +85,10 @@ class Image {
67
85
  const cloned = new Image(target);
68
86
  cloned.type = this.type;
69
87
  cloned.imageId = this.imageId;
88
+ cloned.opacity = this.opacity;
89
+ cloned.headerWidth = this.headerWidth;
90
+ cloned.headerHeight = this.headerHeight;
91
+ cloned.applyTo = this.applyTo;
70
92
  if (this.range) {
71
93
  cloned.range = {
72
94
  tl: this.range.tl.clone(target),
@@ -297,7 +297,11 @@ class Row {
297
297
  this.style[name] = value;
298
298
  this._cells.forEach(cell => {
299
299
  if (cell) {
300
- cell.style[name] = value;
300
+ // Clone object values so each cell gets an independent copy.
301
+ // Without this, mutating a sub-property (e.g. cell.border.top = ...)
302
+ // would leak to every other cell that received the same reference.
303
+ cell.style[name] =
304
+ typeof value === "object" && value !== null ? structuredClone(value) : value;
301
305
  }
302
306
  });
303
307
  }
@@ -7,6 +7,7 @@ import { EventEmitter } from "../../../utils/event-emitter.js";
7
7
  import { SaxParser } from "../../xml/sax.js";
8
8
  import { ExcelStreamStateError } from "../errors.js";
9
9
  import { isDateFmt, excelToDate, decodeOoxmlEscape } from "../../../utils/utils.browser.js";
10
+ import { copyStyle } from "../utils/copy-style.js";
10
11
  import { colCache } from "../utils/col-cache.js";
11
12
  import { Dimensions } from "../range.js";
12
13
  import { Row } from "../row.js";
@@ -178,7 +179,7 @@ class WorksheetReader extends EventEmitter {
178
179
  const styleId = parseInt(node.attributes.s, 10);
179
180
  const style = styles.getStyleModel(styleId);
180
181
  if (style) {
181
- row.style = style;
182
+ row.style = copyStyle(style) ?? {};
182
183
  }
183
184
  }
184
185
  }
@@ -279,7 +280,7 @@ class WorksheetReader extends EventEmitter {
279
280
  if (c.s !== undefined) {
280
281
  const style = styles.getStyleModel(c.s);
281
282
  if (style) {
282
- cell.style = style;
283
+ cell.style = copyStyle(style) ?? {};
283
284
  }
284
285
  }
285
286
  if (c.f) {
@@ -3,7 +3,7 @@ import { Row } from "../row.js";
3
3
  import type { Cell } from "../cell.js";
4
4
  import { Column } from "../column.js";
5
5
  import { DataValidations } from "../data-validations.js";
6
- import type { RowBreak, ColBreak, PageSetup, HeaderFooter, WorksheetProperties, WorksheetView, WorksheetState, AutoFilter, WorksheetProtection, ConditionalFormattingOptions, AddImageRange } from "../types.js";
6
+ import type { RowBreak, ColBreak, PageSetup, HeaderFooter, WorksheetProperties, WorksheetView, WorksheetState, AutoFilter, WorksheetProtection, ConditionalFormattingOptions, AddImageRange, WatermarkOptions } from "../types.js";
7
7
  interface WorksheetWriterOptions {
8
8
  id: number;
9
9
  name?: string;
@@ -97,6 +97,8 @@ declare class WorksheetWriter {
97
97
  startedData: boolean;
98
98
  private _background?;
99
99
  private _headerRowCount?;
100
+ /** Watermark configuration */
101
+ private _watermark;
100
102
  /** Drawing model — populated during commit if images were added */
101
103
  private _drawing?;
102
104
  /** Relationship Id - assigned by WorkbookWriter */
@@ -141,6 +143,19 @@ declare class WorksheetWriter {
141
143
  * Each entry contains imageId and the normalised range (with native anchors).
142
144
  */
143
145
  getImages(): ReadonlyArray<WriterImageModel>;
146
+ /**
147
+ * Add a watermark to the worksheet using an image from `WorkbookWriter.addImage()`.
148
+ * Supports overlay mode (DrawingML with transparency) and header mode (VML behind content).
149
+ */
150
+ addWatermark(options: WatermarkOptions): void;
151
+ /**
152
+ * Get the current watermark configuration.
153
+ */
154
+ getWatermark(): WatermarkOptions | null;
155
+ /**
156
+ * Remove the watermark from the worksheet.
157
+ */
158
+ removeWatermark(): void;
144
159
  /**
145
160
  * Parse the user-supplied range into a normalised internal model
146
161
  * mirroring what the regular Worksheet / Image class does.
@@ -151,6 +151,8 @@ class WorksheetWriter {
151
151
  // auto filter
152
152
  this.autoFilter = options.autoFilter ?? null;
153
153
  this._media = [];
154
+ // watermark
155
+ this._watermark = null;
154
156
  // worksheet protection
155
157
  this.sheetProtection = null;
156
158
  // start writing to stream now
@@ -448,6 +450,57 @@ class WorksheetWriter {
448
450
  getImages() {
449
451
  return this._media;
450
452
  }
453
+ // =========================================================================
454
+ // Watermark
455
+ /**
456
+ * Add a watermark to the worksheet using an image from `WorkbookWriter.addImage()`.
457
+ * Supports overlay mode (DrawingML with transparency) and header mode (VML behind content).
458
+ */
459
+ addWatermark(options) {
460
+ // Remove existing watermark entries (both stored type tags)
461
+ this._media = this._media.filter(m => m._watermarkTag !== true);
462
+ const opacity = options.opacity !== undefined ? Math.max(0, Math.min(1, options.opacity)) : 0.15;
463
+ this._watermark = {
464
+ imageId: String(options.imageId),
465
+ mode: options.mode ?? "overlay",
466
+ opacity,
467
+ headerWidth: options.headerWidth,
468
+ headerHeight: options.headerHeight,
469
+ applyTo: options.applyTo
470
+ };
471
+ if (this._watermark.mode === "overlay") {
472
+ // Coverage range is computed lazily during commit() via _resolveWatermarkRange()
473
+ const entry = {
474
+ type: "image",
475
+ imageId: String(options.imageId),
476
+ range: {
477
+ tl: { nativeCol: 0, nativeColOff: 0, nativeRow: 0, nativeRowOff: 0 },
478
+ br: { nativeCol: 100, nativeColOff: 0, nativeRow: 200, nativeRowOff: 0 },
479
+ editAs: "absolute"
480
+ },
481
+ // Internal tag for dedup — not part of the WriterImageModel type
482
+ _watermarkTag: true,
483
+ opacity
484
+ };
485
+ this._media.push(entry);
486
+ }
487
+ // Note: header mode for streaming writer is limited — the VML file generation
488
+ // happens in WorkbookWriter.addWorksheets(), which handles worksheet.headerImage.
489
+ // We store the config in _watermark and it's picked up by the commit path.
490
+ }
491
+ /**
492
+ * Get the current watermark configuration.
493
+ */
494
+ getWatermark() {
495
+ return this._watermark;
496
+ }
497
+ /**
498
+ * Remove the watermark from the worksheet.
499
+ */
500
+ removeWatermark() {
501
+ this._watermark = null;
502
+ this._media = this._media.filter(m => m._watermarkTag !== true);
503
+ }
451
504
  /**
452
505
  * Parse the user-supplied range into a normalised internal model
453
506
  * mirroring what the regular Worksheet / Image class does.
@@ -626,6 +679,21 @@ class WorksheetWriter {
626
679
  if (this._media.length === 0) {
627
680
  return;
628
681
  }
682
+ // Resolve watermark coverage range from actual worksheet dimensions
683
+ // (at commit time, all rows have been flushed so _dimensions is accurate)
684
+ for (const entry of this._media) {
685
+ if (entry._watermarkTag) {
686
+ const dims = this._dimensions.model;
687
+ const maxCol = dims ? Math.max(dims.right ?? 100, 100) : 100;
688
+ const maxRow = dims ? Math.max(dims.bottom ?? 200, 200) : 200;
689
+ entry.range.br = {
690
+ nativeCol: maxCol,
691
+ nativeColOff: 0,
692
+ nativeRow: maxRow,
693
+ nativeRowOff: 0
694
+ };
695
+ }
696
+ }
629
697
  // Build the drawing model from the stored images.
630
698
  // The drawing XML will be generated later by WorkbookWriterBase.addDrawings().
631
699
  const drawingName = `drawing${this.id}`;
@@ -368,6 +368,78 @@ export interface ImageHyperlinkValue {
368
368
  hyperlink: string;
369
369
  tooltip?: string;
370
370
  }
371
+ /**
372
+ * Watermark placement mode in the Excel worksheet.
373
+ *
374
+ * - `"overlay"` — Places the watermark image as a DrawingML picture on top of cells.
375
+ * Visible on screen AND when printed. Supports transparency via `<a:alphaModFix>`.
376
+ * Users can move/delete the watermark unless the sheet is protected.
377
+ *
378
+ * - `"header"` — Places the watermark image in the page header using VML.
379
+ * Renders behind cell content. Visible in Page Layout view and Print Preview.
380
+ * Cannot be accidentally moved/deleted. The standard "true watermark" approach.
381
+ */
382
+ export type WatermarkMode = "overlay" | "header";
383
+ /**
384
+ * Options for adding a watermark to a worksheet.
385
+ *
386
+ * @example Overlay watermark (visible on screen + prints):
387
+ * ```typescript
388
+ * const imgId = workbook.addImage({ buffer: pngData, extension: "png" });
389
+ * worksheet.addWatermark({
390
+ * imageId: imgId,
391
+ * mode: "overlay",
392
+ * opacity: 0.15
393
+ * });
394
+ * ```
395
+ *
396
+ * @example Header watermark (behind content, prints correctly):
397
+ * ```typescript
398
+ * const imgId = workbook.addImage({ buffer: pngData, extension: "png" });
399
+ * worksheet.addWatermark({
400
+ * imageId: imgId,
401
+ * mode: "header"
402
+ * });
403
+ * ```
404
+ */
405
+ export interface WatermarkOptions {
406
+ /** Image ID obtained from `workbook.addImage()`. */
407
+ imageId: string | number;
408
+ /**
409
+ * Watermark placement mode.
410
+ * @default "overlay"
411
+ */
412
+ mode?: WatermarkMode;
413
+ /**
414
+ * Opacity for overlay mode (0 = fully transparent, 1 = fully opaque).
415
+ * Expressed as a percentage in OOXML (e.g. 0.15 = 15000 out of 100000).
416
+ * Only applies to `"overlay"` mode. In `"header"` mode, transparency
417
+ * must be baked into the image itself (use a PNG with alpha channel).
418
+ * @default 0.15
419
+ */
420
+ opacity?: number;
421
+ /**
422
+ * Image width in points (for "header" mode VML rendering).
423
+ * @default 467.25
424
+ */
425
+ headerWidth?: number;
426
+ /**
427
+ * Image height in points (for "header" mode VML rendering).
428
+ * @default 311.25
429
+ */
430
+ headerHeight?: number;
431
+ /**
432
+ * Which header sections to apply the watermark to (only for "header" mode).
433
+ *
434
+ * - `"all"` — applies to oddHeader, evenHeader, and firstHeader
435
+ * - `"odd"` — applies only to oddHeader (standard pages)
436
+ * - `"even"` — applies only to evenHeader
437
+ * - `"first"` — applies only to firstHeader
438
+ *
439
+ * @default "all"
440
+ */
441
+ applyTo?: "all" | "odd" | "even" | "first";
442
+ }
371
443
  export type Location = {
372
444
  top: number;
373
445
  left: number;
@@ -513,6 +513,35 @@ function formatNumberPattern(val, fmt) {
513
513
  const decimalPlaces = decFmt.replace(/[^0#?]/g, "").length;
514
514
  // Round the value
515
515
  const roundedVal = roundTo(scaledVal, decimalPlaces);
516
+ // When value is zero and the format has no required '0' digit placeholders,
517
+ // '?' placeholders become spaces and '#' placeholders produce nothing.
518
+ // This handles accounting format zero sections like "-"?? → "- " (dash + spaces).
519
+ if (roundedVal === 0 && !intFmt.includes("0") && !decFmt.includes("0")) {
520
+ let result = "";
521
+ for (const ch of intFmt) {
522
+ if (ch === "?") {
523
+ result += " ";
524
+ }
525
+ else if (ch !== "#" && ch !== ",") {
526
+ // Preserve literal characters (already unquoted at this point)
527
+ result += ch;
528
+ }
529
+ }
530
+ if (decimalPlaces > 0) {
531
+ // Only emit the decimal point if the decimal format has '?' or '0' placeholders.
532
+ // Pure '#' decimal digits produce nothing for zero values.
533
+ const hasDecContent = /[0?]/.test(decFmt);
534
+ if (hasDecContent) {
535
+ result += ".";
536
+ for (const ch of decFmt) {
537
+ if (ch === "?") {
538
+ result += " ";
539
+ }
540
+ }
541
+ }
542
+ }
543
+ return sign + result;
544
+ }
516
545
  // Split into integer and decimal parts
517
546
  const [intPart, decPart = ""] = roundedVal.toString().split(".");
518
547
  // Check if format has literal characters mixed with digit placeholders (like "0-0", "000-0000")
@@ -552,16 +581,49 @@ function formatNumberPattern(val, fmt) {
552
581
  if (intFmt.includes(",")) {
553
582
  formattedInt = commaify(intPart);
554
583
  }
555
- // Pad integer with leading zeros if needed
584
+ // Pad integer with leading zeros/spaces if needed
585
+ // '0' placeholder → pad with "0", '?' placeholder → pad with " "
556
586
  const minIntDigits = (intFmt.match(/0/g) ?? []).length;
587
+ const totalIntSlots = (intFmt.match(/[0?]/g) ?? []).length;
557
588
  if (formattedInt.length < minIntDigits) {
558
589
  formattedInt = "0".repeat(minIntDigits - formattedInt.length) + formattedInt;
559
590
  }
591
+ if (formattedInt.length < totalIntSlots) {
592
+ formattedInt = " ".repeat(totalIntSlots - formattedInt.length) + formattedInt;
593
+ }
594
+ // '#' integer placeholder: suppress "0" when there are no required '0' or '?' digits
595
+ // and the integer value is zero (e.g. "#" format with value 0 → empty)
596
+ if (formattedInt === "0" && minIntDigits === 0 && totalIntSlots === 0) {
597
+ formattedInt = "";
598
+ }
560
599
  }
561
600
  // Format decimal part
562
601
  let formattedDec = "";
563
602
  if (decimalPlaces > 0) {
564
- formattedDec = "." + (decPart + "0".repeat(decimalPlaces)).substring(0, decimalPlaces);
603
+ const rawDec = (decPart + "0".repeat(decimalPlaces)).substring(0, decimalPlaces);
604
+ // Process each decimal digit position according to its placeholder:
605
+ // '0' → always show digit, '?' → show digit or space, '#' → show digit or nothing (trim trailing)
606
+ const decChars = rawDec.split("");
607
+ // Walk from the end: '#' trailing zeros are removed, '?' trailing zeros become spaces
608
+ for (let i = decFmt.length - 1; i >= 0; i--) {
609
+ if (i >= decChars.length) {
610
+ continue;
611
+ }
612
+ if (decFmt[i] === "#" && decChars[i] === "0") {
613
+ decChars[i] = "";
614
+ }
615
+ else if (decFmt[i] === "?" && decChars[i] === "0") {
616
+ decChars[i] = " ";
617
+ }
618
+ else {
619
+ break; // stop at first non-zero or '0' placeholder
620
+ }
621
+ }
622
+ const decStr = decChars.join("");
623
+ // Only emit decimal point if there is content after it
624
+ if (decStr.length > 0) {
625
+ formattedDec = "." + decStr;
626
+ }
565
627
  }
566
628
  return sign + formattedInt + formattedDec;
567
629
  }
@@ -12,6 +12,8 @@ interface DrawingAnchor {
12
12
  tooltip?: string;
13
13
  rId: string;
14
14
  };
15
+ /** Alpha modulation for transparency (OOXML percentage, e.g. 15000 = 15%). */
16
+ alphaModFix?: number;
15
17
  };
16
18
  range: any;
17
19
  }
@@ -32,6 +34,8 @@ interface ImageMedium {
32
34
  hyperlink?: string;
33
35
  tooltip?: string;
34
36
  };
37
+ /** Opacity 0-1 for watermark overlay mode. */
38
+ opacity?: number;
35
39
  }
36
40
  /**
37
41
  * Resolves a media filename into the drawing-level relative target path.