浅谈前端水印技术的几种实现方法

427 阅读7分钟

水印技术是一种在数字内容中嵌入标识信息的方法,用于涉密保护、版权保护、品牌推广或内容防伪。在前端开发中,水印技术可以通过多种方式实现。本文将介绍几种常用的前端水印实现方法,包括使用 CSS 伪元素、背景图像、SVG 叠加、Canvas 以及 WebGL等等。

废话不多说,有一句话叫Talk is cheap. Show me the code.下面让我们一起步入正题。


1. 使用 CSS 伪元素实现水印

方法概述: 通过 CSS 的 ::before::after 伪元素,可以在网页元素的前面或后面添加水印。此方法适合于静态或简单的水印需求,但也可以通过 JavaScript 动态修改水印内容。

实现示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Repeating Text Watermark</title>
    <style>
      .watermarked-content {
        position: relative;
        width: 100%;
        height: 400px;
        background-color: #eaeaea;
        margin: 20px auto;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 24px;
        text-align: center;
        --watermark-text: 'Default Watermark'; /* 默认水印文本 */

        /* 使用 ::before 伪元素创建水印文字 */
        background-image: repeating-linear-gradient(
            rgba(0, 0, 0, 0.1) 1px,
            transparent 1px
          ),
          repeating-linear-gradient(
            90deg,
            rgba(0, 0, 0, 0.1) 1px,
            transparent 1px
          );
        background-size: 200px 200px;
        background-position: center;
      }

      /* 设置水印伪元素 */
      .watermarked-content::before {
        content: var(--watermark-text); /* 动态使用 CSS 变量作为水印文本 */
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        display: block;
        color: rgba(0, 0, 0, 0.1);
        font-size: 20px;
        transform: rotate(-30deg); /* 旋转水印 */
        pointer-events: none;
        white-space: nowrap;
        z-index: 1;
        opacity: 1;
        background-repeat: repeat;
        background-size: 200px 100px;
      }
    </style>
  </head>
  <body>
    <div class="watermarked-content" id="watermarkedElement">
      <!-- 内容区域 -->
      Your Content Here
    </div>

    <script>
      // JavaScript 动态修改水印文本
      function changeWatermark(text) {
        const watermarkedElement =
          document.getElementById('watermarkedElement');
        // 使用CSS变量来改变伪元素中的文本
        watermarkedElement.style.setProperty('--watermark-text', `"${text}"`);
      }

      // 默认设置水印
      changeWatermark('Initial Watermark');
    </script>
  </body>
</html>

image.png

这种方法的优点有两点,一是实现简单,易于调整样式;二是支持通过 JavaScript 动态修改水印内容。但它也有一定的局限性,主要适用于静态水印,动态更新较为有限。


2. 使用背景图像实现水印

通过设置背景图像的方式,在网页元素中创建重复的水印。可以使用 SVG 图像来生成水印,并通过 JavaScript 动态修改背景图像的 URL 来改变水印内容。

实现示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Background Image Watermark with Dynamic Content</title>
    <style>
      .watermarked-content {
        position: relative;
        width: 100%;
        height: 400px;
        background-color: #f5f5f5;
        background-repeat: repeat;
        background-size: 200px 100px;
      }
    </style>
  </head>
  <body>
    <div class="watermarked-content" id="watermarkedContent">content here</div>

    <script>
      function generateSvgWatermark(text) {
        return `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><text x="50%" y="50%" dy="0.35em" text-anchor="middle" fill="rgba(0,0,0,0.1)" font-size="16" transform="rotate(-30)">${text}</text></svg>`;
      }

      function updateWatermark(newText) {
        document.getElementById(
          'watermarkedContent'
        ).style.backgroundImage = `url('${generateSvgWatermark(newText)}')`;
      }

      // Initial watermark
      updateWatermark('12345');
    </script>
  </body>
</html>

image.png

这种方式的优点是支持大面积背景水印,样式可复杂;同时可以动态生成和更新水印图像。

它的缺点是可能对页面性能产生影响,尤其是频繁更新时;同时水印更新可能需要较高的计算开销。


3. 使用 <video> 元素和水印层叠

<video> 元素上叠加一个透明的水印层,可以利用 CSS 和 HTML 来实现。适用于需要对视频内容进行水印处理的场景。

实现示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Video Watermark</title>
    <style>
      .video-container {
        position: relative;
        width: 100%;
        height: auto;
      }
      .video-container video {
        width: 100%;
        height: auto;
      }
      .watermark-overlay {
        position: absolute;
        top: 10px;
        left: 10px;
        color: rgba(255, 255, 255, 0.5);
        font-size: 24px;
        pointer-events: none;
      }
    </style>
  </head>
  <body>
    <div class="video-container">
      <video src="your-video.mp4" controls></video>
      <div class="watermark-overlay" id="watermarkOverlay">Watermark</div>
    </div>

    <script>
      function updateWatermark(newText) {
        document.getElementById('watermarkOverlay').textContent = newText;
      }
      updateWatermark('Updated Watermark');
    </script>
  </body>
