页面水印的实现以及防删除方案

news2024/11/25 10:48:04

水印相关

  • 引言
  • 绘制一个水印
  • 输出背景图
  • 封装一点点细节
  • 图片加水印
  • 防止水印删除
    • 问题
    • 解决方案

引言

在企业里为了防止信息泄露和保护知识产权,通常会在页面和图片上添加水印
前端页面水印的添加一般有这几种方式:dom 元素循环、canvas 输出背景图、svg 实现背景图、图片添加水印

dom 元素循环 性能太低也不优雅,一般不采用这种方式
svg 实现背景图 与 canvas 类似,且兼容性不如 canvas
图片添加水印 是针对在图片上加的水印

本篇文章重点讲一下canvas 输出背景图、以及防止删除的方案来生成水印方式

绘制一个水印

目标是实现页面上按照一定的排列展示的水印,那么首先要用 canvas 画出一个水印

我们先在 html 放一个 canvas 标签,对它进行一个绘制

<template>
  <canvas id="water"></canvas>
</template>
onMounted(() => {
  const canvas: HTMLCanvasElement = document.getElementById(
    'water'
  ) as HTMLCanvasElement
  canvas.width = 440
  canvas.height = 400
  const ctx = canvas.getContext('2d')
  if (ctx) {
    ctx.font = '60px PingFang SC'
    ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'
    ctx.rotate(-0.4)
    ctx.fillText('krryguo', 20, 280)
  }
})

页面效果如图:
请添加图片描述
一个水印画出来了,怎么衍生多个水印并添加到指定页面中呢?

输出背景图

直接循环多份代码绘制是不合理的。我们可以利用 background 属性 repeat 特性,将水印展示成多个且平铺在整个页面中

将 canvas 生成的画布输出成base64的字符串,来作为页面的背景图。那么 dom 结构可以不需要 canvas 元素了,动态生成即可

<template>
  <div class="water-mark">首页</div>
</template>
onMounted(() => {
  const canvas: HTMLCanvasElement = document.createElement('canvas')
  canvas.width = 440
  canvas.height = 400
  const ctx = canvas.getContext('2d')
  if (ctx) {
    ctx.font = '60px PingFang SC'
    ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'
    ctx.rotate(-0.4)
    ctx.fillText('krryguo', 40, 200)
  }
  const imgStr = canvas.toDataURL('image/png')

  const waterDom = document.getElementsByClassName(
    'water-mark'
  )[0] as HTMLElement
  waterDom.style.background = `url(${imgStr})`
})

实现的效果图:
请添加图片描述
看到这里,有朋友不高兴了,排列的太整齐,你这水印有问题啊~

最简单解决方式就直接绘制 两个斜对称排列 的水印即可

onMounted(() => {
  const canvas: HTMLCanvasElement = document.createElement('canvas')
  canvas.width = 880 // 原有的基础上增加一倍宽度
  canvas.height = 400
  const ctx = canvas.getContext('2d')
  if (ctx) {
    ctx.font = '60px PingFang SC'
    ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'
    ctx.rotate(-0.4)
    ctx.fillText('krryguo', 40, 200)
    ctx.fillText('krryguo', 350, 556) // 再画一个
  }
  const imgStr = canvas.toDataURL('image/png')

  const waterDom = document.getElementsByClassName(
    'water-mark'
  )[0] as HTMLElement
  waterDom.style.background = `url(${imgStr})`
})

再看效果图:
请添加图片描述

封装一点点细节

interface WatermarkOptions {
  // 宽度
  width?: number
  // 高度
  height?: number
  // 水印内容
  content?: string
  // 水印字体
  font?: string
  // 水印颜色
  color?: string
  // 透明度
  opacity?: number
  // 偏转角度
  degree?: number
  // 偏移量
  x1?: number
  y1?: number
  x2?: number
  y2?: number
}

