注:此文章并不是特别详细,只做参考

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 文件签名

在此我们只关注几个关键数据块

表1.1

数据块符号

数据块名称

多数据块

是否可选

位置限制

IHDR

文件头数据块

第一块

PLTE

调色板数据块

在IDAT之前

IDAT

图像数据块

与其他IDAT连续

IEND

图像结束数据

最后一个数据块

 通过nio读取文件 存入实例的buffer中 验证是否为PNG图像,这里我从阿里巴巴图标库下载了几个图片(后续只针对此图片进行处理)

java 图像读取jpg java读取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一下看看:


java 图像读取jpg java读取png_nio buffer_02

图1.1



根据官方描述

java 图像读取jpg java读取png_java 图像读取jpg_03

可知,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图片转为字符画

PNG2Character: PNG图片转换为字符画

参考文章:

PNG (Portable Network Graphics) Specification

PNG的算法原理 - 知乎

音视频入门-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

https:///@duhroach/how-png-works-f1174e3cc7b7