在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校验码:


屏幕快照 2018-09-19 上午11.18.26.png

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


屏幕快照 2018-09-19 上午11.18.36.png

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


屏幕快照 2018-09-19 上午11.19.21.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数据写入本地文件,也可以保存数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容