在 LCD 上显示 png 图片
- [21.1 PNG 简介](#21.1 PNG 简介)
- [21.2 libpng 简介](#21.2 libpng 简介)
- [21.3 zlib 移植](#21.3 zlib 移植)
- [21.4 libpng 移植](#21.4 libpng 移植)
- [21.5 libpng 使用说明](#21.5 libpng 使用说明)
- [21.6 libpng 应用编程](#21.6 libpng 应用编程)
上一章介绍了如何使用 libjpeg 库对 jpeg 图像进行解码、并显示到 LCD 屏上,除了 jpeg 图像之外,png图像也很常见,那本章我们就来学习如何对 png 图像进行解码、并显示到 LCD 屏上。本章将会讨论如下主题。
⚫ PNG 简介;
⚫ libpng 库简介;
⚫ libpng 库移植;
⚫ 使用 libpng 库函数对 PNG 图像进行解码;
21.1 PNG 简介
以下的这些内容都是从网络上截取下来的。PNG(便携式网络图形格式 PortableNetwork Graphic Format,简称 PNG)是一种采用无损压缩算法的位图格式,其设计目的是试图替代 GIF 和 TIFF 文件,同时增加一些 GIF 文件所不具备的特性。PNG 使用从LZ77 派生的无损数据压缩算法,它压缩比高,生成文件体积小,并且支持透明效果,所以被广泛使用。
特点
⚫ 无损压缩 :PNG 文件采用 LZ77 算法的派生算法进行压缩,其结果是获得高的压缩比,不损失数据。它利用特殊的编码方法标记重复出现的数据,因而对图像的颜色没有影响,也不可能产生颜色的损失,这样就可以重复保存而不降低图像质量。
⚫ 体积小 :在保证图片清晰、逼真、不失真的前提下,PNG 使用从 LZ77 派生的无损数据压缩算法,它压缩比高,生成文件体积小; ⚫ 索引彩色模式:PNG-8 格式与 GIF 图像类似,同样采用 8 位调色板将 RGB 彩色图像转换为索引彩色图像。图像中保存的不再是各个像素的彩色信息,而是从图像中挑选出来的具有代表性的颜色编号,每一编号对应一种颜色,图像的数据量也因此减少,这对彩色图像的传播非常有利。
⚫ 更优化的网络传输显示 :PNG 图像在浏览器上采用流式浏览,即使经过交错处理的图像会在完全下载之前提供浏览者一个基本的图像内容,然后再逐渐清晰起来。它允许连续读出和写入图像数据,这个特性很适合于在通信过程中显示和生成图像。
⚫ 支持透明效果:PNG 可以为原图像定义 256 个透明层次,使得彩色图像的边缘能与任何背景平滑地融合,从而彻底地消除锯齿边缘。这种功能是 GIF 和 JPEG 没有的。关于 PNG 格式就介绍这么多。
21.2 libpng 简介
对于 png 图像,我们可以使用 libpng 库对其进行解码,跟 libjpeg 一样,它也是一套免费、开源的 C 语言函数库,支持对 png 图像文件解码、编码等功能。
21.3 zlib 移植
zlib 其实是一套包含了数据压缩算法的函式库,此函数库为自由软件,是一套免费、开源的 C 语言函数库,所以我们可以获取到它源代码。
libpng 依赖于 zlib 库,所以要想移植 libpng 先得移植 zlib 库才可以,zlib 也好、libpng 也好,其实移植过程非常简单,无非就是下载源码、编译源码这样的一些工作,那本小节就向大家介绍如何移植 zlib。在移植之前,先给大家说明一下,我们的开发板出厂系统都是已经移植好了这些库,其实是可以直接使用的,但是作为学习,必须要自己亲自把这些库给移植到开发板,这是非常重要的!
下载源码包
我们可以进入到 https://www.zlib.net/fossils/这个链接地址下载 zlib 源码包:
往下翻,找到一个合适的版本,这里我们就选择 1.2.10 版本的 zlib:
点击文件名就可以下载了,下载成功之后就会得到.tar.gz 格式的压缩文件:
编译源码
将下载的 zlib-1.2.10.tar.gz 压缩文件拷贝到 Ubuntu 系统的用户家目录下,然后将其解压开:
c
tar -xzf zlib-1.2.10.tar.gz
![](https://file.jishuzhan.net/article/1789694819136704513/63040c06119ac7c5c9581709ae853a86.webp)
解压之后就会得到 zlib-1.2.10 文件夹,这就是 zlib 的源代码目录。
在编译 zlib 之前,我们先在 tools 目录下创建一个名为 zlib 的文件夹,作为 zlib 库的安装目录:
接着我们进入到 zlib 的源码目录 zlib-1.2.10,如下所示:
同样也是执行三部曲:配置、编译、安装,一套流程下来就 OK 了!
在此之前,先对交叉编译工具的环境进行初始化,使用 source 执行交叉编译工具安装目录下的environment-setup-cortexa7hf-neon-poky-linux-gnueabi 脚本文件(如果已经初始化过了,那就不用再进行初始化了):
c
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
执行下面这条命令对 zlib 工程进行配置:
c
./configure --prefix=/home/dt/tools/zlib/
--prefix 选项指定 zlib 库的安装目录,将家目录下的 tools/zlib 作为 zlib 库的安装目录。
配置完成之后,直接 make 编译:
c
make
![](https://file.jishuzhan.net/article/1789694819136704513/dd3418dba7690472ad272d43577a88e2.webp)
编译完成之后,接着执行 make install 安装即可!
c
make ins
![](https://file.jishuzhan.net/article/1789694819136704513/9a16f6d35ede5ecea4eb1f620c9a4f77.webp)
安装目录下的文件夹介绍
进入到 zlib 库的安装目录:
头文件目录 include 以及库文件目录 lib。
至此,zlib 库就已经编译好了,接下来我们需要把编译得到的库文件拷贝到开发板。
移植到开发板
进入到 zlib 安装目录下,将 lib 目录下的所有动态链接库文件拷贝到开发板 Linux 系统/usr/lib 目录;注意在拷贝之前,需要先将出厂系统中原有的 zlib 库文件删除,在开发板 Linux 系统下执行命令:
c
rm -rf /usr/lib/libz.* /lib/libz.*
删除之后,再将我们编译得到的 zlib 库文件拷贝到开发板/usr/lib 目录,拷贝库文件时,需要注意符号链接的问题,不能破坏原有的符号链接。
拷贝过去之后,开发板/usr/lib 目录下就应该存在这些库文件,如下所示:
![](https://file.jishuzhan.net/article/1789694819136704513/7157a7267f95f8650b1cf4c5ef09b46c.webp)
21.4 libpng 移植
下载源码包
首先下载 libpng 源码包,进入 https://github.com/glennrp/libpng/releases 链接地址,如下:
下载完成之后,就会得到 libpng 的源码包:
编译源码
将下载的 libpng-1.6.35.tar.gz 压缩包文件拷贝到 Ubuntu 系统的用户家目录下,接着将其解压:
解压之后得到 libpng-1.6.35 文件夹,这便是 libpng 的源码目录。
在编译 libpng 之前,先在 tools 目录下创建一个名为 png 的文件夹,作为 libpng 库的安装目录:
接着我们进入到 libpng 源码目录下,同样也是执行三部曲:配置、编译、安装,一套流程下来就 OK 了!
在此之前,先对交叉编译工具的环境进行初始化,使用 source 执行交叉编译工具安装目录下的environment-setup-cortexa7hf-neon-poky-linux-gnueabi 脚本文件(如果已经初始化过了,那就不用再进行初始化了):
c
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
libpng 依赖于 zlib 库,前面我们已经将 zlib 库编译成功了,但是我们得告知编译器 zlib 库的安装目录,这样编译器才能找到 zlib 的库文件以及头文件,编译 libpng 的时才不会报错。
执行下面这三条命令,将 zlib 库安装目录下的 include 和 lib 路径导出到环境变量:
c
export LDFLAGS="${LDFLAGS} -L/home/dt/tools/zlib/lib"
export CFLAGS="${CFLAGS} -I/home/dt/tools/zlib/include"
export CPPFLAGS="${CPPFLAGS} -I/home/dt/tools/zlib/include"
![](https://file.jishuzhan.net/article/1789694819136704513/b4b4956a7e3748b9c5fcd4d5061a92c4.webp)
接着执行下面这条命令对 libpng 源码工程进行配置:
c
./configure --prefix=/home/dt/tools/png --host=arm-poky-linux-gnueabi
--prefix 选项指定 libpng 的安装目录,将家目录下的 tools/png 作为 libpng 的安装目录。
接着执行 make 进行编译:
c
make
![](https://file.jishuzhan.net/article/1789694819136704513/01415fbfca7b960682289de53228365f.webp)
最后执行 make install 安装即可!
c
make install
![](https://file.jishuzhan.net/article/1789694819136704513/bf442a0db0e1397d8298bc41e706ae81.webp)
安装目录下的文件夹介绍
进入到 libpng 安装目录:
移植到开发板
进入到 libpng 安装目录,将 bin 目录下的所有测试工具拷贝到开发板 Linux 系统/usr/bin 目录;将 lib 目录下的所有库文件拷贝到 Linux 系统/usr/lib 目录,注意在拷贝之前,先将开发板出厂系统中已经移植好的libpng 库文件删除,执行下面这条命令:
c
rm -rf /lib/libpng* /usr/lib/libpng*
删除之后,再将编译得到的 libpng 库文件拷贝到开发板/usr/lib 目录,拷贝库文件时,需要注意符号链接的问题,不能破坏原有的符号链接。
拷贝过去之后,开发板/usr/lib 目录下就应该存在这些库文件,如下所示:
21.5 libpng 使用说明
本小节向大家简单地介绍如何使用 libpng 对 png 图像进行解码,libpng 除了解码功能之外,还包含编码功能,也就是创建 png 压缩文件,当然,这个笔者就不再介绍了。libpng 官方提供一份非常详细地使用文档,笔者也是参考了这份文档给大家进行介绍的,这份文档的链接地址如下:
http://www.libpng.org/pub/png/libpng-1.4.0-manual.pdf
http://www.libpng.org/pub/png/libpng-manual.txt
这两份文档的内容是一样的,第一份是 pdf 文档、第二份是 txt 文档,如果大家想更加深入的了解、学习,那么可以查阅这份文档
libpng 的数据结构
首先,使用 libpng 库需要包含它的头文件<png.h>。png.h 头文件中包含了 API、数据结构的申明,libpng中有两个很重要的数据结构体:png_struct 和 png_info。png_struct 作为 libpng 库函数内部使用的一个数据结构体,除了作为传递给每个 libpng 库函数调用的第一个变量外,在大多数情况下不会被用户所使用。使用 libpng 之前,需要创建一个 png_struct 对象并对其进行初始化操作,该对象由 libpng 库内部使用,调用 libpng 库函数时,通常需要把这个对象作为参数传入。png_info 数据结构体描述了 png 图像的信息,在以前旧的版本中,用户可以直接访问 png_info 对象中的成员,譬如查看图像的宽、高、像素深度、修改解码参数等;然而,这往往会导致出现一些问题,因此新的版本中专门开发了一组 png_info 对象的访问接口:get 方法 png_get_XXX 和 set 方法 png_set_XXX,建议大家通过 API 来访问这些成员
创建和初始化 png_struct 对象
首先第一步是创建 png_struct 对象、并对其进行初始化操作,使用 png_create_read_struct()函数创建一个 png_struct 对象、并完成初始化操作,read 表示我们需要创建的是一个用于 png 解码的 png_struct 对象;同理可以使用 png_create_write_struct()创建一个用于 png 编码的 png_struct 对象。
png_create_read_struct 函数原型如下所示:
c
png_structp png_create_read_struct(png_const_charp user_png_ver, png_voidp error_ptr, png_error_ptr error_fn,
png_error_ptr warn_fn);
它的是返回值是一个 png_structp 指针,指向一个 png_struct 对象;所以 png_create_read_struct()函数创建 png_struct 对象之后,会返回一个指针给调用者,该指针指向所创建的 png_struct 对象。但如果创建对象失败,则会返回 NULL,所以调用者可以通过判断返回值是否为 NULL 来确定 png_create_read_struct()函数执行是否成功!
该函数有 4 个参数,第一个参数 user_png_ver 指的是 libpng 的版本信息,通常将其设置为PNG_LIBPNG_VER_STRING,这是 png.h 头文件中定义的一个宏,其内容便是 libpng 的版本号信息,如下:
c
#define PNG_LIBPNG_VER_STRING "1.6.35"
创建、初始化 png_struct 对象时,调用者可以指定自定义的错误处理函数和自定义的警告处理函数,通过参数 error_fn 指向自定义的错误处理函数、通过参数 warn_fn 指向自定义的警告处理函数,而参数 error_ptr表示传递给这些函数所使用的数据结构的指针;当然也可将它们设置为 NULL,表示使用 libpng 默认的错误处理函数以及警告函数。使用示例如下:
c
png_structp png_ptr = NULL;
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr)
return -1;
创建和初始化 png_info 对象
png_info数据结构体描述了png图像的信息,同样也需要创建png_info对象,调用png_create_info_struct()函数创建一个 png_info 对象,其函数原型如下所示:
c
png_infop png_create_info_struct(png_const_structrp png_ptr);
该函数返回一个 png_infop 指针,指向一个 png_info 对象,所以 png_create_info_struct()函数创建 png_info对象之后,会将它的指针返回给调用者;如果创建失败,则会返回 NULL,所以调用者可以通过判断返回值是否为 NULL 来确定函数调用是否成功!
该函数有一个参数,需要传入一个png_struct对象的指针,内部会将它们之间建立关联,当销毁png_struct对象时、也可将 png_info 对象销毁。使用示例如下:
c
png_infop info_ptr = NULL;
info_ptr = png_create_info_struct(png_const_structrp png_ptr);
if (NULL == info_ptr) {
png_destroy_read_struct(&png_ptr, NULL, NULL);
return -1;
}
png_destroy_read_struct()函数用于销毁 png_struct 对象的函数,后面再给大家介绍。
设置错误返回点
调用 png_create_read_struct()函数创建 png_struct 对象时,调用者可以指定一个自定义的错误处理函数,当 libpng 工作发生错误时,它就会执行这个错误处理函数;但如果调用者并未指定自定义的错误处理函数,那么 libpng 将会使用默认的错误处理函数,其实默认的错误处理函数会执行一个跳转动作,跳转到程序中的某一个位置,我们把这个位置称为错误返回点。
这样,当调用者未指定自定义错误处理函数时,当 libpng 遇到错误时,它会执行默认错误处理函数,而默认错误处理函数会跳转到错误返回点,通常这个错误返回点就是在我们程序中的某个位置,我们期望libpng 发生错误时能够回到我们的程序中,为什么要这样做呢?因为发生错误时不能直接终止退出,而需要执行释放、销毁等清理工作,譬如前面创建的 png_struct 和 png_info 对象,需要销毁,避免内存泄漏。
那如何在我们的程序中设置错误返回点呢?在此之前,笔者需要向大家介绍两个库函数:setjmp 和longjmp。
setjmp 和 longjmp
在 C 语言中,在一个函数中执行跳转,我们可以使用 goto 语句,笔者也经常使用 goto 语句,尤其是在开发驱动程序时;但 goto 语句只能在一个函数内部进行跳转,不能跨越函数,譬如从 func1()函数跳转到func2()函数,如果想要实现这种跨越函数间的跳转,在 Linux 下,我们可以使用库函数 setjmp 和 longjmp。
setjmp 函数用于设置跳转点,也就是跳转位置;longjmp 执行跳转,那么它会跳转到 setjmp 函数所设置的跳转点,来看看这两个函数的原型:
c
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
可以看到 setjmp 和 longjmp 函数都有一个 env 参数,这是一个 jmp_buf 类型的参数,jmp_buf 是一种特殊类型,当调用 setjmp()时,它会把当前进程环境的各种信息保存到 env 参数中,而调用 longjmp()也必须指定相同的参数,这样才可跳转到 setjmp 所设置的跳转点。
从编程角度来看,调用 longjmp()函数后,看起来就和第二次调用 setjmp()返回时完全一样,可以通过检查 setjmp()函数的返回值,来区分 setjmp()是初次调用返回还是第二次"返回",初始调用返回值为 0,后续"伪"返回的返回值为 longjmp()调用中参数 val 所指定的任意值,通过对 val 参数使用不同的值,可以区分出程序中跳转到同一位置的多个不同的起跳位置。
所以,通常情况下,调用 longjmp()时,不会将参数 val 设置为 0,这样将会导致无法区分 setjmp()是初次返回还是后续的"伪"返回,这里大家要注意!好,那么关于这两个函数就向大家介绍这么多,我们来看一个例子:
c
示例代码 21.5.1 setjmp/longjmp 函数使用示例
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
static jmp_buf buf;
static void hello(void) {
printf("hello world!\n");
longjmp(buf,1);
printf("Nice to meet you!\n");
}
int main(void) {
if(0 == setjmp(buf)) {
printf("First return\n");
hello();
}
else
printf("Second return\n");
exit(0);
}
我们直接在 Ubuntu 系统下编译运行,运行结果如下所示:
打印结果就不再分析了,上面给大家讲解地非常清楚了。
libpng 设置错误返回点
libpng 库默认也使用 setjmp/longjmp 这两个库函数组合来处理发生错误时的跳转,当 libpng 遇到错误时,执行默认错误处理函数,默认错误处理函数会调用 longjmp()来进行跳转,所以我们需要使用 setjmp()来 为 libpng 设置一个错误返回点。设置方法如下:
c
/* 设置错误返回点 */
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return -1;
}
png_jmpbuf()函数可以获取到 png_struct 对象中的 jmp_buf 变量,那么后续 libpng 库调用 longjmp 执行跳转时也是使用这个变量。我们可以在错误返回点执行一些清理工作。
指定数据源
也就是指定需要进行解码的 png 图像,通常可以使用多种方式来指定数据源,譬如文件输入流、内存中的数据流等,这里笔者以文件输入流为例。
libpng 提供了 png_init_io()函数,png_init_io()可以指定数据源,该数据源以文件输入流的方式提供,来看看函数原型:
c
png_init_io(png_structrp png_ptr, png_FILE_p fp);
第一个参数是 png_ptr,指向 png_struct 对象;而第二个参数 fp 则是一个 png_FILE_p 类型指针,其实就是标准 I/O 中的 FILE *指针。所以由此可知,我们需要先使用 fopen()函数将 png 文件打开,然后得到指向该文件的 FILE *类型指针。
使用示例如下:
c
FILE *png_file = NULL;
/* 打开 png 文件 */
png_file = fopen("image.png", "r"); //以只读方式打开
if (NULL == png_file) {
perror("fopen error");
return -1;
}
/* 指定数据源 */
png_init_io(png_ptr, png_file);
读取 png 图像数据并解码
从 png 文件中读取数据并进行解码,将解码后的图像数据存放在内存中,待用户读取。关于这一步的操作,libpng 提供了两种方式去处理:high-level 接口处理和 low-level 接口处理。其实 high-level 只是对 lowlevel 方式进行了一个封装,使用 high-level 接口非常方便只需一函数即可,但缺点是灵活性不高、被限定了;而 low-level 接口恰好相反,灵活性高、但需要用户调用多个 API;所以具体使用哪种方式要看你的需求。
high-level 接口
通常在满足以下两个条件时使用 high-level 接口:
⚫ 用户的内存空间足够大,可以一次性存放整个 png 文件解码后的数据;
⚫ 数据输出格式限定为 libpng 预定义的数据转换格式。
在满足以上两个条件时,可以使用 high-level 接口,libpng 预定义数据转换类型包括:
后面的注释说明大家自己去翻译,我怕翻译错了,把你们带入坑!
这些转换当中,还不包括背景颜色设置(透明图)、伽马变换、抖动和填充物等,使用 high-level 接口只能使用以上这些预定义的转换类型,而其它的配置则保持默认。
high-level 接口只需要使用一个函数 png_read_png(),调用该函数将一次性把整个 png 文件的图像数据解码出来、将解码后的数据存放在内存中,如下所示:
c
png_read_png(png_structrp png_ptr, png_inforp info_ptr, int transforms, png_voidp params);
g_read_png(png_structrp png_ptr, png_inforp info_ptr, int transforms, png_voidp params);第一个参数 png_ptr 为指向 png_struct 对象的指针,第二个参数 info_ptr 为指向 png_info 对象的指针;而第三个参数 transforms 为整型参数,取值为上表所列出的 libpng 预定义的数据转换类型,可以使用 or(C语言的或 | 运算符)组合多个转换类型。使用示例如下:
c
png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);
该函数相当于调用一系列 low-level 函数(下文将会介绍),调用顺序如下所示:
⑴、调用 png_read_info 函数获得 png 图像信息;
⑵、根据参数 transforms 所指定的转换类型对数据输出转换格式进行设置;
⑶、调用 png_read_image 一次性把整个 png 文件的图像数据解码出来、并将解码后的数据存放在内存中。
⑷、调用 png_read_end 结束解码。
low-level 接口
使用 low-level 接口,需要用户将函数 png_read_png()所做的事情一步一步执行:
a)、读取 png 图像的信息
首先我们要调用 png_read_info()函数获取 png 图像的信息:
c
png_read_info(png_ptr, info_ptr);
该函数会把 png 图像的信息读入到 info_ptr 指向的 png_info 对象中。
b)、查询图像的信息
前面提到 png_read_info()函数会把 png 图像的信息读入到 png_info 对象中,接下来我们可以调用 libpng提供的 API 查询这些信息。
c
unsigned int width = png_get_image_width(png_ptr, info_ptr); //获取 png 图像的宽度
unsigned int height = png_get_image_height(png_ptr, info_ptr); //获取 png 图像的高度
unsigned char depth = png_get_bit_depth(png_ptr, info_ptr); //获取 png 图像的位深度
unsigned char color_type = png_get_color_type(png_ptr, info_ptr); //获取 png 图像的颜色类型
color type 在 png.h 头文件中定义,如下所示:
c
/* These describe the color_type field in png_info. */
/* color type masks */
#define PNG_COLOR_MASK_PALETTE 1
#define PNG_COLOR_MASK_COLOR 2
#define PNG_COLOR_MASK_ALPHA 4
/* color types. Note that not all combinations are legal */
#define PNG_COLOR_TYPE_GRAY 0
#define PNG_COLOR_TYPE_PALETTE (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_PALETTE)
#define PNG_COLOR_TYPE_RGB (PNG_COLOR_MASK_COLOR)
#define PNG_COLOR_TYPE_RGB_ALPHA (PNG_COLOR_MASK_COLOR |
PNG_COLOR_MASK_ALPHA)
#define PNG_COLOR_TYPE_GRAY_ALPHA (PNG_COLOR_MASK_ALPHA)
/* aliases */
#define PNG_COLOR_TYPE_RGBA PNG_COLOR_TYPE_RGB_ALPHA
#define PNG_COLOR_TYPE_GA PNG_COLOR_TYPE_GRAY_ALPHA
c)、设置解码输出参数(转换参数)
这步非常重要,用户可以指定数据输出转换的格式,比如 RGB888,BGR888、ARGB8888 等数据输出格式,libpng 提供了很多 set 方法(png_set_xxxxx 函数)来实现这些设置,例如如下代码:
c
unsigned char depth = png_get_bit_depth(png_ptr, info_ptr);
unsigned char color_type = png_get_color_type(png_ptr, info_ptr);
if (16 == depth)
png_set_strip_16(png_ptr); //将 16 位深度转为 8 位深度
if (8 > depth)
png_set_expand(png_ptr); //如果位深小于 8,则扩展为 24-bit RGB
if (PNG_COLOR_TYPE_GRAY_ALPHA == color_type)
png_set_gray_to_rgb(png_ptr); //如果是灰度图,则转为 RGB
关于这些函数的作用和使用方法,大家可以打开 libpng 的头文件 png.h 进行查看,每个函数它都有相应的注释信息以及参数列表。如上我们列举了几个 png_set_xxx 转换函数,这种转换函数还很多,这里便不再一一进行介绍,具体请查看 libpng 的使用手册以了解他们的作用。
虽然 libpng 提供了很多转换函数,可以调用它们对数据的输出格式进行设置,但是用户的需求是往往无限的,很多输出格式 libpng 并不是原生支持的,譬如 YUV565、RGB565、YUYV 等,为了解决这样的问题,libpng 允许用户设置自定义转换函数,可以让用户注册自定义转换函数给 libpng 库,libpng 库对输出数据进行转换时,会调用用户注册的自定义转换函数进行转换。
调用者通过 png_set_read_user_transform_fn()函数向 libpng 注册一个自定义转换函数,另外调用者还可以通过 png_set_user_transform_info()函数告诉 libpng 自定义转换函数的用户自定义数据结构和输出数据的详细信息,比如颜色深度、颜色通道(channel)等等。关于这些内容,大家自己去查阅 libpng 的使用帮助文档。
d)、更新 png 数据的详细信息
经过前面的设置之后,信息肯定会有一些变化,我们需要调用 png_read_update_info 函数更新信息:
c
png_read_update_info(png_ptr, info_ptr);
该函数将会更新保存在 info_ptr 指向的 png_info 对象中的图像信息。
e)、读取 png 数据并解码
前面设置完成之后,接下来便可对 png 文件的数据进行解码了。调用 png_read_image()函数可以一次性把整个 png 文件的图像数据解码出来、并将解码后的数据存放在用户提供的内存区域中,使用示例如下:
c
png_read_image(png_ptr, row_pointers);
该函数无返回值,参数 png_ptr 指向 png_struct 对象;第二个参数 row_pointers 是一个 png_bytepp 类型的指针变量,也就是 unsigned char **,是一个指针数组,如下所示:
c
png_bytep row_pointers[height];
调用该函数,需要调用者提供足够大的内存空间,可以保存整个图像的数据,这个内存空间的大小通常是解码后数据的总大小;调用者分配内存空间后,需要传入指向每一行的指针数组,如下所示:
c
png_bytep row_pointers[height] = {0};
size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);//获取每一行数据的字节大小
int row;
/* 为每一行数据分配一个缓冲区 */
for (row = 0; row < height; row++)
row_pointers[row] = png_malloc(png_ptr, rowbytes);
png_read_image(png_ptr, row_pointers);
Tips:png_malloc()函数是 libpng 提供的一个 API,其实就等价于库函数 malloc。除了 png_read_image()函数之外,我们也可以调用 png_read_rows()一次解码 1 行或多行数据、并将解码后的数据存放在用于提供的内存区域中,譬如:
c
size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);//获取每一行数据的字节大小
png_bytep row_buf = png_malloc(png_ptr, rowbytes);//分配分缓冲、用于存储一行数据
int row;
for (row = 0; row < height; row++) {
png_read_rows(png_ptr, &row_buf, NULL, 1);//每次读取、解码一行数据(最后一个数字 1 表示每次 1 行)
/* 对这一行数据进行处理: 譬如刷入 LCD 显存进行显示 */
do_something();
}
png_read_rows 会自动跳转处理下一行数据。
由此可知,在 low-level 接口,调用 png_read_image()或 png_read_rows()函数都需要向 libpng 提供用于存放数据的内存区域。但是在 high-level 接口中,调用 png_read_png()时我们并不需要自己分配缓冲区,png_read_png()函数内部会自动分配一块缓冲区,那我们如何获取到它分配的缓冲区呢?通过 png_get_rows()函数得到,下小节介绍。
f)、png_read_end()结束读取、解码
当整个 png 文件的数据已经读取、解码完成之后,我们可以调用 png_read_end()结束,代码如下:
c
png_read_end(png_ptr, info_ptr);
读取解码后的数据
解码完成之后,我们便可以去获取解码后的数据了,要么那它们做进一步的处理、要么直接刷入显存显示到 LCD 上;对于 low-level 方式,存放图像数据的缓冲区是由调用者分配的,所以直接从缓冲区中获取数据即可!
对于 high-level 方式,存放图像数据的缓冲区是由 png_read_png()函数内部所分配的,并将缓冲区与png_struct 对象之间建立了关联,我们可以通过 png_get_rows()函数获取到指向每一行数据缓冲区的指针数组,如下所示:
c
png_bytepp row_pointers = NULL;
row_pointers = png_get_rows(png_ptr, info_ptr);//获取到指向每一行数据缓冲区的指针数组
当我们销毁 png_struct 对象时,由 png_read_png()所分配的缓冲区也会被释放归还给操作系统。
结束销毁对象
调用 png_destroy_read_struct()销毁 png_struct 对象,该函数原型如下所示:
c
void png_destroy_read_struct(png_structpp png_ptr_ptr, png_infopp info_ptr_ptr, png_infopp end_info_ptr_ptr);
使用方法如下:
c
png_destroy_read_struct(png_ptr, info_ptr, NULL);
21.6 libpng 应用编程
经过上一小节的介绍后,相信大家已经知道了如何使用 libpng 库对 png 图像进行解码,本小节我们将进行实战,使用 libpng 库对一张指定的 png 图像进行解码,并在 LCD 上显示图像。示例代码如下所示:
c
示例代码 21.6.1 libpng 应用编程示例代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <png.h>
static int width; //LCD X 分辨率
static int height; //LCD Y 分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
static unsigned long line_length; //LCD 一行的长度(字节为单位)
static unsigned int bpp; //像素深度 bpp
static int show_png_image(const char *path) {
png_structp png_ptr = NULL;
png_infop info_ptr = NULL;
FILE *png_file = NULL;
unsigned short *fb_line_buf = NULL; //行缓冲区:用于存储写入到 LCD 显存的一行数据
unsigned int min_h, min_w;
unsigned int valid_bytes;
unsigned int image_h, image_w;
png_bytepp row_pointers = NULL;
int i, j, k;
/* 打开 png 文件 */
png_file = fopen(path, "r"); //以只读方式打开
if (NULL == png_file) {
perror("fopen error");
return -1;
}
/* 分配和初始化 png_ptr、info_ptr */
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr) {
fclose(png_file);
return -1;
}
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_read_struct(&png_ptr, NULL, NULL);
fclose(png_file);
return -1;
}
/* 设置错误返回点 */
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(png_file);
return -1;
}
/* 指定数据源 */
png_init_io(png_ptr, png_file);
/* 读取 png 文件 */
png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);
image_h = png_get_image_height(png_ptr, info_ptr);
image_w = png_get_image_width(png_ptr, info_ptr);
printf("分辨率: %d*%d\n", image_w, image_h);
/* 判断是不是 RGB888 */
if ((8 != png_get_bit_depth(png_ptr, info_ptr)) &&
(PNG_COLOR_TYPE_RGB != png_get_color_type(png_ptr, info_ptr))) {
printf("Error: Not 8bit depth or not RGB color");
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(png_file);
return -1;
}
/* 判断图像和 LCD 屏那个的分辨率更低 */
if (image_w > width)
min_w = width;
else
min_w = image_w;
if (image_h > height)
min_h = height;
else
min_h = image_h;
valid_bytes = min_w * bpp / 8;
/* 读取解码后的数据 */
fb_line_buf = malloc(valid_bytes);
row_pointers = png_get_rows(png_ptr, info_ptr);//获取数据
unsigned int temp = min_w * 3; //RGB888 一个像素 3 个 bit 位
for(i = 0; i < min_h; i++) {
// RGB888 转为 RGB565
for(j = k = 0; j < temp; j += 3, k++)
fb_line_buf[k] = ((row_pointers[i][j] & 0xF8) << 8) |
((row_pointers[i][j+1] & 0xFC) << 3) |
((row_pointers[i][j+2] & 0xF8) >> 3);
memcpy(screen_base, fb_line_buf, valid_bytes);//将一行数据刷入显存
screen_base += width; //定位到显存下一行
}
/* 结束、销毁/释放内存 */
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
free(fb_line_buf);
fclose(png_file);
return 0; }
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 传参校验 */
if (2 != argc) {
fprintf(stderr, "usage: %s <png_file>\n", argv[0]);
exit(-1);
}
/* 打开 framebuffer 设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
line_length = fb_fix.line_length;
bpp = fb_var.bits_per_pixel;
screen_size = line_length * fb_var.yres;
width = fb_var.xres;
height = fb_var.yres;
/* 将显示缓冲区映射到进程地址空间 */
screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 显示 BMP 图片 */
memset(screen_base, 0xFF, screen_size);//屏幕刷白
show_png_image(argv[1]);
/* 退出 */
munmap(screen_base, screen_size); //取消映射
close(fd); //关闭文件
exit(EXIT_SUCCESS); //退出进程
}
代码不再进行讲解,示例代码中所使用到的函数都已经给大家介绍过,上述示例代码使用的是 high-level接口处理方式,直接调用了 png_read_png,一次性把整个 png 文件的数据解码出来,由于得到的数据是RGB888 格式,所以我们需要将其转为 RGB565,转换完成之后将其刷入到显存中。
接下来我们编译示例代码,这里要注意下,使用交叉编译器编译代码时,需要指定 libpng 库和 zlib 库,如下所示:
c
${CC} -o testApp testApp.c -I/home/dt/tools/png/include -L/home/dt/tools/png/lib -L/home/dt/tools/zlib/lib -
lpng -lz
![](https://file.jishuzhan.net/article/1789694819136704513/0da6befa176ebc77e07e1becc38d5469.webp)
使用-I 选项指定 libpng 的头文件(也就是安装目录下的 include 目录),不需要指定 zlib 的头文件;使用了两次-L 选项,分别指定了 libpng 和 zlib 的库目录(也就是安装目录下的 lib 目录);再使用-l 选项指定需要链接的库(z 表示 libz.so、png 表示 libpng.so)。
将编译得到的可执行文件拷贝到开发板 Linux 系统的用户家目录下,并准备一个 png 文件,接着执行测试程序(执行测试程序前,先关闭出厂系统的 Qt GUI 应用程序):
可以看到打印出了一些警告信息,原因是新版本的 libpng 增强了检查,发出了警告;不过这并不影响我们的使用,可以忽略。
此时开发板 LCD 上会显示我们指定的 png 图像,如下所示:
本章内容到此结束!