@acrodata/watermark
Version:
Add watermark to your page
442 lines (435 loc) • 16.1 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, ElementRef, NgZone, booleanAttribute, numberAttribute, Directive, Input } from '@angular/core';
/** 用于标记是否需要保护 */
const attributeNameTag = 'data-watermark-tag';
const observeOptions = {
childList: true,
subtree: true,
attributeFilter: ['style', 'class', attributeNameTag],
};
/** 获取 DataSetKey */
function getDataSetKey(attributeName) {
return attributeName
.split('-')
.slice(1)
.reduce((prev, cur, index) => {
if (index === 0) {
return cur;
}
return `${prev}${cur[0].toUpperCase() + cur.slice(1)}`;
});
}
/** 将样式对象转换为字符串 */
const getStyleStr = (style) => {
let str = '';
Object.keys(style).forEach(key => {
const k = key.replace(/([A-Z])/g, '-$1').toLowerCase();
if (style[key] !== '' && style[key] != null) {
str += `${k}:${style[key]};`;
}
});
return str;
};
/** 创建随机 ID */
const getRandomId = (prefix = '') => {
const uid = window.btoa(decodeURI(encodeURIComponent(prefix)));
return `${uid}-${new Date().getTime()}-${Math.floor(Math.random() * Math.pow(10, 8))}`;
};
/** 获取水印挂载节点 */
const getContainer = (container) => {
let dom;
if (typeof container === 'string') {
dom = document.querySelector(container);
if (!dom) {
throw new Error(`The watermark container element '${container}' not found!`);
}
}
else {
dom = container ?? document.body;
}
return dom;
};
/** 盲水印解密 */
const decrypt = (ctx) => {
const originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const data = originalData.data;
for (let i = 0; i < data.length; i++) {
// 筛选每个像素点的R值
if (i % 4 == 0) {
if (data[i] % 2 == 0) {
// 如果 R 值为偶数,说明这个点是没有水印信息的,将其 R 值设为0
data[i] = 0;
}
else {
// 如果 R 值为奇数,说明这个点是有水印信息的,将其 R 值设为255
data[i] = 255;
}
}
else if (i % 4 == 3) {
// 透明度不作处理
continue;
}
else {
// G、B 值设置为 0,不影响
data[i] = 0;
}
}
// 至此,带有水印信息的点都将展示为 `255,0,0`,而没有水印信息的点将展示为 `0,0,0`,将结果绘制到画布
ctx.putImageData(originalData, 0, 0);
};
const createHost = (watermarkTag) => {
const dom = document.createElement('div');
// 可以隐藏元素的 CSS 属性
const hiddenCSS = {
'display': 'block !important',
'position': 'static !important',
'opacity': '1 !important',
'visibility': 'visible !important',
'transform': 'none !important',
'clip-path': 'none !important',
};
dom.setAttribute('style', getStyleStr(hiddenCSS));
dom.setAttribute(attributeNameTag, watermarkTag);
return dom;
};
function getDrawPattern(opts) {
const { text, gapX, gapY, offsetY, offsetX, width, height, rotate, opacity, fontSize, fontStyle, fontVariant, fontWeight, fontFamily, fontColor, textAlign, textBaseline, image, blindText, blindFontSize, blindOpacity, } = opts;
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const ratio = 1;
const canvasWidth = (Number(gapX) + Number(width)) * ratio;
const canvasHeight = (Number(gapY) + Number(height)) * ratio;
const canvasOffsetLeft = Number(offsetX) || Number(gapX) / 2;
const canvasOffsetTop = Number(offsetY) || Number(gapY) / 2;
canvas.setAttribute('width', `${canvasWidth}px`);
canvas.setAttribute('height', `${canvasHeight}px`);
if (ctx) {
const markWidth = width * ratio;
const markHeight = height * ratio;
// 1. 根据元素中心点旋转
ctx.translate(canvasOffsetLeft * ratio, canvasOffsetTop * ratio);
ctx.translate(markWidth / 2, markHeight / 2); // 1
ctx.rotate((Math.PI / 180) * Number(rotate));
ctx.translate(-markWidth / 2, -markHeight / 2); // 1
// 是否需要增加盲水印文字
if (blindText) {
// 盲水印需要低透明度
ctx.globalAlpha = blindOpacity;
ctx.font = `${blindFontSize}px normal`;
ctx.fillText(blindText, 0, 0);
}
// 设置透明度
ctx.globalAlpha = opacity;
// 优先使用图片
if (image) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.referrerPolicy = 'no-referrer';
img.src = image;
img.onload = () => {
ctx.drawImage(img, 0, 0, markWidth, markHeight);
resolve({
url: ctx.canvas.toDataURL(),
width: canvasWidth,
height: canvasHeight,
});
};
return;
}
// 获取文本的最大宽度
const texts = typeof text === 'string' ? text.split('\n') : text;
const widths = texts.map(item => ctx.measureText(item).width);
const maxWidth = Math.max(...widths);
const markSize = Number(fontSize) * ratio;
// 设置文本对齐方式
ctx.textAlign = textAlign;
// 设置文本位置
ctx.textBaseline = textBaseline;
// 设置字体颜色
ctx.fillStyle = fontColor;
// 设置字体
ctx.font = getFont(`${markSize}px`);
// 文案宽度大于画板宽度
if (maxWidth > width) {
ctx.font = getFont(`${markSize / 2}px`);
}
// 多行文本的上下间距
const textGap = 4;
// 获取行高
const lineHeight = markSize;
// 计算水印在 y 轴上的初始位置
let initY = (markHeight - (fontSize + 4) * texts.length - textGap * (texts.length - 1)) / 2;
initY = initY < 0 ? 0 : initY;
for (let i = 0; i < texts.length; i++) {
ctx.fillText(texts[i] || '', markWidth / 2, initY + lineHeight * (i + 1) + textGap * i);
}
resolve({
url: ctx.canvas.toDataURL(),
width: canvasWidth,
height: canvasHeight,
});
}
function getFont(fontSize) {
return `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize} ${fontFamily}`;
}
return reject();
});
}
const defaultOptions = {
gapX: 100,
gapY: 100,
offsetX: 0,
offsetY: 0,
width: 120,
height: 60,
opacity: 0.15,
rotate: -24,
fontSize: 16,
fontWeight: '400',
fontStyle: 'normal',
fontVariant: 'normal',
fontColor: '#000',
fontFamily: 'sans-serif',
textAlign: 'center',
textBaseline: 'alphabetic',
secure: true,
blindFontSize: 16,
blindOpacity: 0.005,
repeat: 'multiply',
zIndex: 9999,
};
class Watermark {
/** 水印配置 */
options = {};
/** 水印挂载容器 */
container;
/** 水印的宿主节点 */
watermarkHost;
/** 水印节点 */
watermarkDom;
/** 水印样式 */
style = {
pointerEvents: 'none',
position: 'absolute',
inset: 0,
};
watermarkTag = getRandomId('watermark');
shadowRoot;
mutationObserver = null;
constructor(options = {}) {
this.options = Object.assign({}, defaultOptions, options);
this._render();
}
static decrypt = decrypt;
update(options = {}) {
this.options = {
...this.options,
...options,
};
this._render();
}
show() {
if (this.watermarkDom) {
this.style['display'] = 'block';
this.watermarkDom.setAttribute('style', getStyleStr(this.style));
}
}
hide() {
if (this.watermarkDom) {
this.style['display'] = 'none';
this.watermarkDom.setAttribute('style', getStyleStr(this.style));
}
}
destroy() {
this.shadowRoot = undefined;
if (this.watermarkHost) {
this.watermarkHost.remove();
this.watermarkHost = undefined;
}
if (this.watermarkDom) {
this.watermarkDom.remove();
this.watermarkDom = undefined;
}
this._destroyMutationObserver();
}
_shouldRerender = (mutation) => {
// 修改样式或属性
if (mutation.type === 'attributes') {
if (mutation.attributeName === attributeNameTag) {
return true;
}
if (this.watermarkTag === this._getNodeRandomId(mutation.target)) {
return true;
}
}
// 删除节点
if (mutation.removedNodes.length &&
this.watermarkTag === this._getNodeRandomId(mutation.removedNodes[0])) {
return true;
}
return false;
};
_getNodeRandomId = (node) => {
return node?.dataset?.[getDataSetKey(attributeNameTag)];
};
_destroyMutationObserver = () => {
if (this.mutationObserver) {
this.mutationObserver.takeRecords();
this.mutationObserver.disconnect();
this.mutationObserver = null;
}
};
_getWatermarkDom = async () => {
if (!this.watermarkDom) {
this.watermarkDom = document.createElement('div');
}
const bgConfig = await getDrawPattern(this.options);
if (bgConfig?.url) {
const bgImg = bgConfig.url;
this.style['zIndex'] = this.options.zIndex;
if (this.options.repeat === 'multiply') {
this.style['backgroundImage'] = `url(${bgImg}), url(${bgImg})`;
this.style['backgroundRepeat'] = 'repeat';
this.style['backgroundPosition'] = `${bgConfig.width / 2}px ${bgConfig.height / 2}px, 0 0`;
}
else {
this.style['backgroundImage'] = `url(${bgImg})`;
this.style['backgroundRepeat'] = 'repeat';
this.style['backgroundPosition'] = '';
if (this.options.repeat === 'none') {
this.style['backgroundRepeat'] = 'no-repeat';
this.style['backgroundPosition'] = this.options.position || 'center';
}
}
if (!this.options.container) {
this.style['position'] = 'fixed';
}
if (this.options.scrollHeight) {
const height = this.options.scrollHeight;
this.style['height'] = isNaN(Number(height)) ? height : height + 'px';
}
this.watermarkDom.setAttribute('style', getStyleStr(this.style));
}
this.watermarkDom.setAttribute(attributeNameTag, this.watermarkTag);
return this.watermarkDom;
};
async _render() {
this._destroyMutationObserver();
// 获取水印挂载节点
this.container = getContainer(this.options.container);
// 获取水印父节点
if (!this.watermarkHost) {
this.watermarkHost = createHost(this.watermarkTag);
this.container.append(this.watermarkHost);
}
// 获取水印 DOM
this.watermarkDom = await this._getWatermarkDom();
// 删除已有水印
if (this.watermarkHost) {
const children = this.watermarkHost.childNodes || [];
children.forEach(child => {
this.watermarkHost.removeChild(child);
});
}
// 判断是否支持 Shadow DOM
if (typeof this.watermarkHost.attachShadow === 'function') {
if (!this.shadowRoot) {
this.shadowRoot = this.watermarkHost.attachShadow({ mode: 'open' });
}
}
else {
this.shadowRoot = this.watermarkHost;
}
this.shadowRoot.append(this.watermarkDom);
if (MutationObserver && this.options.secure) {
this.mutationObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (this._shouldRerender(mutation)) {
this.destroy();
this._render();
return;
}
});
});
this.mutationObserver.observe(this.container, observeOptions);
if (this.shadowRoot) {
this.mutationObserver.observe(this.shadowRoot, observeOptions);
}
}
}
}
class WatermarkDirective {
options = {};
container;
secure = true;
zIndex = 9999;
scrollHeight;
_elementRef = inject(ElementRef);
_ngZone = inject(NgZone);
_watermark;
ngOnInit() {
const el = this._elementRef.nativeElement;
this._watermark = this._ngZone.runOutsideAngular(() => new Watermark({
...this.options,
container: this.container || (el.childNodes.length > 0 ? el : null),
secure: this.secure,
zIndex: this.zIndex,
scrollHeight: this.scrollHeight,
}));
}
ngOnChanges() {
this.update(this.options);
}
ngOnDestroy() {
this.destroy();
}
update(options) {
this._watermark?.update(options);
}
show() {
this._watermark?.show();
}
hide() {
this._watermark?.hide();
}
destroy() {
this._watermark?.destroy();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: WatermarkDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "18.2.7", type: WatermarkDirective, isStandalone: true, selector: "[watermark]", inputs: { options: ["watermarkOptions", "options"], container: ["watermarkContainer", "container"], secure: ["watermarkSecure", "secure", booleanAttribute], zIndex: ["watermarkZIndex", "zIndex", numberAttribute], scrollHeight: ["watermarkScrollHeight", "scrollHeight"] }, host: { properties: { "style.position": "\"relative\"" } }, exportAs: ["watermark"], usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: WatermarkDirective, decorators: [{
type: Directive,
args: [{
selector: '[watermark]',
exportAs: 'watermark',
standalone: true,
host: {
'[style.position]': '"relative"',
},
}]
}], propDecorators: { options: [{
type: Input,
args: [{ alias: 'watermarkOptions' }]
}], container: [{
type: Input,
args: [{ alias: 'watermarkContainer' }]
}], secure: [{
type: Input,
args: [{ alias: 'watermarkSecure', transform: booleanAttribute }]
}], zIndex: [{
type: Input,
args: [{ alias: 'watermarkZIndex', transform: numberAttribute }]
}], scrollHeight: [{
type: Input,
args: [{ alias: 'watermarkScrollHeight' }]
}] } });
/*
* Public API Surface of watermark
*/
/**
* Generated bundle index. Do not edit.
*/
export { Watermark, WatermarkDirective, defaultOptions };
//# sourceMappingURL=acrodata-watermark.mjs.map