Skip to content
文档章节

数字水印

本文介绍常用的数字水印方案

参考:

  1. hua1995116
  2. AlloyTeam

LSB图片算法

利用图片中 RGB 通道中 R通道对图片像素影响最小,更改数据,将内容隐藏在其中。

预览链接 网页预览打开

数字水印

优缺点

  1. 优点: 不需要原图就能恢复水印
  2. 缺点:可以被攻击破解

Canvas 获取图片数据大小为 图片 width * height * 4 , 前 3位是 RGB 3 个通道的数据,第 4 位是透明值,前端常见的 RGBA

加密规则:对 R 通道最低位进行写入数据,如果要写入数据在该位有数据,则最低位设为 1,否则设为0, 反映到 RGB 数值上,则是 最低位为1 時为 技术,否则为 偶数。

解密规则: 对 R 通道进行处理,R 的分量最低位为 1 则该像素设为红色,R 的分量最低位为 0 则该像素设为黑色

代码
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>canvas-dark-watermark</title>
  </head>

  <body>
    <canvas id="canvas" width="300" height="400"></canvas>
    <canvas id="canvas2" width="300" height="400"></canvas>
    <script>
      function mergeData(ctx, newData, color, originalData) {
        const oData = originalData.data;
        let bit;
        let offset;

        switch (color) {
          case 'R':
            bit = 0;
            offset = 3;
            break;
          case 'G':
            bit = 1;
            offset = 2;
            break;
          case 'B':
            bit = 2;
            offset = 1;
            break;
        }

        const dataLength = oData.length;

        // 只处理目标通道数据
        for (let i = bit; i < dataLength; i += 4) {
          // 新数据最低位为0
          if (newData[i + offset] === 0 && oData[i] % 2 == 1) {
            if (oData[i] === 255) {
              oData[i]--;
            } else {
              oData[i]++;
            }
          } else if (newData[i + offset] !== 0 && oData[i] % 2 === 0) {
            // 新数据存在
            if (oData[i] === 255) {
              oData[i]--;
            } else {
              oData[i]++;
            }
          }
        }

        ctx.putImageData(originalData, 0, 0);
      }

      function processData(ctx, originalData) {
        var data = originalData.data;

        for (let i = 0; i < data.length; i++) {
          if (i % 4 == 0) {
            // R分量
            if (data[i] % 2 == 0) {
              data[i] = 0;
            } else {
              data[i] = 255;
            }
          } else if (i % 4 == 3) {
            // alpha通道不做处理
            continue;
          } else {
            // 关闭其他分量,不关闭也不影响答案
            data[i] = 0;
          }
        }
        // 将结果绘制到画布
        ctx.putImageData(originalData, 0, 0);
      }

      function encodeImg(src) {
        const canvas = document.getElementById('canvas');
        let textData;
        let originalData;

        const canvasWidth = canvas.width;
        const canvasHeight = canvas.height;

        const ctx = canvas.getContext('2d');

        ctx.font = '40px Microsoft Yahei';
        ctx.fillStyle = 'red';
        ctx.fillText('得時笔记', 50, 100, canvasWidth, canvasHeight);

        textData = ctx.getImageData(0, 0, canvasWidth, canvasHeight).data;

        const img = new Image(canvasWidth, canvasHeight);

        img.src = src;

        img.onload = function () {
          ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight);

          originalData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);

          mergeData(ctx, textData, 'R', originalData);
        };
      }

      function decodeImg() {
        const ctx = document.getElementById('canvas').getContext('2d');

        const ctx2 = document.getElementById('canvas2').getContext('2d');

        const originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
        console.log(originalData);

        processData(ctx2, originalData);
      }

      encodeImg('/img/flower.jpg');

      setTimeout(() => {
        decodeImg();
      }, 1000);
    </script>

    <style>
      #canvas {
        border: 1px solid red;
        float: left;
      }

      #canvas2 {
        border: 1px solid green;
        float: left;
      }

      #canvas3 {
        border: 1px solid green;
        float: left;
      }
    </style>
  </body>
</html>

:::