watermark-js-nine 1.0.0 → 1.0.2

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 (2) hide show
  1. package/package.json +2 -3
  2. package/src/waterMark.ts +0 -1134
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "watermark-js-nine",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A powerful web watermark component supporting text and image watermarks with tiling",
5
5
  "main": "dist/waterMark.js",
6
- "module": "src/waterMark.ts",
6
+ "module": "dist/waterMark.js",
7
7
  "types": "dist/waterMark.d.ts",
8
8
  "scripts": {
9
9
  "build": "tsc",
@@ -25,7 +25,6 @@
25
25
  "license": "MIT",
26
26
  "files": [
27
27
  "dist",
28
- "src",
29
28
  "README.md"
30
29
  ],
31
30
  "repository": {
package/src/waterMark.ts DELETED
@@ -1,1134 +0,0 @@
1
- // ===================== 类型定义 =====================
2
- export interface WatermarkConfig {
3
- type: "text" | "image"; // 水印类型
4
- tilling: boolean; // 是否平铺,true为全图水印,false为单个水印
5
- // 文字水印配置
6
- value?: string; // 水印文本(支持\n换行)
7
- font?: string; // 字体,如 "20pt SimSun"
8
- fillStyle?: string; // 文本颜色,如 "rgba(192,192,192,0.6)"
9
- // 图片水印配置
10
- image?: {
11
- type?: string; // 图片类型 png/jpg
12
- data?: string; // 图片base64数据
13
- size?: [number, number]; // 图片大小 [width, height] 单位mm
14
- opacity?: number; // 不透明度 0-100
15
- };
16
- // 通用配置
17
- rotate?: number; // 旋转角度
18
- offset?: [number, number]; // 偏移量 [x, y] 单位mm
19
- horizontal?: number; // 横向间距mm(平铺时生效)
20
- vertical?: number; // 纵向间距mm(平铺时生效)
21
- place?: PlaceType; // 单个水印位置(单个水印时生效)
22
- svgContainer?: SVGSVGElement; // SVG容器(内部使用)
23
- }
24
-
25
- type PlaceType =
26
- | "topLeft"
27
- | "topCenter"
28
- | "topRight"
29
- | "middleLeft"
30
- | "middleCenter"
31
- | "middleRight"
32
- | "bottomLeft"
33
- | "bottomCenter"
34
- | "bottomRight";
35
-
36
- // ===================== 常量定义 =====================
37
- const NEW_PT_PER_INCH = 72;
38
- const NEW_MM_PER_INCH = 25.4;
39
-
40
- const PlaceMap: Record<string, PlaceType> = {
41
- TL: "topLeft",
42
- TC: "topCenter",
43
- TR: "topRight",
44
- ML: "middleLeft",
45
- MC: "middleCenter",
46
- MR: "middleRight",
47
- BL: "bottomLeft",
48
- BC: "bottomCenter",
49
- BR: "bottomRight",
50
- };
51
-
52
- // ===================== 工具类 =====================
53
- class WatermarkUtils {
54
- private static zoomValue = 1;
55
-
56
- static setZoom(zoom: number) {
57
- this.zoomValue = zoom || 1;
58
- }
59
-
60
- static getDpi(isPrint = false): number {
61
- const zoom = isPrint ? 1 : this.zoomValue;
62
- return zoom * 100 * 0.96;
63
- }
64
-
65
- static mm2px(mm: number, isPrint = false): number {
66
- return (mm * this.getDpi(isPrint)) / NEW_MM_PER_INCH;
67
- }
68
-
69
- static mm2pxDpi(mm: number, dpi: number): number {
70
- return Math.round((mm * dpi) / 25.4);
71
- }
72
-
73
- static pt2px(pt: number, isPrint = false): number {
74
- return (pt * this.getDpi(isPrint)) / NEW_PT_PER_INCH;
75
- }
76
-
77
- static createSvgElement<T extends SVGElement>(
78
- tagName: string,
79
- id?: string
80
- ): T {
81
- const el = document.createElementNS(
82
- "http://www.w3.org/2000/svg",
83
- tagName
84
- ) as T;
85
- if (id) el.id = id;
86
- return el;
87
- }
88
-
89
- static setAttr(el: Element, name: string, value: string | number): void {
90
- el.setAttribute(name, String(value));
91
- }
92
-
93
- static getFontSize(font: string): number {
94
- const fonts = font.split(" ");
95
- for (const f of fonts) {
96
- if (f.includes("pt")) {
97
- return this.pt2px(parseFloat(f));
98
- }
99
- if (f.includes("px")) {
100
- return parseFloat(f);
101
- }
102
- }
103
- return 20;
104
- }
105
-
106
- static getTextMetrics(
107
- text: string,
108
- font: string
109
- ): { width: number; height: number; lines: string[] } {
110
- const fontSize = this.getFontSize(font);
111
- const lines = text.split("\n");
112
- let maxLen = 0;
113
- for (const line of lines) {
114
- const len = this.analyzeStringLength(line);
115
- if (len > maxLen) maxLen = len;
116
- }
117
- return {
118
- width: fontSize * maxLen,
119
- height: fontSize * lines.length,
120
- lines,
121
- };
122
- }
123
-
124
- static analyzeStringLength(text: string): number {
125
- let length = 0;
126
- const regCn =
127
- /[\u4e00-\u9fa5]|[\u3002|\uff1f|\uff01|\uff0c|\u3001|\uff1b|\uff1a|\u201c|\u201d|\u2018|\u2019|\uff08|\uff09|\u300a|\u300b|\u3008|\u3009|\u3010|\u3011|\u300e|\u300f|\u300c|\u300d|\ufe43|\ufe44|\u3014|\u3015|\u2026|\u2014|\uff5e|\ufe4f|\uffe5]/;
128
- for (const char of text) {
129
- length += regCn.test(char) ? 1 : 0.5;
130
- }
131
- return length;
132
- }
133
-
134
- static getRotatedBounds(
135
- width: number,
136
- height: number,
137
- rotateDeg: number
138
- ): { offsetX: number; offsetY: number; newWidth: number; newHeight: number } {
139
- const rad = (rotateDeg * Math.PI) / 180;
140
- const sin = Math.sin(rad);
141
- const cos = Math.cos(rad);
142
- const newWidth = Math.abs(width * cos) + Math.abs(height * sin);
143
- const newHeight = Math.abs(width * sin) + Math.abs(height * cos);
144
- return {
145
- offsetX: (newWidth - width) / 2,
146
- offsetY: (newHeight - height) / 2,
147
- newWidth,
148
- newHeight,
149
- };
150
- }
151
-
152
- static loadImage(src: string): Promise<{ width: number; height: number }> {
153
- return new Promise((resolve, reject) => {
154
- const img = new Image();
155
- img.crossOrigin = "";
156
- img.src = src;
157
- img.onload = () => resolve({ width: img.width, height: img.height });
158
- img.onerror = () => reject(new Error("图片加载失败"));
159
- });
160
- }
161
-
162
- static isImageElement(el: HTMLElement): boolean {
163
- return el.tagName.toLowerCase() === "img";
164
- }
165
- /**
166
- * 获取图片的大小
167
- * @param {String} base64 图片数据
168
- * @returns 返回图片大小
169
- */
170
- static getImageSize(base64: string) {
171
- const image = new Image();
172
- image.crossOrigin = "";
173
- image.src = base64;
174
- return new Promise((resolve, reject) => {
175
- image.onload = () =>
176
- resolve({ width: image.width, height: image.height });
177
- image.onerror = () => reject(new Error("图片加载失败"));
178
- });
179
- }
180
-
181
- /**
182
- * 获取水印旋转后的起始位置以及宽高
183
- * @param {Number} width 水印原始宽
184
- * @param {Number} height 水印原始高
185
- * @param {Number} rotate 旋转角度
186
- * @returns [x,y,w,h]
187
- */
188
- static getStart(width: number, height: number, rotate: number) {
189
- // 1 计算旋转的弧度
190
- let rad = (rotate * Math.PI) / 180;
191
- // 2 计算弧度的正弦余弦值
192
- let sin = Math.sin(rad);
193
- let cos = Math.cos(rad);
194
- // 获取旋转后的宽高
195
- let nWidth = Math.abs(width * cos) + Math.abs(height * sin);
196
- let nHeight = Math.abs(width * sin) + Math.abs(height * cos);
197
- // 起始位置为新的宽(高)减去旧的宽(高) 的一半
198
- let startX = (nWidth - width) / 2;
199
- let startY = (nHeight - height) / 2;
200
- return [startX, startY, nWidth, nHeight];
201
- }
202
-
203
- static getImageSizeByUrl(
204
- url: string
205
- ): Promise<{ width: number; height: number }> {
206
- return new Promise((resolve) => {
207
- const img = new Image();
208
- img.src = url;
209
- img.onload = () => {
210
- resolve({ width: img.width, height: img.height });
211
- };
212
- });
213
- }
214
-
215
- static imageOnload(fun: () => Promise<boolean>): Promise<boolean> {
216
- return new Promise(async (resolve) => {
217
- const isOK = await fun();
218
- if (isOK) {
219
- resolve(isOK);
220
- }
221
- });
222
- }
223
- static getMaxOfArray(numArray: number[]) {
224
- return Math.max.apply(null, numArray);
225
- }
226
- }
227
-
228
- // ===================== 主水印类 =====================
229
- export default class NewWatermark {
230
- private elementId: string;
231
- private config: WatermarkConfig | Array<WatermarkConfig>;
232
- private element: HTMLElement | null = null;
233
- private isImageElement = false;
234
- private isPrint: boolean;
235
- private observer!: IntersectionObserver; // 监听水印是否正确加载
236
-
237
- /**
238
- * 创建水印实例
239
- * @param elementId 目标元素的id
240
- * @param config 水印配置
241
- * @param isPrint 是否为打印模式
242
- * @param zoom 缩放比例
243
- */
244
- constructor(
245
- elementId: string,
246
- config: WatermarkConfig | Array<WatermarkConfig>,
247
- isPrint = false,
248
- zoom = 1
249
- ) {
250
- this.elementId = elementId;
251
- this.config = this.mergeDefaultConfig(config);
252
- this.isPrint = isPrint;
253
- WatermarkUtils.setZoom(zoom);
254
- }
255
-
256
- private getRandomPlace(obj: Object) {
257
- const values = Object.values(obj);
258
- const randomIndex = Math.floor(Math.random() * values.length);
259
- return values[randomIndex];
260
- }
261
- /**
262
- * 合并默认配置
263
- */
264
- private mergeDefaultConfig(
265
- config: WatermarkConfig | Array<WatermarkConfig>
266
- ): WatermarkConfig | Array<WatermarkConfig> {
267
- const defaultConfig: WatermarkConfig = {
268
- type: "text", // text | image
269
- tilling: true, // true 平铺 | false 单个
270
- value: "默认水印", // 水印文本
271
- font: "bold 20px Serif", // 字体
272
- fillStyle: "rgba(192,192,192,0.6)", // 字色
273
- rotate: 0, // 旋转角度
274
- offset: [0, 0], // 偏移量 mm
275
- horizontal: 0, // 横向间距 mm tilling为true时生效
276
- vertical: 0, // 纵向间距 mm tilling为true时生效
277
- place: "middleCenter", // 单个水印位置
278
- image: {
279
- // 图片水印配置
280
- type: "png", // 图片类型
281
- data: "", // 图片base64数据
282
- size: [50, 50], // 图片大小 mm
283
- opacity: 100, // 不透明度
284
- },
285
- };
286
- if (Array.isArray(config)) {
287
- return config.map((cfg) => ({ ...defaultConfig, ...cfg }));
288
- }
289
- return { ...defaultConfig, ...config };
290
- }
291
-
292
- /**
293
- * 初始化并渲染水印
294
- */
295
- async init(): Promise<void> {
296
- // 1. 根据id获取元素
297
- this.element = document.getElementById(this.elementId);
298
- if (!this.element) {
299
- console.error(`元素 #${this.elementId} 不存在`);
300
- return;
301
- }
302
-
303
- // 2. 区分元素类型(图片或其他)
304
- this.isImageElement = WatermarkUtils.isImageElement(this.element);
305
-
306
- if (this.isImageElement) {
307
- await this.renderImageWatermark();
308
- return;
309
- }
310
- // 4. 根据tilling区分水印类型并渲染
311
- if (Array.isArray(this.config)) {
312
- for (const cfg of this.config) {
313
- this.createSvgContainer(cfg); // 3. 创建SVG容器
314
- if (cfg.tilling) {
315
- await this.renderTillingWatermark(cfg);
316
- } else {
317
- await this.renderSingleWatermark(cfg);
318
- }
319
- }
320
- } else {
321
- // 3. 创建SVG容器
322
- this.createSvgContainer(this.config);
323
- if (this.config.tilling) {
324
- // 全图平铺水印
325
- await this.renderTillingWatermark(this.config);
326
- } else {
327
- // 单个水印
328
- await this.renderSingleWatermark(this.config);
329
- }
330
- }
331
- }
332
-
333
- private waterListener(_el: HTMLElement): void {
334
- if (!_el) return;
335
- this.observer = new IntersectionObserver(
336
- (entries) => {
337
- if (entries[0].isIntersecting) {
338
- // 在可视区内时,重新初始化水印
339
- _el.innerHTML = "";
340
- // this.initWatermarkSingle();
341
- }
342
- },
343
- { threshold: 0.2 }
344
- );
345
- this.observer.observe(_el);
346
- }
347
-
348
- /**
349
- * 创建SVG容器
350
- */
351
- private createSvgContainer(_config: WatermarkConfig): void {
352
- if (!this.element) return;
353
-
354
- const wmId = Math.random().toString(36).substring(2);
355
- const svgContainer = WatermarkUtils.createSvgElement<SVGSVGElement>(
356
- "svg",
357
- `watermark-${wmId}`
358
- );
359
-
360
- WatermarkUtils.setAttr(svgContainer, "width", "100%");
361
- WatermarkUtils.setAttr(svgContainer, "height", "100%");
362
- svgContainer.style.cssText =
363
- "position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;overflow:hidden;user-select:none;z-index:999;";
364
-
365
- // 设置viewBox
366
- const width = this.element.clientWidth || this.element.offsetWidth;
367
- const height = this.element.clientHeight || this.element.offsetHeight;
368
- WatermarkUtils.setAttr(svgContainer, "viewBox", `0 0 ${width} ${height}`);
369
-
370
- // 为目标元素设置相对定位
371
- if (this.isImageElement) {
372
- // 图片元素需要包装
373
- this.wrapImageElement();
374
- } else {
375
- // this.element.style.position = "relative";
376
- // this.element.style.overflow = "hidden";
377
- this.element.style.cssText += "position:relative;overflow:hidden;";
378
- _config && (_config.svgContainer = svgContainer);
379
- this.element.appendChild(svgContainer);
380
- }
381
- }
382
-
383
- /**
384
- * 包装图片元素(为图片创建一个容器)
385
- */
386
- private wrapImageElement(): void {
387
- if (!this.element) return;
388
-
389
- const parent = this.element.parentElement;
390
- if (!parent) return;
391
-
392
- // 创建包装容器
393
- const wrapper = document.createElement("div");
394
- wrapper.style.cssText = `
395
- position: relative;
396
- display: inline-block;
397
- width: ${this.element.offsetWidth}px;
398
- height: ${this.element.offsetHeight}px;
399
- overflow: hidden;
400
- `;
401
-
402
- // 将图片移入容器
403
- parent.insertBefore(wrapper, this.element);
404
- wrapper.appendChild(this.element);
405
- // wrapper.appendChild(this.svgContainer);
406
-
407
- // // 更新viewBox
408
- // WatermarkUtils.setAttr(
409
- // this.svgContainer,
410
- // "viewBox",
411
- // `0 0 ${this.element.offsetWidth} ${this.element.offsetHeight}`
412
- // );
413
- }
414
-
415
- /**
416
- * 渲染平铺水印(全图水印)
417
- */
418
- private async renderTillingWatermark(
419
- _config: WatermarkConfig
420
- ): Promise<void> {
421
- if (!_config.svgContainer) return;
422
- const gEle = WatermarkUtils.createSvgElement<SVGGElement>("g");
423
- _config.svgContainer.appendChild(gEle);
424
- // svgEle.style.cssText =
425
- // "position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;overflow:hidden;user-select:none;";
426
- // 创建pattern元素
427
- const patternId = `pattern-${Math.random().toString(36).substring(2)}`;
428
- const patternEle =
429
- WatermarkUtils.createSvgElement<SVGPatternElement>("pattern");
430
- WatermarkUtils.setAttr(patternEle, "id", `${patternId}`);
431
- WatermarkUtils.setAttr(patternEle, "patternUnits", "userSpaceOnUse");
432
- WatermarkUtils.setAttr(
433
- patternEle,
434
- "patternTransform",
435
- `rotate(${_config?.rotate || 0})`
436
- );
437
- // 创建填充矩形
438
- const rectEle = WatermarkUtils.createSvgElement<SVGRectElement>("rect");
439
- WatermarkUtils.setAttr(rectEle, "fill", `url(#${patternId})`); // rect的填充为水印需要平铺元素的id(必须)
440
- WatermarkUtils.setAttr(rectEle, "x", 0);
441
- WatermarkUtils.setAttr(rectEle, "y", 0);
442
- WatermarkUtils.setAttr(rectEle, "width", "100%");
443
- WatermarkUtils.setAttr(rectEle, "height", "100%");
444
- rectEle.style.pointerEvents = "none";
445
-
446
- if (_config?.type === "image") {
447
- await this.createTillingImageWatermark(patternEle, _config);
448
- } else {
449
- this.createTillingTextWatermark(patternEle, _config);
450
- }
451
-
452
- gEle.appendChild(patternEle);
453
- gEle.appendChild(rectEle);
454
- _config.svgContainer.appendChild(gEle);
455
- }
456
-
457
- /**
458
- * 创建平铺图片水印
459
- */
460
- private async createTillingImageWatermark(
461
- patternEle: SVGPatternElement,
462
- _config: WatermarkConfig
463
- ): Promise<void> {
464
- const imgConfig = _config.image!;
465
- const horizontal = _config.horizontal || 50;
466
- const vertical = _config.vertical || 100;
467
- // 模拟图片URL(实际使用时替换为真实数据)
468
- const imgUrl = imgConfig.data
469
- ? `data:image/${imgConfig.type || "png"};base64,${imgConfig.data}`
470
- : "https://cdn.jsdelivr.net/gh/zhangpanfei/static@demo/img/test.jpg";
471
-
472
- const imageWidth = WatermarkUtils.mm2px(imgConfig.size?.[0] || 100);
473
- const imageHeight = WatermarkUtils.mm2px(imgConfig.size?.[1] || 50);
474
-
475
- const patternWidth = imageWidth + horizontal;
476
- const patternHeight = imageHeight + vertical;
477
-
478
- WatermarkUtils.setAttr(patternEle, "width", patternWidth);
479
- WatermarkUtils.setAttr(patternEle, "height", patternHeight);
480
-
481
- const imageEle = WatermarkUtils.createSvgElement<SVGImageElement>("image");
482
- imageEle.setAttributeNS(
483
- "http://www.w3.org/1999/xlink",
484
- "xlink:href",
485
- imgUrl
486
- );
487
- // WatermarkUtils.setAttr(imageEle, "width", imageWidth);
488
- // WatermarkUtils.setAttr(imageEle, "height", imageHeight);
489
- imageEle.style.opacity = String((imgConfig.opacity ?? 100) / 100);
490
-
491
- try {
492
- const { width, height } = await WatermarkUtils.loadImage(imgUrl);
493
- WatermarkUtils.setAttr(
494
- imageEle,
495
- "transform",
496
- `scale(${imageWidth / width},${imageHeight / height})`
497
- );
498
- } catch (e) {
499
- console.error("图片加载失败", e);
500
- }
501
-
502
- patternEle.appendChild(imageEle);
503
- }
504
-
505
- /**
506
- * 创建平铺文字水印
507
- */
508
- private createTillingTextWatermark(
509
- patternEle: SVGPatternElement,
510
- _config: WatermarkConfig
511
- ): void {
512
- const text = _config.value || "默认水印";
513
- const font = _config.font || "bold 20px Serif";
514
- const fillStyle = _config.fillStyle || "rgba(192,192,192,0.6)";
515
- const horizontal = WatermarkUtils.mm2px(_config.horizontal || 0);
516
- const vertical = WatermarkUtils.mm2px(_config.vertical || 0);
517
-
518
- const fontSize = WatermarkUtils.getFontSize(font);
519
- const metrics = WatermarkUtils.getTextMetrics(text, font);
520
-
521
- const patternWidth = metrics.width + horizontal; // 平铺宽度计算为字体大小加一之后乘以最长字符串的长度再加上横向间距
522
- const patternHeight = metrics.height + vertical; // 平铺的高度计算为字体大小加一之后乘以行数再加上纵向间距
523
- WatermarkUtils.setAttr(patternEle, "width", patternWidth);
524
- WatermarkUtils.setAttr(patternEle, "height", patternHeight);
525
-
526
- const textEle = WatermarkUtils.createSvgElement<SVGTextElement>("text");
527
-
528
- for (let i = 0; i < metrics.lines.length; i++) {
529
- const tspan = WatermarkUtils.createSvgElement<SVGTSpanElement>("tspan");
530
- WatermarkUtils.setAttr(tspan, "x", 0);
531
- WatermarkUtils.setAttr(tspan, "y", fontSize * (i + 1));
532
- tspan.textContent = metrics.lines[i];
533
- textEle.appendChild(tspan);
534
- }
535
- textEle.style.cssText = `font:${font};fill:${fillStyle};text-anchor:start;font-size:${fontSize}px;letter-spacing:${
536
- fontSize / 10
537
- }pt`;
538
-
539
- patternEle.appendChild(textEle);
540
- }
541
-
542
- /**
543
- * 渲染单个水印
544
- */
545
- private async renderSingleWatermark(_config: WatermarkConfig): Promise<void> {
546
- if (!_config.svgContainer || !this.element) return;
547
-
548
- const wmPatternId = Math.random().toString(36).substring(2);
549
- const gEle = WatermarkUtils.createSvgElement<SVGGElement>(
550
- "g",
551
- `watermarkSingleG${wmPatternId}`
552
- );
553
- _config.svgContainer.appendChild(gEle);
554
-
555
- if (_config.type === "image") {
556
- await this.createSingleImageWatermark(gEle, _config);
557
- } else {
558
- this.createSingleTextWatermark(gEle, _config);
559
- }
560
- }
561
-
562
- /**
563
- * 创建单个图片水印
564
- */
565
- private async createSingleImageWatermark(
566
- gEle: SVGGElement,
567
- _config: WatermarkConfig
568
- ): Promise<void> {
569
- if (!this.element || !_config.svgContainer) return;
570
- const imgConfig = _config.image!;
571
- const imgUrl = imgConfig.data
572
- ? `data:image/${imgConfig.type || "png"};base64,${imgConfig.data}`
573
- : "https://cdn.jsdelivr.net/gh/zhangpanfei/static@demo/img/test.jpg";
574
-
575
- const imageWidth = WatermarkUtils.mm2px(imgConfig.size?.[0] || 100);
576
- const imageHeight = WatermarkUtils.mm2px(imgConfig.size?.[1] || 50);
577
-
578
- const imageEle = WatermarkUtils.createSvgElement<SVGImageElement>("image");
579
- imageEle.setAttributeNS(
580
- "http://www.w3.org/1999/xlink",
581
- "xlink:href",
582
- imgUrl
583
- );
584
- imageEle.style.opacity = String((imgConfig.opacity ?? 100) / 100);
585
- let _scale = "";
586
- const { width, height } = await WatermarkUtils.loadImage(imgUrl);
587
- _scale = imageWidth / width + "," + imageHeight / height;
588
- WatermarkUtils.setAttr(
589
- imageEle,
590
- "transform",
591
- "scale(" + imageWidth / width + "," + imageHeight / height + ")"
592
- );
593
- gEle.appendChild(imageEle);
594
- // 计算位置
595
- this.calculateImagePosition(_config, imageEle, gEle, _scale);
596
- }
597
-
598
- /**
599
- * 单个图片计算位置 始终沿着九宫格不超出范围定位和旋转
600
- */
601
- private calculateImagePosition(
602
- _config: WatermarkConfig,
603
- imageEle: SVGImageElement,
604
- gEle: SVGGElement,
605
- _scale: string
606
- ): void {
607
- if (!this.element || !_config.svgContainer) return;
608
- // 计算位置
609
- const offset = _config.offset || [0, 0];
610
- const _rotate = _config.rotate ?? 0;
611
- const place = _config.place || this.getRandomPlace(PlaceMap);
612
- const t = (gEle as any).getBBox(); // 容器
613
- if (t.width === 0 && t.height === 0 && t.x === 0 && t.y === 0) {
614
- // 监听不在可视区内的元素
615
- // this.waterListener();
616
- } else {
617
- // 若已加载 则取消监听
618
- this.observer?.disconnect();
619
- }
620
- const regc = WatermarkUtils.getStart(t.width, t.height, +_rotate);
621
- const bound = (
622
- _config.svgContainer.getAttribute("viewBox")?.split(" ") ?? [0, 0, 0, 0]
623
- ).map(Number);
624
- // x y 偏移量
625
- const x = WatermarkUtils.mm2px(offset[0], this.isPrint),
626
- y = WatermarkUtils.mm2px(offset[1], this.isPrint);
627
- let textX = 0,
628
- textY = 0,
629
- textTransform = "";
630
- switch (place) {
631
- case PlaceMap.TL:
632
- // 始终沿着左侧中心点顺时针旋转
633
- textX = x + regc[0];
634
- textY = y + regc[1];
635
- textTransform = `rotate(${_config.rotate} ${x + regc[2] / 2} ${
636
- y + regc[3] / 2
637
- }) translate(${textX} ${textY}) scale(${_scale})`;
638
- break;
639
- case PlaceMap.TC:
640
- // 始终沿着中心点Y轴旋转
641
- textX = bound[2] / 2 - t.width / 2 + x;
642
- textY = y + regc[3] / 2 - t.height / 2;
643
- textTransform = `rotate(${_config.rotate} ${textX + t.width / 2} ${
644
- regc[3] / 2 + y
645
- }) translate(${textX} ${textY}) scale(${_scale})`;
646
- break;
647
- case PlaceMap.TR:
648
- // 始终沿着右侧中心点逆时针旋转
649
- textX = bound[2] + x - regc[2] + regc[0];
650
- textY = y + regc[1];
651
- textTransform = `rotate(${_config.rotate} ${
652
- bound[2] + x - regc[2] / 2
653
- } ${regc[3] / 2 + y}) translate(${textX} ${textY}) scale(${_scale})`;
654
- break;
655
- case PlaceMap.ML:
656
- // 始终沿着左侧X轴变动
657
- textX = x + regc[0];
658
- textY = bound[3] / 2 + y - t.height / 2;
659
- textTransform = `rotate(${_config.rotate} ${x + regc[2] / 2} ${
660
- bound[3] / 2 + y
661
- }) translate(${textX} ${textY}) scale(${_scale})`;
662
- break;
663
- case PlaceMap.MC:
664
- // 始终沿着中心点旋转
665
- const xt = bound[2] / 2 + x;
666
- const yt = bound[3] / 2 + y;
667
- textX = xt - t.width / 2;
668
- textY = yt - t.height / 2;
669
- textTransform = `rotate(${_config.rotate} ${xt} ${yt}) translate(${textX} ${textY}) scale(${_scale})`;
670
- break;
671
- case PlaceMap.MR:
672
- // 始终沿着右侧x轴变动
673
- textX = bound[2] + x - regc[2] + regc[0];
674
- textY = bound[3] / 2 + y - t.height / 2;
675
- textTransform = `rotate(${_config.rotate} ${
676
- bound[2] + x - regc[2] / 2
677
- } ${bound[3] / 2 + y}) translate(${textX} ${textY}) scale(${_scale})`;
678
- break;
679
- case PlaceMap.BL:
680
- // 始终沿着左侧中心点顺时针旋转
681
- textX = x + regc[0];
682
- textY = bound[3] + y - regc[1] - t.height;
683
- textTransform = `rotate(${_config.rotate} ${x + regc[2] / 2} ${
684
- bound[3] + y - regc[3] / 2
685
- }) translate(${textX} ${textY}) scale(${_scale})`;
686
- break;
687
- case PlaceMap.BC:
688
- // 始终沿着Y轴变动
689
- textX = bound[2] / 2 - t.width / 2 + x;
690
- textY = bound[3] + y - regc[1] - t.height;
691
- textTransform = `rotate(${_config.rotate} ${textX + t.width / 2} ${
692
- bound[3] + y - regc[3] / 2
693
- }) translate(${textX} ${textY}) scale(${_scale})`;
694
- break;
695
- case PlaceMap.BR:
696
- // 始终沿着右侧中心点逆时针旋转
697
- textX = bound[2] + x - regc[2] + regc[0];
698
- textY = bound[3] + y - t.height - regc[1];
699
- textTransform = `rotate(${_config.rotate} ${
700
- bound[2] + x - regc[2] / 2
701
- } ${
702
- bound[3] + y - regc[3] / 2
703
- }) translate(${textX} ${textY}) scale(${_scale})`;
704
- break;
705
- }
706
- imageEle.setAttribute("transform", textTransform);
707
- }
708
-
709
- /**
710
- * 创建单个文字水印
711
- */
712
- private createSingleTextWatermark(
713
- gEle: SVGGElement,
714
- _config: WatermarkConfig
715
- ): void {
716
- if (!this.element || !_config.svgContainer) return;
717
- const config = _config;
718
- const text = config.value || "默认水印";
719
- const font = config.font || "bold 20px Serif";
720
- const fillStyle = config.fillStyle || "rgba(192,192,192,0.6)";
721
- const offset = config.offset || [0, 0];
722
- const fontSize = WatermarkUtils.getFontSize(font);
723
- const metrics = WatermarkUtils.getTextMetrics(text, font);
724
- const textEle = WatermarkUtils.createSvgElement<SVGTextElement>("text");
725
- WatermarkUtils.setAttr(textEle, "fill", fillStyle);
726
- textEle.style.font = font;
727
- WatermarkUtils.setAttr(textEle, "letter-spacing", `${fontSize / 10}pt`);
728
- gEle.appendChild(textEle);
729
-
730
- // 初始位置
731
- const x = WatermarkUtils.mm2px(offset[0]),
732
- y = WatermarkUtils.mm2px(offset[1]);
733
- for (let i = 0; i < metrics.lines.length; i++) {
734
- const tspan = WatermarkUtils.createSvgElement<SVGTSpanElement>("tspan");
735
- WatermarkUtils.setAttr(tspan, "x", x);
736
- WatermarkUtils.setAttr(tspan, "y", fontSize * (i + 1));
737
- WatermarkUtils.setAttr(tspan, "font-size", fontSize);
738
- tspan.textContent = metrics.lines[i];
739
- textEle.appendChild(tspan);
740
- }
741
- this.calculateTextPosition(_config, textEle, x, y, fontSize);
742
- }
743
- /**
744
- * 单个文字计算位置
745
- */
746
- private calculateTextPosition(
747
- _config: WatermarkConfig,
748
- textEle: SVGTextElement,
749
- x: number,
750
- y: number,
751
- fontSize: number
752
- ): void {
753
- if (!this.element || !_config.svgContainer) return;
754
- const t = textEle.getBBox(); // 容器
755
- let place = _config.place;
756
- // 如果place为random 则随机获取一个位置
757
- if (!place) {
758
- place = this.getRandomPlace(PlaceMap);
759
- }
760
- const rotate = _config.rotate || 0;
761
- const child = textEle.children;
762
- const bound = (
763
- _config.svgContainer.getAttribute("viewBox")?.split(" ") ?? [0, 0, 0, 0]
764
- ).map(Number);
765
- let _n = 0;
766
- for (let i = 0; i < child.length; i++) {
767
- const ele = child[i] as SVGTSpanElement;
768
- if (i === 0) {
769
- _n = ele.getBBox().y;
770
- }
771
- const regc = WatermarkUtils.getStart(t.width, t.height, rotate);
772
- let textX = x + regc[0],
773
- textY = 0,
774
- textTransform = "",
775
- eleX = textX,
776
- eleY = regc[1] + y + fontSize * (i + 1);
777
- switch (place) {
778
- case PlaceMap.TL:
779
- // 始终沿着左侧中心点顺时针旋转
780
- // textX = x + regc[0];
781
- textY = y + regc[1];
782
- textTransform = `rotate(${rotate} ${regc[2] / 2 + x} ${
783
- regc[3] / 2 + y
784
- })`;
785
- // eleX = x + regc[0];
786
- // eleY = regc[1] + y + fontSize * (i + 1);
787
- break;
788
- case PlaceMap.TC:
789
- // 始终沿着中心点Y轴旋转
790
- const xt = bound[2] / 2 - t.width / 2;
791
- textX = xt + x;
792
- textY = y + regc[3] / 2 - t.height / 2;
793
- textTransform = `rotate(${rotate} ${xt + t.width / 2 + x} ${
794
- regc[3] / 2 + y
795
- })`;
796
- // eleX = xt + x;
797
- eleX = textX;
798
- // eleY = regc[1] + y + fontSize * (i + 1);
799
- break;
800
- case PlaceMap.TR:
801
- // 始终沿着右侧中心点逆时针旋转
802
- textX = bound[2] - regc[2] + regc[0] + x;
803
- textY = y + regc[1];
804
- textTransform = `rotate(${rotate} ${bound[2] - regc[2] / 2 + x} ${
805
- regc[3] / 2 + y
806
- })`;
807
- // eleX = bound[2] - regc[2] + regc[0] + x;
808
- eleX = textX;
809
- // eleY = regc[1] + y + fontSize * (i + 1);
810
- break;
811
- case PlaceMap.ML:
812
- // 始终沿着左侧X轴变动
813
- // textX = x + regc[0];
814
- textY = bound[3] / 2 + y;
815
- textTransform = `rotate(${rotate} ${x + regc[2] / 2} ${textY})`;
816
- // eleX = x + regc[0];
817
- eleY = textY - t.height / 2 + fontSize * (i + 1);
818
- break;
819
- case PlaceMap.MC:
820
- // 始终沿着中心点旋转
821
- const yt = bound[3] / 2 + y;
822
- textX = bound[2] / 2 + x;
823
- textY = yt - t.height;
824
- textTransform = `rotate(${rotate} ${textX} ${yt})`;
825
- eleX = textX - t.width / 2;
826
- eleY = yt - t.height / 2 + fontSize * (i + 1);
827
- break;
828
- case PlaceMap.MR:
829
- // 始终沿着右侧x轴变动
830
- textX = bound[2] + x - regc[2] + regc[0];
831
- textY = bound[3] / 2 + y;
832
- textTransform = `rotate(${rotate} ${
833
- bound[2] + x - regc[2] / 2
834
- } ${textY})`;
835
- eleX = bound[2] + x - regc[2] + regc[0];
836
- eleY = textY - t.height / 2 + fontSize * (i + 1);
837
- break;
838
- case PlaceMap.BL:
839
- // 始终沿着左侧中心点顺时针旋转
840
- // textX = x + regc[0];
841
- textY = bound[3] + y - regc[1];
842
- textTransform = `rotate(${rotate} ${regc[2] / 2 + x} ${
843
- bound[3] + y - regc[3] / 2
844
- })`;
845
- eleX = regc[0] + x;
846
- eleY = bound[3] + y - regc[1] - t.height + fontSize * (i + 1);
847
- break;
848
- case PlaceMap.BC:
849
- // 始终沿着Y轴变动
850
- textX = bound[2] / 2 - t.width / 2 + x;
851
- textY = bound[3] + y - regc[1];
852
- textTransform = `rotate(${rotate} ${textX + t.width / 2} ${
853
- bound[3] + y - regc[3] / 2
854
- })`;
855
- eleX = bound[2] / 2 - t.width / 2 + x;
856
- eleY = bound[3] + y - regc[1] - t.height + fontSize * (i + 1);
857
- break;
858
- case PlaceMap.BR:
859
- // 始终沿着右侧中心点逆时针旋转
860
- textX = bound[2] + x - regc[2] + regc[0];
861
- textY = bound[3] + y - regc[1];
862
- textTransform = `rotate(${rotate} ${bound[2] + x - regc[2] / 2} ${
863
- bound[3] + y - regc[3] / 2
864
- })`;
865
- eleX = bound[2] + x - regc[2] + regc[0];
866
- eleY = bound[3] + y - t.height - regc[1] + fontSize * (i + 1);
867
- break;
868
- default:
869
- break;
870
- }
871
- WatermarkUtils.setAttr(textEle, "x", textX);
872
- WatermarkUtils.setAttr(textEle, "y", textY);
873
- WatermarkUtils.setAttr(textEle, "transform", textTransform);
874
- WatermarkUtils.setAttr(ele, "x", eleX);
875
- WatermarkUtils.setAttr(ele, "y", eleY - _n);
876
- }
877
- }
878
-
879
- // 以下为以图片为基底的水印
880
- private async renderImageWatermark(): Promise<void> {
881
- const _image_element = this.element as HTMLImageElement;
882
- if (!_image_element) return;
883
- _image_element.onload = () => {
884
- if (_image_element.getAttribute("watermark") != "true") {
885
- this.imageWatermark(this.config);
886
- }
887
- };
888
- }
889
-
890
- private async imageWatermark(
891
- _config: Array<WatermarkConfig> | WatermarkConfig
892
- ): Promise<void> {
893
- if (!this.element) return;
894
- const image = this.element as HTMLImageElement;
895
- const canvas = document.createElement("canvas");
896
- const ctx = canvas.getContext("2d");
897
- if (!ctx) return;
898
- let size = { width: 0, height: 0 };
899
- if (image.naturalWidth && image.naturalHeight) {
900
- size = { width: image.naturalWidth, height: image.naturalHeight };
901
- } else {
902
- size = await WatermarkUtils.getImageSizeByUrl(image.src);
903
- }
904
- const iw = size.width,
905
- ih = size.height;
906
- canvas.width = iw;
907
- canvas.height = ih;
908
-
909
- // 绘制底图
910
- ctx.drawImage(image, 0, 0);
911
- const arr = Array.isArray(_config) ? _config : [_config];
912
- console.log(3, arr);
913
-
914
- for (let index = 0; index < arr.length; index++) {
915
- const setting = arr[index];
916
- if (
917
- !setting ||
918
- (setting.type === "text" && !setting.value) ||
919
- (setting.type === "image" && !setting?.image?.data)
920
- ) {
921
- continue;
922
- }
923
- if (iw == 0 || ih == 0 || setting == null || !ctx) {
924
- continue;
925
- }
926
- // 配置
927
- const bound = [0, 0, iw, ih]; // 容器
928
- const offset = setting.offset || [0, 0];
929
- const rotate = setting.rotate || 0; //旋转角度
930
- const _type = setting.type; // image | text
931
- const _tilling = setting.tilling; // false 单个 | true 平铺
932
- const place = setting.place || this.getRandomPlace(PlaceMap);
933
- const _vertical = setting.vertical || 0;
934
- const _horizontal = setting.horizontal || 0;
935
- const font = setting.font || "16pt SimSun";
936
- if (_type === "image") {
937
- let nOpacity = setting.image?.opacity || 100;
938
- let imageWidth = 100;
939
- let imageHeight = 100;
940
- if (setting.image && setting.image.size) {
941
- imageWidth = WatermarkUtils.mm2pxDpi(setting.image.size[0], 96);
942
- imageHeight = WatermarkUtils.mm2pxDpi(setting.image.size[1], 96);
943
- }
944
-
945
- await WatermarkUtils.imageOnload(() => {
946
- return new Promise((resolve) => {
947
- const _img = new Image();
948
- _img.crossOrigin = "anonymous";
949
- _img.src = "data:image/png;base64," + setting.image?.data;
950
- // _img.src = "https://tse1-mm.cn.bing.net/th/id/OIP-C.o8onZxvvNC118n8dh0Q3ZQHaHa?rs=1&pid=ImgDetMain"; // 模拟图片
951
- _img.width = imageWidth;
952
- _img.height = imageHeight;
953
- const vertical = WatermarkUtils.mm2pxDpi(_vertical, 96);
954
- const horizontal = WatermarkUtils.mm2pxDpi(_horizontal, 96);
955
- _img.onload = () => {
956
- if (!_tilling) {
957
- ctx.save();
958
- const regc = WatermarkUtils.getStart(
959
- imageWidth,
960
- imageHeight,
961
- rotate
962
- );
963
- const _objXY = this.translateXY(
964
- place,
965
- offset,
966
- imageWidth,
967
- bound,
968
- regc
969
- );
970
- // 原点修改以及旋转
971
- ctx.translate(_objXY.translateX, _objXY.translateY);
972
- ctx.rotate((Math.PI / 180) * rotate);
973
- ctx.globalAlpha = nOpacity / 100; // 透明度设置
974
- // 绘制图片
975
- ctx.drawImage(
976
- _img,
977
- -imageWidth / 2,
978
- -imageHeight / 2,
979
- imageWidth,
980
- imageHeight
981
- );
982
- ctx.restore();
983
- } else {
984
- ctx.save();
985
- //平铺比例
986
- const scale = 3;
987
- //缩小后图像宽度
988
- const n1 = _img.width;
989
- //缩小后图像高度
990
- const n2 = _img.height;
991
- //平铺横向个数
992
- const n3 = (iw / (n1 + horizontal)) * scale;
993
- //平铺纵向个数
994
- const n4 = (ih / (n2 + vertical)) * scale;
995
- ctx.translate(iw / 2, ih / 2);
996
- ctx.rotate((rotate * Math.PI) / 180);
997
- ctx.translate(-iw / 2, -ih / 2);
998
- for (let i = -15; i < n3; i++) {
999
- for (let j = -15; j < n4; j++) {
1000
- ctx.globalAlpha = (100 - nOpacity) / 100; // 透明度设置
1001
- ctx.drawImage(
1002
- _img,
1003
- i * (n1 + horizontal),
1004
- j * (n2 + vertical),
1005
- n1,
1006
- n2
1007
- );
1008
- }
1009
- }
1010
- ctx.restore();
1011
- }
1012
- resolve(true);
1013
- };
1014
- });
1015
- });
1016
- } else {
1017
- const v = setting.value || ""; //水印文本
1018
- const vs = v.split("\n");
1019
- const vertical = WatermarkUtils.mm2pxDpi(_vertical, 96); //纵向间距
1020
- const horizontal = WatermarkUtils.mm2pxDpi(_horizontal, 96); //横向间距
1021
- const fontSize = parseInt(font.replace(/[^\d]/g, " "));
1022
- const fs = (fontSize * 96) / 72;
1023
- ctx.fillStyle = setting.fillStyle || "rgba(192,192,192,0.6)"; //文本颜色
1024
- ctx.font = font;
1025
- const width_Num = [];
1026
- for (let i = 0; i < vs.length; i++) {
1027
- width_Num.push(ctx.measureText(vs[i]).width);
1028
- }
1029
- const maxWidth = WatermarkUtils.getMaxOfArray(width_Num); // 以最大文字的宽度为准
1030
- const fontH = parseInt(fs * vs.length + "");
1031
- const regc = WatermarkUtils.getStart(maxWidth, fontH, rotate); // 文本绘制区域
1032
-
1033
- const moveX = 0;
1034
-
1035
- if (!_tilling) {
1036
- ctx.save();
1037
- ctx.textAlign = "center"; // 文本对齐方式
1038
- ctx.textBaseline = "middle"; // 设置文本的垂直对齐方式
1039
- const _objXY = this.translateXY(place, offset, maxWidth, bound, regc);
1040
- ctx.translate(_objXY.translateX, _objXY.translateY);
1041
- ctx.rotate((Math.PI / 180) * rotate);
1042
- for (let vi = 0; vi < vs.length; vi++) {
1043
- ctx.fillText(vs[vi], moveX, -fontH / 2 + fs / 2 + fs * vi);
1044
- }
1045
- ctx.restore();
1046
- } else {
1047
- ctx.save();
1048
- ctx.translate(iw / 2, ih / 2);
1049
- ctx.rotate((Math.PI / 180) * rotate);
1050
- for (let i = -iw; i < iw; ) {
1051
- for (let j = -ih; j < ih; ) {
1052
- for (let vi = 0; vi < vs.length; vi++) {
1053
- ctx.fillText(vs[vi], i, j + fs * vi);
1054
- }
1055
- j = j + fontH + (vertical ? vertical : 0);
1056
- }
1057
- i = i + maxWidth + (horizontal ? horizontal : 0);
1058
- }
1059
- ctx.restore();
1060
- }
1061
- }
1062
- }
1063
- image.src = canvas.toDataURL("image/png");
1064
- image.setAttribute("watermark", "true");
1065
- }
1066
-
1067
- private translateXY(
1068
- place: PlaceType,
1069
- offset: number[],
1070
- elWidth: number,
1071
- boundRect: number[],
1072
- elRect: number[]
1073
- ) {
1074
- let translateX = 0;
1075
- let translateY = 0;
1076
- const bound = boundRect;
1077
- const regc = elRect;
1078
- const offsetX = offset[0];
1079
- const offsetY = offset[1];
1080
- const tw = elWidth;
1081
- switch (place) {
1082
- case "topLeft":
1083
- case "middleLeft":
1084
- case "bottomLeft":
1085
- translateX = offsetX + regc[2] / 2;
1086
- if (place === "topLeft") {
1087
- translateY = offsetY + regc[3] / 2;
1088
- } else if (place === "middleLeft") {
1089
- translateY = offsetY + bound[3] / 2;
1090
- } else if (place === "bottomLeft") {
1091
- translateY = offsetY + bound[3] - regc[3] / 2;
1092
- }
1093
- break;
1094
- case "topCenter":
1095
- case "middleCenter":
1096
- case "bottomCenter":
1097
- translateX = offsetX + bound[2] / 2;
1098
- if (place === "topCenter") {
1099
- translateY = offsetY + regc[3] / 2;
1100
- } else if (place === "middleCenter") {
1101
- translateY = offsetY + bound[3] / 2;
1102
- } else if (place === "bottomCenter") {
1103
- translateY = offsetY + bound[3] - regc[3] / 2;
1104
- }
1105
- break;
1106
- case "topRight":
1107
- case "middleRight":
1108
- case "bottomRight":
1109
- translateX = offsetX + bound[2] - tw / 2 - regc[0];
1110
- if (place === "topRight") {
1111
- translateY = offsetY + regc[3] / 2;
1112
- } else if (place === "middleRight") {
1113
- translateY = offsetY + bound[3] / 2;
1114
- } else if (place === "bottomRight") {
1115
- translateY = offsetY + bound[3] - regc[3] / 2;
1116
- }
1117
- break;
1118
- }
1119
- return {
1120
- translateX: translateX,
1121
- translateY: translateY,
1122
- };
1123
- }
1124
-
1125
- /**
1126
- * 移除水印
1127
- */
1128
- destroy(): void {
1129
- if (this.element) {
1130
- this.element.remove();
1131
- this.element = null;
1132
- }
1133
- }
1134
- }