const createWatermark = ({
  width = 880,
  height = 400,
  content = 'krryguo',
  font = '60px PingFang SC',
  color = 'rgba(156, 162, 169, 0.3)',
  opacity = 1,
  degree = -23,
  x1 = 40,
  y1 = 200,
  x2 = 350,
  y2 = 556
}: WatermarkOptions): string => {
  const canvas: HTMLCanvasElement = document.createElement('canvas')
  canvas.width = width
  canvas.height = height
  const ctx = canvas.getContext('2d')
  if (ctx) {
    ctx.font = font
    ctx.fillStyle = color
    ctx.globalAlpha = opacity
    // 顺时针旋转的弧度,计算公式: degree * Math.PI / 180
    ctx.rotate((degree * Math.PI) / 180)
    ctx.fillText(content, x1, y1)
    ctx.fillText(content, x2, y2)
  }
  return canvas.toDataURL('image/png')
}

const setWatermarkClass = (url: string, className: string): void => {
  const style = document.createElement('style')
  style.innerHTML = `.${className} {background-image: url(${url});}`
  document.head.appendChild(style)
}

可在有需要加水印的地方调用 setWatermarkClass,传入自定义配置和指定的 class 名,再在对应元素设置该 class 名即可加上水印

onMounted(() => {
  setWatermarkClass(
    createWatermark({
      content: 'krryblog'
    }),
    'my-water-mark'
  )
})
<template>
  <!-- 这里加上 my-water-mark 水印的类名 -->
  <div class="water-mark my-water-mark">首页</div>
</template>

图片加水印

同理,先读取图片,canvas 在图片上绘制水印

import waterUrl from '@/assets/water.jpeg'

const createImgWatermark = async (
  {
    url = '',
    textAlign = 'center',
    textBaseline = 'middle',
    font = '30px PingFang SC',
    fillStyle = '#fff',
    x = 120,
    y = 50,
    position = 'top-start'
  },
  content: string = '这是水印'
) => {
  const canvas: HTMLCanvasElement = document.createElement('canvas')
  const img = new Image()
  img.src = url
  img.setAttribute('crossOrigin', 'Anonymous')
  return new Promise((resolve) => {
    img.onload = () => {
      canvas.width = img.width
      canvas.height = img.height
      const ctx = canvas.getContext('2d')
      if (ctx) {
        ctx.drawImage(img, 0, 0)
        ctx.textAlign = textAlign
        ctx.textBaseline = textBaseline
        ctx.font = font
        ctx.fillStyle = fillStyle
        switch (position) {
          case 'top-end':
            x = img.width - x
            break
          case 'bottom-start':
            y = img.height - y
            break
          case 'bottom-end':
            x = img.width - x
            y = img.height - y
            break
        }
        ctx.fillText(content, x, y)
      }
      resolve(canvas.toDataURL())
    }
  })
}

const setImgWatermark = (url: string, dom: HTMLImageElement) => {
  dom.src = url
}

onMounted(async () => {
  const url = await createImgWatermark({
    url: waterUrl,
    font: '50px PingFang SC',
    x: 160,
    y: 70,
    position: 'bottom-end'
  })
  setImgWatermark(url, document.querySelector('img') as HTMLImageElement)
})
<template>
  <img width="600" />
</template>

效果图:
请添加图片描述

防止水印删除

前端生成水印的安全性是很弱的,懂点前端知识的人都会打开控制台修改去掉水印

这里提供一个方案,禁止用户删除 class 来防止水印删除

window 提供了一个监听器MutationObserver:监视对 DOM 树所做更改的能力
API 方法

  • disconnect()
    阻止 MutationObserver 实例继续接收的通知,直到再次调用其 observe()方法,该观察者对象包含的回调函数都不会再被调用
  • observe()
    配置 MutationObserver 在 DOM 更改匹配给定选项时,通过其回调函数开始接收通知
  • takeRecords()
    从 MutationObserver 的通知队列中删除所有待处理的通知,并将它们返回到 MutationRecord 对象的新 Array 中

先获取有水印类名的NodeList,遍历所有元素为其添加一个MutationObserver监听器来监听 dom 属性的变化,获取目标元素的classList,若不存在水印的 class 类名,就执行添加。执行添加之后要立刻暂停监听disconnect(),防止【添加】操作又触发监听器,最后再执行observe() 重新观察

