LV18-01-LCD应用编程-05-显示PNG图片

本文主要是LCD应用编程——显示PNG图片的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
其他网站 kernel - Linux source code (v4.15) - Bootlin linux内核源码在线查看
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
https://elixir.bootlin.com/linux/latest/source 在线阅读linux kernel源码
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)

一、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 没有的。

二、libpng 简介

1. 简介

对于 png 图像,我们可以使用 libpng 库对其进行解码,跟 libjpeg 一样,它也是一套免费、开源的 C 语言函数库,支持对 png 图像文件解码、编码等功能。

2. zlib库移植

zlib 是一套包含了数据压缩算法的函式库,此函数库为自由软件, 是一套免费、开源的 C 语言函数库,所以我们可以获取到它源代码。

libpng 依赖于 zlib 库, 所以要想移植 libpng 先得移植 zlib 库才可以, zlib 也好、 libpng 也好,其实移植过程非常简单,无非就是下载源码、编译源码这样的一些工作。

2.1 下载源码

zlib的官网在这里:zlib Home Site

image-20241013110431614

我们来这里下载源码:Index of /fossils (zlib.net),这里我按照教程选择这个1.2.10版本。

image-20241013110642552

下载后我们解压可以得到:

1
2
3
tar xf zlib-1.2.10.tar.gz
cd zlib-1.2.10
ls
image-20241013110845239

2.2 编译源码

  • 创建安装目录
1
mkdir -p /home/sumu/9arm-linux-lib/zlib-1.2.10/zlib_out
  • 配置编译选项
1
2
export CC=arm-linux-gnueabihf-gcc	#设置交叉编译器
./configure --prefix=/home/sumu/9arm-linux-lib/zlib-1.2.10/zlib_out

–prefix 选项指定 zlib 库的安装目录,将家目录下的 9arm-linux-lib/zlib-1.2.10/zlib_out 作为 zlib 库的安装目录。zlib的配置命令无法配置交叉编译工具链所以这里使用直接修改CC环境变量。

image-20241013112235314
  • 编译
1
make
image-20241013112212188
  • 安装
1
make install
image-20241013112305438

安装完毕后,我们会得到下面的文件:

image-20241013112457650

会发现也有两个软链接,拷贝的时候需要注意一下。

2.3 拷贝到共享目录

1
2
3
4
cp -avf ~/9arm-linux-lib/zlib-1.2.10/zlib_out ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/zlib-1.2.10
cd ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/zlib-1.2.10/lib
cp libz.so.1.2.10 libz.so.1
cp libz.so.1.2.10 libz.so

2.4 移植到开发板

先拷贝到nfs服务器目录:

1
cp -a ~/9arm-linux-lib/zlib-1.2.10/zlib_out ~/4nfs/zlib-1.2.10

然后在串口终端,进入到 zlib 安装目录,将 lib 目录下的所有库文件拷贝到 Linux 系统/usr/lib 目录,注意在拷贝之前,先将开发板出厂系统中已经移植好的zlib库文件删除,执行下面这条命令:

1
2
3
4
5
6
7
8
9
10
# 先备份删除的文件
cd /usr/lib
tar -czf libz_usr_lib.bk.tar.gz libz.*
mv libz_usr_lib.bk.tar.gz ~/nfs_temp/

cd /lib
tar -czf libz_lib.bk.tar.gz libz.*
mv libz_lib.bk.tar.gz ~/nfs_temp/

rm -rf /usr/lib/libz.* /lib/libz.*

删除之后,再将编译得到的 zlib 库文件拷贝到开发板/usr/lib 目录,拷贝库文件时,需要注意符号链接的问题,不能破坏原有的符号链接。

