由于最近做项目,遇到一个功能需要做,是往PNG图片里面写入隐藏的数据,然后将图片通过微信的方式分享出去,这时候保存下来的图片,可以从里面读取出写入的隐藏数据。
首先,我们需要了解什么是PNG格式,以及PNG格式的图片数据是如何存储的,我们能将我们的数据写入到什么地方。
具体的PNG格式文件的详细信息介绍可以参考以下地址:
png的故事:获取图片信息和像素内容 - WEB前端 - 伯乐在线
PNG文件结构分析 ---Png解析 - DoubleLi - 博客园
在文章中我们了解到,PNG格式的文件,除了开始的8个字节是固定的,后面的格式都是一个一个的数据块结构,也就是chunk。
每个chunk的结构又是固定的:
4个字节的数据长度
4个字节的chunk类型
不定长度的数据内容
4个字节的CRC校验码
那么根据这个结构,我们就可以很简单的去解析PNG图片了。
为什么上面会提到解析PNG图片,是因为我们知道具体的格式后,才能知道如何往里面写入数据,同时,也知道如何去解析我们写入后的数据。
根据后一篇文章,我们可以发现在类型是tEXt的时候,可以存放一些我们需要的数据,那么我的目的就是为png增加一个tEXt的chunk,这样,既不影响图片的显示,也顺利将数据写入了png图片中。那么将这个块写入什么位置呢?其实我做了一个简单的处理,就是放入到IEND的chunk之前,因为IEND是png的结束标识,而有些文章会建议将数据存在IEND的数据块中,因为IEND的数据内容是空的,所以可以写入。其实这是不好的,因为IEND作为PNG结束chunk,是不可以变的,也就是说最后的12个字节是不允许修改的,不然系统就会提示这个PNG图片有问题。(ios系统会直接提示)
上面知道了如何解析,也知道了要把数据写到什么chunk里,也明确了信息写入的地方,那么我们就着手开始往里面添加数据了。
由于是在iOS上实现的,具体贴出来的代码是iOS中的。
NSData *data = UIImagePNGRepresentation(image);
NSMutableData *newData = [[NSMutableData alloc]init];
NSUInteger start = 0;
//1、获取文件格式,前8个字节
[self readByte:data start:start length:8];
//2、读取数据块
BOOL isContinue = YES;
start = 8;
while (isContinue) {
//先读取chunk的前四个字节,得到数据的长度
Byte *chunkDataLength = [self readByte:data start:start length:4];
long length = [self translateLong:chunkDataLength length:4];
start += 4;
//再读取4个字节,得到数据chunk的类型
Byte *chunkTypeData = [self readByte:data start:start length:4];
NSMutableString *typeStr = [NSMutableString new];
for(int i = 0;i<4;i++){
Byte n = *(chunkTypeData+i);
[typeStr appendFormat:@"%c",n];
}
// Byte *dataByte = [self readByte:data start:start+4 length:length];
// NSMutableData *crcData = [NSMutableData new];
// [crcData appendBytes:chunkTypeData length:4];
// [crcData appendBytes:dataByte length:length];
// NSUInteger crcddd = [crcData crc32];
// Byte *crcByte = [self readByte:data start:start+4+length length:4];
// NSLog(@"%@ 数据类型: 数据长度:%ld ; 计算的校验位 :%x %x %x %x ; 读取的校验位:%x %x %x %x",typeStr,length,(int)((crcddd>>24)&0xff),(int)((crcddd>>16)&0x00ff),(int)((crcddd>>8)&0x0000ff),(int)(crcddd&0x000000ff),
// *(crcByte+0),*(crcByte+1),*(crcByte+2),*(crcByte+3));
//
if([@"IEND" isEqualToString:typeStr]){
//复制头数据
Byte *headerByte = [self readByte:data start:0 length:start-4];
[newData appendBytes:headerByte length:start-4];
/****** start:添加要写入的数据 ******/
//计算要写入的数据长度
NSData *strData = [@"要写入的内容字符串" dataUsingEncoding:NSUTF8StringEncoding];
//1、写入数据长度
NSUInteger strLength = strData.length;
Byte *lengthDataByte = [self translateLongToByte:strLength length:4];
[newData appendBytes:lengthDataByte length:4];
free(lengthDataByte);
//2、写入数据块类型
NSData *typeData = [@"tEXt" dataUsingEncoding:NSUTF8StringEncoding];
Byte *byte = (Byte *)malloc(4);
[typeData getBytes:byte length:4];
[newData appendBytes:byte length:4];
//3、写入字符串数据
[newData appendData:strData];
//4、写入crc
NSMutableData *crcData = [NSMutableData new];
[crcData appendBytes:byte length:4];
[crcData appendData:strData];
NSUInteger crcddd = [crcData crc32];
NSUInteger value = crcddd;
Byte *buffer = (Byte *)malloc(4);
for (int i = 0; i < 4; i++) {
*(buffer+3-i) = (Byte) (value & 0x000000ff);// 将最低位保存在最低位
value = value >> 8; // 向右移8位
}
[newData appendBytes:buffer length:4];
/****** end:添加要写入的数据 ******/
//复制后面底部数据
Byte *saliByte = [self readByte:data start:start-4 length:(data.length-start+4)];
[newData appendBytes:saliByte length:(data.length-start+4)];
isContinue = NO;
break;
}
start+=(length + 8);
free(chunkDataLength);
free(chunkTypeData);
}
上面就是具体的读取文本和写入文本的地方。上面的处理其实是将数据进行复制,也就是复制一个数据,然后把数据写入到新的数据中,这样就相当于新创建了一张PNG图片,然后是写入数据的新图片。
接下来补充几张图片。第一张是对于原始的图片数据,读取图片中的每个chunk的数据类型和长度,以及crc校验码:
在上面的图片中,我们明显看到了整个png图片中的数据结构,以及对应的数据块。这张pNG图片的数据还是旧数据,不包含我们需要添加的文本信息。下面是添加了文本信息后的图片数据。
当我们有新的图片数据后,我们如果调用系统的方法来讲NSData数据转化为图片,那么这时候会出现部分数据丢失,包括添加的tEXt数据,也就是说,这种情况,我们不建议使用系统的方法来转化图片,而是直接以流的方式写入文件。下面的截图,是使用了系统方式保存图片后新的PNG图片读取时的数据内容。
通过对比,我们明显的发型,数据中的iDOT和tEXt数据丢失了,这是我们不希望看到的。
有几个方法需要补充一下
-(Byte *)readByte:(NSData *)data start:(NSUInteger)start length:(NSUInteger)length{
long newLength = length;
if(data.length < start+length){
newLength = data.length-start;
}
Byte *buffer = (Byte *)malloc(newLength);
[data getBytes:buffer range:NSMakeRange(start, newLength)];
return buffer;
}
-(Byte *)translateLongToByte:(long)value length:(long)length{
Byte *buffer = (Byte *)malloc(length);
long number = value;
for (int i = 0; i < length; i++) {
*(buffer+length-1-i) = (Byte) (number & 0x0f);// 将最低位保存在最低位
number = number >> 4; // 向右移8位
}
return buffer;
}
-(long)translateLong:(Byte *)byteData length:(int)length {
long value = 0;
for(int i = 0; i < length; i++){
long valuei = *(byteData+i);
value += (valuei << (8*(length-i-1)));
}
return value;
}
crc方法
-(int32_t)crc32
{
uint32_t *table = malloc(sizeof(uint32_t) * 256);
uint32_t crc = 0xffffffff;
uint8_t *bytes = (uint8_t *)[self bytes];
for (uint32_t i=0; i<256; i++) {
table[i] = i;
for (int j=0; j<8; j++) {
if (table[i] & 1) {
table[i] = (table[i] >>= 1) ^ 0xedb88320;
} else {
table[i] >>= 1;
}
}
}
for (int i=0; i<self.length; i++) {
crc = (crc >> 8) ^ table[(crc & 0xff) ^ bytes[i]];
}
crc ^= 0xffffffff;
free(table);
return crc;
}
在上面处理中,我们一直保持着NSData的类型,而不能讲NSData直接通过IOS自有方法去存储图片,因为这样会将图片里写入的这部分数据删除掉,具体原因未知。所以后续的办法就是通过NSData将数据直接由微信分享出去(微信的版本会影响微信存储的图片类型),这样保存的图片就不会丢失数据。也可以直接将NSData数据写入本地文件,也可以保存数据。