图片隐写术顾名思义就是通过某些手段在展示的图片上隐藏某些信息,我国古代常有画家在自己的画作上隐匿自己的名字,比如,李唐的《万壑松风图》,他将自己名字隐藏在画里,可能是为了防伪造吧。
现如今有些场景下我们使用到的图片,需要在图片中隐匿一些信息,大多数是为了图片版权,防止滥用。比如摄影师辛苦拍摄的照片,被窃取,盗用的情况很多,如何证明这张图是出自你手?李唐靠笔墨,我们靠代码
思路
想要修改图片,手段无外乎就是将图片资源读取成我们能操作的资源格式,常用的格式就是二进制数据流

实现
按照这个思路,我们可以这样很简单的实现出来
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
canvas {
width: 1000px;
}
</style>
</head>
<body>
<script>
// 创建一个隐藏文本的函数
function hideTextInImage(imageUrl, textToHide) {
const canvas = document.createElement("canvas");
const img = new Image();
img.onload = function () {
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// 将文本转换为二进制
const textBinary = textToHide.split('').map(char => char.charCodeAt(0).toString(2).padStart(8, '0')).join('');
// 获取图像的像素数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
let textIndex = 0;
// 遍历图像的每个像素
for (let i = 0; i < pixels.length; i += 4) {
if (textIndex < textBinary.length) {
// 将文本的二进制逐位隐藏在像素的最低有效位
pixels[i] = pixels[i] & ~1 | parseInt(textBinary[textIndex]);
textIndex++;
}
}
// 将修改后的像素数据放回画布
ctx.putImageData(imageData, 0, 0);
// 将包含隐藏文本的新图像显示在页面上
document.body.appendChild(canvas);
};
img.src = imageUrl;
}
// 要隐藏的文本
const textToHide = "你好,我是蜗牛";
// 输入图片 URL
const imageUrl = "./bg.jpg";
// 调用函数来隐藏文本
hideTextInImage(imageUrl, textToHide);
</script>
</body>
</html>
效果是正常的

解释一下我们干了什么:
- 通过canvas的drawImage方法将图片绘制在画布上
- 字符串的
charCodeAt()
方法返回一个整数,用于获取指定的字符的 Unicode 编码,其值介于0
和65535
之间。 padStart(targetLength, padString)
方法用另一个字符串padString填充当前字符串(如果需要会重复填充),直到达到给定的长度targetLength。填充是从当前字符串的开头开始的。
我们使用了 padStart(8, '0')
来确保每个字符的 Unicode 编码值在二进制表示中都占据 8 位,即一个字节的长度。为什么长度一定要是 8,那是因为后面我们要将这些二进制位嵌入图像的 LSB
中, LSB
中的长度是8位(LSB 是什么?别急,下面会给看官老爷解释)。先看一眼字符串被转化为二进制之后的效果:

- 借助 canvas 上的 getImageData 方法读取到图片的像素数据,长这样:
(需要知道的是,每四个值是一组,描述一个像素点)
LSB 的全称是 "Least Significant Bit",它是二进制数值中最低有效位(即最右边的位)的缩写。在数字表示中,每位的位置都代表不同的权重,而最低有效位则是权重最小的位,其对整体数值的影响最小。对于一个 8 位二进制数值来说,LSB 就是最右边的那一位。
例如,在一个 8 位的二进制数值 10110101 中,最低有效位就是最右边的那一位,即 1。在进行图像处理或隐写操作时,我们可以将一个比特(0 或 1)嵌入到这个 LSB 中,而不会引起明显的变化。
- 所以我们是将需要隐匿的字符串的二进制分散的植入到达图像数据的最后一位(对图像本身其实是有影响的,但是基本看不出来)
如此一来,我们的数据成功隐写入了图像中
什么?你要验证真伪,你怀疑这张图是伪造的?
读取隐匿的内容
我就知道你肯定是会有这个疑问的,李唐把他的名字隐匿在了他的《万壑松风图》中,那就一定有办法能看出来他的名字,若是看不出来,这幅画也就无从辨明真伪了
那我们就需要一个能再从图片中提取隐藏文本的方法
ini
// 创建一个提取隐藏文本的函数
function extractHiddenTextFromImage(imageUrl) {
const canvas = document.createElement("canvas");
const img = new Image();
img.onload = function () {
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
let extractedBinary = "";
// 遍历图像的每个像素,提取隐藏的二进制信息
for (let i = 0; i < pixels.length; i += 4) {
// 从每个像素的最低有效位提取二进制信息
extractedBinary += (pixels[i] & 1).toString();
}
// 将二进制信息转换为文本
let extractedText = "";
for (let i = 0; i < extractedBinary.length; i += 8) {
const byte = extractedBinary.substr(i, 8);
const charCode = parseInt(byte, 2);
extractedText += String.fromCharCode(charCode);
}
console.log("隐写的内容:", extractedText);
};
img.src = imageUrl;
}
// 输入包含隐藏信息的图片 URL
const imageUrlWithHiddenText = "./bg.png";
// 调用函数来提取文本
extractHiddenTextFromImage(imageUrlWithHiddenText);
注意:该代码中的图片名为 bg.png, 是我先将上一张隐写完成的图片重新保存在本地之后的一张图
能理解写入的代码,那就肯定能理解读取的代码,同样是借助canvse,将图片LSB中的值又读取出来,再转换回字符串,这里便不再解释了
看一眼解析出来的效果:

乱码了,看的出来,程序没问题,能执行,只不过我们隐写的中文,再被提取出来后乱码,因为在上面的代码示例中,使用的是 charCodeAt()
方法获取字符的 Unicode 编码值,然后将编码值转换为二进制字符串并进行隐藏。然而,这种方式可能不适用于中文等多字节字符,因为 Unicode 编码值的范围在 ASCII 字符之外,而中文字符通常需要多个字节来表示。
所以,这里我就先把要隐匿的内容写成英文
ini
// 要隐藏的文本
const textToHide = "Hello, I am clever_Snail";
重新保存图片再测试提取功能:

nice!!!
总归是隐写成功没有问题,你的有价值的图片被盗用的话,这就是证据!
总结
这是一个你工作中可能遇得上的一个图片冷知识,点赞收藏以后遇到就不慌了,不过对于中文,我目前仍然没有更好的处理方案,不过这已经足够应对你在工作中的遇到的这个需求了。今天的头皮先挠到这里,中文提取有思路的小伙伴可以评论区讨论一下