1
cp -avf ~/nfs_temp/zlib-1.2.10/lib/* /usr/lib

拷贝完毕后检查一下软链接:

1
ls -alh /usr/lib/libz*
image-20241013113931404

3. libpng 移植

3.1 下载源码

这个的官网在这里:libpng Home Page

image-20241013163719902

我们到这里下载源码:Tags · pnggroup/libpng (github.com),这里我直接选择最新版本:

image-20241013164409026

我们找到这个1.6.44版本,下载后解压:

1
2
3
tar xf libpng-1.6.44.tar.gz
cd libpng-1.6.44
ls
image-20241013164615321

3.2 编译源码

  • 创建安装目录
1
mkdir -p /home/sumu/9arm-linux-lib/libpng-1.6.44/libpng_out
  • 配置编译选项

libpng 依赖于 zlib 库,前面我们已经将 zlib 库编译成功了,但是我们得告知编译器 zlib 库的安装目录,这样编译器才能找到 zlib 的库文件以及头文件,编译 libpng 的时才不会报错。

1
2
3
export LDFLAGS="${LDFLAGS} -L/home/sumu/9arm-linux-lib/zlib-1.2.10/zlib_out/lib"
export CFLAGS="${CFLAGS} -I/home/sumu/9arm-linux-lib/zlib-1.2.10/zlib_out/include"
export CPPFLAGS="${CPPFLAGS} -I/home/sumu/9arm-linux-lib/zlib-1.2.10/zlib_out/include"

然后我们来配置一下源码工程:

1
./configure --host=arm-linux-gnueabi --prefix=/home/sumu/9arm-linux-lib/libpng-1.6.44/libpng_out
image-20241013165015077
  • 编译
1
make
image-20241013165116048

可以看到我们配置的zlib的目录是生效的,不出意外的话,到这里就编译结束了。

  • 安装
1
make install
image-20241013165215109

然后我们来看一下输出目录的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
sumu@sumu-virtual-machine:~/9arm-linux-lib/libpng-1.6.44$ cd libpng_out/
sumu@sumu-virtual-machine:~/9arm-linux-lib/libpng-1.6.44/libpng_out$ tree
.
├── bin
│   ├── libpng16-config
│   ├── libpng-config -> libpng16-config
│   ├── pngfix
│   └── png-fix-itxt
├── include
│   ├── libpng16
│   │   ├── pngconf.h
│   │   ├── png.h
│   │   └── pnglibconf.h
│   ├── pngconf.h -> libpng16/pngconf.h
│   ├── png.h -> libpng16/png.h
│   └── pnglibconf.h -> libpng16/pnglibconf.h
├── lib
│   ├── libpng16.a
│   ├── libpng16.la
│   ├── libpng16.so -> libpng16.so.16.44.0
│   ├── libpng16.so.16 -> libpng16.so.16.44.0
│   ├── libpng16.so.16.44.0
│   ├── libpng.a -> libpng16.a
│   ├── libpng.la -> libpng16.la
│   ├── libpng.so -> libpng16.so
│   └── pkgconfig
│   ├── libpng16.pc
│   └── libpng.pc -> libpng16.pc
└── share
└── man
├── man3
│   ├── libpng.3
│   └── libpngpf.3
└── man5
└── png.5

9 directories, 23 files

可以看到文件还是比较多的。lib目录下也有不少的软链接。

3.3 拷贝到共享目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cp -avf ~/9arm-linux-lib/libpng-1.6.44/libpng_out ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/libpng-1.6.44
cd ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/libpng-1.6.44

cd ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/libpng-1.6.44/bin
cp -avf libpng16-config libpng-config

cd ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/libpng-1.6.44/include
cp -avf libpng16/pngconf.h pngconf.h
cp -avf libpng16/png.h png.h
cp -avf libpng16/pnglibconf.h pnglibconf.h

cd ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/libpng-1.6.44/lib
cp -avf libpng16.so.16.44.0 libpng16.so
cp -avf libpng16.so.16.44.0 libpng16.so.16

cp -avf libpng16.a libpng.a
cp -avf libpng16.la libpng.la
cp -avf libpng16.so libpng.so

cd ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/libpng-1.6.44/lib/pkgconfig
cp -avf libpng16.pc libpng.pc

cd ~/1sharedfiles/linux_develop/imx6ull-app-demo/lib/libpng-1.6.44

3.4 移植到开发板

先拷贝到nfs服务器目录:

1
cp -avf ~/9arm-linux-lib/libpng-1.6.44/libpng_out ~/4nfs/libpng-1.6.44

然后在串口终端,进入到 libpng 安装目录,将 bin 目录下的所有测试工具拷贝到开发板 Linux 系统/usr/bin 目录(也可以不拷贝,也不怎么用);将 lib 目录下的所有库文件拷贝到 Linux 系统/usr/lib 目录,注意在拷贝之前,先将开发板出厂系统中已经移植好的libpng 库文件删除,执行下面这条命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 先备份删除的文件
cd /usr/lib
ls -alh libpng*
tar -czf libpng_usr_lib.bk.tar.gz libpng*
mv libpng_usr_lib.bk.tar.gz ~/nfs_temp/

# 其实libpng相关的库并没有放在/lib目录中,所以这里其实可以不用管
cd /lib
ls -alh libpng*
tar -czf libpng_lib.bk.tar.gz libpng*
mv libpng_lib.bk.tar.gz ~/nfs_temp/

rm -rvf /lib/libpng* /usr/lib/libpng*

删除之后,再将编译得到的 libpng 库文件拷贝到开发板/usr/lib 目录,拷贝库文件时,需要注意符号链接的问题,不能破坏原有的符号链接。

1
cp -avf ~/nfs_temp/libpng-1.6.44/lib/lib* /usr/lib

拷贝过去之后,开发板/usr/lib 目录下就应该存在这些库文件:

1
ls -alh /usr/lib/libpng*

如下所示:

image-20241013171315056

三、libpng 的使用

libpng 除了解码功能之外,还包含编码功能,也就是创建 png 压缩文件,这里我们主要是学习PNG图片的解码,将一张PNG图片显示到LCD屏幕上。

1. 官方参考文档

libpng 官方提供一份非常详细地使用文档,文档的主页在这里:PNG (Portable Network Graphics) Home Site (libpng.org)

image-20241013184139133

我们点击左上角的General Information,可以来到这里,其实吧,一直往下拉也能找到:PNG Documentation (libpng.org),然后我们点击下图的文档:

image-20241013183910923

我们就可以就看到一些说明文档链接啦:

image-20241013183941341

我们可以参考以下两份文档来学习。

2. 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 来访问这些成员。

3. 创建和初始化 png_struct 对象

首先第一步是创建 png_struct 对象、并对其进行初始化操作,使用 png_create_read_struct()函数创建一个 png_struct 对象、并完成初始化操作, read 表示我们需要创建的是一个用于 png 解码的 png_struct 对象;同理可以使用 png_create_write_struct()创建一个用于 png 编码的 png_struct 对象。

1
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 的版本号信息,如下:

1
#define PNG_LIBPNG_VER_STRING "1.6.44"

创建、初始化 png_struct 对象时, 调用者可以指定自定义的错误处理函数和自定义的警告处理函数,通过参数 error_fn 指向自定义的错误处理函数、通过参数 warn_fn 指向自定义的警告处理函数, 而参数 error_ptr表示传递给这些函数所使用的数据结构的指针; 当然也可将它们设置为 NULL,表示使用 libpng 默认的错误处理函数以及警告函数。 使用示例如下:

1
2
3
4
5
6
png_structp png_ptr = NULL;
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr)
{
return -1;
}

4. 创建和初始化 png_info 对象

png_info数据结构体描述了png图像的信息,同样也需要创建png_info对象,调用png_create_info_struct()函数创建一个 png_info 对象,其函数原型如下所示:

1
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 对象销毁。 使用示例如下:

1
2
3
4
5
6
7
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 对象。

5. 设置错误返回点

5.1 错误返回点的概念

调用 png_create_read_struct()函数创建 png_struct 对象时,调用者可以指定一个自定义的错误处理函数,当 libpng 工作发生错误时,它就会执行这个错误处理函数;但如果调用者并未指定自定义的错误处理函数,那么 libpng 将会使用默认的错误处理函数,其实默认的错误处理函数会执行一个跳转动作,跳转到程序中的某一个位置,我们把这个位置称为错误返回点。

这样,当调用者未指定自定义错误处理函数时, 当 libpng 遇到错误时, 它会执行默认错误处理函数,而默认错误处理函数会跳转到错误返回点,通常这个错误返回点就是在我们程序中的某个位置,我们期望libpng 发生错误时能够回到我们的程序中, 为什么要这样做呢?因为发生错误时不能直接终止退出,而需要执行释放、销毁等清理工作,例如前面创建的 png_struct 和 png_info 对象,需要销毁,避免内存泄漏。

那如何在我们的程序中设置错误返回点呢? 在此之前,需要学习两个库函数: setjmp 和 longjmp。

在 C 语言中,在一个函数中执行跳转,我们可以使用 goto 语句,尤其是在开发驱动程序时;但 goto 语句只能在一个函数内部进行跳转,不能跨越函数,例如从 func1()函数跳转到func2()函数,如果想要实现这种跨越函数间的跳转,在 Linux 下,我们可以使用库函数 setjmp 和 longjmp。

5.2 setjmp 和 longjmp

setjmp 函数用于设置跳转点,也就是跳转位置; longjmp 执行跳转,那么它会跳转到 setjmp 函数所设置的跳转点,来看看这两个函数的原型:

1
2
3
4
#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()是初次返回还是后续的“伪”返回,这里要注意,可以看一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

static jmp_buf buf;
static int cnt = 0;

static void hello(void)
{
cnt++;
printf("hello world!\n");
longjmp(buf,cnt); // 执行跳转
printf("Nice to meet you!\n");
}

int main(void)
{
int ret = -1;

ret = setjmp(buf); //设置跳转
printf("setjmp ret = %d\n", ret);
if(ret == 0)
{
printf("First return\n");
}
else
{
printf("Second return\n");
}
if(cnt < 2)
{
hello();
}
printf("exit main\n");
exit(0);
}

它的打印信息如下:

image-20241013190551607

5.3 libpng 设置错误返回点

libpng 库默认也使用 setjmp/longjmp 这两个库函数组合来处理发生错误时的跳转,当 libpng 遇到错误时,执行默认错误处理函数,默认错误处理函数会调用 longjmp()来进行跳转,所以我们需要使用 setjmp()来为 libpng 设置一个错误返回点。设置方法如下:

1
2
3
4
5
6
/* 设置错误返回点 */
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 执行跳转时也是使用这个变量。 我们可以在错误返回点执行一些清理工作。

