@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.
- package/dist/browser/index.browser.d.ts +2 -0
- package/dist/browser/index.browser.js +2 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/modules/excel/image.d.ts +27 -2
- package/dist/browser/modules/excel/image.js +23 -1
- package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +16 -1
- package/dist/browser/modules/excel/stream/worksheet-writer.js +68 -0
- package/dist/browser/modules/excel/types.d.ts +72 -0
- package/dist/browser/modules/excel/utils/drawing-utils.d.ts +4 -0
- package/dist/browser/modules/excel/utils/drawing-utils.js +5 -0
- package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +4 -0
- package/dist/browser/modules/excel/utils/ooxml-paths.js +15 -0
- package/dist/browser/modules/excel/utils/watermark-image.d.ts +67 -0
- package/dist/browser/modules/excel/utils/watermark-image.js +383 -0
- package/dist/browser/modules/excel/worksheet.d.ts +39 -1
- package/dist/browser/modules/excel/worksheet.js +99 -0
- package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
- package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
- package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
- package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
- package/dist/browser/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
- package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
- package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
- package/dist/browser/modules/excel/xlsx/xlsx.browser.js +53 -1
- package/dist/browser/modules/pdf/core/pdf-writer.d.ts +1 -1
- package/dist/browser/modules/pdf/core/pdf-writer.js +2 -1
- package/dist/browser/modules/pdf/index.d.ts +1 -1
- package/dist/browser/modules/pdf/render/page-renderer.d.ts +29 -1
- package/dist/browser/modules/pdf/render/page-renderer.js +394 -25
- package/dist/browser/modules/pdf/render/pdf-exporter.js +84 -47
- package/dist/browser/modules/pdf/types.d.ts +235 -0
- package/dist/cjs/index.js +5 -2
- package/dist/cjs/modules/excel/image.js +23 -1
- package/dist/cjs/modules/excel/stream/worksheet-writer.js +68 -0
- package/dist/cjs/modules/excel/utils/drawing-utils.js +5 -0
- package/dist/cjs/modules/excel/utils/ooxml-paths.js +19 -0
- package/dist/cjs/modules/excel/utils/watermark-image.js +386 -0
- package/dist/cjs/modules/excel/worksheet.js +99 -0
- package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
- package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
- package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
- package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
- package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
- package/dist/cjs/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
- package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +134 -7
- package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +52 -0
- package/dist/cjs/modules/pdf/core/pdf-writer.js +2 -1
- package/dist/cjs/modules/pdf/render/page-renderer.js +396 -25
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +83 -46
- package/dist/esm/index.browser.js +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/modules/excel/image.js +23 -1
- package/dist/esm/modules/excel/stream/worksheet-writer.js +68 -0
- package/dist/esm/modules/excel/utils/drawing-utils.js +5 -0
- package/dist/esm/modules/excel/utils/ooxml-paths.js +15 -0
- package/dist/esm/modules/excel/utils/watermark-image.js +383 -0
- package/dist/esm/modules/excel/worksheet.js +99 -0
- package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +3 -2
- package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +6 -1
- package/dist/esm/modules/excel/xlsx/xform/drawing/blip-fill-xform.js +0 -1
- package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +22 -6
- package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +5 -1
- package/dist/esm/modules/excel/xlsx/xform/drawing/vml-drawing-xform.js +103 -4
- package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +135 -8
- package/dist/esm/modules/excel/xlsx/xlsx.browser.js +53 -1
- package/dist/esm/modules/pdf/core/pdf-writer.js +2 -1
- package/dist/esm/modules/pdf/render/page-renderer.js +394 -25
- package/dist/esm/modules/pdf/render/pdf-exporter.js +84 -47
- package/dist/iife/excelts.iife.js +2390 -469
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +47 -47
- package/dist/types/index.browser.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/modules/excel/image.d.ts +27 -2
- package/dist/types/modules/excel/stream/worksheet-writer.d.ts +16 -1
- package/dist/types/modules/excel/types.d.ts +72 -0
- package/dist/types/modules/excel/utils/drawing-utils.d.ts +4 -0
- package/dist/types/modules/excel/utils/ooxml-paths.d.ts +4 -0
- package/dist/types/modules/excel/utils/watermark-image.d.ts +67 -0
- package/dist/types/modules/excel/worksheet.d.ts +39 -1
- package/dist/types/modules/excel/xlsx/xform/drawing/blip-fill-xform.d.ts +2 -1
- package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +3 -1
- package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +3 -0
- package/dist/types/modules/excel/xlsx/xform/drawing/vml-drawing-xform.d.ts +19 -0
- package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +1 -0
- package/dist/types/modules/pdf/core/pdf-writer.d.ts +1 -1
- package/dist/types/modules/pdf/index.d.ts +1 -1
- package/dist/types/modules/pdf/render/page-renderer.d.ts +29 -1
- package/dist/types/modules/pdf/types.d.ts +235 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
}
|