隐写
一、认识
通过 RGB
分量值的小量变动 来实现图片隐写术。
下面是一张看似普通的图片,但其中却藏有另一个肉眼无法识别的图像哦。
这是如果把上图每个色彩空间和数字 3
进行逻辑与运算,再把亮度增强 85
倍,可以得到下图。
简单的说,上述的处理过程可以理解为对图片像素的处理,也就是说,加密的信息散布在每个像素点上。
我们知道图片的像素信息里存储着 RGB
的色值,R
、G
、B
分别为该像素的红、绿、蓝通道,每个通道的分量值范围在 0~255
,16
进制则是 00~FF
。在 CSS
中经常使用其 16
进制形式,比如指定博客头部背景色为 #A9D5F4
。其中 R
(红色)的 16
进制值为 A9
,换算成十进制为 169
。这时候,对 R
分量的值+1
,即为 170
,整个像素 RGB
值为 #AAD5F4
,别说你看不出差别,就连火眼金金的 像素眼设计师都察觉不出来呢。于此同时,修改 G
、B
的分量值,也是我们无法察觉的。因此可以得出重要结论:RGB
分量值的小量变动,是肉眼无法分辨的,不影响对图片的识别。
基于 RGB
分量值最小变动思想, 我们通过 R
通道的奇偶加密规则实现像素合并与分离
二、思路
通过 R
通道的奇偶加密规则实现像素合并与分离思路如下:
-
imageData1
在imageData2
中有像素点: 将R
偶数的通道+1
变为奇数 -
imageData1
在imageData2
中没有像素点: 将R
奇数的通道+1
变为偶数
基于 R
通道的像素分离: 基于合并规则, imageData1
的 R
通道像素点在 imageData2
中为奇数, 因此:
-
imageData2
的R
通道像素点为奇数: 说明在imageData1
中有像素点, 将R
通道填充为255
-
imageData2
的R
通道像素点为偶数: 说明在imageData1
中没有像素点, 将R
通道关闭,更改为0
-
imageData2
中的G
通道和B
通道为了达到分离效果,可以将其关闭,更改为0
三、加密
3.1 JS
function mergeData(params) {
const { ctx, imageData1, imageData2 } = params || {};
const data1 = imageData1.data;
const data2 = imageData2.data;
for (let i = 0; i < data2.length; i += 4) {
if (data1[i + 3] === 0 && data2[i] % 2 === 1) {
// data1 alpha 通道值为 0,data2 通道值为奇数
if (data2[i] === 255) {
data2[i]--;
} else {
data2[i]++;
}
} else if (data1[i + 3] !== 0 && data2[i] % 2 === 0) {
// data1 alpha 通道值不为 0,data2 通道值为偶数
if (data2[i] === 255) {
data2[i]--;
} else {
data2[i]++;
}
}
}
ctx.putImageData(imageData2, 0, 0);
}
function encodeImage(params) {
return new Promise((resolve) => {
const { src = "", text = "" } = params || {};
let textData;
let originalData;
const img = new Image();
img.onload = function () {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
ctx.font = "30px Microsoft Yahei";
ctx.fillText(text, 60, 130);
textData = ctx.getImageData(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
originalData = ctx.getImageData(0, 0, canvas.width, canvas.height);
mergeData({
ctx,
imageData1: textData,
imageData2: originalData,
});
const encodeImg = canvas.toDataURL("image/png");
resolve(encodeImg);
};
img.src = src;
});
}
async function run() {
const encodeImg = await encodeImage({
src: "../../images/origin.png",
text: "柏拉文的秘密",
});
const img = new Image();
img.src = encodeImg;
document.body.appendChild(img);
}
run();
3.2 HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>隐写术-加密</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>
四、解密
4.1 JS
function processData(params) {
const { ctx, imageData } = params || {};
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
if (data[i] % 2 == 0) {
data[i] = 0;
} else {
data[i] = 255;
}
data[i + 1] = 0; // 关闭其他分量, 不处理的话也可以, 会显示原图
data[i + 2] = 0; // 关闭其他分量, 不处理的话也可以, 会显示原图
}
ctx.putImageData(imageData, 0, 0);
}
function decodeImage(params) {
return new Promise((resolve) => {
const { src = "" } = params || {};
let originalData;
const img = new Image();
img.onload = function () {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
ctx.drawImage(img, 0, 0);
originalData = ctx.getImageData(
0,
0,
ctx.canvas.width,
ctx.canvas.height
);
processData({
ctx,
imageData: originalData,
});
const decodeImg = canvas.toDataURL("image/png");
resolve(decodeImg);
};
img.src = src;
});
}
async function run() {
const decodeImg = await decodeImage({
src: "../../images/encode.png",
});
const img = new Image();
img.src = decodeImg;
document.body.appendChild(img);
}
run();
4.2 HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>隐写术-解密</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>