6. 指定数据源

就是指定需要进行解码的 png 图像,通常可以使用多种方式来指定数据源,例如文件输入流、内存中的数据流等,这里以文件输入流为例。

libpng 提供了 png_init_io()函数, png_init_io()可以指定数据源,该数据源以文件输入流的方式提供,来看看函数原型 :

1
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 *类型指针。

使用示例如下:

1
2
3
4
5
6
7
8
9
10
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);

7. 读取 png 图像数据并解码

从 png 文件中读取数据并进行解码,将解码后的图像数据存放在内存中,待用户读取。 关于这一步的操作, libpng 提供了两种方式去处理: high-level 接口处理和 low-level 接口处理。其实 high-level 只是对 lowlevel 方式进行了一个封装,使用 high-level 接口非常方便只需一个函数即可,但缺点是灵活性不高、被限定了;而 low-level 接口恰好相反, 灵活性高、但需要用户调用多个 API; 所以具体使用哪种方式要看需求。

7.1 high-level 接口

通常在满足以下两个条件时使用 high-level 接口:

  • 用户的内存空间足够大, 可以一次性存放整个 png 文件解码后的数据;

  • 数据输出格式限定为 libpng 预定义的数据转换格式。

在满足以上两个条件时, 可以使用 high-level 接口, libpng 预定义数据转换类型包括:

