在PNG图片中写入隐藏信息

   由于最近做项目,遇到一个功能需要做,是往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图片中写入隐藏信息》 屏幕快照 2018-09-19 上午11.18.26.png

  在上面的图片中,我们明显看到了整个png图片中的数据结构,以及对应的数据块。这张pNG图片的数据还是旧数据,不包含我们需要添加的文本信息。下面是添加了文本信息后的图片数据。

《在PNG图片中写入隐藏信息》 屏幕快照 2018-09-19 上午11.18.36.png

  当我们有新的图片数据后,我们如果调用系统的方法来讲NSData数据转化为图片,那么这时候会出现部分数据丢失,包括添加的tEXt数据,也就是说,这种情况,我们不建议使用系统的方法来转化图片,而是直接以流的方式写入文件。下面的截图,是使用了系统方式保存图片后新的PNG图片读取时的数据内容。

《在PNG图片中写入隐藏信息》 屏幕快照 2018-09-19 上午11.19.21.png
《在PNG图片中写入隐藏信息》 屏幕快照 2018-09-19 上午11.18.46.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数据写入本地文件,也可以保存数据。

    原文作者:小书同学
    原文地址: https://www.jianshu.com/p/50a21d70e962
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