前言:在github的发现一个谷歌开源的jpeg格式图片的压缩工具,它可以在主观质量无损的条件下, 将jpeg图片减少20%~30%的码率,于是当时测试了一下压缩效果,图片质量95压缩率大概在20%~30%。我们司平台是漫画阅读app,整个app除了图片还是图片,特别是首页,用了大量的高清无码的图片,于是我想将guetzli用于首页漫画封面图片的压缩,但是guetzli并没有提供php的接口,所以当时的实现是将需要压缩的图片放入redis队列,创建一个进程在凌晨基本没有用户访问的时候通过exec调用guetzli工具对redis队列消费处理。(至于为什么要这样,请看下面网上大佬的测试报告)
以下数据引用自这篇博客:谷歌开源图片压缩算法Guetzli实测体验报告
Guetzli资源消耗:
官方文档说,1MPIX的图片处理需要消耗300M内存。
实测一个1MB大小1920x2560的图片,有4.9MPIX。理论消耗内存1474MB,实际消耗1009MB内存,实际与理论基本相符。由此看出这个工具是个内存消耗大户,60G内存只够处理200MPIX,也就是同时处理40张左右的1920x2560图片。
CPU消耗则一直是100%单核占用。当然实际使用多核机器可以同时跑多个进程。有多少核就能跑多少个guetzli任务。
压缩图片耗时跟图片大小程离散相关,图片越大,耗时越久
guetzli劣势:
由于guetzli时效性差,图片越大压缩时间越长,而且资源占用大,简直就是内存消耗大户。
guetzli优势:
实测对质量在90或以下的jpg图片,guetzli输出的新图质量不会降低。而实际压缩率能够达到平均压缩率29%。
兼容性比较好,输出的jpeg格式图片通用性非常高。没有webp、sharpp那种协议不兼容的困扰。
在客户端jpeg格式的图片编解码速度比其他私有协议快很多。
下面说说我们将要做的:
从github下载guetzli的源码编译后会生成一个guetzli的工具,以及一个静态库文件,我们需要做的就是创建一个php扩展调用这个静态库提供的压缩功能。
动态加载so模块:利用ext_skel工具编译生成so模块,修改php.ini文件,动态加载即可
静态编译:将编写的模块静态编译到PHP,需要重新编译PHP
面临的问题:
注:php扩展加载方式可以分为静态编译与动态加载。(区别在于,静态编译需要重新编译php才能使用扩展,动态加载是将php扩展编译成so动态库库,修改php.ini,php在运行时会动态加载,不需要重新编译php),我们这里需要将扩展编译成so动态库。
1、php动态so库需要依赖guetzli编译生成的静态库,guetzli的静态库在编译的时候生成的目标文件是地址相关的,而动态库的一个特点是动态库是被进程动态加载到一个不确定的地址,所以动态库编译时的目标文件为地址无关的相对路径,而没有绝对路径。在这里我们的php动态库需要引用guetzli的静态库,所以我们需要修改guetzli的Makefile文件;
2、guetzli源码使用c++编写的,但是我们的php扩展接口是用c进行编写,由于c函数跟c++函数在编译的时候对参数的处理不一样,所以这里需要导出一个c接口,供php扩展调用;
创建php扩展模板:
注:本文章不详细讨论php扩展开发基础知识,后面会详细写一篇php扩展方面的文章;
1、用php源码扩展目录ext下的ext_skel工具创建php扩展的模板,在ext目录下运行./ext_skel --extname=myguetzli命令(myguetzli是我们需要创建扩展的名称),这个工具会在etx目录下创建一个myguetzli目录,里面包含我们扩展开发所必需的基本文件。
2、我们需要使用到的是config.m4、myguetzli.c、myguetzli.h这三个文件
3、我们在myguetzli目录下创建一个guetzli目录用于存放guetzli的静态库以及导出的c函数接口文件
php扩展模板有了,我们接下来解决上面提到问题:
第一步:修改guetzli源码目录的Makefile文件,让guetzli在make编译的时候生成的目标代码是地址不相关的。(生成地址不相关的目标文件,需要在编译的时候加上-fPIC编译选项)
1、guetzli源码目录下有一个guetzli_static.make,这个是用于编译guetzli静态库的Makefile文件;
2、在这里文件里找到下面这行,并加上红色部分的选项,保存;
3、运行make进行编译即可。会在bin/Release目录下生成一个guetzli_static.a的静态库文件;到这里第一个问题就解决了。
//4、将guetzli_static.a静态库拷贝到php扩展myguetzli目录下的guetzli/lib目录,guetzli源码目录的guetzli目录里面的头文件也拷贝到myguetzli目录下的guetzli目录
ALL_CXXFLAGS += $(CXXFLAGS) $(ALL_CPPFLAGS) -O3 -g -std=c++11 -fPIC `pkg-config --static --cflags libpng || libpng-config --static --cflags` 第二步:创建接口文件 1、guetzli源码目录下有一个guetzli.cc文件,这个是guetzli提供的工具源代码,写好了对guetzli压缩功能的调用, 我们在这个文件里面增加一个函数,并用c函数的方式编译这个函数 进入扩展myguetzli下的guetzli目录,创建两个文件(guetzli.cpp、guetzli.h) 新建一个guetzli.h文件,extern告诉编译器按c函数的方式编译,这样才能被c所调用
在guetzli.cc文件新增MyGuetzli函数#guetzli.h文件 #ifdef __cplusplus extern "C" { #endif int MyGuetzli(char* filename, char* savefilename, int quality, int memlimit_mb); #ifdef __cplusplus } #endif
#guetzli.cpp文件,将guetzli源码目录guetzli.cc文件的代码复制过来,并增加下面的代码 int MyGuetzli(char* filename, char* savefilename, int quality, int memlimit_mb) { int verbose = 0; // int quality = kDefaultJPEGQuality; memlimit_mb = kDefaultMemlimitMB; std::string in_data = ReadFileOrDie(filename); std::string out_data; guetzli::Params params; params.butteraugli_target = static_cast
( guetzli::ButteraugliScoreForQuality(quality)); guetzli::ProcessStats stats; if (verbose) { stats.debug_output_file = stderr; } static const unsigned char kPNGMagicBytes[] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', }; if (in_data.size() >= 8 && memcmp(in_data.data(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) { int xsize, ysize; std::vector rgb; if (!ReadPNG(in_data, &xsize, &ysize, &rgb)) { fprintf(stderr, "Error reading PNG data from input file\n"); return 1; } double pixels = static_cast (xsize) * ysize; if (memlimit_mb != -1 && (pixels * kBytesPerPixel / (1 << 20) > memlimit_mb || memlimit_mb < kLowestMemusageMB)) { fprintf(stderr, "Memory limit would be exceeded. Failing.\n"); return 1; } if (!guetzli::Process(params, &stats, rgb, xsize, ysize, &out_data)) { fprintf(stderr, "Guetzli processing failed\n"); return 1; } } else { guetzli::JPEGData jpg_header; if (!guetzli::ReadJpeg(in_data, guetzli::JPEG_READ_HEADER, &jpg_header)) { fprintf(stderr, "Error reading JPG data from input file\n"); return 1; } double pixels = static_cast (jpg_header.width) * jpg_header.height; if (memlimit_mb != -1 && (pixels * kBytesPerPixel / (1 << 20) > memlimit_mb || memlimit_mb < kLowestMemusageMB)) { fprintf(stderr, "Memory limit would be exceeded. Failing.\n"); return 1; } if (!guetzli::Process(params, &stats, in_data, &out_data)) { fprintf(stderr, "Guetzli processing failed\n"); return 1; } } WriteFileOrDie(savefilename, out_data); return 0; }
2、在myguetzli扩展下的guetzli目录执行如下代码
g++ guetzli.cpp -std=c++11 -fPIC -shared -I/home/liaokw/下载/guetzli -L/home/liaokw/下载/guetzli/bin/Release -lguetzli_static -o ./lib/libguetzli.so
-I:指定guetzli的头文件
-L:指定guetzli静态库
-l:guetzli静态库的名称
-o:生成动态库保存路径
libguetzli.so这个动态库就是我们扩展所要要使用的,这个动态库主要作为调用guetzli的接口
第三步:php扩展开发
1、修改config.m4,编写一些宏函数,用于检测以及添加扩展所依赖的库,以及环境(扩展依赖于第二部编译的动态库,guetzli依赖于png库)
在config.m4文件添加以下代码
PHP_ARG_WITH(png-dir, for myguetzli support, Make sure that the comment is aligned: [ --with-png-dir Include myguetzli support]) if test "$PHP_PNG_DIR" != "no"; then SEARCH_PATH="$PHP_PNG_DIR /usr/lib /usr/local/lib" # you might want to change this SEARCH_FOR="include/png.h" # you most likely want to change this for i in $SEARCH_PATH ; do if test -f $i/$SEARCH_FOR; then PNG_DIR=$i AC_MSG_RESULT(found in $i) fi done if test -z "$PNG_DIR" -o -z "$PNG_DIR/include"; then AC_MSG_RESULT([$PNG_DIR not found 1]) AC_MSG_ERROR([$SEARCH_PATH || $PNG_DIR || Please reinstall the png distribution]) fi dnl 头文件路径 PHP_ADD_INCLUDE($PNG_DIR/include) PHP_CHECK_LIBRARY(png,png_create_read_struct, [ PHP_ADD_LIBRARY_WITH_PATH(png, $PNG_DIR/lib, MYGUETZLI_SHARED_LIBADD) AC_DEFINE(HAVE_PNGLIB,1,[libpng yes ]) ],[ AC_MSG_ERROR([$PNG_DIR/include no found 2]) ],[ -L$PNG_DIR/lib -lpng ]) PHP_SUBST(MYGUETZLI_SHARED_LIBADD) fi 上面的代码主要用于在configure添加一个配置参数--with-png-dir,用于指定png库所在路径 if test "$PHP_MYGUETZLI_DIR" != "no"; then SEARCH_PATH="$PHP_MYGUETZLI_DIR guetzli" # you might want to change this SEARCH_FOR="/guetzli.h" # you most likely want to change this for i in $SEARCH_PATH ; do if test -f $i/$SEARCH_FOR; then GUETZLI_DIR=$i AC_MSG_RESULT(found in $i) fi done if test -z "$GUETZLI_DIR" -o -z "$GUETZLI_DIR/"; then AC_MSG_RESULT([$GUETZLI_DIR not found 1]) AC_MSG_ERROR([$SEARCH_PATH || $GUETZLI_DIR || Please reinstall the zhtmltopdf distribution]) fi dnl 头文件路径 PHP_ADD_INCLUDE($GUETZLI_DIR) PHP_ADD_LIBRARY_WITH_PATH(guetzli, $GUETZLI_DIR/lib, MYGUETZLI_SHARED_LIBADD) AC_DEFINE(HAVE_MYGUETZLILIB,1,[libguetzli yes ]) dnl PHP_CHECK_LIBRARY(guetzli,MyGuetzli, dnl [ dnl PHP_ADD_LIBRARY_WITH_PATH(guetzli, $GUETZLI_DIR/lib, MYGUETZLI_SHARED_LIBADD) dnl AC_DEFINE(HAVE_MYGUETZLILIB,1,[libguetzli yes ]) dnl ],[ dnl AC_MSG_ERROR([$GUETZLI_DIR/include no found 2]) dnl ],[ dnl -L$GUETZLI_DIR/lib -lguetzli dnl ]) PHP_SUBST(MYGUETZLI_SHARED_LIBADD) fi
上面的宏代码用于检测以及添加我们在第二步生成的动态库
通过执行phpize程序,会将上面一系列宏函数替换成检测编译环境的shell代码,并配置我们所需要的依赖库
2、编辑myguetzli.c扩展文件,需要实现一个myguetzlijpg函数给用户层调用,修改两处地方以及引用guetzli.h文件,用于使用接口库导出的函数
3、编译扩展并安装即可使用了
哎,第一次写文章,逻辑处理的不够好,写的也一塌糊涂,漏洞百出;很多东西自己知道但是不知道该如何表达出来;