// 添加监听器
const addListioner = (className: string) => {
  const MutationObserver = window.MutationObserver
  // 获取所有添加了水印类名的 dom
  const containerList: NodeListOf<HTMLElement> = document.querySelectorAll(
    `.${className}`
  )
  if (MutationObserver) {
    containerList.forEach((container) => {
      let observer = new MutationObserver(() => {
        // 获取 class 集合
        const classList: DOMTokenList = container.classList
        if (!Object.values(classList).includes(className)) {
          // 如果 classList 中不存在水印的类名,就重新添加
          container.classList.add(className)
          // 暂停监听,防止上面的操作又触发监听器
          observer.disconnect()
          // 然后再重新开始观察
          addObserve(observer, container)
        }
      })
      // 每个元素开启观察
      addObserve(observer, container)
    })
  }
}
// 开启观察
const addObserve = (mutation: MutationObserver, container: Element) => {
  mutation.observe(container, {
    // 观察器的配置,需要观察属性的变动
    attributes: true
  })
}

然后在 onMounted 添加一下监听器

onMounted(async () => {
  // TODO ...
  addListioner('my-water-mark')
})

问题

但是这又有一个问题,用户可以在控制台改变水印 class 里面的样式,而 MutationObserver 无法监听。如图:
请添加图片描述
这样水印说没就直接没啦

解决方案

解决方法:可以使用 style 属性 渲染水印,即 内联样式 ,这样若样式改变了就说明 dom 属性改变,也就可以监听到了

需要注意的是:要设置 !important,把优先级提到最高防止被恶意覆盖,后面的监听也要加上优先级的判断

onMounted(async () => {
  const bgUrl = createWatermark({
    content: 'krryblog'
  })

  // 使用 style 属性渲染水印
  const dom = document.querySelector('.water-mark-style') as HTMLElement
  // 设置样式优先级最高
  dom.style.setProperty('background-image', `url(${bgUrl})`, 'important')
})

缺点是控制台查看 dom 结构会有一大坨样式在这里…
请添加图片描述
最后再加上监听 顺带整合了 class 类名渲染、style 属性渲染两种监听方法

interface StyleType {
  key: string
  value: string
}

onMounted(async () => {
  const bgUrl = createWatermark({
    content: 'krryblog'
  })

  // 使用 style 属性渲染水印
  const dom = document.querySelector('.water-mark-style') as HTMLElement
  // 设置样式优先级最高
  dom.style.setProperty('background-image', `url(${bgUrl})`, 'important')
  addListioner('water-mark-style', {
    key: 'background-image',
    value: `url("${bgUrl}")` // js 获取的样式值 url 里面加了 "",所以这里加上比对
  })
})

// 添加监听器
const addListioner = (className: string, style?: StyleType) => {
  const MutationObserver = window.MutationObserver
  // 获取所有添加了水印类名的 dom
  const containerList: NodeListOf<HTMLElement> = document.querySelectorAll(
    `.${className}`
  )
  if (MutationObserver) {
    containerList.forEach((container: HTMLElement) => {
      // 每个元素监听
      const observer = new MutationObserver(() => {
        let flag = false // 触发改变的标识
        if (style) {
          // style 属性渲染水印
          // 获取 style 属性
          const styleCss: CSSStyleDeclaration = container.style
          // 需要比对样式是否存在、样式值是否相同、样式优先级是否最高
          if (
            !styleCss.getPropertyValue(style.key) ||
            styleCss.getPropertyValue(style.key) !== style.value ||
            styleCss.getPropertyPriority(style.key) !== 'important'
          ) {
            // 重新设置样式
            styleCss.setProperty(style.key, style.value, 'important')
            flag = true
          }
        } else {
          // class 类名渲染水印
          // 获取 class 集合
          const classList: DOMTokenList = container.classList
          if (!Object.values(classList).includes(className)) {
            // 如果 classList 中不存在水印的类名,就重新添加
            container.classList.add(className)
            flag = true
          }
        }
        if (flag) {
          // 暂停监听,防止上面的操作又触发监听器
          observer.disconnect()
          // 然后再重新开始观察
          addObserve(observer, container)
        }
      })
      // 每个元素开启观察
      addObserve(observer, container)
    })
  }
}
// 开启观察
const addObserve = (mutation: MutationObserver, container: Element) => {
  mutation.observe(container, {
    // 观察器的配置,需要观察属性的变动
    attributes: true
  })
}