libpng 预定义转换类型 说明
PNG_TRANSFORM_IDENTITY No transformation
PNG_TRANSFORM_STRIP_16 Strip 16-bit samples to 8 bits
PNG_TRANSFORM_STRIP_ALPHA Discard the alpha channel
PNG_TRANSFORM_PACKING Expand 1, 2 and 4-bit samples to bytes
PNG_TRANSFORM_PACKSWAP Change order of packed pixels to LSB first
PNG_TRANSFORM_EXPAND Perform set_expand()
PNG_TRANSFORM_INVERT_MONO Invert monochrome images
PNG_TRANSFORM_SHIFT Normalize pixels to the sBIT depth
PNG_TRANSFORM_BGR Flip RGB to BGR, RGBA to BGRA
PNG_TRANSFORM_SWAP_ALPHA Flip RGBA to ARGB or GA to AG
PNG_TRANSFORM_INVERT_ALPHA Change alpha from opacity to transparency
PNG_TRANSFORM_SWAP_ENDIAN Byte-swap 16-bit samples
PNG_TRANSFORM_GRAY_TO_RGB Expand grayscale samples to RGB (or GA to RGBA)

这些转换当中,还不包括背景颜色设置(透明图) 、伽马变换、抖动和填充物等,使用 high-level 接口只能使用以上这些预定义的转换类型,而其它的配置则保持默认。

high-level 接口只需要使用一个函数 png_read_png(),调用该函数将一次性把整个 png 文件的图像数据解码出来、将解码后的数据存放在内存中,如下所示:

1
png_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语言的或 | 运算符)组合多个转换类型。 使用示例如下:

1
png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);

该函数相当于调用一系列 low-level 函数(后面会学习) , 调用顺序如下所示:

(1) 调用 png_read_info 函数获得 png 图像信息;

(2)根据参数 transforms 所指定的转换类型对数据输出转换格式进行设置;

(3)调用 png_read_image 一次性把整个 png 文件的图像数据解码出来、并将解码后的数据存放在内存中。

(4)调用 png_read_end 结束解码。

7.2 low-level 接口

使用 low-level 接口,需要用户将函数 png_read_png()所做的事情一步一步执行:

(1)读取 png 图像的信息

首先我们要调用 png_read_info()函数获取 png 图像的信息:

1
png_read_info(png_ptr, info_ptr);

该函数会把 png 图像的信息读入到 info_ptr 指向的 png_info 对象中。

(2)查询图像的信息

前面提到 png_read_info()函数会把 png 图像的信息读入到 png_info 对象中,接下来我们可以调用 libpng提供的 API 查询这些信息。

1
2
3
4
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 头文件中定义,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 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

(3)设置解码输出参数(转换参数)

这步非常重要,用户可以指定数据输出转换的格式,比如 RGB888, BGR888、 ARGB8888 等数据输出格式, libpng 提供了很多 set 方法(png_set_xxxxx 函数)来实现这些设置, 例如如下代码:

1
2
3
4
5
6
7
8
9
10
11
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 的使用帮助文档。

(4)更新 png 数据的详细信息

经过前面的设置之后, 信息肯定会有一些变化, 我们需要调用 png_read_update_info 函数更新信息:

1
png_read_update_info(png_ptr, info_ptr);

该函数将会更新保存在 info_ptr 指向的 png_info 对象中的图像信息。

(5)读取 png 数据并解码

前面设置完成之后, 接下来便可对 png 文件的数据进行解码了。调用 png_read_image()函数可以一次性把整个 png 文件的图像数据解码出来、并将解码后的数据存放在用户提供的内存区域中, 使用示例如下:

1
png_read_image(png_ptr, row_pointers);

该函数无返回值,参数 png_ptr 指向 png_struct 对象;第二个参数 row_pointers 是一个 png_bytepp 类型的指针变量,也就是 unsigned char **,是一个指针数组,如下所示:

1
png_bytep row_pointers[height];

调用该函数,需要调用者提供足够大的内存空间,可以保存整个图像的数据,这个内存空间的大小通常是解码后数据的总大小;调用者分配内存空间后, 需要传入指向每一行的指针数组,如下所示:

1
2
3
4
5
6
7
8
9
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 行或多行数据、并将解码后的数据存放在用于提供的内存区域中,如:

1
2
3
4
5
6
7
8
9
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()函数得到,后面再学习。

(6)png_read_end()结束读取、解码

当整个 png 文件的数据已经读取、解码完成之后,我们可以调用 png_read_end()结束,代码如下:

1
png_read_end(png_ptr, info_ptr);

8. 读取解码后的数据

解码完成之后,我们便可以去获取解码后的数据了,要么那它们做进一步的处理、要么直接刷入显存显示到 LCD 上; 对于 low-level 方式, 存放图像数据的缓冲区是由调用者分配的, 所以直接从缓冲区中获取数据即可!

对于 high-level 方式,存放图像数据的缓冲区是由 png_read_png()函数内部所分配的,并将缓冲区与png_struct 对象之间建立了关联,我们可以通过 png_get_rows()函数获取到指向每一行数据缓冲区的指针数组,如下所示:

1
2
png_bytepp row_pointers = NULL;
row_pointers = png_get_rows(png_ptr, info_ptr);//获取到指向每一行数据缓冲区的指针数组

当我们销毁 png_struct 对象时,由 png_read_png()所分配的缓冲区也会被释放归还给操作系统。

9. 结束销毁对象

调用 png_destroy_read_struct()销毁 png_struct 对象,该函数原型如下所示:

1
void png_destroy_read_struct(png_structpp png_ptr_ptr, png_infopp info_ptr_ptr, png_infopp end_info_ptr_ptr);

使用方法如下:

1
png_destroy_read_struct(png_ptr, info_ptr, NULL);

四、显示实例

代码和Makefile文件可以看这里:LV18_LCD_DEVICE/05_lcd_show_png · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)

image-20241013193615783