UNPKG

@acrodata/watermark

Version:
442 lines (435 loc) 16.1 kB
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