前端水印svg生成方案&svg动态宽高计算

有个web项目要有水印需求,后端可以提供文本或者图片。 参考网络上的前端水印方案,目前选择的是获取文本,通过文本生成svg渲染水印。

1. svg生成文本水印

参考文章6. 前端水印生成方案 的svg方案是ES6语法的,项目问题,我把它改写成ES5语法了。 js代码如下:

代码语言:javascript
复制
     /* svg 实现水印 */
     function wmSvg(wmText, container) {
          if (!wmText || typeof wmText !== 'string') return;
      var svgStr = '<svg xmlns="http://www.w3.org/2000/svg" width="300px" height="200px">  <text x="50%" y="50%" dy="12px"    text-anchor="middle"    stroke="#000000"    stroke-width="1"    stroke-opacity="0.2"    fill="none"    transform="rotate(-45, 120 120)"    style="font-size: 20px;"> '+ wmText +'</text></svg>'; 
      var base64Url = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svgStr)));  
      var __wm = document.querySelector('.__wm');        
      var wmDiv = __wm || document.createElement("div");
      wmDiv.className = '_wm';
      wmDiv.setAttribute('style', 'position:absolute;top:0;left:0;width:100%;height:100%;z-index:2000;pointer-events:none;background-repeat;background-image:url('+base64Url+');');


      if(!container) container = document.body;
      container.style.position = 'relative';
      container.insertBefore(wmDiv, container.firstChild);

 }

 // 调用方法,生成水印
 wmSvg(&#39;Celine~&#39;);</code></pre></div></div><p>渲染效果:</p><figure class=""><div class="rno-markdown-img-url" style="text-align:center"><div class="rno-markdown-img-url-inner" style="width:100%"><div style="width:100%"><img src="https://cdn.static.attains.cn/app/developer-bbs/upload/1723307417959562708.webp" /></div></div></div></figure><p>image.png</p><h2 id="5q66q" name="2.-%E5%8A%A8%E6%80%81%E8%AE%A1%E7%AE%97svg%E7%9A%84%E5%AE%BD%E9%AB%98">2. 动态计算svg的宽高</h2><blockquote><p> 本次项目中的水印文字是由后端提供的,水印文字是由用户自定义。再上面的代码中,生成的svg宽高是写死的300*200px,如果文字太长,这个宽度不够容纳就会有遮挡效果,所以自己用粗略估算的方式,动态计算了svg的宽高。

js代码如下:

代码语言:javascript
复制
     /* svg 实现水印 */
     function wmSvg(wmText, container) {
          if (!wmText || typeof wmText !== 'string') return;
      var fontSize = 16,
          rotateAngle = 30,
          textLen = wmText.length, // 字符串长度(中文双字符和英文单字符都是按照一个字符计算)
          // textWidth = Math.ceil((textLen -20)*fontSize) + 200, // 时间戳是必须项的计算方式
          textWidth = Math.ceil(textLen *fontSize), // 通用版的计算方式
          textWidth =  textWidth &lt; 300 ? 300 : textWidth, // 设置最小长度,不然文字少水印太密集。
          svgWidth = Math.ceil(textWidth * Math.cos(Math.PI/180*rotateAngle)),
          svgHeight = Math.ceil(textWidth * Math.sin(Math.PI/180*rotateAngle)),
          rotateX = Math.floor(svgWidth/2),
          rotateY = Math.floor(svgHeight/2);

      console.log(textLen, textWidth);
      console.log(svgWidth,svgHeight);

      // 计算规则说明:
      // 本次项目的水印格式是:“用户名 自定义内容 时间戳” ,比如“celine 个人博客 2023-08-04 12:00:00”
      // 其中,时间戳 -&gt; 单字符,必须要有,拿出来单独计算下。大致占20个字符长度,渲染宽度是150px左右,此处取宽容值200px。(若没有必须的时间戳,可以用通用计算方式)
      // 自定义内容 -&gt; 默认为中文双字符(取宽容场景)。渲染宽度和字体大小相关,所以此处粗略用字符长度*字体大小。
      // svg的宽高是根据文本长度结合选择角度,根据直角三角形的勾股定理做计算
      // rotate 的旋转中心点偏移XY,根据svg宽高取一半。


      var svgStr = &#39;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; width=&#34;&#39;+svgWidth+&#39;px&#34; height=&#34;&#39;+svgHeight+&#39;px&#34;&gt;  &lt;text x=&#34;50%&#34; y=&#34;50%&#34; dy=&#34;12px&#34;    text-anchor=&#34;middle&#34;    stroke=&#34;#000000&#34;    stroke-width=&#34;1&#34;    stroke-opacity=&#34;0.2&#34;    fill=&#34;none&#34;    transform=&#34;rotate(-&#39;+rotateAngle+&#39;, &#39;+rotateX+&#39; ,&#39;+rotateY+&#39;)&#34;    style=&#34;font-size: &#39;+fontSize+&#39;px;&#34;&gt; &#39;+ wmText +&#39;&lt;/text&gt;&lt;/svg&gt;&#39;; 
      var base64Url = &#39;data:image/svg+xml;base64,&#39; + window.btoa(unescape(encodeURIComponent(svgStr)));  
      var __wm = document.querySelector(&#39;._wm&#39;);        
      var wmDiv = __wm || document.createElement(&#34;div&#34;);
      wmDiv.className = &#39;_wm&#39;;
      wmDiv.setAttribute(&#39;style&#39;, &#39;position:absolute;top:0;left:0;width:100%;height:100%;z-index:2000;pointer-events:none;background-repeat:repeat;background-image:url(&#39;+base64Url+&#39;);&#39;);

      if(!container) container = document.body;
      container.style.position = &#39;relative&#39;;
      container.insertBefore(wmDiv, container.firstChild);

 }

 // 调用方法,生成水印
 // wmSvg(&#39;Celine~&#39;);
 wmSvg(&#39;Celine 我的博客是有个性的不要随便惹毛它哦 2023-08-04 12:00:00&#39;);</code></pre></div></div><figure class=""><div class="rno-markdown-img-url" style="text-align:center"><div class="rno-markdown-img-url-inner" style="width:100%"><div style="width:100%"><img src="https://cdn.static.attains.cn/app/developer-bbs/upload/1723307418226146115.webp" /></div></div></div></figure><p>效果图</p><h2 id="1djf8" name="3.-%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E6%8E%A7%E5%88%B6%E5%8F%B0%E5%88%A0%E6%B0%B4%E5%8D%B0%E5%91%A2%EF%BC%9F">3. 如何防止控制台删水印呢?</h2><p>这个就可以用到<code>new MutationObserver()</code>这个东西了。这个方法的详解,可以看这篇 csdn:MutationObserver详解,

下面直接贴demo的代码:

代码语言:javascript
复制
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div>
<p>我是页面内容,哈哈哈</p>
<div style="width: 450px;height: 800px;background: pink;">我是有指定宽高的div,我是来占大位的,哈哈哈</div>

</div>

<script type="text/javascript">

 // 调用方法,生成水印
 // wmSvg(&#39;Celine~&#39;);
 wmSvg(&#39;Celine~ 2023-08-04 12:00:00&#39;);
 // wmSvg(&#39;Celine 我的博客是有个性的不要随便惹毛它哦我的博客是有个性的不要随便惹毛它哦我的博客是有个性的不要随便惹毛它哦我的博客是有个性的不要随便惹毛它哦我的博客是有个性的不要随便惹毛它哦我的博客是有个性的不要随便惹毛它哦 2023-08-04 12:00:00&#39;);

 /* svg 实现水印 */
 function wmSvg(wmText, container) {
      if (!wmText || typeof wmText !== &#39;string&#39;) return;

      var fontSize = 16,
          rotateAngle = 30,
          textLen = wmText.length, // 字符串长度(中文双字符和英文单字符都是按照一个字符计算)
          // textWidth = Math.ceil((textLen -20)*fontSize) + 200, // 时间戳是必须项的计算方式
          textWidth = Math.ceil(textLen *fontSize), // 通用版的计算方式
          textWidth =  textWidth &lt; 300 ? 300 : textWidth, // 设置最小长度,不然文字少水印太密集。
          textWidth =  textWidth &gt; 600 ? 600 : textWidth, // 设置最大长度,不然文字多水印跑了。

          svgWidth = Math.ceil(textWidth * Math.cos(Math.PI/180*rotateAngle)),
          svgHeight = Math.ceil(textWidth * Math.sin(Math.PI/180*rotateAngle)),
          rotateX = Math.floor(svgWidth/2)-50,
          rotateY = Math.floor(svgHeight/2)+50;

      var svgStr = &#39;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; width=&#34;&#39;+svgWidth+&#39;px&#34; height=&#34;&#39;+svgHeight+&#39;px&#34;&gt;  &lt;text x=&#34;50%&#34; y=&#34;50%&#34; dy=&#34;12px&#34;    text-anchor=&#34;middle&#34;    stroke=&#34;#000000&#34;    stroke-width=&#34;1&#34;    stroke-opacity=&#34;0.2&#34;    fill=&#34;none&#34;    transform=&#34;rotate(-&#39;+rotateAngle+&#39;, &#39;+rotateX+&#39; ,&#39;+rotateY+&#39;)&#34;    style=&#34;font-size: &#39;+fontSize+&#39;px;&#34;&gt; &#39;+ wmText +&#39;&lt;/text&gt;&lt;/svg&gt;&#39;;

      createWm(svgStr, container);          

 }

 /* 抽离生成base64和填充水印功能,单独一个方法,便于后面的监听复用。*/
 function createWm(svgStr, container){
    var base64Url = &#39;data:image/svg+xml;base64,&#39; + window.btoa(unescape(encodeURIComponent(svgStr)));  
    if (document.querySelector(&#39;._wm&#39;)) return; // 已有水印直接退出。
    // var wmDiv = document.querySelector(&#39;._wm&#39;) || document.createElement(&#34;div&#34;); // 这种方式会导致监听死循环,因为新水印的svgStr和页面上不一致,会一直触发监听。
    var wmDiv = document.createElement(&#34;div&#34;);
    wmDiv.className = &#39;_wm&#39;;
    var wmStyleStr = &#39;position:absolute;top:0;left:0;width:100%;height:100%;z-index:2000;pointer-events:none;background-repeat:repeat;background-image:url(&#39;+base64Url+&#39;);&#39;

    wmDiv.setAttribute(&#39;style&#39;, wmStyleStr);


    if(!container) container = document.body;
    container.style.position = &#39;relative&#39;;
    container.insertBefore(wmDiv, container.firstChild);


    wmObserver(svgStr, container, wmStyleStr);

 }


 /* 水印监听 */
 function wmObserver(svgStr, container, wmStyleStr){
    const options = {
        childList: true,
        attributes: true,
        characterData: true,
        subtree: true,
        attributeOldValue: true,
        characterDataOldValue: true
    }

    // console.log(window.MutationObserver, window.WebKitMutationObserver, window.MozMutationObserver );
    var mutation = new MutationObserver(function(mutationRecoards, observer){
        console.log(mutationRecoards, observer);
        // 没有水印时
         var wmDiv = document.querySelector(&#39;._wm&#39;);
         if (!wmDiv) {
           createWm(svgStr, container);
           return;
         }

         // 水印样式被修改时
         if (wmDiv.getAttribute(&#39;style&#39;) != wmStyleStr) {
           wmDiv.setAttribute(&#39;style&#39;, wmStyleStr);
         }

     });

      // 安装监听
      mutation.observe(container, options);

 }

</script>

</body>
</html>

4. 真实打印呢?

这是我目前遇到的困境,前面的方案页面打印机打印除了是没有水印的。。
谁有什么好方案再告知下。


参考文章

  1. svg文本<text>详解
    挺详细的一篇讲解,有代码有效果图。其中有多行文字的svg展示代码。我是因为要设置水印样式查看了该文章。
  2. 简略回顾了下三角函数。主要是要看下Js中的Math.sin()里面填入的角度要怎么设置。(举例:30°的话,Math.sin(Math.PI/180*30)
  3. jquery.watermark.js 在网页中添加水印,打印时水印背景不见了,办法来了
    号称可以通过print-color-adjust:exact;来改变打印时没有水印的情况。但我自己的代码(没有使用jquery.watermark.js的)初步尝试,真实打印机打印处理依旧没有。后续再探究下。
  4. Js和Canvas实现水印且控制台不可删除
    防止控制台删水印那段可以好好借鉴下。

  1. 【Web技术】谈谈水印实现的几种方式 没认真看,有需要再来看。
  2. 前端水印生成方案 svg方案参考的文章,里面有canvas方案(HTMLCanvasElement.toDataURL)。
  3. 个人博客krryblog:页面水印的实现以及防删除方案 一个毕业去京东,现在在腾讯的前端小大佬?还是很佩服他的强迫症的,哈哈哈。这篇文章很详细的给出的代码方案,值得借鉴。
  4. csdn:MutationObserver详解