监听效果查看:
请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2122617.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vite项目配置本地开发使用https访问

在Vite项目中启用HTTPS以安全地使用navigator.mediaDevices.getUserMedia() 引言 在现代Web开发中&#xff0c;保护用户隐私和数据安全是至关重要的。特别是在涉及到媒体捕获功能&#xff0c;如使用用户的摄像头或麦克风时&#xff0c;Web应用需要遵循严格的安全准则。naviga…

《生成式人工智能行业自律倡议》发布,BAT、华为小米 OPPO 荣耀、北大清华等参编

8 月 29 日,在成都举办的 2024 年中国网络文明大会上,《生成式人工智能行业自律倡议》正式发布。 《生成式人工智能行业自律倡议》从保障数据和算法模型安全合规、促进内容生态建设、追求技术创新与质量提升、遵循价值观与伦理道德标准和促进交流合作与开放共建等方面发出行…

Marin说PCB之在CST软件中如何搭建两端子电容器--01

今天是教师节&#xff0c;小编首先祝愿所有的老师们节日快乐&#xff0c;当然还有我的那些国外的老师们&#xff0c;道友们懂得都懂啊&#xff0c;我就不说破了&#xff0c;都毕业很多年了&#xff0c;小编我还是很怀念大学的时光的&#xff0c;毕竟那个时候我也是有很多女粉丝…

CCOS2024盛大举办, 四川眼科医院专家亮相盛会并作精彩分享

名医荟萃&#xff0c;共襄盛举&#xff1b;学术争鸣&#xff0c;共话未来。9月4日-8日&#xff0c;中华医学会第二十八次眼科学术大会&#xff08;CCOS2024&#xff09;在武汉国际会议中心和武汉国际博览中心隆重举行&#xff01; 此次大会汇聚了来自全国各地的一万多名眼科同…

一、windows11交叉编译ffmpeg的android版本库

目录 1、工具准备 &#xff08;1&#xff09;MSYS2 &#xff08;2&#xff09;NDK&#xff08;也可直接用android studio安装的&#xff09; &#xff08;3&#xff09;ffmpeg源码 2、环境配置 3、创建编译脚本 4、编译 曾经看到一个博客说&#xff0c;不要用windows编译…

过程设计例题

答案&#xff1a;D 知识点&#xff1a; 体系结构设计 定义软件系统各主要部件之间的关系 数据设计 基于E-R图确定软件涉及的文件系统及数据库的表结构 接口设计&#xff08;人机界面设计&#xff09; 软件内部&#xff0c;软件和操作系统间以及软件和人之间如何通信 过程…

基于Springboot的鲜花销售网站的设计与实现

项目描述 这是一款基于Springboot的鲜花销售网站的系统 模块描述 鲜花销售系统 1、用户 登录 在线注册 浏览商品 鲜花搜索 订购商品 查询商品详情 水果分类查看 水果加购物车 下单结算 填写收货地址 2、管理员 登录 用户管理 商品管理 订单管理 账户管理 截图

代码随想录算法训练营第二十三天| 455. 分发饼干、376. 摆动序列、53. 最大子序和

今日内容 贪心理论基础Leetcode. 455 分发饼干Leetcode. 376 摆动序列Leetcode. 53 最大子序和 贪心理论基础 贪心算法的本质就是选择每一阶段的最优&#xff0c;达到全局上的最优。 贪心算法和之前学到的所有方法相比&#xff0c;它没有固定的使用套路&#xff0c;也没有固…

IEEE 802.11a OFDM系统的仿真

&#xff08;内容源自详解MATLAB&#xff0f;SIMULINK 通信系统建模与仿真 刘学勇编著第九章内容&#xff0c;有兴趣的读者请阅读原书&#xff09; ​ ​ ​ clear all %%%%%%%参数设计部分%%%%%%%Nsp52;%系统子载波数&#xff08;不包括直流载波&#xff09; Nfft64;%FF…

【QT】自制一个简单的小闹钟,能够实现语音播报功能

