图片隐写技术介绍:
【如何在图片中塞入二维码不被发现】 https://www.bilibili.com/video/BV12R4y1G72d/?share_source=copy_web&vd_source=d944df449598b7e51bbc29cddb033275
png图片格式介绍:
https://www.cnblogs.com/senior-engineer/p/9548347.html
https://zhuanlan.zhihu.com/p/23890809
图片隐写,借助的是图片的每个像素点的RGB值,比如取RGB中的B值,这个值的二进制的最低位是0还是1,在肉眼上看看不出丝毫区别,我们就可以利用这个数据位,我们知道二维码一般都是两种颜色,黑色和白色,黑色的像素点用1表示,白色用0表示,我们就把二维码的每个像素点转换成1和0,在把这些1和0存到承载二维码的本体图片的像素的一个颜色值通道的二进制值的最低位上。这就是大概原理。
如果要从载体图片读取二维码,需要知道二维码的长和宽,以便读取正确的二维码的1和0值,然后在把1和0反转成黑色或白色,就能解析出构成二维码了。所以我们需要把二维码的长和宽写入到载体图片中。
这就需要png格式的图片一定是要符合png的标准,否则在这一步会让图片损坏。要辨别png格式,不能只看文件的扩展名为png,需要看文件的署名是否是png规定的署名。符合png标准的png的文件用二进制编辑器打开,最开始的8个字节一定是:0x89 0x50 0x4e 0x47 0x0d 0x0a 0x1a 0x0a。只有先确认了这样的字节,才能进行接下来的步骤。
我们需要把二维码图片的长和宽的值写入png文件中,借助png的辅助数据块的格式tEXt,可以实现该目的。我们还需要知道一个IEND数据块,它一定是位于png二进制文件的最后,数据格式是固定的{x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82}。知道这些,我们就可以把tExt块写入iEND块之前。为了后续读取这个tExt块,需要把tExt的块的数据区域的最后一个位置写成整个tExt块数组的长度。具体看代码。
这就是我对实现png格式图片隐写二维码的总结。
扩展:处理隐藏二维码,其实还可以把一些数据编码成二进制也是可以隐藏这些信息的。RGB三个通道都可以同时做这样的写值处理。如果二维码不只是两种颜色,而且想要还原的时候保持与原二维码图一致的话,我的思路是,隐写二维码的像素点的颜色值前23位,载体的每个像素的RGB值的最低位按顺序写入二维码的23个二进制,这要求载体图片的长和宽大概是二维码图片的长和宽的8倍甚至更多,思路是一样的,只不过解析的时候,需要做像素之间的对应关系。
全部代码:代码中的路径使用了硬编码,如果需要运行代码,要自己准备图片和改路径名称。
public class PicHide
{
private Bitmap _map = null;
private Bitmap _qrMap = null;
private int _w = 0;
private int _h = 0;
private string _path = string.Empty;
private string _qrPath = string.Empty;
private int _qw = 0, _qh = 0;
public PicHide(string picName, string qrPath)
{
_path = picName;
_qrPath = qrPath;
_map = new Bitmap(picName);
_w = _map.Width;
_h = _map.Height;
_qrMap = new Bitmap(qrPath);
_qw = _qrMap.Width;
_qh = _qrMap.Height;
if (_qw > _w || _qh > _h)
{
throw new Exception("隐写的二维码长宽必须小于图片本身的长宽");
}
}
public Color GetRGB(int x, int y)
{
var c = _map.GetPixel(x, y);
return c;
}
public void HidePic()
{
for (int i = 0; i < _w; i++)
{
for (int j = 0; j < _h; j++)
{
if (i < _qw && j < _qh)
{
var rgb = _qrMap.GetPixel(i, j);
int cBit = 0;
if (rgb.R >= 128 && rgb.B >= 128 && rgb.G >= 128)
{
cBit = 1;
}
var curColor = _map.GetPixel(i, j);
var hideColor = convertRGB(curColor, cBit);
setColor(i, j, hideColor);
}
else
{
var c = GetRGB(i, j);
var nc = convertRGB(c);
setColor(i, j, nc);
}
}
}
_map.Save("new1.png");
_map.Dispose();
_qrMap.Dispose();
_qrMap = null;
_map = null;
AddQrCodeWH("new1.png", _qw, _qh);
}
public int b(int x, int y)
{
return GetRGB(x, y).B;
}
public static string GetHideQrCode(string path)
{
var str = GetWh(path);
var arr = str.Split(",");
int w = int.Parse(arr[0].Replace("w=", ""));
int h = int.Parse(arr[1].Replace("h=", ""));
var map = new Bitmap(path);
var qrMap = new Bitmap(w, h);
for (int i = 0; i < w; i++)
{
for (int j = 0; j < h; j++)
{
var color = map.GetPixel(i, j);
int val = GetqrColorbit(color);
if (val == 0)
{
qrMap.SetPixel(i, j, Color.Black);
}
else
{
qrMap.SetPixel(i, j, Color.White);
}
}
}
map.Dispose();
qrMap.Save("newCode.png");
qrMap.Dispose();
map = null;
qrMap = null;
return "";
}
public static string GetWh(string path)
{
var file = File.OpenRead(path);
var iEnd = new byte[12] { 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 };
file.Seek(file.Length - iEnd.Length - 5, SeekOrigin.Begin);
var allLenArr = new byte[1];
file.Read(allLenArr, 0, 1);
var allLen = allLenArr[0];
file.Seek(file.Length - iEnd.Length - allLen, SeekOrigin.Begin);
var lenArr = new byte[4];
file.Read(lenArr, 0, 4);
file.Seek(4, SeekOrigin.Current);
var len = (lenArr[0] << 24) | (lenArr[1] << 16) | (lenArr[2] << 8) | (lenArr[3]);
var payload = new byte[len - 1];
file.Read(payload, 0, len - 1);
var str = System.Text.Encoding.UTF8.GetString(payload);
file.Close();
file.Dispose();
return str;
}
public int bRemoveLast(int b)
{
var val = 0xFE;
return b & val;
}
public Color convertRGB(Color c)
{
Color removeColor = Color.FromArgb(c.A, c.R, c.G, bRemoveLast(c.B));
return removeColor;
}
public Color convertRGB(Color c, int i)
{
Color hideColor = Color.FromArgb(c.A, c.R, c.G, (bRemoveLast(c.B) | i));
return hideColor;
}
public void setColor(int x, int y, Color c)
{
_map.SetPixel(x, y, c);
}
public static int GetqrColorbit(Color c)
{
var x = 0x01;
return c.B & x;
}
private uint getCrc32(byte[] inStr)
{
uint[] crc32Table = new uint[256];
uint i, j;
uint crc;
// 生成 CRC32 表
for (i = 0; i < 256; i++)
{
crc = i;
for (j = 0; j < 8; j++)
{
if ((crc & 1) != 0)
{
crc = (crc >> 1) ^ 0xEDB88320;
}
else
{
crc >>= 1;
}
}
crc32Table[i] = crc;
}
crc = 0xffffffff;
for (i = 0; i < inStr.Length; i++)
{
crc = (crc >> 8) ^ crc32Table[(crc & 0xFF) ^ inStr[i]];
}
crc ^= 0xFFFFFFFF;
return crc;
}
public void AddQrCodeWH(string fileName, int w, int h)
{
var file = File.Open(fileName, FileMode.Open, FileAccess.ReadWrite);
string payLoad = $"w={w},h={h}";
var payArr = System.Text.Encoding.UTF8.GetBytes(payLoad);
var len = payArr.Length + 1;
var buf = new byte[12 + payArr.Length + 1];
buf[0] = (byte)(len >> 24 & 0xff);
buf[1] = (byte)(len >> 16 & 0xff);
buf[2] = (byte)(len >> 8 & 0xff);
buf[3] = (byte)(len & 0xff);
buf[4] = Convert.ToByte('t');
buf[5] = Convert.ToByte('E');
buf[6] = Convert.ToByte('X');
buf[7] = Convert.ToByte('t');
for (int j = 0; j < payArr.Length; j++)
buf[j + 8] = payArr[j];
buf[payArr.Length + 8] = (byte)buf.Length;
var crc = this.getCrc32(payArr);
buf[len + 8] = (byte)(crc >> 24 & 0xff);
buf[len + 9] = (byte)(crc >> 16 & 0xff);
buf[len + 10] = (byte)(crc >> 8 & 0xff);
buf[len + 11] = (byte)(crc & 0xff);
var iEnd = new byte[12] { 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 };
var lenArr = new byte[4];
file.Seek(file.Length - iEnd.Length, SeekOrigin.Begin);
file.Write(buf, 0, buf.Length);
file.Write(iEnd, 0, iEnd.Length);
file.Close();
file.Dispose();
file = null;
}
//public string testwhInfo()
//{
// var file = File.OpenRead(_path);
// var iEnd = new byte[12] { 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 };
// file.Seek(file.Length - iEnd.Length-5, SeekOrigin.Begin);
// var allLenArr = new byte[1];
// file.Read(allLenArr, 0, 1);
// var allLen = allLenArr[0];
// file.Seek(file.Length - iEnd.Length - allLen, SeekOrigin.Begin);
// var lenArr = new byte[4];
// file.Read(lenArr, 0, 4);
// file.Seek(4, SeekOrigin.Current);
// var len = (lenArr[0] << 24) | (lenArr[1] << 16) | (lenArr[2] << 8) | (lenArr[3]);
// var payload=new byte[len-1];
// file.Read(payload, 0, len-1);
// var str=System.Text.Encoding.UTF8.GetString(payload);
// return str;
//}
//public void AddQrCodeWH(int w, int h)
//{
// var file = File.Open(_path, FileMode.Open, FileAccess.ReadWrite);
// string payLoad = $"w={w},h={h}";
// var payArr = System.Text.Encoding.UTF8.GetBytes(payLoad);
// var len = payArr.Length + 1;
// var buf = new byte[12 + payArr.Length + 1];
// buf[0] = (byte)(len >> 24 & 0xff);
// buf[1] = (byte)(len >> 16 & 0xff);
// buf[2] = (byte)(len >> 8 & 0xff);
// buf[3] = (byte)(len & 0xff);
// buf[4] = Convert.ToByte('t');
// buf[5] = Convert.ToByte('E');
// buf[6] = Convert.ToByte('X');
// buf[7] = Convert.ToByte('t');
// for (int j = 0; j < payArr.Length; j++)
// buf[j + 8] = payArr[j];
// buf[payArr.Length + 8] = (byte)buf.Length;
// var crc = this.getCrc32(payArr);
// buf[len + 8] = (byte)(crc >> 24 & 0xff);
// buf[len + 9] = (byte)(crc >> 16 & 0xff);
// buf[len + 10] = (byte)(crc >> 8 & 0xff);
// buf[len + 11] = (byte)(crc & 0xff);
// var iEnd = new byte[12] { 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 };
// var lenArr = new byte[4];
// file.Seek(file.Length - iEnd.Length, SeekOrigin.Begin);
// file.Write(buf, 0, buf.Length);
// file.Write(iEnd, 0, iEnd.Length);
// file.Close();
// file.Dispose();
// file = null;
//}
}
View Code
internal class Program
{
static PicHide picHide = null;
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
picHide = new PicHide("o.png", "qrCode.png");
picHide.HidePic();
Console.WriteLine("hiden ok");
Thread.Sleep(3000);
PicHide.GetHideQrCode("new1.png");
}
}
调用