接到新需求,要求实现九宫格水印,也就是九个方位,不超出容器范围的水印,包括单个的和平铺的,先来看以元素为基底的水印。
以元素为基底的水印为svg水印,结构为如下图所示:
- 九方位的【单个文字】水印:
- 配置:
const setting = [ { fillStyle: "rgba(0,0,200,1)", // 样式 font: "20pt SimSun", // 字体 "rotate": '45', // 旋转角度 "place": "topLeft", // 显示位置,接收九个值 "tiling": "false", // 是否平铺 offset: '0.0,0.0', // 距离 type: 'text', "value": "左上角旋转45度水印", // 水印内容 }, { fillStyle: "rgba(0,0,200,1)", font: "20pt SimSun", "rotate": '90', "place": "topCenter", "tiling": "false", offset: '0.0,0.0', type: 'text', "value": "上居中旋转90度", },{ fillStyle: "rgba(0,0,200,1)", font: "20pt SimSun", "rotate": '120', "place": "topRight", "tiling": "false", offset: '0.0,0.0', type: 'text', "value": "右上角旋转120度水印", },{ fillStyle: "rgba(0,0,200,1)", font: "20pt SimSun", "rotate": '0', "place": "middleLeft", "tiling": "false", offset: '0.0,0.0', type: 'text', "value": "左侧水印", },{ fillStyle: "rgba(0,0,200,1)", font: "20pt SimSun", "rotate": '0', "place": "middleRight", "tiling": "false", offset: '0.0,0.0', type: 'text', "value": "右侧水印", },{ fillStyle: "rgba(0,0,200,1)", font: "20pt SimSun", "rotate": '120', "place": "bottomLeft", "tiling": "false", offset: '0.0,0.0', type: 'text', "value": "左下角旋转120度水印", },{ fillStyle: "rgba(0,0,200,1)", font: "20pt SimSun", "rotate": '90', "place": "bottomCenter", "tiling": "false", offset: '0.0,0.0', type: 'text', "value": "下居中旋转90度水印", }, { fillStyle: "rgba(0,0,200,1)", font: "20pt SimSun", "rotate": '-120', "place": "bottomRight", "tiling": "false", offset: '0.0,0.0', type: 'text', "value": "下居中旋转-120度水印", }, { fillStyle: "red", font: "20pt SimSun", "rotate": '0', "place": "middleCenter", "tiling": "false", offset: '0.0,0.0', type: 'text', "value": "居中水印", }]; const w = new Watermark(setting, document.querySelector('#content') as HTMLElement, false, 0.7); w.init();
- 单个文字水印时,垂直间距vertical、水平间距horizontal不生效,间距offset生效,位置place生效。
- 九方位的【单个图片】水印:
- 配置:
const setting = [ { "rotate": '45', "place": "topLeft", "tiling": "false", offset: '0.0,0.0', type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 0, // 图片不透明度 }, }, { "rotate": '0', "place": "topCenter", "tiling": "false", offset: '0.0,0.0', type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 0, // 图片不透明度 }, },{ "rotate": '300', "place": "topRight", "tiling": "false", offset: '0.0,0.0', type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 0, // 图片不透明度 }, },{ "rotate": '0', "place": "middleLeft", "tiling": "false", offset: '0.0,0.0', type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 0, // 图片不透明度 }, },{ "rotate": '0', "place": "middleRight", "tiling": "false", offset: '0.0,0.0', type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 0, // 图片不透明度 }, },{ "rotate": '120', "place": "bottomLeft", "tiling": "false", offset: '0.0,0.0', type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 0, // 图片不透明度 }, },{ "rotate": '90', "place": "bottomCenter", "tiling": "false", offset: '0.0,0.0', type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 0, // 图片不透明度 }, }, { "rotate": '-120', "place": "bottomRight", "tiling": "false", offset: '0.0,0.0', type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 0, // 图片不透明度 }, }, { "rotate": '0', "place": "middleCenter", "tiling": "false", offset: '0.0,0.0', type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 0, // 图片不透明度 }, }]; const w = new Watermark(setting, document.querySelector('#content') as HTMLElement, false, 0.7); w.init();
- 单个图片水印时,配置项的垂直间距vertical、水平间距horizontal、fillStyle、font、value不生效,间距offset、位置place、image配置生效。
- 【文字】平铺水印效果图:
- 配置:
const setting = [ { fillStyle: "rgba(0,0,200,1)", font: "20pt SimSun", "rotate": '30', "tiling": "true", type: 'text', "value": "旋转30度的\n水平间距10\n垂直间距40\n的文字平铺", vertical: 40, horizontal: 10, } ]; const w = new Watermark(setting, document.querySelector('#content') as HTMLElement, false, 0.7); w.init();
- 文字水印平铺时,配置的place和offset不生效,垂直间距vertical、水平间距horizontal生效。
- 【图片】平铺水印效果图:
- 配置:
const setting = [ { "rotate": '30', "tiling": "true", type: 'image', image: { type: 'jpg', // 图片类型 data: 'base64文件流', // 图片数据 size: [50, 50], // 图片大小 单位px nOpacity: 50, // 图片不透明度 }, vertical: 40, // 垂直间距 horizontal: 10, // 水平间距 } ]; const w = new Watermark(setting, document.querySelector('#content') as HTMLElement, false, 0.7); w.init();
- 图片水印平铺时,配置的place和offset不生效,垂直间距vertical、水平间距horizontal、image配置生效。
接下来是水印代码:直接复制成新文件,引入使用即可。
/**
* 水印分为图片水印与文字水印
*/
import { getState } from 'src/config/store';
const PT_PER_INCH = 72;
const MM_PER_INCH = 25.4;
let zoomNum = 0;
export interface WatermarkOptions {
type: string;
element?: HTMLElement | null;
fontSize?: string;
color?: string;
image?: {
type?: string;
data?: string;
size?: Array<number>;
nOpacity?: number;
};
value?: string;
horizontal?: number;
vertical?: number;
width?: number;
height?: number;
font?: string;
offset?: string;
fillStyle?: string;
tiling?: string;
rotate?: string;
place?: string;
svgEle?: SVGElement | null;
}
const place = {
TL: 'topLeft',
TC: 'topCenter',
TR: 'topRight',
ML: 'middleLeft',
MC: 'middleCenter',
MR: 'middleRight',
BL: 'bottomLeft',
BC: 'bottomCenter',
BR: 'bottomRight',
};
export class Watermark {
options: Array<WatermarkOptions> = [];
isPrint: boolean = false;
static isPageMrk: boolean = true;
constructor(options: Array<WatermarkOptions> | WatermarkOptions, el = document.getElementById('brightness'), isPrint = false, zoom?: number) {
zoomNum = zoom ?? 0;
this.isPrint = isPrint;
const wmPatternId = Math.random().toString(36).substr(2);
const svgEle = Watermark.createNS('svg', 'watermarkSingle' + wmPatternId);
Watermark.setAttr(svgEle, 'width', '100%');
Watermark.setAttr(svgEle, 'height', '100%');
const defaultOptions = {
type: 'text', // 水印类型(文本/图片)
image: {
// 图片水印的参数
type: 'png', // 图片类型
data: '', // 图片数据
size: [100, 50], // 图片大小 单位px
nOpacity: 100, // 图片不透明度
},
value: '默认水印', // 水印文本
horizontal: 50, // 横向间距mm
vertical: 100, // 纵向间距mm
width: 0, // 宽度(文字水印 单个水印用来计算位子)
height: 0, // 高度(文字水印 单个水印用来计算位子)
font: 'bold 20px Serif', // 字体
offset: '0,0', // 水印默认偏移量
fillStyle: 'rgba(192,192,192,0.6)', // 字色
tiling: 'true', // 是否平铺
rotate: '45', // 倾斜度
place: place.MC, // 单个水印的位置
svgEle: svgEle,
};
const _opts = !Array.isArray(options) ? [options] : options;
for (let item of _opts) {
const target = item;
const returnedTarget = Object.assign({}, defaultOptions, target);
// 是否弧度计算水印
const radian = false;
// 旋转使用弧度时则需要换算为角度
returnedTarget.rotate = radian ? ((parseFloat(item.rotate ?? '') * 180) / Math.PI).toString() : parseFloat(item.rotate ?? '').toString();
if (!item.element && el) {
returnedTarget.element = el;
}
if (returnedTarget.element) {
// 不为全屏水印时则 先清空需要初始化的dom
if (returnedTarget.element.id !== 'brightness') {
returnedTarget.element.innerHTML = '';
} else {
Watermark.isPageMrk = false;
}
returnedTarget.element.style.cssText += 'position:relative;overflow:hidden;';
returnedTarget.element.appendChild(svgEle);
}
this.options.push(returnedTarget);
}
}
init() {
this.options.forEach((item: WatermarkOptions) => {
if (item.tiling && item.tiling !== 'false') {
const wm = new WatermarkSvg(item, item.element, this.isPrint);
wm.initWatermark();
} else {
const wms = new WatermarkSingle(item, item.element, this.isPrint);
wms.initWatermarkSingle();
}
});
}
static qi(id: string) {
return document.getElementById(id);
}
static ne(el: string, id?: string | undefined) {
const e = document.createElement(el);
e.id = id || '';
return e;
}
static createNS(label: string, id?: string | undefined) {
const el = document.createElementNS('http://www.w3.org/2000/svg', label);
el.id = id || '';
return el;
}
static setAttr(labelEle: { setAttribute: (arg0: any, arg1: any) => void }, attrName: string, attrValue: string | number) {
labelEle.setAttribute(attrName, attrValue);
}
static mm2px(mm: number | any, isPrint = false) {
return (mm * Watermark.getDpi(isPrint)) / MM_PER_INCH;
}
static pt2px(pt: number, isPrint = false) {
return (pt * Watermark.getDpi(isPrint)) / PT_PER_INCH;
}
/**
* 裁剪文本
* @param {String} text 文本数据
* @returns 返回裁剪后的数据
*/
static cutText(text: string | any[]) {
if (text.length <= 60) {
return text;
}
return text.slice(0, 60);
}
// 求斜边距离
static diagonal(w: number, h: number) {
return Math.sqrt(w * w + h * h);
}
static getDpi(isPrint = false) {
const zoom = zoomNum === 0 ? parseFloat(getState('Common', 'zoom')) : zoomNum;
let type = isNaN(zoom);
// 判断缩放率是否为数字 若不是 则返回1 用以计算字体大小
let zoomValue = type ? 1 : zoom;
if (isPrint) {
zoomValue = 1;
}
return zoomValue * 100 * 0.96;
}
/**
* 获取水印旋转后的起始位置以及宽高
* @param {Number} width 水印原始宽
* @param {Number} height 水印原始高
* @param {Number} rotate 旋转角度
* @returns [x,y,w,h]
*/
static getStart(width: number, height: number, rotate: number) {
// 1 计算旋转的弧度
let rad = (rotate * Math.PI) / 180;
// 2 计算弧度的正弦余弦值
let sin = Math.sin(rad);
let cos = Math.cos(rad);
// 获取旋转后的宽高
let nWidth = Math.abs(width * cos) + Math.abs(height * sin);
let nHeight = Math.abs(width * sin) + Math.abs(height * cos);
// 起始位置为新的宽(高)减去旧的宽(高) 的一半
let startX = (nWidth - width) / 2;
let startY = (nHeight - height) / 2;
return [startX, startY, nWidth, nHeight];
}
/**
* 计算字号
* @param {String} font 字体参数
* @returns 字号
*/
static getFontSize(font: string) {
let fonts = font.split(' ');
let fontSize = 20;
for (let i = 0; i < fonts.length; i++) {
if (fonts[i].indexOf('pt') !== -1) {
fontSize = Watermark.pt2px(parseFloat(fonts[i].slice(0, fonts[i].indexOf('pt'))));
}
if (fonts[i].indexOf('px') !== -1) {
fontSize = parseFloat(fonts[i].slice(0, fonts[i].indexOf('px')));
}
}
return fontSize;
}
/**
* 获取字符串长度中文(符号)为1 英文(符号)为0.5
* @param {Array} texts 字符串数组
* @returns 返回最大长度(用来计算间距或者位置)
*/
static getTextLong(texts: any[]) {
let maxCharLength = texts[0].length;
for (let item of texts) {
if (maxCharLength < item.length) {
maxCharLength = Watermark.analyString(item);
} else {
maxCharLength = Watermark.analyString(texts[0]);
}
}
return maxCharLength;
}
/**
* 解析字符串
* @param {String} text 字符串
* @returns 返回字符串长度
*/
static analyString(text: string | any[]) {
let length = 0;
const regCn =
/[\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]/;
for (let i = 0; i < text.length; i++) {
if (regCn.test(text[i])) {
length += 1;
} else {
length += 0.5;
}
}
return length;
}
// 求水印文字的宽高(包含换行)
static getTextConfig(text: string, font: any) {
let size = Watermark.getFontSize(font); // 获取字体大小
let texts = text.split('\n'); // 获取text进行分析有无换行\n 如果有则高度返回的是(\n的数量+1)宽度则为最多字数的行*字体大小
let cWidth = size * Watermark.getTextLong(texts);
let cHeight = size * texts.length;
return [cWidth, cHeight]; // 返回text节点集合与宽度高度
}
/**
* 获取图片的大小
* @param {String} base64 图片数据
* @returns 返回图片大小
*/
static getImageSize(base64: string) {
const image = new Image();
image.crossOrigin = '';
image.src = base64;
return new Promise((resolve, reject) => {
image.onload = function () {
const { width, height } = image;
resolve({ width, height });
};
});
}
}
/**
* 初始化单个水印
*/
class WatermarkSingle {
observer!: IntersectionObserver; // 监听水印是否正确加载
options: WatermarkOptions;
isPrint: boolean = false;
funStatic = Watermark;
constructor(options: WatermarkOptions, el = document.getElementById('brightness'), isPrint = false) {
this.options = options;
this.options.element = el;
this.isPrint = isPrint;
}
getRandomPlace(obj: Object) {
const values = Object.values(obj);
const randomIndex = Math.floor(Math.random() * values.length);
return values[randomIndex];
}
getOffset(offset: string) {
let [x, y] = offset.split(',');
return [this.funStatic.mm2px(parseFloat(x), this.isPrint), this.funStatic.mm2px(parseFloat(y), this.isPrint)];
}
waterListener() {
const _el = this.options.element;
if (!_el) return;
this.observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
// 在可视区内时,重新初始化水印
_el.innerHTML = '';
this.initWatermarkSingle();
}
},
{ threshold: 0.2 },
);
this.observer.observe(_el);
}
async initWatermarkSingle() {
const options = this.options as any;
// SVG容器
const wmPatternId = Math.random().toString(36).substr(2);
const svgEle = options.svgEle ?? this.funStatic.createNS('svg', 'watermarkSingle' + wmPatternId);
const gEle = this.funStatic.createNS('g', 'watermarkSingleG' + wmPatternId);
svgEle.appendChild(gEle);
// svg的viewBox计算为el的宽与高
// 需要判断是否为全屏水印,若为全屏水印则需要计算出全屏的宽高,若为文档水印则需要计算出文档的宽高
if (options.element?.id === 'brightness' || this.isPrint) {
this.funStatic.isPageMrk && this.funStatic.setAttr(
svgEle,
'viewBox',
`0 0 ${options.element.clientWidth} ${options.element.clientHeight}`,
);
} else {
const boxw = options.dw ? this.funStatic.mm2px(options.dw, this.isPrint) : options.element.clientWidth;
const boxh = options.dh ? this.funStatic.mm2px(options.dh, this.isPrint) : options.element.clientHeight;
this.funStatic.isPageMrk && this.funStatic.setAttr(svgEle, 'viewBox', `0 0 ${boxw} ${boxh}`);
}
// 水印X,Y轴偏移量
const offset = (options.offset?.split(',') ?? [0, 0]).map(Number);
// 水印显示位置
let realPlace = options.place;
// 如果place为random 则随机获取一个位置
if (realPlace === 'random') {
realPlace = this.getRandomPlace(place);
}
if (options.type === 'image') {
// 图片水印
const imgUrl = 'https://cdn.jsdelivr.net/gh/zhangpanfei/static@demo/img/test.jpg'; // 模拟图片
// const imgUrl = 'data:image/' + options.image?.type + ';base64,' + options.image?.data;
const _size = options.image.size ?? [100, 50];
const imageWidth = this.funStatic.mm2px(_size[0], this.isPrint);
const imageHeight = this.funStatic.mm2px(_size[1], this.isPrint);
const imageEle = this.funStatic.createNS('image');
const _opacity = this.options.image?.nOpacity ?? 100;
imageEle.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', imgUrl);
imageEle.style.opacity = (100 - _opacity) / 100 + '';
let _scale = '';
await this.funStatic.getImageSize(imgUrl).then((res: any) => {
_scale = imageWidth / res.width + ',' + imageHeight / res.height;
this.funStatic.setAttr(
imageEle,
'transform',
'scale(' + imageWidth / res.width + ',' + imageHeight / res.height + ')',
);
});
gEle.appendChild(imageEle);
const t = (gEle as any).getBBox(); // 容器
if (t.width === 0 && t.height === 0 && t.x === 0 && t.y === 0) {
// 监听不在可视区内的元素
this.waterListener();
} else {
// 若已加载 则取消监听
this.observer?.disconnect();
}
const _rotate = this.options.rotate ?? 0;
const regc = this.funStatic.getStart(t.width, t.height, +_rotate);
const bound = (svgEle.getAttribute('viewBox')?.split(' ') ?? [0, 0, 0, 0]).map(Number);
// x y 偏移量
const x = this.funStatic.mm2px(offset[0], this.isPrint),
y = this.funStatic.mm2px(offset[1], this.isPrint);
let textX = 0,
textY = 0,
textTransform = '';
switch (realPlace) {
case place.TL:
// 始终沿着左侧中心点顺时针旋转
textX = x + regc[0];
textY = y + regc[1];
textTransform = `rotate(${this.options.rotate} ${x + regc[2] / 2} ${y + regc[3] / 2}) translate(${textX} ${textY}) scale(${_scale})`;
break;
case place.TC:
// 始终沿着中心点Y轴旋转
textX = bound[2] / 2 - t.width / 2 + x;
textY = y + regc[3] / 2 - t.height / 2;
textTransform = `rotate(${this.options.rotate} ${textX + t.width / 2} ${regc[3] / 2 + y}) translate(${textX} ${textY}) scale(${_scale})`;
break;
case place.TR:
// 始终沿着右侧中心点逆时针旋转
textX = bound[2] + x - regc[2] + regc[0];
textY = y + regc[1];
textTransform = `rotate(${this.options.rotate} ${bound[2] + x - regc[2] / 2} ${regc[3] / 2 + y}) translate(${textX} ${textY}) scale(${_scale})`;
break;
case place.ML:
// 始终沿着左侧X轴变动
textX = x + regc[0];
textY = bound[3] / 2 + y - t.height / 2;
textTransform = `rotate(${this.options.rotate} ${x + regc[2] / 2} ${bound[3] / 2 + y}) translate(${textX} ${textY}) scale(${_scale})`;
break;
case place.MC:
// 始终沿着中心点旋转
const xt = bound[2] / 2 + x;
const yt = bound[3] / 2 + y;
textX = xt - t.width / 2;
textY = yt - t.height / 2;
textTransform = `rotate(${this.options.rotate} ${xt} ${yt}) translate(${textX} ${textY}) scale(${_scale})`;
break;
case place.MR:
// 始终沿着右侧x轴变动
textX = bound[2] + x - regc[2] + regc[0];
textY = bound[3] / 2 + y - t.height / 2;
textTransform = `rotate(${this.options.rotate} ${bound[2] + x - regc[2] / 2} ${bound[3] / 2 + y}) translate(${textX} ${textY}) scale(${_scale})`;
break;
case place.BL:
// 始终沿着左侧中心点顺时针旋转
textX = x + regc[0];
textY = bound[3] + y - regc[1] - t.height;
textTransform = `rotate(${this.options.rotate} ${x + regc[2] / 2} ${bound[3] + y - regc[3] / 2}) translate(${textX} ${textY}) scale(${_scale})`;
break;
case place.BC:
// 始终沿着Y轴变动
textX = bound[2] / 2 - t.width / 2 + x;
textY = bound[3] + y - regc[1] - t.height;
textTransform = `rotate(${this.options.rotate} ${textX + t.width / 2} ${
bound[3] + y - regc[3] / 2
}) translate(${textX} ${textY}) scale(${_scale})`;
break;
case place.BR:
// 始终沿着右侧中心点逆时针旋转
textX = bound[2] + x - regc[2] + regc[0];
textY = bound[3] + y - t.height - regc[1];
textTransform = `rotate(${this.options.rotate} ${bound[2] + x - regc[2] / 2} ${
bound[3] + y - regc[3] / 2
}) translate(${textX} ${textY}) scale(${_scale})`;
break;
}
imageEle.setAttribute('transform', textTransform);
} else {
// 字体大小
const fontSize = this.funStatic.getFontSize(options.font);
const width: number = this.funStatic.getTextConfig(options.value, options.font)[0];
const height: number = this.funStatic.getTextConfig(options.value, options.font)[1];
options.width = width;
options.height = height;
const textArr = options.value.split('\n');
// 初始位置
const x = this.funStatic.mm2px(offset[0], this.isPrint),
y = this.funStatic.mm2px(offset[1], this.isPrint);
// 多行文字处理旋转
const textEle = this.funStatic.createNS('text') as any;
textEle.setAttribute('fill', options.fillStyle);
this.funStatic.setAttr(textEle, 'letter-spacing', fontSize / 10 + 'pt');
textEle.style.font = options.font;
gEle.appendChild(textEle);
for (let i = 0; i < textArr.length; i++) {
const tspanEle = this.funStatic.createNS('tspan') as any;
this.funStatic.setAttr(tspanEle, 'y', fontSize * (i + 1));
this.funStatic.setAttr(tspanEle, 'x', x);
this.funStatic.setAttr(tspanEle, 'font-size', fontSize);
tspanEle.innerHTML = textArr[i];
textEle.appendChild(tspanEle);
}
const t = textEle.getBBox(); // 容器
if (t.width === 0 && t.height === 0 && t.x === 0 && t.y === 0) {
// 监听不在可视区内的元素
this.waterListener();
} else {
// 若已加载 则取消监听
this.observer?.disconnect();
}
const child = textEle.children;
const bound = (svgEle.getAttribute('viewBox')?.split(' ') ?? [0, 0, 0, 0]).map(Number);
for (let i = 0; i < child.length; i++) {
const ele = child[i];
const regc = this.funStatic.getStart(t.width, t.height, options.rotate);
let textX = 0,
textY = 0,
textTransform = '',
eleX = 0,
eleY = 0;
switch (realPlace) {
case place.TL:
// 始终沿着左侧中心点顺时针旋转
textX = x + regc[0];
textY = y + regc[1];
textTransform = `rotate(${options.rotate} ${regc[2] / 2 + x} ${regc[3] / 2 + y})`;
eleX = regc[0] + x;
eleY = regc[1] + y + fontSize * (i + 1);
break;
case place.TC:
// 始终沿着中心点Y轴旋转
const xt = bound[2] / 2 - t.width / 2;
textX = xt + x;
textY = y + regc[3] / 2 - t.height / 2;
textTransform = `rotate(${options.rotate} ${xt + t.width / 2 + x} ${regc[3] / 2 + y})`;
eleX = xt + x;
eleY = regc[1] + y + fontSize * (i + 1);
break;
case place.TR:
// 始终沿着右侧中心点逆时针旋转
textX = bound[2] - regc[2] + regc[0] + x;
textY = regc[1] + y;
textTransform = `rotate(${options.rotate} ${bound[2] - regc[2] / 2 + x} ${
regc[3] / 2 + y
})`;
eleX = bound[2] - regc[2] + regc[0] + x;
eleY = regc[1] + y + fontSize * (i + 1);
break;
case place.ML:
// 始终沿着左侧X轴变动
textX = x + regc[0];
textY = bound[3] / 2 + y;
textTransform = `rotate(${options.rotate} ${x + regc[2] / 2} ${textY})`;
eleX = x + regc[0];
eleY = textY - t.height / 2 + fontSize * (i + 1);
break;
case place.MC:
// 始终沿着中心点旋转
const yt = bound[3] / 2 + y;
textX = bound[2] / 2 + x;
textY = yt - t.height;
textTransform = `rotate(${options.rotate} ${textX} ${yt})`;
eleX = textX - t.width / 2;
eleY = yt - t.height / 2 + fontSize * (i + 1);
break;
case place.MR:
// 始终沿着右侧x轴变动
textX = bound[2] + x - regc[2] + regc[0];
textY = bound[3] / 2 + y;
textTransform = `rotate(${options.rotate} ${bound[2] + x - regc[2] / 2} ${textY})`;
eleX = bound[2] + x - regc[2] + regc[0];
eleY = textY - t.height / 2 + fontSize * (i + 1);
break;
case place.BL:
// 始终沿着左侧中心点顺时针旋转
textX = x + regc[0];
textY = bound[3] + y - regc[1];
textTransform = `rotate(${options.rotate} ${regc[2] / 2 + x} ${
bound[3] + y - regc[3] / 2
})`;
eleX = regc[0] + x;
eleY = bound[3] + y - regc[1] - t.height + fontSize * (i + 1);
break;
case place.BC:
// 始终沿着Y轴变动
textX = bound[2] / 2 - t.width / 2 + x;
textY = bound[3] + y - regc[1];
textTransform = `rotate(${options.rotate} ${textX + t.width / 2} ${
bound[3] + y - regc[3] / 2
})`;
eleX = bound[2] / 2 - t.width / 2 + x;
eleY = bound[3] + y - regc[1] - t.height + fontSize * (i + 1);
break;
case place.BR:
// 始终沿着右侧中心点逆时针旋转
textX = bound[2] + x - regc[2] + regc[0];
textY = bound[3] + y - regc[1];
textTransform = `rotate(${options.rotate} ${bound[2] + x - regc[2] / 2} ${
bound[3] + y - regc[3] / 2
})`;
eleX = bound[2] + x - regc[2] + regc[0];
eleY = bound[3] + y - t.height - regc[1] + fontSize * (i + 1);
break;
default:
break;
}
textEle.setAttribute('x', textX);
textEle.setAttribute('y', textY);
textEle.setAttribute('transform', textTransform);
ele.setAttribute('x', eleX);
ele.setAttribute('y', eleY);
}
}
}
}
class WatermarkSvg {
options: WatermarkOptions;
isPrint: boolean = false;
funStatic = Watermark;
constructor(options: WatermarkOptions, el = document.getElementById('brightness'), isPrint = false) {
this.options = options;
this.options.element = el;
this.isPrint = isPrint;
}
async initWatermark() {
const options = this.options as any;
// 横向间距
const ph = parseFloat(options.horizontal);
// 纵向间距
const pv = parseFloat(options.vertical);
const that = this;
// 设置svg标签的样式等
const wmPatternId = Math.random().toString(36).substr(2);
const svgEle = options.svgEle ?? this.funStatic.createNS('svg', 'watermark' + wmPatternId);
this.funStatic.setAttr(svgEle, 'width', '100%');
this.funStatic.setAttr(svgEle, 'height', '100%');
const gEle = this.funStatic.createNS('g', 'watermarkG' + wmPatternId);
svgEle.appendChild(gEle);
// svg的viewBox计算为el的宽与高
// 需要判断是否为全屏水印,若为全屏水印则需要计算出全屏的宽高,若为文档水印则需要计算出文档的宽高
if (options.element.id === 'brightness' || this.isPrint) {
this.funStatic.isPageMrk && this.funStatic.setAttr(
svgEle,
'viewBox',
`0 0 ${options.element.clientWidth} ${options.element.clientHeight}`,
);
} else {
const boxw = options.dw ? this.funStatic.mm2px(options.dw, this.isPrint) : options.element.clientWidth;
const boxh = options.dh ? this.funStatic.mm2px(options.dh, this.isPrint) : options.element.clientHeight;
this.funStatic.isPageMrk && this.funStatic.setAttr(svgEle, 'viewBox', `0 0 ${boxw} ${boxh}`);
}
svgEle.style.cssText =
'position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;overflow:hidden;user-select:none;';
// 设置平铺的样式
const patternEle = this.funStatic.createNS('pattern');
this.funStatic.setAttr(patternEle, 'patternUnits', 'userSpaceOnUse');
this.funStatic.setAttr(patternEle, 'id', `watermarkPattern${wmPatternId}`);
this.funStatic.setAttr(patternEle, 'patternTransform', `rotate(${options.rotate})`);
// 设置平铺的矩形
const rectEle = this.funStatic.createNS('rect');
this.funStatic.setAttr(rectEle, 'fill', `url(#watermarkPattern${wmPatternId})`); // rect的填充为水印需要平铺元素的id(必须)
rectEle.style.cssText = 'pointer-events:none;';
this.funStatic.setAttr(rectEle, 'x', 0);
this.funStatic.setAttr(rectEle, 'y', 0);
this.funStatic.setAttr(rectEle, 'width', '100%');
this.funStatic.setAttr(rectEle, 'height', '100%');
if (options.type === 'image') {
// 图片水印间距由前端控制 单位为px
options.horizontal = ph;
options.vertical = pv;
// 图片水印
const imgUrl = "https://cdn.jsdelivr.net/gh/zhangpanfei/static@demo/img/test.jpg"; // 模拟图片
// const imgUrl = 'data:image/' + options.image.type + ';base64,' + options.image.data;
const imageWidth = this.funStatic.mm2px(options.image.size[0], this.isPrint);
const imageHeight = this.funStatic.mm2px(options.image.size[1], this.isPrint);
// 平铺宽度计算为按图片的宽度之后再加上横向间距
let patternWidth = imageWidth + options.horizontal;
// 平铺的高度计算为图片的高度之后再加上纵向间距
let patternHeight = imageHeight + options.vertical;
this.funStatic.setAttr(patternEle, 'width', patternWidth);
this.funStatic.setAttr(patternEle, 'height', patternHeight);
const imageEle = this.funStatic.createNS('image');
imageEle.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', imgUrl);
imageEle.style.opacity = (100 - options.image.nOpacity) / 100 + '';
await this.funStatic.getImageSize(imgUrl).then((res: any) => {
this.funStatic.setAttr(
imageEle,
'transform',
'scale(' + imageWidth / res.width + ',' + imageHeight / res.height + ')',
);
});
patternEle.appendChild(imageEle);
} else {
// 文字水印最小间距由后端控制 单位为mm
options.horizontal = this.funStatic.mm2px(ph, this.isPrint);
options.vertical = this.funStatic.mm2px(pv, this.isPrint);
let fontSize = this.funStatic.getFontSize(options.font);
let textArr = options.value.split('\n');
let textArrMaxLen = this.funStatic.getTextLong(textArr);
const textEle = this.funStatic.createNS('text');
// 平铺宽度计算为字体大小加一之后乘以最长字符串的长度再加上横向间距
let patternWidth =
fontSize * textArrMaxLen + this.funStatic.pt2px((fontSize / 10) * textArrMaxLen) + options.horizontal;
// 平铺的高度计算为字体大小加一之后乘以行数再加上纵向间距
let patternHeight = fontSize * textArr.length + options.vertical;
this.funStatic.setAttr(patternEle, 'width', patternWidth);
this.funStatic.setAttr(patternEle, 'height', patternHeight);
for (let i = 0; i < textArr.length; i++) {
let tspanEle = this.funStatic.createNS('tspan');
this.funStatic.setAttr(tspanEle, 'y', fontSize * (i + 1));
this.funStatic.setAttr(tspanEle, 'x', 0);
tspanEle.innerHTML = textArr[i];
textEle.appendChild(tspanEle);
}
textEle.style.cssText = `font:${options.font};fill:${
options.fillStyle
};text-anchor:start;font-size:${fontSize}px;letter-spacing:${fontSize / 10}pt`;
patternEle.appendChild(textEle);
}
gEle.appendChild(patternEle);
gEle.appendChild(rectEle);
options.element.appendChild(svgEle);
}
}
以图片为基底的九宫格水印待更新【有点累了,暂时不想写了】......