注:此文章并不是特别详细,只做参考
PNG文件格式解析
PNG 图像格式文件由一个 8 字节的 PNG 文件署名域和 3 个以上的后续数据块(IHDR、IDAT、IEND)组成。
PNG 文件中,每个数据块(比如IHDR,IDAT等)由4个部分组成:
名称 | 字节数 | 说明 |
---|---|---|
Length (长度) | 4 字节 | 指定数据块中数据域的长度,其长度不超过(2^31-1)字节 |
Chunk Type Code (数据块类型码) | 4 字节 | 数据块类型码由 ASCII 字母(A-Z和a-z)组成 |
Chunk Data (数据块数据) | 可变长度 | 存储按照 Chunk Type Code 指定的数据 |
CRC (循环冗余检测) | 4 字节 | 存储用来检测是否有错误的循环冗余码 |
PNG 文件的前八个字节始终包含以下(十进制)值:
137 80 78 71 13 10 26 10
这个签名表示该文件的剩余部分包含一个PNG图像,由一系列与开始区块的 IHDR块,并结束IEND块。
请参阅基本原理:PNG 文件签名。
在此我们只关注几个关键数据块
数据块符号 | 数据块名称 | 多数据块 | 是否可选 | 位置限制 |
---|---|---|---|---|
IHDR | 文件头数据块 | 否 | 否 | 第一块 |
PLTE | 调色板数据块 | 否 | 是 | 在IDAT之前 |
IDAT | 图像数据块 | 是 | 否 | 与其他IDAT连续 |
IEND | 图像结束数据 | 否 | 否 | 最后一个数据块 |
通过nio读取文件 存入实例的buffer中 验证是否为PNG图像,这里我从阿里巴巴图标库下载了几个图片(后续只针对此图片进行处理)
String path = "D:/temp/面性长直发.png";
LoadPngPic loadPngPic = new LoadPngPic(path);
try (FileInputStream stream = new FileInputStream(path.toFile())) {
FileChannel channel = stream.getChannel();
this.buffer = ByteBuffer.allocate((int) channel.size());
channel.read(this.buffer);
this.buffer.flip();
return this;
} catch (IOException e) {
e.printStackTrace();
return null;
}
if(0x89504E470D0A1A0AL.equals(this.buffer.getLong())) {
//是png格式文件
}
IHDR
buffer.getLong(); // 偏移8个字节(长度以及数据块类型 因为第一个肯定是IHDR块 所以在此不做判断)
picture.setWidth(buffer.getInt()); // 宽度
picture.setHeight(buffer.getInt()); // 高度
picture.setDepth(buffer.get()); // 图像深度
picture.setColorType(buffer.get()); // 颜色类型
buffer.get(); //压缩方法(在此用不到 所以无需保存)
picture.setFilterMethod(buffer.get()); //过滤器方法
picture.setInterlaceMethod(buffer.get()); //隔行扫描方法
buffer.getInt(); // 循环冗余检测
此时以及读取了png文件的IHDR数据块 获取到了 宽度、高度、深度、颜色类型、过滤器方法、隔行扫描方法等信息, 此时我们debug一下看看:
根据官方描述
可知,PLTE数据块只有在颜色类型为3时,必定出现。颜色类型为 2 和 6 时 有可能会出现 ,所以此数据块暂且不管
我们之间进入正题看IDAT
IDAT
从表1.1可以看到IDAT数据块是连续的并且出现位置不固定的,所以我们读取的时候使用递归方式,每次读取出标识判断是否是IDAT数据块,如果不是就直接读取下一块
private void readIDAT(Picture picture) {
int length = buffer.getInt();
int identifi = buffer.getInt();
if (identifi != IDAT) {
buffer.get(new byte[length + 4]);
readIDAT(picture);
return;
}
// TODO 如果是IDAT数据块的处理逻辑
}
读取到IDAT数据块之后 就相当于拿到了此png图片的真正数据信息
然后根据图片的颜色类型 进行相应的解析操作
根据图1.1我们看到 此时我们读到的颜色类型为6 (带α通道的真彩色图像)
通过Zlib工具包对IDAT的数据进行反压缩后得到了真实的字节数据
byte[] data = new byte[length];
buffer.get(data);
byte[] decompress = ZLibUtils.decompress(data);
此时数据还是经过过滤后的 具体过滤算法参看
PNG Specification: Filter Algorithms
对数据进行反过滤后 按颜色值获取灰度进行打印
最终效果:
具体代码我已上传至github和码云,如果觉得还可以,麻烦帮忙点个star,谢谢☺:
GitHub - 970100646/PNG2Character: PNG图片转为字符画
参考文章:
PNG (Portable Network Graphics) Specification
音视频入门-11-PNG文件格式详解 | binglingziyu的博客
https://www.zhuyuntao.cn/png%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%E8%A7%A3%E6%9E%90