[Image_Codec]常见图片格式的封装及编解码-Android平台(二)PNG

[TOC]

PNG图片格式

PNG(Portable Network Graphics) 是一种光栅化的,无损压缩的图片文件格式。其设计的目的是替换GIF,是目前网络中用得最广的无损压缩图片格式。我们可以用工具将前面的Bitmap转换为PNG。

下面是从上一章所说的 BMP转换过来的png_4x2_32bit.png的PNG图片,图片比较小,看仔细了:

PNG图片样例

PNG特点

请参考百度百科PNG图片特性

  • 体积小
    网络通讯中因受带宽制约,在保证图片清晰、逼真的前提下,网页中不可能大范围的使用文件较大的bmp格式文件。
  • 无损压缩 PNG文件采用LZ77算法的派生算法进行压缩,其结果是获得高的压缩比,不损失数据。它利用特殊的编码方法标记重复出现的数据,因而对图像的颜色没有影响,也不可能产生颜色的损失,这样就可以重复保存而不降低图像质量。
  • 索引彩色模式
    PNG-8格式与GIF图像类似,同样采用8位调色板将RGB彩色图像转换为索引彩色图像。图像中保存的不再是各个像素的彩色信息,而是从图像中挑选出来的具有代表性的颜色编号,每一编号对应一种颜色,图像的数据量也因此减少,这对彩色图像的传播非常有利。
  • 更优化的网络传输显示
    PNG图像在浏览器上采用流式浏览,即使经过交错处理的图像会在完全下载之前提供浏览者一个基本的图像内容,然后再逐渐清晰起来。它允许连续读出和写入图像数据,这个特性很适合于在通信过程中显示和生成图像。
  • 支持透明效果
    PNG可以为原图像定义256个透明层次,使得彩色图像的边缘能与任何背景平滑地融合,从而彻底地消除锯齿边缘。这种功能是GIF和JPEG没有的。
  • PNG同时还支持真彩和灰度级图像的Alpha通道透明度。
  • 最高支持24位真彩色图像以及8位灰度图像。
  • 支持Alpha通道的透明/半透明特性。
  • 支持图像亮度的Gamma校准信息。
  • 支持存储附加文本信息,以保留图像名称、作者、版权、创作时间、注释等信息。
  • 渐近显示和流式读写,适合在网络传输中快速显示预览效果后再展示全貌。
  • 使用CRC防止文件出错。
  • 最新的PNG标准允许在一个文件内存储多幅图像。

PNG文件结构

PNG图像格式文件(或者称为数据流)由一个8字节的PNG文件署名(PNG file signature)域和按照特定结构组织的3个以上的数据块(chunk)组成。
我们以二进制的形式将PNG图png_4x2_32bit.png打开:

00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0004 0000 0002 0806 0000 007f a87d  ...............}
00000020: 6300 0000 0173 5247 4200 aece 1ce9 0000  c....sRGB.......
00000030: 0004 6741 4d41 0000 b18f 0bfc 6105 0000  ..gAMA......a...
00000040: 0009 7048 5973 0000 0ec3 0000 0ec3 01c7  ..pHYs..........
00000050: 6fa8 6400 0000 1849 4441 5418 5763 0082  o.d....IDAT.Wc..
00000060: ffff 8118 4c81 2110 8029 b0e0 ffff 0005  ....L.!..)......
00000070: 2b10 f0fa b027 6800 0000 0049 454e 44ae  +....'h....IEND.
00000080: 4260 82                                  B`.
  • 文件头
    PNG文件包含8字节的签名:
8950 4e47 0d0a 1a0a
值Value 作用Purpose
89 超出了ASCII字符的范围,避免某些软件将图片当着文本来处理
50 4E 47 ‘PNG’的ASCII值,PNG文件的标识
0D 0A Dos风格的的回车,检测DOS-Unix行结束数据的转换
1A Dos风格的换行符号
0A Unix风格的回车
  • 数据块
    PNG文件中包含3个以上的数据块,数据块间以特定的顺序组成。而数据块又分为关键数据块和辅助数据块。基本每个数据块都用下面的结构描述:
名称 字节数 说明
Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(231-1)字节
Chunk Type Code (数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成
Chunk Data (数据块数据) 可变长度 存储按照Chunk Type Code指定的数据
CRC (循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码
  • 关键数据块包括
数据块符号 数据块名称 多数据块 可选否 位置限制
IHDR 文件头数据块 第一块
IDAT 图像数据块 与其他IDAT连续
IEND 图像结束数据 最后一个数据块

和Bitmap相比,关键数据块多了一个IEND块。

  • IHDR数据块
    IHDR也称为文件头数据块,它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流(文件)中只能有一个文件头数据块。

IHDR文件头数据块由13字节组成

00000000: ---- ---- ---- ---- 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0004 0000 0002 0806 0000 007f a87d   ...............}
00000020: 63-- ---- ---- ---- ---- ---- ---- ----  c....sRGB.......

它的格式如下表所示:

offset 值 Value 域的名称 字节数 说明
0000 000d 13 size 4 数据块大小
4948 4452 IHDR type IHDR的ASCII 数据块的类型
0000 0004 4 Width 4 bytes 图像宽度,以像素为单位
0000 0002 2 Height 4 bytes 图像高度,以像素为单位
08 8 Bit depth 1 byte 图像深度:
索引彩色图像:1,2,4或8
灰度图像:1,2,4,8或16
真彩色图像:8或16
06 6 ColorType 1 byte 颜色类型:
0:灰度图像, 1,2,4,8或16
2:真彩色图像,8或16
3:索引彩色图像,1,2,4或8
4:带α通道数据的灰度图像,8或16
6:带α通道数据的真彩色图像,8或16
00 0 Compression method 1 byte 压缩方法(LZ77派生算法)
00 0 Filter method 1 byte 滤波器方法
00 0 Interlace method 1 byte 隔行扫描方法:
0:非隔行扫描
1: Adam7(由Adam M. Costello开发的7遍隔行扫描方法)
7fa8 7d63 CRC 4 CRC校验码

按照我们的文件来~

  • sRGB数据块
00000020: --00 0000 0173 5247 4200 aece 1ce9 ----  c....sRGB.......
offset 值 Value 域的名称 字节数 说明
00 0000 01 1 size 4 数据块大小
73 5247 42 sRGB type sRGB的ASCII 数据块的类型
00 0 sRGB模式 1 bytes 0 1 2 3四种模式可选,可以参考W3对sRGB的介绍
aece 1ce9 CRC 4 CRC校验码

不是所有的解码器都支持sRGB,所以有sRGB数据块,就必须有gAMA 数据块,也可以有cHRM 数据块,以兼容不支持sRGB的解码器。

  • gAMA 数据块
00000020: ---- ---- ---- ---- ---- ---- ---- 0000  c....sRGB.......
00000030: 0004 6741 4d41 0000 b18f 0bfc 6105 0000  ..gAMA......a...
offset 值 Value 域的名称 字节数 说明
0000 0004 4 size 4 数据块大小
6741 4d41 gAMA type gAMA 的ASCII 数据块的类型
0000 b18f 0 gama校验 1 bytes gama校验
0bfc 6105 CRC 4 CRC校验码

如果有sRGB数据块或者iCCP数据块,gAMA数据块就不用了,被override。

  • pHYs 数据块
    期望的物理像素的尺寸,或比例当显示图片时。
00000030: ---- ---- ---- ---- ---- ---- ---- 0000  ..gAMA......a...
00000040: 0009 7048 5973 0000 0ec3 0000 0ec3 01c7  ..pHYs..........
00000050: 6fa8 64-- ---- ---- ---- ---- ---- ---- o.d....IDAT.Wc..
offset 值 Value 域的名称 字节数 说明
0000 0009 9 size 4 数据块大小
7048 5973 pHYs type pHYs 的ASCII 数据块的类型
0000 0ec3 3779 x axis 4 bytes gama校验
0000 0ec3 3779 y axis 4 bytes gama校验
01 1 Unit specifier 1 单位说明
c76f a8 64 CRC 4 CRC校验码

Unit specifier,有下列定义的值:
0:unit is unknown;只是定义了缩放比例
1:unit is the metre

接下来终于到我们的数据块了

  • IDAT 数据块

关键数据块,这里就数据就是png图片的数据

00000050: ---- --00 0000 1849 4441 5418 5763 0082  o.d....IDAT.Wc..
00000060: ffff 8118 4c81 2110 8029 b0e0 ffff 0005  ....L.!..)......
00000070: 2b10 f0fa b027 6800 0000 0049 454e 44ae  +....'h....IEND.
00000080: 4260 82                                  B`.
offset 值 Value 域的名称 字节数 说明
00 0000 18 24 size 4 数据块大小
49 4441 54 IDAT type IDAT 的ASCII 数据块的类型
1857 6300 82ff ff81
184c 8121 1080 29b0
e0ff ff00 052b 10f0
png编码数据 24 png编码数据
fab0 2768 CRC 4 CRC校验码
  • IEND 数据块
    关键数据块
