前面的文章翻译了libpng的手册,机器翻译,很多地方还是很难懂。
要用好一个别人的代码,还是要上手实际操作才行。
根据前面的手册翻译(Libpng源码的使用)可以知道,libpng是一个成熟的开源库,提供了成熟的用户接口,在移植时一般不需要对源码本身进行太多修改。
源码
下载libpng和zlib的源码,解压拷贝到工程目录下,并修改文件夹名字,去掉版本信息,注意zlib文件夹名称不能随意修改,因为libpng中引用了zlib的头文件,需要保证路径正确
打开RT1052例程的keil 工程,设置头文件路径,
进入libpng/scripts目录,将 pnglibconf.h.prebuilt 文件拷贝到 libpng目录下,并修改文件名为 pnglibconf.h 这个文件就是libpng源码的配置文件,
将libpng和zlib的源码添加到工程中,开源的源码用户接口的c 文件一般都在最外层,直接添加.c文件就可以了,类似下图,zlib中有些用不到的源码,可以不添加
引用
如果是linux环境,要使用libpng其实非常简单,只要在自己的代码中引用 png.h 头文件即可,如果用到了zlib的内容,引用zlib.h 即可,开源软件这点做的特别方便
include 这两个头文件后就可以使用libpng和zlib了。
修改
因为是移植到RT1052上,可能需要输出很多调试信息来定位问题,需要对这两个开源库的打印函数做一些修改。
这两个库本来是为linux系统编写的,里边使用了很多 fprintf() 函数来打印调试、错误、警告信息,RT1052不能直接使用。可以通过修改pngdebug.h文件来实现。
将文件中的调试接口
#ifndef png_debug
# define png_debug(l, m)
#endif
#ifndef png_debug1
# define png_debug1(l, m, p1)
#endif
#ifndef png_debug2
# define png_debug2(l, m, p1, p2)
#endif
替换为
#ifndef png_debug
# define png_debug(l, m) \
do { \
int num_tabs=l; \
char format[256]; \
snprintf(format,256,"%s%s%s",(num_tabs==1 ? "\t" : \
(num_tabs==2 ? "\t\t":(num_tabs>2 ? "\t\t\t":""))), \
m,PNG_STRING_NEWLINE); \
printf(format); \
} while (0) /*((void)0)*/
#endif
#ifndef png_debug1
# define png_debug1(l, m, p1) \
do { \
int num_tabs=l; \
char format[256]; \
snprintf(format,256,"%s%s%s",(num_tabs==1 ? "\t" : \
(num_tabs==2 ? "\t\t":(num_tabs>2 ? "\t\t\t":""))), \
m,PNG_STRING_NEWLINE); \
printf(format,p1); \
} while (0)/*((void)0)*/
#endif
#ifndef png_debug2
# define png_debug2(l, m, p1, p2) \
do { \
int num_tabs=l; \
char format[256]; \
snprintf(format,256,"%s%s%s",(num_tabs==1 ? "\t" : \
(num_tabs==2 ? "\t\t":(num_tabs>2 ? "\t\t\t":""))), \
m,PNG_STRING_NEWLINE); \
printf(format,p1,p2); \
} while (0)/* ((void)0) */
#endif
libpng本来是规定了3个打印级别,这里为了方便,没有使用 PNG_DEBUG 宏定义,直接修改了png_debug ,png_debug1,png_debug2的内容,其实就是代码里的fprintf()不能用串口输出,改为printf(),用串口输出打印。
zlib库的情况差不多,将 zutil.h 中的调试接口修改一下,方便在RT1052上调试
/* Diagnostic functions */
#ifdef ZLIB_DEBUG
# include <stdio.h>
extern int ZLIB_INTERNAL z_verbose;
extern void ZLIB_INTERNAL z_error OF((char *m));
# define Assert(cond,msg) {if(!(cond)) z_error(msg);} // fprintf
# define Trace(x) {if (z_verbose>=0) fprintf x ;}
# define Tracev(x) {if (z_verbose>0) fprintf x ;}
# define Tracevv(x) {if (z_verbose>1) fprintf x ;}
# define Tracec(c,x) {if (z_verbose>0 && (c)) fprintf x ;}
# define Tracecv(c,x) {if (z_verbose>1 && (c)) fprintf x ;}
#else
extern int ZLIB_INTERNAL z_verbose;
# define Assert(cond,msg)
# define Trace(x) {if (z_verbose >= 0) printf x ;}
# define Tracev(x) {if (z_verbose > 0) printf x ;}
# define Tracevv(x) {if (z_verbose > 1) printf x ;}
# define Tracec(c,x) {if (z_verbose > 0 && (c)) printf x ;}
# define Tracecv(c,x) {if (z_verbose > 1 && (c)) printf x ;}
#endif
修改 zutil.c 中的 z_error函数
#ifdef ZLIB_DEBUG
#include <stdlib.h>
# ifndef verbose
# define verbose 0
# endif
int ZLIB_INTERNAL z_verbose = verbose;
void ZLIB_INTERNAL z_error (m)
char *m;
{
// fprintf(stderr, "%s\n", m);
printf( "%s\n", m);
exit(1);
}
#endif
#include <stdlib.h>
# ifndef verbose
# define verbose 0
# endif
int ZLIB_INTERNAL z_verbose = verbose;
void ZLIB_INTERNAL z_error (m)
char *m;
{
// fprintf(stderr, "%s\n", m);
printf( "%s\n", m);
// exit(1);
}
在 zconf.h 中添加下面三个宏定义
#define DZ_PREFIX
#define Z_PREFIX
#define Z_SOLO
在 trees.c 中增加 引用一个头文件
#include "deflate.h"
# include <ctype.h>
剩下的就是使用查找替换功能,把打印函数全部替换就可以了,类似下面这样
设置
移植到RT1052上还有一个头疼的问题要解决:libpng的例子pngtest.c默认使用FILE文件操作 ,使用 png_init_io()函数绑定文件和png结构体指针即可,使用了标准的fopen,fread,fwrite函数,这些函数在RT1052上还无法使用,我没有重定义文件操作函数。幸好libpng提供了另一种读写方式,用户可以自定义png读写函数,可以直接在内存中操作。只需要使用提供png_set_write_fn()和png_set_read_fn() 函数的修改png读写的方法即可。他们的关系如下
因为没有使用标准的文件系统,对读写的控制只能自己来实现,比如我的例程只是把图片写入内存,那我就可以在内存中定义一段存储空间,在定义一个标志,用于表示当前读写的位置。
//定义一个类似文件的结构体 用于在内存中存储处理后的 png文件
typedef struct memImage
{
//一个静态存储区 用于存储图像文件
char png [biHeight*biWidth*3]; //定义足够大空间 png 空间肯定会小于RGB像素空间
//当前写入的数据位置
unsigned int png_cnt;
} memImage;
//声明一个文件实体,用来存储压缩后的png图片数据
static memImage my_image;
我的write_function函数进行的操作就是将传入的数据写入静态存储区中,write_function函数名称可以任意定,但是参数列表必须按照libpng规定的,我的操作如下:
//自定义写入函数 ,用此函数替换标准文件写入函数
static void PNGCBAPI write_function(png_structp pp, png_bytep data, size_t size)
{
// png_get_io_ptr()
memImage * mem = (memImage *) png_get_io_ptr(pp);
memcpy((mem->png + mem->png_cnt) , data, size);
mem->png_cnt += size;
}
png_structp pp 是png的结构体指针,里边包含了png文件的所有内容,
png_bytep data 是要写入的数据,具体是什么可以不用关心,libpng在定义的时候自己确定,
size_t size 是要写入的 data数据大小,单位是字节。
看write_function函数内,其实就是将data写入到pp内,我这个函数写的比较简单,可以参照libpng库提供的其他例子(pngstest.c ,pngimage.c)增加动态内存管理,错误判断等功能,使程序更可靠。
需要注意的是传入的png结构体 pp 包含了好多其他元素,我们只需要把数据写入其数据区即可,libpng提供了png_get_io_ptr(pp) 函数,返回结构体的数据区。
这里也可以看出,libpng对内部封装比较规整,不建议直接去修改源码的其他内容,我们能访问的内容是通过接口函数访问。感兴趣的可以自己深入研究。
以上准备工作基本上就做完了,下面开始进入正式的压缩操作流程。
初始化
首先定义两个png结构的指针
png_structp write_ptr;
png_infop write_info_ptr;
我们对png的操作都是通过这两个结构体实现,这两个结构体包含了png图像的所有信息,因为定义的是两个指针,不能直接使用,需要进行初始化,下面贴出我的初始化函数,其实就是调用了一些列libpng的函数。
//在内存中操作图像文件
// 为libpng 设置用户IO函数 用于在内存中读写文件
void set_png_user_IO(voidp write_io_ptr)
{
error_parameters.file_name = "test.png";
//初始化 write_ptr
write_ptr =
png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if(write_ptr == NULL)
{
printf( "write_ptr %s [%d] \r\n" ,__FILE__,__LINE__);
png_destroy_write_struct(&write_ptr, NULL);
return ;
}
png_set_error_fn(write_ptr, &error_parameters, pngtest_error,
pngtest_warning);
//初始化 write_info_ptr
write_info_ptr = png_create_info_struct(write_ptr);
if(write_info_ptr == NULL)
{
printf("png_create_info_struct %s [%d] \r\n", __FILE__,__LINE__);
// fclose(pPNG);
png_destroy_write_struct(&write_ptr, &write_info_ptr);
return ;
}
write_end_info_ptr = png_create_info_struct(write_ptr);
if (setjmp(png_jmpbuf(write_ptr)))
{
/* If we get here, we had a problem writing the file */
png_destroy_write_struct(&write_ptr, &write_info_ptr);
printf("setjmp error %s [%d] \r\n", __FILE__,__LINE__);
return ;
}
// 重要 设置 libpng 读写函数
// write_io_ptr = png_get_io_ptr(write_ptr);
png_set_write_fn(write_ptr, write_io_ptr, write_function,
NULL);
// png_init_io(write_ptr, pPNG);
//设置压缩等级
png_set_compression_level(write_ptr,
Z_BEST_COMPRESSION);
/* Set other zlib parameters for compressing IDAT */
png_set_compression_mem_level(write_ptr, 8);
png_set_compression_strategy(write_ptr,
Z_DEFAULT_STRATEGY);
png_set_compression_window_bits(write_ptr, 15);
png_set_compression_method(write_ptr, 8);
png_set_compression_buffer_size(write_ptr, 8192); //8192
/* Set zlib parameters for text compression
* If you don't call these, the parameters
* fall back on those defined for IDAT chunks
*/
png_set_text_compression_mem_level(write_ptr, 8);
png_set_text_compression_strategy(write_ptr,
Z_DEFAULT_STRATEGY);
png_set_text_compression_window_bits(write_ptr, 15);
png_set_text_compression_method(write_ptr, 8);
//初始化图像 iHDR
png_set_IHDR(write_ptr, write_info_ptr, biWidth,
biHeight, bit_DEPTH, PNG_COLOR_TYPE_RGB,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
// update write_info
// printf( "png_set_bgr \r\n");
png_set_bgr(write_ptr); // for BMP data BGR -> RGB
// auto flush write data when use low level inteface
// png_set_flush(write_ptr, 50);
// all set finish
png_write_info(write_ptr, write_info_ptr);
}
基本流程是这样
这个函数完成了png结构和png信息结构的初始化,并将iHDR信息写入到内存中。关于iHDR的信息请看这篇文章
PNG文件结构分析 ---Png解析(转),里边有png文件结构的说明,我们简单点,png图片只需要包含iHDR和iDATA部分即可。
写入数据
完成初始化以后就是写入真正的图像数据了,libpng提供了很多函数完成写入,这里我选择了内存占用小的一种,使用
png_write_row(write_ptr, (png_bytep ) &row_pointers[row][0]); // one row eatch time
函数每次写入一行图像,还有其他多种方式,
如
png_write_rows(write_ptr, row_pointers,number_of_rows); //每次写入多行
png_write_image(write_ptr, row_pointers); //一次性写入整幅图像
注意 row_pointers 的数据类型和代表的 意义
png_write_rows / png_write_row / png_write_image 内部自动按照一定规则调用zlib压缩函数,不需要手动干预。zlib压缩的规则可以在前面初始化的时候修改,但是一般就没有必要改动。
下面是写入部分的代码,我是通过UDP接收的原始RGB像素信息,每次传传过来一行像素,写入png图像中,当传输完所有行数据后,调用
png_write_end(write_ptr, write_info_ptr); // write finish
写入png图像的结尾
然后通过串口打印出来16进制字符,方便与png文件结构进行对比。
最后在电脑上又把16进制字符转成二进制文件,后缀改成png,就能用电脑端的图片查看工具查看图片了。你也可以通过其他方式直接传输图像文件。
set_png_user_IO(&my_image);
while(1)
{
bool isCheck = false;
//接收 UDP数据 此处换成图像数据
if(row < biHeight )
{
int len = get_udp_rxData(row_pointers[row], rowcnt);
if(len >= rowcnt)
{
printf("receive row %d %d byte \r\n",row ,len);
tim1 = xTaskGetTickCount();
png_write_row(write_ptr, (png_bytep ) &row_pointers[row][0]); // one row eatch time
tim2 = xTaskGetTickCount();
printf("png_write_row use %d ms \r\n",tim2-tim1);
row++;
}
if(row == biHeight)
cnt= 0;
}
else if(row == biHeight)
{
//接收完成
printf("receive finish \r\n");
tim1 = xTaskGetTickCount();
png_write_end(write_ptr, write_info_ptr); // write finish
tim2 = xTaskGetTickCount();
printf("png_write_end use %d ms \r\n",tim2-tim1);
// png_destroy_write_struct(&write_ptr, &write_info_ptr);
for (int i = 0;i< my_image.png_cnt ;i++)
{
printf("%02X ",my_image.png[i]);
}
// fclose(pPNG);
memset(&my_image,0,sizeof(my_image));
cnt++;
row++;
}
else
{
}
vTaskDelay(pdMS_TO_TICKS(100));
}
经过测试,RT1052 528MHz的主频,32MB SDRAM,压缩一行1920像素(5760 Bytes)的图片,用时大概是1-8ms,时间不等,因为压缩数据的过程是接收一定数据量后才进行的。
这样一幅1920×1200的原始图片,压缩用时大概是3-7秒,没有很严格的测算,感觉还有优化空间。
screen.bmp是源图像,通过UDP发送时只发送了像素信息,BMP的文件信息不发送。receive.png是收到的压缩后图像,因为所用的测试图像有大片单调像素,可以看到图片从6.7MB压缩到83KB,压缩率还是很可观的,当然最后得到的png图片大小是和图像内容相关的,有些画面复杂多变的图像使用png压缩大概只能压缩到1/3或1/2,要更高压缩率可以尝试libjpeg或者libjpeg-turbo,待后续文章介绍。
关于png使用的压缩算法,请查看其他文章。建议稍微了解一下png文件结构,png压缩算法,会对使用libpng有很大帮助。