</html>

image.png

这种方式适用于视频内容的水印添加。实现简单,通过 CSS 调整水印样式。

但同时,它对视频内容的动态处理不如其他方法灵活,还可能需要处理视频播放器的兼容性问题。

4. 使用 SVG 叠加在 HTML 元素上

直接将 SVG 元素叠加在 HTML 元素上进行水印。SVG 可以很容易地进行图形处理和样式调整。

示例如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SVG Overlay Watermark</title>
  <style>
    .content {
      position: relative;
      width: 100%;
      height: 400px;
      background-color: #f5f5f5;
    }
    .watermark-svg {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      opacity: 0.1;
    }
  </style>
</head>
<body>
  <div class="content">
    <svg class="watermark-svg" xmlns="http://www.w3.org/2000/svg">
      <text x="50%" y="50%" text-anchor="middle" fill="black" font-size="48" transform="rotate(-30, 50, 50)">Watermark</text>
    </svg>
    Your content goes here
  </div>
</body>
</html>

image.png

这种方式它的优势在于 SVG 提供了矢量图形的高质量显示。同时,可以精确控制水印的外观和位置。

但也会因此而带来一些问题,比如对复杂 SVG 可能有性能影响,以及动态更新可能需要额外处理。

5、使用 Canvas 生成水印

利用 HTML5 的 Canvas API 动态生成水印图像,适合需要高度自定义的水印效果。通过 JavaScript 可以动态修改水印内容。

示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Dynamic Canvas Watermark</title>
    <style>
      .content {
        position: relative;
        width: 100%;
        height: 400px;
        background-color: #f5f5f5;
        overflow: hidden;
      }
      .dynamic-watermark {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        pointer-events: none; /* Prevent interaction with the watermark canvas */
      }
    </style>
  </head>
  <body>
    <div id="targetElement" class="content">
      <div class="inner-content">Your content goes here.</div>
    </div>

    <script>
      function createAndInsertCanvas() {
        // Get the target element where the Canvas will be inserted
        const targetElement = document.getElementById('targetElement');

        // Create a Canvas element
        const canvas = document.createElement('canvas');
        canvas.className = 'dynamic-watermark';
        canvas.width = targetElement.offsetWidth;
        canvas.height = targetElement.offsetHeight;

        // Append the Canvas to the target element
        targetElement.appendChild(canvas);

        // Get the Canvas 2D context
        const ctx = canvas.getContext('2d');

        // Draw the watermark
        function drawWatermark(text) {
          const width = canvas.width;
          const height = canvas.height;

          ctx.clearRect(0, 0, width, height);

          ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
          ctx.font = '30px Arial';
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.save();

          // Draw repeated watermark text
          const textWidth = ctx.measureText(text).width;
          const spacing = 100; // Space between repeated watermarks
          for (let x = -spacing; x < width; x += textWidth + spacing) {
            for (let y = -spacing; y < height; y += 60 + spacing) {
              // Adjust 60 to the text height
              ctx.translate(x, y);
              ctx.rotate(-Math.PI / 6); // Rotate text if needed
              ctx.fillText(text, 0, 0);
              ctx.rotate(Math.PI / 6);
              ctx.translate(-x, -y);
            }
          }
          ctx.restore();
        }

        // Draw the watermark initially
        drawWatermark('hello world');

        // Redraw watermark on window resize
        window.onresize = () => {
          canvas.width = targetElement.offsetWidth;
          canvas.height = targetElement.offsetHeight;
          drawWatermark();
        };
      }

      window.onload = createAndInsertCanvas;
    </script>
  </body>
</html>

image.png

这种方式的优点在于高度自定义,支持动态生成和更新。同时可以在复杂背景上绘制水印。

但它也有一定的缺陷,一是实现相对复杂,需要处理 Canvas 的尺寸和内容刷新。二是性能开销较大,尤其是频繁更新的场景。

由于灵活性高且ROI相对其他方案较好,因此它是目前主要流行的解决方案。


6. 使用 WebGL 实现水印

通过 WebGL 绘制水印,这种方法适用于需要高性能和复杂水印效果的场景,例如 3D 水印。