00000070: ---- ---- ---- --00 0000 0049 454e 44ae  +....'h....IEND.
00000080: 4260 82                                  B`.
offset 值 Value 域的名称 字节数 说明
00 0000 00 4 size 4 数据块大小
49 454e 44 IEND type IEND 的ASCII 数据块的类型
ae 4260 82 CRC 4 CRC校验码

以上就是PNG文件结构的描述~~~

PNG压缩原理

这部分请参考原博客: PNG格式图片原理

PNG的压缩过程是完全无损的,压缩过的文件可以准确的还原出原图,这要分两个阶段完成:推断(又称过滤[filtering])和压缩。

过滤

差分编码(Delta encoding)是最强大的数字压缩法之一。原理是根据前一个数据的值将后面的值替换成其他值,例如:

[2,3,4,5,6,7,8]可以变成[2,1,1,1,1,1,1],算法是
[2, 3-2=1, 4-3=1, 5-4=1, 6-5=1, 7-6=1, 8-7=1]

这样看就很明显了,如果你的数据是线性相关的(线性相关的意思是,一组数据里的前后值都差别不大,或者具有相关性),就可以把你的数据集转换成一组重复的、低的值,这样的值更容易被压缩。

PNG格式使用了差分编码(Delta encoding)里的过滤。原理是,对于每一行的像素,一个当前的像素都跟它的左边像素、上边的像素和左上角的像素有关系。

image

举个例子,如果我们要编码一个给定的像素通过它与A和B平均值的差异(X-(A+B)/2),那么我们将得到:

image

我们使用了ABC去推断出X的值,然后我们将X替换成更小的值。

需要注意的是,每一行的像素都有可能不同,PNG允许5种不同的推断算法,它们是:

  • 不过滤
  • X-A
  • X-B
  • X-(A+B)/2(又称平均值)
    Paeth推断(A,B,C的线性方法,这种比较复杂可看W3C的规定)

这里说明一下,每一行像素应该选择最适合的过滤算法,这样才能得到最少数量的特殊值。下面是我们关于不同模式的例子:

image

需要注意的是这些过滤器都是对每一行像素起作用而不是单个像素。也就是说过滤器会对每一行的红色像素起作用,再分别对蓝色的像素起作用。(尽管同一行的像素会用同样的过滤器)

现在PNG格式在选择过滤器上有一些不错的方法,开发人员根据对不同类型图片的使用经验摸索出一些不错的规律。例如对于调色板的图像(palette images)和8位的灰色图就不要过滤。对于其他图片,就选择那种能最大限度地减少绝对差异总和的值的过滤器:将所有值的绝对值相加,然后对比不同过滤器得到的值,选择那个相加起来得到最小值的过滤器。

压缩
在一行像素被过滤后,就会执行DEFLATE压缩,这是LZ77延伸出来的一种算法。该算法结合了LZ77编码和哈夫曼编码,它跟PKWARE、PKZIP、GZIP等差不多相同。这种实现方式虽然是现成的,但用在压缩图片数据上,还是有一些需要注意的点:

  • Deflate算法只能匹配3到258个之间符号,所以最大的压缩比只能到1035:1;
  • 如果匹配到的符号小于3,那么你会产生一些额外的开销来表示这些符号;

上面的这两点意味着你的图片大小会受到每一行像素的匹配程度影响。

你可以看一下面这两张图片,左边那张270x90的图只有20k,而右边那张270x92的图是左边那张的2倍大。

image

这似乎不符合逻辑,一张图片多540像素在压缩率上就少了一半。不过我们看仔细点,就能知道原因了,下面这张图表示压缩器怎么去压缩一个给定的像素的。深蓝色代表压缩率很高的区域,黄色/红色代表没怎么被压缩的区域。

这是怎么出现的呢,原因是小图的每一行像素的匹配度更高,那么它的压缩率就更高。你要是调整了大小,匹配度一变化,这种情况就有可能出现。一些潜在的匹配对象不在压缩区域里,它们没有匹配到,这就又可能导致出现一张大图。

image

如果你想知道你的PNG图片的压缩率如何,可以下个PNGThermal看一下。

PNG图片编解码 libpng

PNG图片的编解码,已经有很多开源的项目。libpng都比较成熟的官方的png编解码器,以动态库的形式提供,使用时load libpng的库就可以使用。LodePNG是一个集成度比较高的编解码器,没有依赖,不用加载zlib或libpng等,直接可以使用, https://github.com/lvandeve/lodepng。 LodePNG建议大家去了解一下,通过LodePNG能够很好理解PNG的编解码实现。我们这里侧重用libpng使用。

Android平台上,libpng的源码在external/libpng目录下;当前的版本应该是16.34。

https://sourceforge.net/projects/libpng/files/libpng16/1.6.34/libpng-1.6.34.tar.gz

png采用的压缩算法是分开的:

git clone https://github.com/madler/zlib.git

我们用Android Studio,创建一个纯Native的应用,直接在 native中去编译,使用libpng和zlib。

样例代码请参考github Codec-PngCodec , 这是我们的PngCodec的文件结构:

├── build.gradle
├── CMakeLists.txt
├── libs
├── png
│   └── libpng-1.6.34
├── src
│   ├── AndroidManifest.xml
│   ├── cpp
│   │   └── PngCodecNativeActivity.cpp
│   ├── java
│   └── res
└── zlib
    └── zlib-1.2.11

将libpng和zlib,放到工程目录下。将工程连到CMakeLists.txt。

zlib的CMake

add_library( zlib
             STATIC
             zlib/zlib-1.2.11/adler32.c
             zlib/zlib-1.2.11/compress.c
             zlib/zlib-1.2.11/crc32.c
             zlib/zlib-1.2.11/deflate.c
             zlib/zlib-1.2.11/gzclose.c
             zlib/zlib-1.2.11/gzlib.c
             zlib/zlib-1.2.11/gzread.c
             zlib/zlib-1.2.11/gzwrite.c
             zlib/zlib-1.2.11/infback.c
             zlib/zlib-1.2.11/inflate.c
             zlib/zlib-1.2.11/inftrees.c
             zlib/zlib-1.2.11/inffast.c
             zlib/zlib-1.2.11/trees.c
             zlib/zlib-1.2.11/uncompr.c
             zlib/zlib-1.2.11/zutil.c )

target_include_directories(zlib PRIVATE
            zlib/zlib-1.2.11 )

libpng的CMake

add_library( png
             STATIC
             png/libpng-1.6.34/png.c
             png/libpng-1.6.34/pngerror.c
             png/libpng-1.6.34/pngget.c
             png/libpng-1.6.34/pngmem.c
             png/libpng-1.6.34/pngpread.c
             png/libpng-1.6.34/pngread.c
             png/libpng-1.6.34/pngrio.c
             png/libpng-1.6.34/pngrtran.c
             png/libpng-1.6.34/pngrutil.c
             png/libpng-1.6.34/pngset.c
             png/libpng-1.6.34/pngtrans.c
             png/libpng-1.6.34/pngwio.c
             png/libpng-1.6.34/pngwrite.c
             png/libpng-1.6.34/pngwtran.c
             png/libpng-1.6.34/pngwutil.c )

target_include_directories(png PRIVATE
            png/libpng-1.6.34
            zlib/zlib-1.2.11 )

native应用的CMake

add_library( png_codec
             SHARED
             src/main/cpp/PngCodecNativeActivity.cpp )

target_include_directories(png_codec PRIVATE
             ${ANDROID_NDK}/sources/android/native_app_glue
             png/libpng-1.6.34
             zlib/zlib-1.2.11 )

target_link_libraries( png_codec
                       native_activity_glue
                       android
                       zlib
                       png
                       log )

Native应用我们编译为libpng_codec.so,在AndroidManifest中使用:

    <application
        ...
        android:hasCode="false">
        <activity android:name="android.app.NativeActivity">
            <meta-data
                android:name="android.app.lib_name"
                android:value="png_codec" />

编译时,pnglibconf.h需要放在png/libpng-1.6.34目录下,从scripts/pnglibconf.h.prebuilt拷贝。我们这边注释掉NEON的支持。

下面是一个简易的读取png图片的实现:

void readPngFile(char *name) {
    ALOGE("readPngFile %s\n",  name);
    // 前边几句是扯淡,初始化各种结构
    FILE *file = fopen(name, "rb");

    png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);

    png_infop info_ptr = png_create_info_struct(png_ptr);

    setjmp(png_jmpbuf(png_ptr));

    // 这句很重要
    png_init_io(png_ptr, file);

    // 读文件了
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_EXPAND, 0);

    // 得到文件的宽高色深
    int m_width = png_get_image_width(png_ptr, info_ptr);
    int m_height = png_get_image_height(png_ptr, info_ptr);

    int color_type = png_get_color_type(png_ptr, info_ptr);

    // 申请个内存玩玩,这里用的是c++语法,甭想再c上编过
    int size = m_height * m_width * 4;

    char* head = static_cast<char*>(malloc(size));

    int pos = 0;

    // row_pointers里边就是传说中的rgba数据了
    png_bytep *row_pointers = png_get_rows(png_ptr, info_ptr);

    // 拷贝!!注意,如果你读取的png没有A通道,就要3位3位的读。还有就是注意字节对其的问题,最简单的就是别用不能被4整除的宽度就行了。读过你实在想用,就要在这里加上相关的对齐处理。
    ALOGE("png file %s size(%dx%d) pixles:", name, m_width, m_height);
    for (int i = 0; i < m_height; i++) {
        for (int j = 0; j < (4 * m_width); j += 4) {
            head[pos] = row_pointers[i][j + 2]; // blue
            pos++;
            head[pos] = row_pointers[i][j + 1]; // green
            pos++;
            head[pos] = row_pointers[i][j];   // red
            pos++;
            head[pos] = row_pointers[i][j + 3]; // alpha
            pos++;
            ALOGE("%02x %02x %02x %02x", head[pos-4], head[pos-3], head[pos-2], head[pos-1]);
        }
    }

    free(head);

    // 好了,你可以用这个数据作任何的事情了。。。把它显示出来或者打印出来都行。
    png_destroy_read_struct(&png_ptr, &info_ptr, 0);

    fclose(file);

    return;
}

我们将我们的png_4x2_32bit.png,读出来,pixels为:

0000 00 ff 0000 ffff ff00 00ff 00ff 00ff
ffff ffff 00ff 00ff 0000 ffff ff00 00ff

还记得bmp的时候,你要转为png之前,pixels是什么样的吗?

ffff ff7f 00ff 007f 0000 ff7f ff00 007f 0000 0000 0000 ff00 ff00 0000 00ff 0000

对比一下:

  • 转换的时候,Alpha被去掉了,都设置为不透明的
  • png的扫描方式和bmp吧一样,png是从左上开始扫,bmp却是从左下。

再回过头看看测试代码:

png信息用结构体png_inforp描述,本体是png_info_def,定义在pnginfo.h中。

struct png_info_def
{
   /* The following are necessary for every PNG file */
   png_uint_32 width;  /* width of image in pixels (from IHDR) */
   png_uint_32 height; /* height of image in pixels (from IHDR) */
   png_uint_32 valid;  /* valid chunk data (see PNG_INFO_ below) */
   png_size_t rowbytes; /* bytes needed to hold an untransformed row */
   png_colorp palette;      /* array of color values (valid & PNG_INFO_PLTE) */
   png_uint_16 num_palette; /* number of color entries in "palette" (PLTE) */
   png_uint_16 num_trans;   /* number of transparent palette color (tRNS) */
   png_byte bit_depth;      /* 1, 2, 4, 8, or 16 bits/channel (from IHDR) */
   png_byte color_type;     /* see PNG_COLOR_TYPE_ below (from IHDR) */
   /* The following three should have been named *_method not *_type */
   png_byte compression_type; /* must be PNG_COMPRESSION_TYPE_BASE (IHDR) */
   png_byte filter_type;    /* must be PNG_FILTER_TYPE_BASE (from IHDR) */
   png_byte interlace_type; /* One of PNG_INTERLACE_NONE, PNG_INTERLACE_ADAM7 */

   ... ...
};

png文件用结构体png_structp描述,本体是png_struct_def,定义在pngstruct.h中。

struct png_struct_def
{
#ifdef PNG_SETJMP_SUPPORTED
   jmp_buf jmp_buf_local;     /* New name in 1.6.0 for jmp_buf in png_struct */
   png_longjmp_ptr longjmp_fn;/* setjmp non-local goto function. */
   jmp_buf *jmp_buf_ptr;      /* passed to longjmp_fn */
   size_t jmp_buf_size;       /* size of the above, if allocated */
#endif
   png_error_ptr error_fn;    /* function for printing errors and aborting */
#ifdef PNG_WARNINGS_SUPPORTED
   png_error_ptr warning_fn;  /* function for printing warnings */
#endif
   png_voidp error_ptr;       /* user supplied struct for error functions */
   png_rw_ptr write_data_fn;  /* function for writing output data */
   png_rw_ptr read_data_fn;   /* function for reading input data */
   png_voidp io_ptr;          /* ptr to application struct for I/O functions */
   
   ... ...

libpng都是用c写的,用的结构体,这样实现,C/C++中都可以用。

png信息的读取,用的png_read_png接口,定义在pngread.c中。在png_read_info函数中,采用循环的模式,将PNG的所有数据块数据都读出来。比如IHDR数据块,采用png_handle_IHDR进行处理。

/* Read and check the IDHR chunk */

void /* PRIVATE */
png_handle_IHDR(png_structrp png_ptr, png_inforp info_ptr, png_uint_32 length)
{
   png_byte buf[13];
   png_uint_32 width, height;
   int bit_depth, color_type, compression_type, filter_type;
   int interlace_type;

   png_debug(1, "in png_handle_IHDR");

   if ((png_ptr->mode & PNG_HAVE_IHDR) != 0)
      png_chunk_error(png_ptr, "out of place");

   /* Check the length */
   if (length != 13)
      png_chunk_error(png_ptr, "invalid");

   png_ptr->mode |= PNG_HAVE_IHDR;

   png_crc_read(png_ptr, buf, 13);
   png_crc_finish(png_ptr, 0);

   width = png_get_uint_31(png_ptr, buf);
   height = png_get_uint_31(png_ptr, buf + 4);
   bit_depth = buf[8];
   color_type = buf[9];
   compression_type = buf[10];
   filter_type = buf[11];
   interlace_type = buf[12];

   /* Set internal variables */
   png_ptr->width = width;
   png_ptr->height = height;
   png_ptr->bit_depth = (png_byte)bit_depth;
   png_ptr->interlaced = (png_byte)interlace_type;
   png_ptr->color_type = (png_byte)color_type;
#ifdef PNG_MNG_FEATURES_SUPPORTED
   png_ptr->filter_type = (png_byte)filter_type;
#endif
   png_ptr->compression_type = (png_byte)compression_type;

   /* Find number of channels */
   switch (png_ptr->color_type)
   {
      default: /* invalid, png_set_IHDR calls png_error */
      case PNG_COLOR_TYPE_GRAY:
      case PNG_COLOR_TYPE_PALETTE:
         png_ptr->channels = 1;
         break;

      case PNG_COLOR_TYPE_RGB:
         png_ptr->channels = 3;
         break;

      case PNG_COLOR_TYPE_GRAY_ALPHA:
         png_ptr->channels = 2;
         break;

      case PNG_COLOR_TYPE_RGB_ALPHA:
         png_ptr->channels = 4;
         break;
   }

   /* Set up other useful info */
   png_ptr->pixel_depth = (png_byte)(png_ptr->bit_depth * png_ptr->channels);
   png_ptr->rowbytes = PNG_ROWBYTES(png_ptr->pixel_depth, png_ptr->width);
   png_debug1(3, "bit_depth = %d", png_ptr->bit_depth);
   png_debug1(3, "channels = %d", png_ptr->channels);
   png_debug1(3, "rowbytes = %lu", (unsigned long)png_ptr->rowbytes);
   png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth,
       color_type, interlace_type, compression_type, filter_type);
}

IHDR的大小为13,所以,直接申请13byte的buf。信息保存到info_ptr中。

图片数据,通过png_read_image进行获取。

void PNGAPI
png_read_image(png_structrp png_ptr, png_bytepp image)
{
   png_uint_32 i, image_height;
   int pass, j;
   png_bytepp rp;

   png_debug(1, "in png_read_image");

   if (png_ptr == NULL)
      return;

#ifdef PNG_READ_INTERLACING_SUPPORTED
   if ((png_ptr->flags & PNG_FLAG_ROW_INIT) == 0)
   {
      pass = png_set_interlace_handling(png_ptr);
      /* And make sure transforms are initialized. */
      png_start_read_image(png_ptr);
   }
   else
   { 
      if (png_ptr->interlaced != 0 &&
          (png_ptr->transformations & PNG_INTERLACE) == 0)
      {
         /* Caller called png_start_read_image or png_read_update_info without
          * first turning on the PNG_INTERLACE transform.  We can fix this here,
          * but the caller should do it!
          */
         png_warning(png_ptr, "Interlace handling should be turned on when "
             "using png_read_image");
         /* Make sure this is set correctly */
         png_ptr->num_rows = png_ptr->height;
      }

      /* Obtain the pass number, which also turns on the PNG_INTERLACE flag in
       * the above error case.
       */
      pass = png_set_interlace_handling(png_ptr);
   }
#else
   if (png_ptr->interlaced)
      png_error(png_ptr,
          "Cannot read interlaced image -- interlace handler disabled");

   pass = 1;
#endif

   image_height=png_ptr->height;

   for (j = 0; j < pass; j++)
   {
      rp = image;
      for (i = 0; i < image_height; i++)
      {
         png_read_row(png_ptr, *rp, NULL);
         rp++;
      }
   }
}

在png_read_row中,通过png_read_IDAT_data去获取pixels数据。这里的数据是经过压缩的,需要用到zlib进行解压缩。解压用的zlib的inflateInit() + inflate() + inflateEnd()等函数。

解压用到z_stream_s结构,用以读写zlib输入输出的数据。

typedef struct z_stream_s {
    z_const Bytef *next_in;     /* next input byte */
    uInt     avail_in;  /* number of bytes available at next_in */
    uLong    total_in;  /* total number of input bytes read so far */

    Bytef    *next_out; /* next output byte will go here */
    uInt     avail_out; /* remaining free space at next_out */
    uLong    total_out; /* total number of bytes output so far */

    z_const char *msg;  /* last error message, NULL if no error */
    struct internal_state FAR *state; /* not visible by applications */

    alloc_func zalloc;  /* used to allocate the internal state */
    free_func  zfree;   /* used to free the internal state */
    voidpf     opaque;  /* private data object passed to zalloc and zfree */

    int     data_type;  /* best guess about the data type: binary or text
                           for deflate, or the decoding state for inflate */
    uLong   adler;      /* Adler-32 or CRC-32 value of the uncompressed data */
    uLong   reserved;   /* reserved for future use */
} z_stream;

我们可以自己加log,将解压的PNG图片的像素数据打印出来

PngCodec: png_show_byte bnext_in 1857630082FFFF81184C8121108029B0E0FFFF00052B10F0

这和我们用二进制编辑器编辑png图片时的文件是不是一样的?

  • libpng编码
    编码和解码类似,编码时提供png_write_infopng_write_image,一个写Png文件信息,一个写像素点。我们来看实例。前面我们用png转码工具将bmp转换为png时,丢了Alpha信息,我们现在将Alpha信息加上。
void writePngFile(char *fileName, png_byte* src , int width, int height)
{
    png_structp png_ptr;
    png_infop info_ptr;
    png_colorp palette;

    FILE *fp = fopen(fileName, "wb");
    if (fp == NULL)
        return ;

    png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (png_ptr == NULL)
    {
        fclose(fp);
        return ;
    }

    /* Allocate/initialize the image information data.  REQUIRED */
    info_ptr = png_create_info_struct(png_ptr);
    if (info_ptr == NULL)
    {
        fclose(fp);
        png_destroy_write_struct(&png_ptr,  NULL);
        return ;
    }
    if (setjmp(png_jmpbuf(png_ptr)))
    {
        /* If we get here, we had a problem writing the file */
        fclose(fp);
        png_destroy_write_struct(&png_ptr, &info_ptr);
        return ;
    }

    /* 接下来告诉 libpng 用 fwrite 来写入 PNG 文件,并传给它已按二进制方式打开的 FILE* fp */
    png_init_io(png_ptr, fp);

    /* 设置png文件的属性 */
    png_set_IHDR(png_ptr, info_ptr, width, height, 8,
                 PNG_COLOR_TYPE_RGB,
                 PNG_INTERLACE_NONE,
                 PNG_COMPRESSION_TYPE_BASE,
                 PNG_FILTER_TYPE_BASE);

    /* 分配调色板空间。常数 PNG_MAX_PALETTE_LENGTH 的值是256 */
    palette = (png_colorp)png_malloc(png_ptr, PNG_MAX_PALETTE_LENGTH * sizeof(png_color));

    png_set_PLTE(png_ptr, info_ptr, palette, PNG_MAX_PALETTE_LENGTH);

    /* Write the file header information.  REQUIRED */
    png_write_info(png_ptr, info_ptr);

    /* The easiest way to write the image (you may have a different memory
    * layout, however, so choose what fits your needs best).  You need to
    * use the first method if you aren't handling interlacing yourself.
    */
    png_uint_32 k;
    png_byte *image;
    png_bytep row_pointers[height];

    image = src;

    if (height > PNG_UINT_32_MAX/sizeof(png_bytep))
        png_error (png_ptr, "Image is too tall to process in memory");

    for (k = 0; k < height; k++)
        row_pointers[k] = image + k*width*3;

    /* One of the following output methods is REQUIRED */
    png_write_image(png_ptr, row_pointers);

    //end,进行必要的扫尾工作:
    png_write_end(png_ptr, info_ptr);
    png_free(png_ptr, palette);
    png_destroy_write_struct(&png_ptr, &info_ptr);

    fclose(fp);
    ALOGE("success write png file %s\n", fileName);
    return ;
}

记住,PNG图片是从左上开始扫描的,切格式为RGBA,所以,pixel是数据要对应的调整一下。

void buildPngFile(char *fileName) {
    int width = 4;
    int height = 2;

    png_byte src[] = {
            0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00, 0xff,
            0xff, 0xff, 0xff, 0x7f, 0x00, 0xff, 0x00, 0x7f, 0xff, 0x00, 0x00, 0x7f, 0x00, 0x00, 0xff, 0x7f
    };

    writePngFile(fileName,src,width, height, PNG_COLOR_TYPE_RGBA);

    readPngFile(fileName);
}

log打出来的数据:

~ E/PngCodec: 00 00 00 ff
~ E/PngCodec: 00 00 ff ff
~ E/PngCodec: ff 00 00 ff
~ E/PngCodec: 00 ff 00 ff
~ E/PngCodec: ff ff ff 7f
~ E/PngCodec: 00 ff 00 7f
~ E/PngCodec: 00 00 ff 7f
~ E/PngCodec: ff 00 00 7f

保留Alpha后的PNG图片如下:

保留Alpha后的PNG图片

放大后的效果:

放大后的效果

上面一行不透明,下面一行半透明。

Android平台上,一般不直接去调用libpng的库,Android对编解码提供了统一的管理,这个角色为skia。skia中除了png还可以用其他的,比如jpg,gif等。

Png就介绍到这里,简要的介绍了PNG文件格式,以及用libpng对Png图片进行编解码。再深的,大家继续去探索。

实例代码可以高github里面去下载哦~
Codec-PngCodec

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

推荐阅读更多精彩内容