使用libpng进行PNG图像隐写术






4.96/5 (15投票s)
对PNG图像执行隐写术。
介绍
数字隐写术被定义为在数字媒体中隐藏消息。在这里,我们将研究如何在图像中隐藏信息。在本文中,我将介绍数字隐写术的基本概念以及使用 PNG 图像的概念验证。如果您正在阅读本文,我假设您对 C++ 有扎实的知识,并且对二进制算术有良好的掌握。
趣味事实:这是一种被秘密组织和恐怖组织(如基地组织)秘密共享信息的常用技术。双方事先约定一幅图像和一个位置。想要传递信息的一方将隐藏数据编码到图像中,上传它,第二方下载它。对任何旁观者来说,图像看起来完全正常,但实际上图像包含了任意隐藏数据。来自基地组织的例子可以在这里找到。
背景
在我将要展示的示例中,我们将数据隐藏在 PNG 图像的最低有效位中。隐藏数据在图像中有许多方法,这只是众多技术中的一种。在这里,我将粗略地讨论 PNG 文件规范的一些细节,以提供理解代码的必要背景。
PNG 图像有三种变体;我们将研究真彩色图像。另外两种是灰度和调色板。我们不会使用 Alpha 通道。对于好奇的读者,Alpha 通道提供颜色透明度信息。
PNG 图像由块组成。有许多不同类型的块,每个块都有不同的作用。我们将关注的三个块类型是 IHDR、IDAT 和 IEND 块。
完整的规范可以在这里找到。以下是与本文相关的部分的摘要。
块布局
每个块包含三个或四个字段。
长度 - 一个四字节无符号整数,给出块数据字段中的字节数。长度仅计算数据字段,不包括本身、块类型或 CRC。零是有效长度。虽然编码器和解码器应将长度视为无符号,但其值不得超过 231-1 字节。
块类型 - 一个四字节序列,定义块类型。块类型的每个字节限制在十进制值 65 到 90 和 97 到 122 之间。这些分别对应于 ISO 646 的大写和小写字母(A-Z 和 a-z)方便描述和检查 PNG 数据流。编码器和解码器应将块类型视为固定二进制值,而不是字符串。例如,用 UCS 2 字符集中这些字母的等价物来表示块类型IDAT是不正确的。
块数据 - 适用于块类型的任何数据字节。此字段的长度可以为零。
CRC - 一个四字节 CRC(循环冗余校验码),根据块中的先前字节计算,包括块类型字段和块数据字段,但不包括长度字段。CRC 可用于检查数据损坏。即使是包含无数据的块,CRC 也始终存在。
IHDR 块
四字节块类型字段包含十进制值 73 72 68 82。
IHDR 块应为 PNG 数据流中的第一个块。它包含
宽度 | 4 字节 |
高度 | 4 字节 |
位深度 | 1 字节 |
颜色类型 | 1 字节 |
压缩方法 | 1 字节 |
过滤方法 | 1 字节 |
隔行扫描方法 | 1 字节 |
宽度和高度以像素为单位给出图像尺寸。它们是 PNG 四字节无符号整数。零是无效值。位深度是一个单字节整数,给出每样本的位数。样本在这里表示一种颜色。有效值为 1、2、4、8 和 16,尽管并非所有值都适用于所有颜色类型。每个像素由三个字节组成。这三个字节中的每一个代表一种不同的颜色,在本例中为红色、绿色、蓝色。这些颜色组合起来构成您实际看到的那个像素。
IDAT 块
四字节块类型字段包含十进制值 73 68 65 84。IDAT 块包含实际图像数据,这是压缩算法的输出流。如果您对 PNG 图像的过滤和压缩感兴趣,请查看过滤和压缩。可能存在多个 IDAT 块;如果存在,它们应连续出现,中间没有其他块。压缩数据流然后是所有 IDAT 块数据字段内容的串联。
IEND 块
四字节块类型字段包含十进制值 73 69 78 68。IEND 块标记 PNG 数据流的结束。块的数据字段为空。
PNG 隐写术
如前所述,每个像素由 3 个字节组成,第一个是红色,第二个是绿色,第三个是蓝色。各种研究产生了不同的结果,但最可靠的来源维基百科称,人眼可以区分大约 1000 万种不同的颜色。总共有三个字节,我们可以表示 2(8+3) 或 16,777,216 种不同的颜色。这意味着我们大约可以表示 6,777,216 种颜色,但人眼不会注意到与另外 1000 万种颜色相比的差异。
换句话说
给定以下真彩色像素(这些数据将位于 IDAT 块中)
红色部分 -> 10100100 绿色部分 -> 11101100 蓝色部分 -> 1010100
如果我们更改这三部分为
红色部分 -> 10100101 绿色部分 -> 11101101 蓝色部分 -> 1010101
您可能会猜到这是怎么回事。这意味着我可以更改每个字节的最低有效位,没有人能从视觉上分辨出原始像素和更改后的像素之间的区别,除非他们拥有超人的高清视力(根据我的广泛医学知识,这种视力不存在)。
我们将利用这一点来隐藏消息,方式如下:
要隐藏的一个字节的消息:10101010
8 字节图像数据(即两个像素和第三个像素的红色和绿色字节)
11110000, 10101010, 11001100, 11100011, 11111111, 00000000, 00001111, 10011011
将我们的隐藏消息从最低有效位(LSB)编码到最高有效位(MSB),编码到图像数据的最低有效位中。图像数据随后变为:
11110000, 10101011, 11001100, 11100011, 11111110, 00000001, 00001110, 10011011
现在,我们在图像中隐藏了一个字节的消息数据。再执行数千次,我们就可以隐藏相当多的数据。显然,这不是最高效的方案,因为我们使用的 PNG 图像需要比隐藏数据本身大 8 倍。我们追求的是简单而不是超级酷炫。我们可以通过利用其他隐藏位置、压缩技术、使用更多不可检测的位、Alpha 通道、文本通道以及其他无数种方法来获得更高的效率,但在这里我们将坚持使用最低有效位。
使用代码
程序员注意:下面的所有内容都在 Visual Studio 2012 中完成
为了处理 PNG 图像,我创建了一个 PNG 图像类来实现隐写术部分,并使用了 libpng 库和 zlib 库来实际完成所有的 PNG 操作。值得再次提到的是,PNG 图像文件经过过滤然后压缩,以减小空间占用。因此,我们无法编辑原始 PNG 的 IDAT 块,否则会得到一些非常奇怪的结果(我尝试过,只是为了好玩,当你尝试编码某些内容时,会非常明显)。话虽如此,我们需要 libpng 来解压缩并取消过滤图像。libpng 使用的压缩算法是 deflate,它由 zlib 实现,如果您想知道为什么需要 zlib,那就是这样。
您可以在这里查看 libpng 文档。
PNG_file 类的定义如下
#include <png.h>
/* Class PNG_file
* Contains the data for a PNG file object
*/
class PNG_file {
public:
//Constructor
PNG_file(const char *inputFileName);
//Function for encoding data into the PNG from a file
void encode(const char *fileToEncodeName);
//Function for outputing the newly created PNG to a file
void outputPNG(const char *outputFileName);
//Function for outputing the decoded PNG to a file
void decode(const char *outputFileName);
private:
png_bytep* row_pointers;
png_infop info_ptr;
png_structp read_ptr;
png_structp write_ptr;
};
暂时不用太担心理解每个位是什么意思。我们会一一介绍。
读取 PNG 图像
所以我们要做的第一件事是解压缩和取消过滤我们的 PNG 图像。这一切都发生在 PNG_file
构造函数中
/* PNG Constructor
* Constructor for the PNG_file class Simply reads in a PNG file
*/
PNG_file::PNG_file(const char *inputFileName) {
FILE * inputFile;
unsigned char header[BYTE_SIZE];
inputFile = fopen (inputFileName,"rb");
//Check if the file opened
if(!inputFile)
exit(1);
// START READING HERE
fread(header, 1, PNG_SIG_LENGTH, inputFile);
//Check if it is a PNG
if(png_sig_cmp(header, 0, PNG_SIG_LENGTH))
exit(1);
//Set up libPNG data structures and error handling
read_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!read_ptr)
exit(1);
info_ptr = png_create_info_struct(read_ptr);
if (!info_ptr) {
png_destroy_read_struct(&read_ptr,
(png_infopp)NULL, (png_infopp)NULL);
exit(1);
}
png_infop end_info = png_create_info_struct(read_ptr);
if (!end_info) {
png_destroy_read_struct(&read_ptr, &info_ptr,
(png_infopp)NULL);
exit(1);
}
//End data structure/error handling setup
//Initialize IO for PNG
png_init_io(read_ptr, inputFile);
//Alert libPNG that we read PNG_SIG_LENGTH bytes at the beginning
png_set_sig_bytes(read_ptr, PNG_SIG_LENGTH);
//Read the entire PNG image into memory
png_read_png(read_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
row_pointers = png_get_rows(read_ptr, info_ptr);
//Make sure the bit depth is correct
if(read_ptr->bit_depth != BYTE_SIZE)
exit(1);
fclose(inputFile);
}
解释:首先,我们在要编码隐藏数据的 PNG 文件上打开一个文件流,并声明一个变量 header
,它将包含 PNG 文件签名。接下来的几行读取 PNG 签名,然后使用 libpng
函数 png_sig_cmp
检查签名是否有效。
fread(header, 1, PNG_SIG_LENGTH, inputFile);
//Check if it is a PNG
if(png_sig_cmp(header, 0, PNG_SIG_LENGTH))
exit(1);
之后,我们设置了一些必要的 libpng 数据结构。这里的主要收获是 read_ptr
指向的数据最终将包含所有 PNG 图像数据结构和信息。另外 info_ptr
将包含 IHDR 头块数据。如果您愿意,可以通过操作 IHDR 头进行一般的图像转换。这是代码
//Set up libPNG data structures and error handling
read_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!read_ptr)
exit(1);
info_ptr = png_create_info_struct(read_ptr);
if (!info_ptr) {
png_destroy_read_struct(&read_ptr,
(png_infopp)NULL, (png_infopp)NULL);
exit(1);
}
png_infop end_info = png_create_info_struct(read_ptr);
if (!end_info) {
png_destroy_read_struct(&read_ptr, &info_ptr,
(png_infopp)NULL);
exit(1);
}
//End data structure/error handling setup
下面一行初始化 PNG 的 IO:
//Initialize IO for PNG
png_init_io(read_ptr, inputFile);
libpng 要求我们在读取图像之前告知它是否已从文件流中读取了任何数据,因此我们用以下行告知它。
//Alert libPNG that we read PNG_SIG_LENGTH bytes at the beginning
png_set_sig_bytes(read_ptr, PNG_SIG_LENGTH);
之后,我们将整个 PNG 读取到内存中(效率再次不是此项的首要考虑因素),然后将
row_pointers
设置为指向一个指针数组。该数组中的每个指针都指向一行图像数据。它看起来像这样
row_pointers -> row1ptr -> row1data
row2ptr -> row2data
row3ptr -> row3data
依此类推...
//Read the entire PNG image into memory
png_read_png(read_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
row_pointers = png_get_rows(read_ptr, info_ptr);
最后一行至少会检查位深度是否正确。我们应该检查更多内容以确保图像兼容,但这只是一个 POC。
//TODO ADD A CHECK SO WE ONLY USE COMPATIBLE PNG IMAGES
if(read_ptr->bit_depth != BYTE_SIZE)
exit(1);
将数据编码到图像中
接下来要做的是将数据编码到我们已读入内存的图像中。这由以下函数执行:
void PNG_file::encode(const char *fileToEncodeName) {
//BEGIN ENCODING HERE
FILE * fileToEncode;
unsigned char buffer = 0;
fileToEncode = fopen (fileToEncodeName,"rb");
//Check if the file opened
if(!fileToEncode)
exit(1);
//TODO CONSIDER ADDING CHECK FOR FILES THAT ARE TOO BIG
unsigned long size = filesize(fileToEncodeName);
//This section of code encodes the input file into the picture
//It encodes the input file bit by bit into the least significant
//bits of the original picture file
for(int y=0; y < read_ptr->height; y++) {
int x=0;
//Write the file size into the file y==0 ensures that it only happens
//once
if(y == 0)
for(x; x < SIZE_WIDTH; x++) {
if((size & ipow(2,x)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
}
for(x; x < read_ptr->width*3; x++) {
if(x%BYTE_SIZE == 0) {
if(!fread(&buffer, 1, 1, fileToEncode))
goto loop_end;
}
//png_bytep here = row_pointers[y]+x; for debugging
if((buffer & ipow(2,x%BYTE_SIZE)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
}
//Make sure that we did not use a file too large that it can't be encoded
if(y >= read_ptr->height)
exit(1);
}
//goto jumps here to break out of multiple loops
loop_end:
fclose(fileToEncode);
}
关于第一部分没什么可说的,除了变量 buffer
将包含被编码的字节,从隐藏消息文件到有问题 PNG 图像。size
变量包含要编码的文件的大小。这是代码
//BEGIN ENCODING HERE
FILE * fileToEncode;
unsigned char buffer = 0;
fileToEncode = fopen (fileToEncodeName,"rb");
//Check if the file opened
if(!fileToEncode)
exit(1);
//TODO CONSIDER ADDING CHECK FOR FILES THAT ARE TOO BIG
unsigned long size = filesize(fileToEncodeName);
filesize
函数只是一个计算文件大小的辅助函数。现在,encode 函数的主体有点复杂,所以我将尽力逐行解释
//This section of code encodes the input file into the picture
//It encodes the input file bit by bit into the least significant
//bits of the original picture file
for(int y=0; y < read_ptr->height; y++) {
int x=0;
//Write the file size into the file y==0 ensures that it only happens
//once
if(y == 0)
for(x; x < SIZE_WIDTH; x++) {
if((size & ipow(2,x)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
}
for(x; x < read_ptr->width*3; x++) {
if(x%BYTE_SIZE == 0) {
if(!fread(&buffer, 1, 1, fileToEncode))
goto loop_end;
}
//png_bytep here = row_pointers[y]+x; For debugging
if((buffer & ipow(2,x%BYTE_SIZE)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
}
//Make sure that we did not use a file too large that it can't be encoded
if(y >= read_ptr->height)
exit(1);
}
//goto jumps here to break out of multiple loops
loop_end:
外层循环(主要变量是 y
)控制我们正在编码的图像数据的行。以下代码段负责将我们要编码的文件的大小编码到 PNG 图像中。
if(y == 0)
for(x; x < SIZE_WIDTH; x++) {
if((size & ipow(2,x)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
}
您可能会注意到我将 x
设置为循环外部的 0。我将在稍后解释原因。该
if(y == 0)
部分确保 for 循环仅在编码到第一行时运行。如果不是这样,我们将把大小编码到每一行,这我们不想要。for 循环从 0 运行到 SIZE_WIDTH
。此处 SIZE_WIDTH
是用于包含大小的字节数。在这种情况下,我想要 32 位用于大小,因此 SIZE_WIDTH
是 32。请记住,隐藏消息的每一位都需要 PNG 文件的一个字节。因此,32 位大小将存储在 32 个 PNG 图像字节上。下一部分是:
if((size & ipow(2,x)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
您可能需要盯着它看一会儿,但这正在做的是迭代大小的 32 位中的每一位,检查它们是否为 1,如果是,则将 PNG 字节与 1 进行按位或运算以将该 1 编码到最低有效位,如果该位不是 1,则将 PNG 字节与 0xFE 进行按位与运算,其效果是将最低有效位设置为 0。值得注意的是,ipow
只是一个辅助函数,是 pow
函数的整数实现。下一部分可能会令人困惑。
for(x; x < read_ptr->width*3; x++) {
if(x%BYTE_SIZE == 0) {
if(!fread(&buffer, 1, 1, fileToEncode))
goto loop_end;
}
//png_bytep here = row_pointers[y]+x; for debugging
if((buffer & ipow(2,x%BYTE_SIZE)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
}
循环根据这是图像数据的第几行,从 0 或 32 开始。记住,在图像数据的第 [first] 行中,我们编码了隐藏消息的大小(以字节为单位)。这就是为什么我在循环外初始化 x。我需要允许它在第一次运行时为 32,而在每次后续运行时为 0。循环在 read_ptr->width*3.
之后结束。*3 是因为宽度以像素为单位,每个像素有 3 个字节。
if(x%BYTE_SIZE == 0) {
if(!fread(&buffer, 1, 1, fileToEncode))
goto loop_end;
}
这部分检查 x 是否是 8 的倍数(记住 BYTE_SIZE
== 8)。如果是 8 的倍数,则意味着我们已经编码了 8 位,可以从文件中读取另一个字节进行编码。如果 fread 返回 0,则意味着我们已到达文件末尾,必须跳出嵌套循环。
//png_bytep here = row_pointers[y]+x; for debugging
if((buffer & ipow(2,x%BYTE_SIZE)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
此部分执行实际编码。它迭代从文件中读取的字节的每一位(从右到左)进行编码。如果该位是 1,则将图像中的字节与 1 进行按位或运算以将 LSB 设置为 1,否则,将图像字节的 LSB 设置为 0。循环的最后一部分只是一个粗略的检查,以查看我们是否还有要放置数据的图像行,这意味着我们的隐藏消息太大了。
//Make sure that we did not use a file too large that it can't be encoded
if(y >= read_ptr->height)
exit(1);
解码图像数据
最后要做的是从编码图像中解码隐藏数据。这将检查图像数据每个字节的 LSB(直到我们达到大小),提取它,然后重新组合。decode 函数本质上只是 encode 函数的逆向。这是它:
void PNG_file::decode(const char *outputFileName) {
//BEGIN DECODING HERE
FILE * outputFile;
unsigned char buffer = 0;
outputFile = fopen (outputFileName,"wb");
//Check if the file opened
if(!outputFile)
exit(1);
unsigned int size = 0;
//
for(int y=0; y < read_ptr->height; y++) {
int x=0;
//Write the file size into the file y==0 ensures that it only happens
//once
if(y == 0)
for(x; x < SIZE_WIDTH; x++) {
size |= ((*(row_pointers[0]+x) & 1 ) << x);
}
for(x; x < read_ptr->width*3; x++) {
if((x > SIZE_WIDTH || y > 0) && x%BYTE_SIZE == 0) {
fwrite(&buffer, 1, 1, outputFile);
buffer = 0;
}
//png_bytep here = row_pointers[y]+x; for debugging
if(((read_ptr->width*y)*3+x) == size*BYTE_SIZE+SIZE_WIDTH)
goto loop_end;
buffer |= ((*(row_pointers[y]+x) & 1) << x%BYTE_SIZE);
}
}
//goto jumps here to break out of multiple loops
loop_end:
fclose(outputFile);
}
到此时,我预计您已经知道前面的部分是什么意思了,所以我将跳到重点。
for(int y=0; y < read_ptr->height; y++) {
int x=0;
//Write the file size into the file y==0 ensures that it only happens
//once
if(y == 0)
for(x; x < SIZE_WIDTH; x++) {
size |= ((*(row_pointers[0]+x) & 1 ) << x);
}
for(x; x < read_ptr->width*3; x++) {
if((x > SIZE_WIDTH || y > 0) && x%BYTE_SIZE == 0) {
fwrite(&buffer, 1, 1, outputFile);
buffer = 0;
}
//png_bytep here = row_pointers[y]+x; for debugging
if(((read_ptr->width*y)*3+x) == size*BYTE_SIZE+SIZE_WIDTH)
goto loop_end;
buffer |= ((*(row_pointers[y]+x) & 1) << x%BYTE_SIZE);
}
}
前 8 行现在可能比较容易理解。if 语句和 for 循环从第一行的前 32 个字节中提取长度。这 32 个字节中每个字节的最低有效位被组合成一个 32 位无符号整数,该整数表示编码文件的大小。我们需要知道这个,以便知道何时停止读取。内部 for 循环的工作方式与内部 for 循环 encode 相同。与 encode 不同的是内部 for 循环。这是第一部分:
if((x > SIZE_WIDTH || y > 0) && x%BYTE_SIZE == 0) {
fwrite(&buffer, 1, 1, outputFile);
buffer = 0;
}
if 语句仅在 x 是 8 的倍数时为 true,但*不*在第一次迭代时为 true。我的意思是,我们不希望在第一次迭代时(此时我们刚刚完成提取大小)进入此条件,因为在执行到此时,我们的缓冲区中没有任何内容。它将只是 0(因为我们之前已将其初始化为 0)。所以我们说 x
必须大于 SIZE_WIDTH
,这可以确保它在第一次迭代时不会运行。或者,要满足条件,y 也可以大于 0,因为我们确实希望在第一次迭代之后的每次迭代中检查此条件,此时 x == 0。抱歉,我知道这很令人困惑。您可能必须盯着它看一会儿。if 语句内部将当前解码的字节写入我们的输出文件并将缓冲区重置为 0。
接下来是检查我们是否已到达编码数据末尾的检查。
if(((read_ptr->width*y)*3+x) == size*BYTE_SIZE+SIZE_WIDTH)
goto loop_end;
看!goto
函数在其恰当的位置使用。goto
可以合法地用于一些事情,但跳出嵌套循环是其中之一。总之,if 语句正在检查我们是否已解码所有隐藏数据。read_ptr->width
乘以 y
,因为这是我们已读取的总行数。请记住,该数字以像素为单位,因此我们必须乘以 3 才能获得字节数。最后,我们加上当前行图像数据中的进度,这就 accounted for 了 +x
。另一方面,您得到 size
乘以 BYTE_SIZE
。这是因为 size
变量是以字节为单位的。为了读取一个字节的隐藏数据,我们需要读取 8 个字节的图像数据,所以我们乘以 BYTE_SIZE
(即 8)。最后,您加上 SIZE_WIDTH
,因为除了您读取的隐藏数据之外,您还读取了该数据的大小。请记住 SIZE_WIDTH
是 32。最后,剩下的就是将每个解码数据的位实际放置到缓冲区中。
buffer |= ((*(row_pointers[y]+x) & 1) << x%BYTE_SIZE);
同样,这样的代码可能很难读懂,我会尽力解释。中间部分
((*(row_pointers[y]+x) & 1)
所有这些都在做的是提取图像数据字节的 LSB,这就是我们的编码位。现在我们必须将其正确地对齐到我们的缓冲区中。因为该数据位可能是我们编码字节的 LSB,也可能是我们编码字节的第 4 位。我们必须将其向左移动适当的位数。这就是
<< x%BYTE_SIZE)
起作用的地方。它将位向左移动适当的位数。最后,我们将其与到目前为止的 buffer
进行按位或运算。
图像字节是:11100011
重构的编码字节是:11001101
这意味着我们的编码位是 1
所以我们对其进行按位与运算以获得临时字节
00000001
假设这个特定的位是我们重构编码字节的第三位(从左边计数)。这意味着我们需要将其左移三位。(我们已经用左移运算符完成了)。这就得到了临时字节
00000100
现在我们将其与到目前为止的缓冲区进行按位或运算。因为我们是从最高有效位到最低有效位读取编码位的,所以在按位或运算之前,我们的缓冲区看起来像这样
11001000
并在与临时字节按位或运算后
00000100
11001000 或
_______________
11001100 <- 操作结束时的 buffer
。
如果我们继续这个过程,我们将得到完全解码的字节。
11001101
头文件名为 png_file.h,位于代码部分的开头。下面是一个使用 PNG_file
类的示例 main
#include "PNG_file.h"
void main() {
PNG_file link = PNG_file("link.png");
link.encode("small.png");
link.outputPNG("output.png");
link.decode("decodedfile.png");
}
关注点
所以,使用 libpng 有点糟糕。Windows 版本已经很久没有更新了,文档有点令人困惑。(仍然要给那些只是抽出时间帮助大家的人点赞。)因此,我包含了我为 Visual Studio 2012 调整过的版本以及 zlib。您可能需要调整依赖项才能使其正常工作,但您可以随意重用。希望它能节省一些人的麻烦。最后,这是 PNG_file
类的完整版本,因此您无需下载即可查看。
/* PNG_file
* author: Grant Curell
* Performs IO and encoding and decoding on PNG images
* Feel free to reuse at your leisure. Cite me if you like, but it's no big deal.
* Thanks to the random dudes I bummed the code for ipow and filesize from on
* stackoverflow ;-).
*/
#include <stdio.h>
#include <stdlib.h>
#include "PNG_file.h"
#define PNG_SIG_LENGTH 8 //The signature length for PNG
#define BYTE_SIZE 8 //Size of a byte
#define SIZE_WIDTH 32 //The number of bits used for storing the length of a file
//Must be a multiple of 8
/* Integer power function
* The C++ standard pow function uses doubles and I needed an integer version.
* This is just a standard implementation using modular exponentiation.
*/
int ipow(int base, int exp) {
int result = 1;
while (exp)
{
if (exp & 1)
result *= base;
exp >>= 1;
base *= base;
}
return result;
}
//Dirty function for calculating the size of a file
unsigned int filesize(const char *filename)
{
FILE *f = fopen(filename,"rb"); /* open the file in read only */
unsigned int size = 0;
if (fseek(f,0,SEEK_END)==0) /* seek was successful */
size = ftell(f);
fclose(f);
return size;
}
/* PNG Constructor
* Constructor for the PNG_file class Simply reads in a PNG file
*/
PNG_file::PNG_file(const char *inputFileName) {
FILE * inputFile;
unsigned char header[BYTE_SIZE];
inputFile = fopen (inputFileName,"rb");
//Check if the file opened
if(!inputFile)
exit(1);
// START READING HERE
fread(header, 1, PNG_SIG_LENGTH, inputFile);
//Check if it is a PNG
if(png_sig_cmp(header, 0, PNG_SIG_LENGTH))
exit(1);
//Set up libPNG data structures and error handling
read_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!read_ptr)
exit(1);
info_ptr = png_create_info_struct(read_ptr);
if (!info_ptr) {
png_destroy_read_struct(&read_ptr,
(png_infopp)NULL, (png_infopp)NULL);
exit(1);
}
png_infop end_info = png_create_info_struct(read_ptr);
if (!end_info) {
png_destroy_read_struct(&read_ptr, &info_ptr,
(png_infopp)NULL);
exit(1);
}
//End data structure/error handling setup
//Initialize IO for PNG
png_init_io(read_ptr, inputFile);
//Alert libPNG that we read PNG_SIG_LENGTH bytes at the beginning
png_set_sig_bytes(read_ptr, PNG_SIG_LENGTH);
//Read the entire PNG image into memory
png_read_png(read_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
row_pointers = png_get_rows(read_ptr, info_ptr);
//TODO ADD A CHECK SO WE ONLY USE COMPATIBLE PNG IMAGES
if(read_ptr->bit_depth != BYTE_SIZE)
exit(1);
fclose(inputFile);
}
void PNG_file::encode(const char *fileToEncodeName) {
//BEGIN ENCODING HERE
FILE * fileToEncode;
unsigned char buffer = 0;
fileToEncode = fopen (fileToEncodeName,"rb");
//Check if the file opened
if(!fileToEncode)
exit(1);
//TODO CONSIDER ADDING CHECK FOR FILES THAT ARE TOO BIG
unsigned long size = filesize(fileToEncodeName);
//This section of code encodes the input file into the picture
//It encodes the input file bit by bit into the least significant
//bits of the original picture file
for(int y=0; y < read_ptr->height; y++) {
int x=0;
//Write the file size into the file y==0 ensures that it only happens
//once
if(y == 0)
for(x; x < SIZE_WIDTH; x++) {
if((size & ipow(2,x)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
}
for(x; x < read_ptr->width*3; x++) {
if(x%BYTE_SIZE == 0) {
if(!fread(&buffer, 1, 1, fileToEncode))
goto loop_end;
}
//png_bytep here = row_pointers[y]+x; for debugging
if((buffer & ipow(2,x%BYTE_SIZE)))
*(row_pointers[y]+x) |= 1;
else
*(row_pointers[y]+x) &= 0xFE;
}
//Make sure that we did not use a file too large that it can't be encoded
if(y >= read_ptr->height)
exit(1);
}
//goto jumps here to break out of multiple loops
loop_end:
fclose(fileToEncode);
}
void PNG_file::decode(const char *outputFileName) {
//BEGIN DECODING HERE
FILE * outputFile;
unsigned char buffer = 0;
outputFile = fopen (outputFileName,"wb");
//Check if the file opened
if(!outputFile)
exit(1);
unsigned int size = 0;
//
for(int y=0; y < read_ptr->height; y++) {
int x=0;
//Write the file size into the file y==0 ensures that it only happens
//once
if(y == 0)
for(x; x < SIZE_WIDTH; x++) {
size |= ((*(row_pointers[0]+x) & 1 ) << x);
}
for(x; x < read_ptr->width*3; x++) {
if((x > SIZE_WIDTH || y > 0) && x%BYTE_SIZE == 0) {
fwrite(&buffer, 1, 1, outputFile);
buffer = 0;
}
//png_bytep here = row_pointers[y]+x; for debugging
if(((read_ptr->width*y)*3+x) == size*BYTE_SIZE+SIZE_WIDTH)
goto loop_end;
buffer |= ((*(row_pointers[y]+x) & 1) << x%BYTE_SIZE);
}
}
//goto jumps here to break out of multiple loops
loop_end:
fclose(outputFile);
}
void PNG_file::outputPNG(const char *outputFileName) {
//START WRITING HERE
FILE * outputFile;
outputFile = fopen (outputFileName,"wb");
//Check if the file opened
if(!outputFile)
exit(1);
//Initialize the PNG structure for writing
write_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!write_ptr)
exit(1);
png_init_io(write_ptr, outputFile);
//Set the rows in the PNG structure
png_set_rows(write_ptr, info_ptr, row_pointers);
//Write the rows to the file
png_write_png(write_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
fclose(outputFile);
}
有一个函数我没有在代码部分解释。那就是 outputPNG
函数。它输出 PNG 文件的编码版本。其中只有两个值得注意的点是函数 png_set_rows
,它设置我们修改过的行以供写入。png_write_png
实际将图像写入存储。具体细节请参考 libpng 文档,这里。
在其他代码中使用 libpng for Windows
在下载部分,我包含了包含 libpng 的 VS2012 项目。为了编译它,它依赖于 zlib,我也包含了它。为了使其工作,您需要在 VS 中打开该项目,在一个新的解决方案中。然后您必须将 zlib 项目添加到该解决方案中。一旦两者都在里面,就编译 zlib。记下 zlib.lib 文件被输出的位置。然后,在 libpng 项目的属性中,在 C++ 常规下,您必须将 zlib 源文件夹添加到其他包含目录。然后在库管理器常规下,确保 zlib.lib 被列为其他依赖项,并将包含 zlib.lib 的目录添加到其他库目录。希望这能帮助到一些人,因为在 Windows 下让 libpng 工作对我来说非常困难。
历史
这是版本 1。我一直在努力,而且我明天要为考试学习,所以我没有编辑语法 ;-D。