简单示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebGL Watermark with Text</title>
    <style>
      .watermarked-content {
        position: relative;
        width: 100%;
        height: 400px;
        background-color: #f5f5f5;
      }
      canvas {
        width: 100%;
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div class="watermarked-content">
      <canvas id="webglCanvas"></canvas>
      Your content goes here
    </div>
    <script>
      function initWebGLWatermark() {
        const canvas = document.getElementById('webglCanvas');
        const gl = canvas.getContext('webgl');

        if (!gl) {
          console.error('WebGL not supported');
          return;
        }

        canvas.width = canvas.parentElement.offsetWidth;
        canvas.height = canvas.parentElement.offsetHeight;

        // Create the text texture using a 2D canvas
        const textCanvas = document.createElement('canvas');
        const textCtx = textCanvas.getContext('2d');
        textCanvas.width = 512;
        textCanvas.height = 128;
        textCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        textCtx.font = '48px Arial';
        textCtx.textAlign = 'center';
        textCtx.textBaseline = 'middle';
        textCtx.fillText(
          'Watermark',
          textCanvas.width / 2,
          textCanvas.height / 2
        );

        // Create texture
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(
          gl.TEXTURE_2D,
          0,
          gl.RGBA,
          gl.RGBA,
          gl.UNSIGNED_BYTE,
          textCanvas
        );
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

        // Vertex shader program
        const vsSource = `
        attribute vec2 a_position;
        attribute vec2 a_texCoord;
        varying vec2 v_texCoord;
        void main() {
          gl_Position = vec4(a_position, 0.0, 1.0);
          v_texCoord = a_texCoord;
        }
      `;

        // Fragment shader program
        const fsSource = `
        precision mediump float;
        varying vec2 v_texCoord;
        uniform sampler2D u_texture;
        void main() {
          gl_FragColor = texture2D(u_texture, v_texCoord);
        }
      `;

        // Create shader
        function createShader(type, source) {
          const shader = gl.createShader(type);
          gl.shaderSource(shader, source);
          gl.compileShader(shader);
          if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            console.error(
              'An error occurred compiling the shaders: ' +
                gl.getShaderInfoLog(shader)
            );
            gl.deleteShader(shader);
            return null;
          }
          return shader;
        }

        const vertexShader = createShader(gl.VERTEX_SHADER, vsSource);
        const fragmentShader = createShader(gl.FRAGMENT_SHADER, fsSource);

        // Create program
        function createProgram(vs, fs) {
          const program = gl.createProgram();
          gl.attachShader(program, vs);
          gl.attachShader(program, fs);
          gl.linkProgram(program);
          if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
            console.error(
              'Unable to initialize the shader program: ' +
                gl.getProgramInfoLog(program)
            );
            return null;
          }
          return program;
        }

        const program = createProgram(vertexShader, fragmentShader);
        gl.useProgram(program);

        // Define the positions and texture coordinates for the rectangle (full-screen quad)
        const vertices = new Float32Array([
          -1.0, -1.0, 0.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 0.0, 0.0, -1.0,
          1.0, 0.0, 0.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0,
        ]);

        // Create buffer
        const positionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

        // Get attribute and uniform locations
        const positionLocation = gl.getAttribLocation(program, 'a_position');
        const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
        const textureLocation = gl.getUniformLocation(program, 'u_texture');

        // Enable attributes
        gl.enableVertexAttribArray(positionLocation);
        gl.enableVertexAttribArray(texCoordLocation);
        gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 16, 0);
        gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 16, 8);

        // Bind the texture
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.uniform1i(textureLocation, 0);

        // Clear the canvas
        gl.clearColor(1.0, 1.0, 1.0, 1.0); // White
        gl.clear(gl.COLOR_BUFFER_BIT);

        // Draw
        gl.drawArrays(gl.TRIANGLES, 0, 6);
      }

      window.onload = initWebGLWatermark;
    </script>
  </body>
</html>

image.png

它的优点是高度灵活,支持复杂的图形和效果。同时高性能,适合需要处理大量数据的场景。

但它也有一个比较明显的缺陷,就是学习成本较高,实现复杂,需要深入了解 WebGL。同时对不支持 WebGL 的设备或浏览器可能需要降级处理。


面临的技术挑战与总结

每种水印技术都有其独特的优势和适用场景:

  • CSS 伪元素:实现简单,适合静态内容,但动态更新能力有限。
  • 背景图像:适合大面积水印,支持动态内容生成,但可能影响性能。
  • Canvas:高度自定义,适用于复杂效果,但实现和性能管理较复杂。
  • SVG 叠加:高质量图形,适用于精确控制,但复杂 SVG 可能影响性能。
  • WebGL:适合高性能和复杂场景,但实现难度大。

选择合适的水印技术取决于你的具体需求和场景。大多数场景下,我们多采用Canvas方案,因其高度自定义且难度适中。

然而实际场景中也会面临各种技术挑战,如下:

1)删除元素:恶意用户可能会尝试删除水印元素,以去除水印标识。

2)修改元素属性:用户可以通过开发者工具或 JavaScript 修改页面中的水印元素属性,比如隐藏。

3)修改内容图层:用户放弃修改水印元素,转而修改内容元素的图层,覆盖水印图层。

上述皆是明印,用户很容易通过肉眼察觉而进行某些破坏行为。然而在某些场景下,我们不希望水印影响用户体验,因此也会采用暗印,比如涉密等场景中,而暗印就会涉及编码和解码等一系列问题。尽管如此,暗印虽然难以察觉,但也可能被恶意用户破解或去除。

关注我,下一篇文章,我们将一起探讨下如何防止水印被用户恶意去除问题,打造更安全的前端水印:防破解技术与暗水印应用