在 LCD 上显示 png 图片
PNG 简介
无损压缩:PNG 使用 LZ77 派生算法进行无损压缩,确保图像质量不受损,且压缩比高
体积小:通过高压缩比,PNG 文件体积小,适合网络传输
索引彩色模式:PNG-8 格式采用8位调色板,将RGB图像转换为索引彩色图像,减少数据量
优化的网络传输显示:PNG 支持流式浏览,允许图像在未完全下载前显示基本内容,适合网络通信
支持透明效果:PNG 提供256级透明度,使图像边缘与背景平滑融合,这是GIF和JPEG不具备的特性
libpng 简介
libpng 是用于处理 PNG 图像的库
功能:支持对 PNG 图像文件进行解码和编码
性质:免费、开源的 C 语言函数库
对比:与 libjpeg 类似
zlib 移植
zlib 简介
zlib 是一个包含数据压缩算法的函数库,是免费、开源的 C 语言库
libpng 依赖:libpng 依赖于 zlib 库,因此在移植 libpng 之前需要先移植 zlib
下载源码包
下载链接: https://www.zlib.net/fossils/
选择 1.2.10 版本的 zlib
编译源码
将下载的 zlib-1.2.10.tar.gz 压缩文件拷贝到 Ubuntu 系统的用户家目录下
tar -xzf zlib-1.2.10.tar.gz
- 解压之后就会得到 zlib-1.2.10 文件夹,这就是 zlib 的源代码目录
先在 tools 目录下创建一个名为 zlib 的文件夹,作为 zlib 库的安装目录
配置
先对交叉编译工具的环境进行初始化
- source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
对 zlib 工程进行配置
./configure --prefix=/home/alientek/tools/zlib/
–prefix 选项指定 zlib 库的安装目录
编译
- make
安装
- make install
安装目录下的文件夹介绍
头文件目录 include
库文件目录 lib
移植到开发板
将出厂系统中原有的 zlib 库文件删除
- rm -rf /usr/lib/libz.* /lib/libz.*
将编译得到的 zlib 库文件拷贝到开发板/usr/lib 目录,拷贝库文件时,需要注意符号
链接的问题,不能破坏原有的符号链接- 开发板/usr/lib 目录下的 zlib 库文件
libpng 移植
下载源码包
- 下载链接: https://github.com/glennrp/libpng/releases
编译源码
将下载的 libpng-1.6.35.tar.gz 压缩包文件拷贝到 Ubuntu 系统
解压:tar -xzf libpng-1.6.35.tar.gz
解压之后得到 libpng-1.6.35 文件夹,这便是 libpng 的源码目录
在编译 libpng 之前,先在 tools 目录下创建一个名为 png 的文件夹,作为 libpng 库的安装目录
配置
先对交叉编译工具的环境进行初始化
- source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
得告知编译器 zlib 库的安装目录,将 zlib 库安装目录下的 include 和 lib 路径导出到环境变量
- export LDFLAGS=“ L D F L A G S − L / h o m e / d t / t o o l s / z l i b / l i b " e x p o r t C F L A G S = " {LDFLAGS} -L/home/dt/tools/zlib/lib" export CFLAGS=" LDFLAGS−L/home/dt/tools/zlib/lib"exportCFLAGS="{CFLAGS} -I/home/dt/tools/zlib/include”
export CPPFLAGS=“${CPPFLAGS} -I/home/dt/tools/zlib/include”
- export LDFLAGS=“ L D F L A G S − L / h o m e / d t / t o o l s / z l i b / l i b " e x p o r t C F L A G S = " {LDFLAGS} -L/home/dt/tools/zlib/lib" export CFLAGS=" LDFLAGS−L/home/dt/tools/zlib/lib"exportCFLAGS="{CFLAGS} -I/home/dt/tools/zlib/include”
对 libpng 源码工程进行配置
./configure --prefix=/home/alientek/tools/png --host=arm-poky-linux-gnueabi
–prefix 选项指定 zlib 库的安装目录
编译
- make
安装
- make install
安装目录下的文件夹介绍
- 同样包含了 bin、include、lib 这些目录
移植到开发板
将开发板出厂系统中已经移植好的
libpng 库文件删除- rm -rf /lib/libpng* /usr/lib/libpng*
将编译得到的 libpng 库文件拷贝到开发板/usr/lib 目录
- 开发板/usr/lib 目录下的 libpng 相关库文件
libpng 使用说明
libpng 还包含编码功能,但本文不作介绍。libpng 官方提供了详细使用文档
PDF 文档:http://www.libpng.org/pub/png/libpng-1.4.0-manual.pdf
TXT 文档:
http://www.libpng.org/pub/png/libpng-manual.txt
libpng 的数据结构
头文件:使用 libpng 需要包含头文件 <png.h>,其中包含 API 和数据结构的声明
关键数据结构:libpng 有两个重要的数据结构体:png_struct 和 png_info
png_struct
用于 libpng 函数内部
作为传递给每个 libpng 函数调用的第一个变量
用户需要创建并初始化这个对象,但通常不会直接使用
png_info
描述 PNG 图像的信息
旧版本中,用户可以直接访问 png_info 成员,如图像宽、高、像素深度等
新版本中,建议通过 png_get_XXX 和 png_set_XXX 接口来访问和修改 png_info 成员,以避免问题
创建和初始化 png_struct 对象
使用 png_create_read_struct() 创建一个用于 PNG 解码的 png_struct 对象
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);第一个参数 user_png_ver 通常设置为 PNG_LIBPNG_VER_STRING(png.h 头文件中定义的一个宏),包含 libpng 的版本信息
- #define PNG_LIBPNG_VER_STRING “1.6.35”
error_ptr、error_fn、warn_fn 参数用于自定义错误处理和警告处理函数,也可设置为 NULL,使用 libpng 默认的处理函数
返回一个指向 png_struct 对象的指针(png_structp)
如果创建失败,返回 NULL,通过判断返回值是否为 NULL 来确认函数执行是否成功
示例:
png_structp png_ptr = NULL;
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr)
return -1;
使用 png_create_write_struct() 创建一个用于 PNG 编码的 png_struct 对象
创建和初始化 png_info 对象
创建 png_info 对象
使用 png_create_info_struct() 函数创建 png_info 对象
png_infop png_create_info_struct(png_const_structrp png_ptr);
需要传入一个 png_struct 对象的指针,内部会建立 png_struct 和 png_info 对象之间的关联
返回一个指向 png_info 对象的指针(png_infop)
如果创建成功,返回指向 png_info 对象的指针。
如果创建失败,返回 NULL,调用者可通过判断返回值是否为 NULL 来确认函数调用是否成功
当销毁 png_struct 对象时,也可以销毁 png_info 对象
示例:
png_infop info_ptr = NULL;
info_ptr = png_create_info_struct(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() 时,可以指定一个自定义的错误处理函数
如果未指定自定义错误处理函数,libpng 会使用默认的错误处理函数
默认错误处理函数
- 默认的错误处理函数会执行一个跳转动作,跳转到程序中的某个位置,这个位置称为错误返回点
错误返回点
当 libpng 遇到错误时,若使用默认错误处理函数,它会跳转到错误返回点
错误返回点是程序中预先设定的一个位置,通常用于执行清理工作,如释放和销毁 png_struct 和 png_info 对象,避免内存泄漏
避免直接终止
- 发生错误时,不直接终止程序,而是跳转到错误返回点执行必要的清理工作
设置错误返回点的方法
使用 setjmp 和 longjmp 库函数来设置错误返回点
函数跳转
在 C 语言中,goto 语句用于函数内部跳转,但不能跨越函数
使用 setjmp 和 longjmp 库函数可以实现跨越函数的跳转
setjmp 函数
- setjmp(jmp_buf env) 用于设置跳转点,保存当前进程环境信息到 env 参数中
longjmp 函数
longjmp(jmp_buf env, int val) 用于执行跳转,跳转到 setjmp 设置的跳转点
longjmp 调用后,类似于第二次调用 setjmp 返回
返回值区分
setjmp 初次调用返回 0
longjmp 调用后,setjmp 返回 val 参数指定的值,用于区分不同的起跳位置
val 参数设置
- 通常 longjmp 调用时,val 参数不为 0,以区分 setjmp 的初次返回和后续“伪”返回
编程举例
代码
- #include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
- #include <stdio.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);
}
- 验证 - - libpng 设置错误返回点 - 错误处理机制:
libpng 库使用 setjmp/longjmp 组合处理错误跳转
- 错误返回点设置:
使用 setjmp() 为 libpng 设置错误返回点
- 错误处理流程:
当 libpng 遇到错误时,默认错误处理函数调用 longjmp() 进行跳转。
需要在代码中设置错误返回点,以便在错误发生时进行处理
- /* 设置错误返回点 */
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return -1;
}
指定数据源
需要指定进行解码的 PNG 图像,数据源可以是文件输入流、内存中的数据流等,这里以文件输入流为例
png_init_io() 函数
ibpng 提供 png_init_io() 函数来指定数据源,数据源以文件输入流的方式提供
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 * 类型指针
示例:
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 文件
- 从 PNG 文件中读取数据并解码,将解码后的图像数据存放在内存中供用户读取
处理方式
High-level 接口:封装了 low-level 接口,使用方便,只需一个函数,但灵活性不高
Low-level 接口:灵活性高,但需要用户调用多个 API
high-level 接口
High-level 接口使用条件
内存空间足够大,可一次性存放解码后的数据
数据输出格式为 libpng 预定义的格式
- libpng 预定义的数据转换类型
High-level 接口限制
- 预定义的转换类型,不包括背景颜色设置、伽马变换、抖动和填充物
High-level 接口函数
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 预定义的转换类型
函数调用示例
- png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);
High-level 接口调用顺序
调用 png_read_info 获取 PNG 图像信息
根据 transforms 设置数据输出格式
调用 png_read_image 解码并存放数据
调用 png_read_end 结束解码
low-level 接口
使用 low-level 接口,需要用户将函数 png_read_png()所做的事情一步一步执行
1、读取 PNG 图像信息
调用 png_read_info() 获取 PNG 图像信息并存入 png_info 对象
png_read_info(png_ptr, info_ptr);
2、查询图像信息
使用 libpng 提供的 API 查询图像的宽度、高度、位深度和颜色类型
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 头文件中定义,如上所示:
3、设置解码输出参数
根据需要调用 png_set_xxxxx 函数设置数据输出格式,例如:
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 中可以查看每个函数的注释信息和参数列表
具体使用方法和更多转换函数请参考 libpng 的使用手册
自定义转换函数
libpng 提供了多种转换函数,但不支持所有可能的输出格式(如 YUV565、RGB565、YUYV)
用户可以设置自定义转换函数来解决这些问题
注册自定义转换函数
使用 png_set_read_user_transform_fn() 注册自定义转换函数
使用 png_set_user_transform_info() 提供自定义转换函数所需的用户数据结构和输出数据详细信息(如颜色深度和颜色通道)
有关自定义转换函数和其他详细信息,可以查阅 libpng 的使用帮助文档
4、更新 PNG 数据信息
调用 png_read_update_info() 更新 png_info 对象中的图像信息
png_read_update_info(png_ptr, info_ptr);
5、读取并解码 PNG 数据
解码 PNG 数据
调用 png_read_image() 一次性解码整个 PNG 文件的数据并存入用户提供的内存区域
png_read_image(png_ptr, row_pointers);
参数 png_ptr 指向 png_struct 对象
第二个参数 row_pointers 是一个 png_bytepp 类型的指针变量,也就是 unsigned char **(指针数组)
- png_bytep row_pointers[height];
无返回值
需要提供足够大的内存空间来保存解码后的数据,并通过 row_pointers 数组传入每一行的指针
内存分配
调用 png_read_image() 一次性解码整个 PNG 文件的数据并存入用户提供的内存区域
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);使用 png_malloc() 为每一行数据分配缓冲区
png_malloc() 是 libpng 提供的 API,等价于 malloc()
也可以调用 png_read_rows()一次解码 1 行或多行数据、并将解码
后的数据存放在用于提供的内存区域中ize_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 接口中,需要手动分配内存区域
在 high-level 接口中,调用 png_read_png() 时 libpng 会自动分配缓冲区
6、结束解码
调用 png_read_end() 结束读取和解码过程
png_read_end(png_ptr, info_ptr);
读取解码后的数据
解码后处理
- 解码完成后,可以获取解码后的数据进行进一步处理或直接显示到 LCD 上
Low-Level 方式
- 缓冲区由调用者分配,直接从缓冲区中获取数据即可
High-Level 方式
缓冲区由 png_read_png() 函数内部分配,并与 png_struct 对象关联
使用 png_get_rows() 函数获取指向每一行数据缓冲区的指针数组
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 对象
- void png_destroy_read_struct(png_structpp png_ptr_ptr, png_infopp info_ptr_ptr, png_infopp end_info_ptr_ptr);
使用方法
- png_destroy_read_struct(png_ptr, info_ptr, NULL);
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); //退出进程 }
编写流程和上面介绍的libpng 使用说明一致
使用的是 high-level
接口处理方式解码得到的数据是 RGB888 格式,需要转换为 RGB565 格式
转换后的数据刷入显存中进行显示
编译代码
- ${CC} -o testApp testApp.c -I/home/alientek/tools/png/include -L/home/alientek/tools/png/lib -L/home/alientek/tools/zlib/lib -
lpng -lz
执行测试
看到打印出了一些警告信息,原因是新版本的 libpng 增强了检查,发出了警告;不影响
我们的使用,可以忽略