做了一个自制的小闹钟&#xff0c;能够自己输入时间&#xff0c;以及对应的闹铃&#xff0c;时间到了自动播放设定的闹铃&#xff0c;可以随时取消重新设定&#xff0c;采用分文件编译 注意&#xff1a;需要在.pro文件中加入&#xff1a;QT core gui texttospeech 代码…

使用C++编写一个语音播报时钟(Qt)

要求&#xff1a;当系统时间达到输入的时间时&#xff0c;语音播报对话框中的内容。定时可以取消。qt界面如上图所示。组件如下&#xff1a; countdownEdit作为书写目标时间的line_edit start_btn作为开始和停止的按钮 stop_btn作为取消的按钮 systimelab显示系统时间的lab tex…

绿色医院建的中央空调无线集中控制系统

在医疗服务水平不断提高的今天&#xff0c;空调能耗已成为医院建筑能耗的主要组成部分&#xff0c;达到总建筑能耗的50%至60%。如何提高医院中央空调系统的能效&#xff0c;成为绿色医院建设中亟待解决的问题。而中央空调无线集中控制系统的出现&#xff0c;为医院节能运行提供…

Etsy店铺又被封了?教你申诉和防封技巧!

熟悉美国Etsy平台的用户都知道&#xff0c;该平台对店铺的监管非常严格&#xff0c;店铺被封的情况时有发生。 无论是新开的店铺&#xff0c;还是已经有一定订单量的店铺&#xff0c;都可能面临被封的风险&#xff0c;突如其来的封号&#xff0c;会让很多卖家束手无策。 那么&a…

国内AI论文写作推荐工具有哪些?试试这7款

在当前信息爆炸的时代&#xff0c;AI写作工具已经成为学术研究和写作的重要助手。这些工具不仅能够提高写作效率&#xff0c;还能帮助用户生成高质量的文稿。以下是七款值得推荐的国内AI论文写作工具&#xff1a; 一、千笔-AIPassPaper 千笔-AIPassPaper是一款功能强大且全面…

MDK keil STM32 局部变量不能查看值,显示为not in scope

用MDK调试程序&#xff0c;查看变量时watch窗口总是和 一、方法1&#xff1a;优化级别改为Level 0 1 编译器把这个局部变量给优化掉了&#xff0c;并没有在内存中生成&#xff0c;把优化级别改为Level 0&#xff0c;重新编译。 Keil默认优化是等级3&#xff0c;最高优化&…

MMO 地图传送,UI系统框架设计

地图传送 创建传送点 建碰撞器触发 //位置归零 建一个传送门cube放到要传送的位置&#xff08;这个teleporter1是传出的区域 这是从另一张地图传入时的传送门 创建一个脚本TeleporterObject给每个传送cube都绑上脚本 通过脚本&#xff0c;让传送门在编辑器下面还能绘制出来 …

第三部分:6---进程程序替换

目录 进程替换&#xff1a; execl函数解析&#xff1a; 多进程替换的本质&#xff1a; exec系列函数解析&#xff1a; 进程替换不会替换原进程的环境变量&#xff1a; 进程替换&#xff1a; 通过 fork 创建的进程&#xff0c;在最初会执行父进程代码的一部分&#xff0c;这…

动态规划-最长回文子序列

题目描述 给你一个字符串 s &#xff0c;找出其中最长的回文子序列&#xff0c;并返回该序列的长度。 子序列定义为&#xff1a;不改变剩余字符顺序的情况下&#xff0c;删除某些字符或者不删除任何字符形成的一个序列。 示例 1&#xff1a; 输入&#xff1a;s "bcbbab…

图书馆上新了!新华书店×度小满推出“开学季悦读计划”公益活动

2024年9月&#xff0c;新学期伊始&#xff0c;度小满携手新华书店启动“开学季悦读计划”公益行动。本次活动以“开学季&#xff0c;图书馆上新书”为主题&#xff0c;向度小满“小满助力计划”公益项目落地过的五个地区共六所小学和初中捐赠3300余本图书&#xff0c;让孩子们开…

李飞飞团队 ReKep:空间智能机器人可整合 GPT-4o;苹果首款 AI 手机 iPhone 16 发布丨RTE 开发者日报

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE&#xff08;Real-Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…