@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
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Zero-dependency text-to-PNG watermark image generator.
3
+ *
4
+ * Renders text into a semi-transparent PNG suitable for use as an Excel watermark.
5
+ * Uses a built-in bitmap font for ASCII characters — no Canvas or external fonts required.
6
+ * PNG data is deflate-compressed using the archive module's built-in compressor.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const png = createTextWatermarkImage("CONFIDENTIAL", {
11
+ * fontSize: 48,
12
+ * color: { r: 128, g: 128, b: 128 },
13
+ * opacity: 40,
14
+ * rotation: -45
15
+ * });
16
+ * const imgId = workbook.addImage({ buffer: png, extension: "png" });
17
+ * worksheet.addWatermark({ imageId: imgId });
18
+ * ```
19
+ */
20
+ // =============================================================================
21
+ // Public API
22
+ // =============================================================================
23
+ import { deflateRawCompressed } from "../../archive/compression/deflate-fallback.js";
24
+ /**
25
+ * Generate a PNG image containing watermark text.
26
+ *
27
+ * The image has an alpha channel so the watermark is semi-transparent.
28
+ * Works in both Node.js and browsers with zero dependencies.
29
+ */
30
+ export function createTextWatermarkImage(text, options) {
31
+ const fontSize = options?.fontSize ?? 48;
32
+ const color = options?.color ?? { r: 128, g: 128, b: 128 };
33
+ const opacity = Math.max(0, Math.min(100, options?.opacity ?? 40));
34
+ const rotation = options?.rotation ?? -45;
35
+ const padding = options?.padding ?? 20;
36
+ // Scale factor: built-in font is 8px tall
37
+ const scale = Math.max(1, Math.round(fontSize / GLYPH_HEIGHT));
38
+ // Render text to unrotated bitmap
39
+ const { width: textW, height: textH, pixels: textPixels } = renderTextBitmap(text, scale);
40
+ // Add padding
41
+ const paddedW = textW + padding * 2;
42
+ const paddedH = textH + padding * 2;
43
+ const paddedPixels = new Uint8Array(paddedW * paddedH);
44
+ for (let y = 0; y < textH; y++) {
45
+ for (let x = 0; x < textW; x++) {
46
+ paddedPixels[(y + padding) * paddedW + (x + padding)] = textPixels[y * textW + x];
47
+ }
48
+ }
49
+ // Rotate
50
+ const { width: rotW, height: rotH, pixels: rotPixels } = rotateBitmap(paddedPixels, paddedW, paddedH, rotation);
51
+ // Convert to RGBA PNG
52
+ const alpha = Math.round((opacity / 100) * 255);
53
+ const rgba = new Uint8Array(rotW * rotH * 4);
54
+ for (let i = 0; i < rotW * rotH; i++) {
55
+ const a = rotPixels[i];
56
+ if (a > 0) {
57
+ rgba[i * 4] = color.r;
58
+ rgba[i * 4 + 1] = color.g;
59
+ rgba[i * 4 + 2] = color.b;
60
+ rgba[i * 4 + 3] = Math.round((a / 255) * alpha);
61
+ }
62
+ // else fully transparent (already 0)
63
+ }
64
+ return encodePng(rgba, rotW, rotH);
65
+ }
66
+ // =============================================================================
67
+ // Bitmap Font — 8px tall monospace ASCII (CP437-style, printable range 32-126)
68
+ // =============================================================================
69
+ const GLYPH_WIDTH = 6;
70
+ const GLYPH_HEIGHT = 8;
71
+ /**
72
+ * Compact glyph data: each character is 8 bytes (one byte per row, 6 bits used).
73
+ * Bit 5 = leftmost pixel, bit 0 = rightmost pixel.
74
+ */
75
+ const FONT_DATA = {
76
+ // space
77
+ 32: [0, 0, 0, 0, 0, 0, 0, 0],
78
+ // !
79
+ 33: [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00],
80
+ // "
81
+ 34: [0x0a, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00],
82
+ // #
83
+ 35: [0x0a, 0x0a, 0x1f, 0x0a, 0x1f, 0x0a, 0x0a, 0x00],
84
+ // $
85
+ 36: [0x04, 0x0f, 0x14, 0x0e, 0x05, 0x1e, 0x04, 0x00],
86
+ // %
87
+ 37: [0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03, 0x00],
88
+ // &
89
+ 38: [0x0c, 0x12, 0x14, 0x08, 0x15, 0x12, 0x0d, 0x00],
90
+ // '
91
+ 39: [0x04, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00],
92
+ // (
93
+ 40: [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02, 0x00],
94
+ // )
95
+ 41: [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08, 0x00],
96
+ // *
97
+ 42: [0x00, 0x04, 0x15, 0x0e, 0x15, 0x04, 0x00, 0x00],
98
+ // +
99
+ 43: [0x00, 0x04, 0x04, 0x1f, 0x04, 0x04, 0x00, 0x00],
100
+ // ,
101
+ 44: [0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x08],
102
+ // -
103
+ 45: [0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00],
104
+ // .
105
+ 46: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00],
106
+ // /
107
+ 47: [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00],
108
+ // 0-9
109
+ 48: [0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e, 0x00],
110
+ 49: [0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00],
111
+ 50: [0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f, 0x00],
112
+ 51: [0x1f, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0e, 0x00],
113
+ 52: [0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02, 0x00],
114
+ 53: [0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e, 0x00],
115
+ 54: [0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e, 0x00],
116
+ 55: [0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00],
117
+ 56: [0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e, 0x00],
118
+ 57: [0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c, 0x00],
119
+ // :
120
+ 58: [0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00],
121
+ // ;
122
+ 59: [0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x04, 0x08],
123
+ // <
124
+ 60: [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02, 0x00],
125
+ // =
126
+ 61: [0x00, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x00, 0x00],
127
+ // >
128
+ 62: [0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08, 0x00],
129
+ // ?
130
+ 63: [0x0e, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04, 0x00],
131
+ // @
132
+ 64: [0x0e, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0e, 0x00],
133
+ // A-Z
134
+ 65: [0x0e, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11, 0x00],
135
+ 66: [0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e, 0x00],
136
+ 67: [0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e, 0x00],
137
+ 68: [0x1c, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1c, 0x00],
138
+ 69: [0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f, 0x00],
139
+ 70: [0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10, 0x00],
140
+ 71: [0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f, 0x00],
141
+ 72: [0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11, 0x00],
142
+ 73: [0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00],
143
+ 74: [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0c, 0x00],
144
+ 75: [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00],
145
+ 76: [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f, 0x00],
146
+ 77: [0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11, 0x00],
147
+ 78: [0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11, 0x00],
148
+ 79: [0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00],
149
+ 80: [0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10, 0x00],
150
+ 81: [0x0e, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0d, 0x00],
151
+ 82: [0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11, 0x00],
152
+ 83: [0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e, 0x00],
153
+ 84: [0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00],
154
+ 85: [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00],
155
+ 86: [0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04, 0x00],
156
+ 87: [0x11, 0x11, 0x11, 0x15, 0x15, 0x1b, 0x11, 0x00],
157
+ 88: [0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11, 0x00],
158
+ 89: [0x11, 0x11, 0x0a, 0x04, 0x04, 0x04, 0x04, 0x00],
159
+ 90: [0x1f, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1f, 0x00],
160
+ // [ \ ]
161
+ 91: [0x0e, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0e, 0x00],
162
+ 92: [0x00, 0x10, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00],
163
+ 93: [0x0e, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0e, 0x00],
164
+ // ^ _ `
165
+ 94: [0x04, 0x0a, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00],
166
+ 95: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00],
167
+ 96: [0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00],
168
+ // a-z
169
+ 97: [0x00, 0x00, 0x0e, 0x01, 0x0f, 0x11, 0x0f, 0x00],
170
+ 98: [0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x1e, 0x00],
171
+ 99: [0x00, 0x00, 0x0e, 0x10, 0x10, 0x11, 0x0e, 0x00],
172
+ 100: [0x01, 0x01, 0x0d, 0x13, 0x11, 0x11, 0x0f, 0x00],
173
+ 101: [0x00, 0x00, 0x0e, 0x11, 0x1f, 0x10, 0x0e, 0x00],
174
+ 102: [0x06, 0x09, 0x08, 0x1c, 0x08, 0x08, 0x08, 0x00],
175
+ 103: [0x00, 0x00, 0x0f, 0x11, 0x0f, 0x01, 0x0e, 0x00],
176
+ 104: [0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00],
177
+ 105: [0x04, 0x00, 0x0c, 0x04, 0x04, 0x04, 0x0e, 0x00],
178
+ 106: [0x02, 0x00, 0x06, 0x02, 0x02, 0x12, 0x0c, 0x00],
179
+ 107: [0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12, 0x00],
180
+ 108: [0x0c, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00],
181
+ 109: [0x00, 0x00, 0x1a, 0x15, 0x15, 0x11, 0x11, 0x00],
182
+ 110: [0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11, 0x00],
183
+ 111: [0x00, 0x00, 0x0e, 0x11, 0x11, 0x11, 0x0e, 0x00],
184
+ 112: [0x00, 0x00, 0x1e, 0x11, 0x1e, 0x10, 0x10, 0x00],
185
+ 113: [0x00, 0x00, 0x0d, 0x13, 0x0f, 0x01, 0x01, 0x00],
186
+ 114: [0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10, 0x00],
187
+ 115: [0x00, 0x00, 0x0e, 0x10, 0x0e, 0x01, 0x1e, 0x00],
188
+ 116: [0x08, 0x08, 0x1c, 0x08, 0x08, 0x09, 0x06, 0x00],
189
+ 117: [0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0d, 0x00],
190
+ 118: [0x00, 0x00, 0x11, 0x11, 0x11, 0x0a, 0x04, 0x00],
191
+ 119: [0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0a, 0x00],
192
+ 120: [0x00, 0x00, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x00],
193
+ 121: [0x00, 0x00, 0x11, 0x11, 0x0f, 0x01, 0x0e, 0x00],
194
+ 122: [0x00, 0x00, 0x1f, 0x02, 0x04, 0x08, 0x1f, 0x00],
195
+ // { | } ~
196
+ 123: [0x02, 0x04, 0x04, 0x08, 0x04, 0x04, 0x02, 0x00],
197
+ 124: [0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00],
198
+ 125: [0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08, 0x00],
199
+ 126: [0x00, 0x00, 0x08, 0x15, 0x02, 0x00, 0x00, 0x00]
200
+ };
201
+ // =============================================================================
202
+ // Bitmap Rendering
203
+ // =============================================================================
204
+ /** Render text string to a grayscale bitmap (0 = transparent, 255 = opaque). */
205
+ function renderTextBitmap(text, scale) {
206
+ const charW = GLYPH_WIDTH * scale;
207
+ const charH = GLYPH_HEIGHT * scale;
208
+ const width = text.length * charW;
209
+ const height = charH;
210
+ const pixels = new Uint8Array(width * height);
211
+ for (let ci = 0; ci < text.length; ci++) {
212
+ const code = text.charCodeAt(ci);
213
+ const glyph = FONT_DATA[code] ?? FONT_DATA[63]; // fallback to '?'
214
+ const xOff = ci * charW;
215
+ for (let row = 0; row < GLYPH_HEIGHT; row++) {
216
+ const bits = glyph[row];
217
+ for (let col = 0; col < GLYPH_WIDTH; col++) {
218
+ if (bits & (1 << (GLYPH_WIDTH - 1 - col))) {
219
+ // Fill scaled pixel block
220
+ for (let sy = 0; sy < scale; sy++) {
221
+ for (let sx = 0; sx < scale; sx++) {
222
+ const px = xOff + col * scale + sx;
223
+ const py = row * scale + sy;
224
+ if (px < width && py < height) {
225
+ pixels[py * width + px] = 255;
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ return { width, height, pixels };
234
+ }
235
+ /** Rotate a grayscale bitmap by the given angle in degrees. */
236
+ function rotateBitmap(pixels, srcW, srcH, angleDeg) {
237
+ if (angleDeg === 0) {
238
+ return { width: srcW, height: srcH, pixels };
239
+ }
240
+ const rad = (angleDeg * Math.PI) / 180;
241
+ const cos = Math.cos(rad);
242
+ const sin = Math.sin(rad);
243
+ // Compute bounding box of rotated rectangle
244
+ const corners = [
245
+ { x: 0, y: 0 },
246
+ { x: srcW, y: 0 },
247
+ { x: srcW, y: srcH },
248
+ { x: 0, y: srcH }
249
+ ];
250
+ let minX = Infinity;
251
+ let minY = Infinity;
252
+ let maxX = -Infinity;
253
+ let maxY = -Infinity;
254
+ for (const c of corners) {
255
+ const rx = c.x * cos - c.y * sin;
256
+ const ry = c.x * sin + c.y * cos;
257
+ minX = Math.min(minX, rx);
258
+ minY = Math.min(minY, ry);
259
+ maxX = Math.max(maxX, rx);
260
+ maxY = Math.max(maxY, ry);
261
+ }
262
+ const dstW = Math.ceil(maxX - minX);
263
+ const dstH = Math.ceil(maxY - minY);
264
+ const dst = new Uint8Array(dstW * dstH);
265
+ // Inverse rotation: for each dst pixel, find the source pixel
266
+ const invCos = cos; // cos(-θ) = cos(θ)
267
+ const invSin = -sin; // sin(-θ) = -sin(θ)
268
+ for (let dy = 0; dy < dstH; dy++) {
269
+ for (let dx = 0; dx < dstW; dx++) {
270
+ // Map dst to world, then inverse-rotate to source
271
+ const wx = dx + minX;
272
+ const wy = dy + minY;
273
+ const sx = Math.round(wx * invCos - wy * invSin);
274
+ const sy = Math.round(wx * invSin + wy * invCos);
275
+ if (sx >= 0 && sx < srcW && sy >= 0 && sy < srcH) {
276
+ dst[dy * dstW + dx] = pixels[sy * srcW + sx];
277
+ }
278
+ }
279
+ }
280
+ return { width: dstW, height: dstH, pixels: dst };
281
+ }
282
+ // =============================================================================
283
+ // PNG Encoder (RGBA, deflate-compressed, with alpha)
284
+ // =============================================================================
285
+ /** Encode RGBA pixel data to a PNG file. */
286
+ function encodePng(rgba, width, height) {
287
+ // Build IDAT data: filter byte (0 = None) + raw RGBA for each row
288
+ const rawRowSize = 1 + width * 4; // filter byte + pixels
289
+ const rawData = new Uint8Array(rawRowSize * height);
290
+ for (let y = 0; y < height; y++) {
291
+ rawData[y * rawRowSize] = 0; // filter: None
292
+ rawData.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * rawRowSize + 1);
293
+ }
294
+ // Wrap in zlib stream with deflate compression
295
+ const deflated = zlibCompress(rawData);
296
+ // PNG signature
297
+ const sig = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
298
+ // IHDR chunk
299
+ const ihdr = new Uint8Array(13);
300
+ writeU32BE(ihdr, 0, width);
301
+ writeU32BE(ihdr, 4, height);
302
+ ihdr[8] = 8; // bit depth
303
+ ihdr[9] = 6; // color type: RGBA
304
+ ihdr[10] = 0; // compression
305
+ ihdr[11] = 0; // filter
306
+ ihdr[12] = 0; // interlace
307
+ const ihdrChunk = pngChunk(0x49484452, ihdr);
308
+ // IDAT chunk
309
+ const idatChunk = pngChunk(0x49444154, deflated);
310
+ // IEND chunk
311
+ const iendChunk = pngChunk(0x49454e44, new Uint8Array(0));
312
+ // Concatenate
313
+ const result = new Uint8Array(sig.length + ihdrChunk.length + idatChunk.length + iendChunk.length);
314
+ let offset = 0;
315
+ result.set(sig, offset);
316
+ offset += sig.length;
317
+ result.set(ihdrChunk, offset);
318
+ offset += ihdrChunk.length;
319
+ result.set(idatChunk, offset);
320
+ offset += idatChunk.length;
321
+ result.set(iendChunk, offset);
322
+ return result;
323
+ }
324
+ /** Build a PNG chunk: length(4) + type(4) + data + crc32(4). */
325
+ function pngChunk(type, data) {
326
+ const chunk = new Uint8Array(12 + data.length);
327
+ writeU32BE(chunk, 0, data.length);
328
+ writeU32BE(chunk, 4, type);
329
+ chunk.set(data, 8);
330
+ // CRC32 over type + data
331
+ const crc = crc32(chunk.subarray(4, 8 + data.length));
332
+ writeU32BE(chunk, 8 + data.length, crc);
333
+ return chunk;
334
+ }
335
+ /** Write a 32-bit big-endian unsigned int. */
336
+ function writeU32BE(buf, offset, value) {
337
+ buf[offset] = (value >>> 24) & 0xff;
338
+ buf[offset + 1] = (value >>> 16) & 0xff;
339
+ buf[offset + 2] = (value >>> 8) & 0xff;
340
+ buf[offset + 3] = value & 0xff;
341
+ }
342
+ /** Wrap raw data in a zlib stream with deflate compression. */
343
+ function zlibCompress(data) {
344
+ // Zlib header: CMF=0x78, FLG=0x01 (deflate, no dict, check bits)
345
+ const deflated = deflateRawCompressed(data, 6);
346
+ const adler = adler32(data);
347
+ const result = new Uint8Array(2 + deflated.length + 4);
348
+ result[0] = 0x78;
349
+ result[1] = 0x01;
350
+ result.set(deflated, 2);
351
+ writeU32BE(result, 2 + deflated.length, adler);
352
+ return result;
353
+ }
354
+ /** Compute Adler-32 checksum. */
355
+ function adler32(data) {
356
+ let a = 1;
357
+ let b = 0;
358
+ for (let i = 0; i < data.length; i++) {
359
+ a = (a + data[i]) % 65521;
360
+ b = (b + a) % 65521;
361
+ }
362
+ return (b << 16) | a;
363
+ }
364
+ /** CRC32 lookup table. */
365
+ const CRC_TABLE = /* @__PURE__ */ (() => {
366
+ const table = new Uint32Array(256);
367
+ for (let n = 0; n < 256; n++) {
368
+ let c = n;
369
+ for (let k = 0; k < 8; k++) {
370
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
371
+ }
372
+ table[n] = c;
373
+ }
374
+ return table;
375
+ })();
376
+ /** Compute CRC32 checksum. */
377
+ function crc32(data) {
378
+ let crc = 0xffffffff;
379
+ for (let i = 0; i < data.length; i++) {
380
+ crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
381
+ }
382
+ return (crc ^ 0xffffffff) >>> 0;
383
+ }
@@ -104,6 +104,8 @@ class Worksheet {
104
104
  this.conditionalFormattings = [];
105
105
  // for form controls (legacy checkboxes, etc.)
106
106
  this.formControls = [];
107
+ // watermark configuration
108
+ this._watermark = null;
107
109
  }
108
110
  get name() {
109
111
  return this._name;
@@ -978,6 +980,79 @@ class Worksheet {
978
980
  return image && image.imageId;
979
981
  }
980
982
  // =========================================================================
983
+ // Watermark
984
+ /**
985
+ * Add a watermark to the worksheet using an image from `workbook.addImage()`.
986
+ *
987
+ * The watermark can be placed in one of two modes:
988
+ *
989
+ * - **overlay** (default): Places the watermark image as a drawing on top of cells.
990
+ * Visible on screen AND when printed. Supports transparency via DrawingML `alphaModFix`.
991
+ *
992
+ * - **header**: Places the watermark image in the page header using VML.
993
+ * Visible in Page Layout view and when printed. Renders behind cell content.
994
+ * Transparency must be baked into the image (PNG with alpha channel).
995
+ *
996
+ * @param options - Watermark configuration
997
+ *
998
+ * @example Overlay watermark with transparency:
999
+ * ```typescript
1000
+ * const imgId = workbook.addImage({ buffer: pngData, extension: "png" });
1001
+ * worksheet.addWatermark({ imageId: imgId, opacity: 0.15 });
1002
+ * ```
1003
+ *
1004
+ * @example Header watermark (behind content):
1005
+ * ```typescript
1006
+ * const imgId = workbook.addImage({ buffer: pngData, extension: "png" });
1007
+ * worksheet.addWatermark({ imageId: imgId, mode: "header" });
1008
+ * ```
1009
+ */
1010
+ addWatermark(options) {
1011
+ // Remove any existing watermark media entries first
1012
+ this._media = this._media.filter(m => m.type !== "watermark" && m.type !== "headerImage");
1013
+ this._watermark = {
1014
+ imageId: String(options.imageId),
1015
+ mode: options.mode ?? "overlay",
1016
+ opacity: options.opacity,
1017
+ headerWidth: options.headerWidth,
1018
+ headerHeight: options.headerHeight,
1019
+ applyTo: options.applyTo
1020
+ };
1021
+ if (this._watermark.mode === "overlay") {
1022
+ // Add as a special "watermark" media entry for the drawing pipeline
1023
+ const model = {
1024
+ type: "watermark",
1025
+ imageId: String(options.imageId),
1026
+ opacity: options.opacity
1027
+ };
1028
+ this._media.push(new Image(this, model));
1029
+ }
1030
+ else {
1031
+ // Header mode: add as a "headerImage" media entry for the VML pipeline
1032
+ const model = {
1033
+ type: "headerImage",
1034
+ imageId: String(options.imageId),
1035
+ headerWidth: options.headerWidth,
1036
+ headerHeight: options.headerHeight,
1037
+ applyTo: options.applyTo
1038
+ };
1039
+ this._media.push(new Image(this, model));
1040
+ }
1041
+ }
1042
+ /**
1043
+ * Get the current watermark configuration, or null if none is set.
1044
+ */
1045
+ getWatermark() {
1046
+ return this._watermark;
1047
+ }
1048
+ /**
1049
+ * Remove the watermark from the worksheet.
1050
+ */
1051
+ removeWatermark() {
1052
+ this._watermark = null;
1053
+ this._media = this._media.filter(m => m.type !== "watermark" && m.type !== "headerImage");
1054
+ }
1055
+ // =========================================================================
981
1056
  // Form Controls (Legacy Checkboxes)
982
1057
  /**
983
1058
  * Add a form control checkbox to the worksheet.
@@ -1293,6 +1368,7 @@ class Worksheet {
1293
1368
  pivotTables: this.pivotTables,
1294
1369
  conditionalFormattings: this.conditionalFormattings,
1295
1370
  formControls: this.formControls.map(fc => fc.model),
1371
+ watermark: this._watermark,
1296
1372
  drawing: this._drawing
1297
1373
  };
1298
1374
  // =================================================
@@ -1348,6 +1424,29 @@ class Worksheet {
1348
1424
  this.views = value.views;
1349
1425
  this.autoFilter = value.autoFilter;
1350
1426
  this._media = value.media.map(medium => new Image(this, medium));
1427
+ // Restore watermark state from media entries
1428
+ this._watermark = value.watermark ?? null;
1429
+ if (!this._watermark) {
1430
+ for (const medium of this._media) {
1431
+ if (medium.type === "watermark") {
1432
+ this._watermark = {
1433
+ imageId: medium.imageId ?? "",
1434
+ mode: "overlay",
1435
+ opacity: medium.opacity
1436
+ };
1437
+ break;
1438
+ }
1439
+ else if (medium.type === "headerImage") {
1440
+ this._watermark = {
1441
+ imageId: medium.imageId ?? "",
1442
+ mode: "header",
1443
+ headerWidth: medium.headerWidth,
1444
+ headerHeight: medium.headerHeight
1445
+ };
1446
+ break;
1447
+ }
1448
+ }
1449
+ }
1351
1450
  this.sheetProtection = value.sheetProtection;
1352
1451
  this.tables = value.tables.reduce((tables, table) => {
1353
1452
  const t = new Table(this, table);
@@ -104,10 +104,11 @@ class ContentTypesXform extends BaseXform {
104
104
  });
105
105
  });
106
106
  }
107
- // VML extension is needed for comments or form controls
107
+ // VML extension is needed for comments, form controls, or header watermarks
108
108
  const hasComments = model.commentRefs && model.commentRefs.length > 0;
109
109
  const hasFormControls = model.formControlRefs && model.formControlRefs.length > 0;
110
- if (hasComments || hasFormControls) {
110
+ const hasHeaderWatermark = model.hasHeaderWatermark === true;
111
+ if (hasComments || hasFormControls || hasHeaderWatermark) {
111
112
  xmlStream.leafNode("Default", {
112
113
  Extension: "vml",
113
114
  ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing"
@@ -35,7 +35,12 @@ class BaseCellAnchorXform extends BaseXform {
35
35
  if (match) {
36
36
  const name = match[1];
37
37
  const mediaId = options.mediaIndex[name];
38
- return options.media[mediaId];
38
+ const medium = options.media[mediaId];
39
+ // Preserve alphaModFix (transparency) from the picture model if present
40
+ if (medium && model.alphaModFix !== undefined) {
41
+ return { ...medium, alphaModFix: model.alphaModFix };
42
+ }
43
+ return medium;
39
44
  }
40
45
  }
41
46
  return undefined;
@@ -13,7 +13,6 @@ class BlipFillXform extends BaseXform {
13
13
  render(xmlStream, model) {
14
14
  xmlStream.openNode(this.tag);
15
15
  this.map["a:blip"].render(xmlStream, model);
16
- // TODO: options for this + parsing
17
16
  xmlStream.openNode("a:stretch");
18
17
  xmlStream.leafNode("a:fillRect");
19
18
  xmlStream.closeNode();
@@ -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
  }
@@ -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
  }