人间观察
步入社会后,你会发现,老人说的话都是对的。
前面讲了些Android的jni知识和bitmap的实践,接下来几篇应该都是Android中jni的一些实践。这篇我们对Android中图片在jni层利用libjpeg-turbo
进行大小压缩,并且压缩后不失真,清晰度和原图基本无差别。
背景
libjpeg
开源的JPEG图像库,它使用非常广泛,Android也依赖libjpeg
来压缩图片,但是Android不是直接使用libjpeg
,而是基于一个叫Skia
的开源项目来作为的图像处理引擎,Skia
对libjpeg
进行了良好的封装。libjpeg
在压缩图像时,有一个参数叫optimize_coding
,这个参数的设置直接影响图片的质量和大小。
如果设置optimize_coding
为true,将会使得压缩图像过程中基于图像数据计算哈弗曼表(关于图片压缩中的哈弗曼表,可以百度下查阅相关资料),由于这个计算会显著消耗空间和时间,默认值被设置为false。采用默认哈夫曼表进行计算。optimize_coding
在Skia
中默认值也是false。
随着时间的推移现在 Android 手机性能越来越好,Google 在Android 7.0后已经设置为true了。如下代码可以看到。
源码地址:
/androidossearch?query=SkImageDecoder_libjpeg.cpp
>=android 7.0 后的源码// ...省略其它代码// Tells libjpeg-turbo to compute optimal Huffman coding tables// for the image. This improves compression at the cost of// slower encode performance.cinfo.optimize_coding = TRUE;jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);// ...省略其它代码
也就是说在Android 7.0前中无论你怎么压缩(尺寸压缩,质量压缩,Matrix 矩阵变换)它都会导致图片质量变差,而且在app应用层是无法修改的。但是我们可以自己编译libjpeg
来设置这个参数为true,既然Android 7.0 后已经是optimize_coding = TRUE;
还有必要自己编译libjpeg
来设置这个参数为true 不过没关系,我们就拿这个来学习jni也是挺好的。这个库也支持解压缩(有兴趣的可以研究下),接下来我们看下如何实现压缩。
在有ugc功能的app中,拍照上传图片的时候基本都会进行压缩。
但是我看了下快手,微信,抖音的apk里。 快手里有用这个压缩库,微信抖音好像并使用libjpeg,也可能是改了so的名字,也可能出于7.0后已经是true了,也可能是用的Android 系统提供的API,也可能直接上传的原图云端进行的压缩。
压缩效果对比
下图是一张2.4MB的原始图片采用30%的压缩后是632KB,4倍还是可以的。清晰度对比如下,几乎看不出来差别,但是如果压缩到10%,图片有稍微的清晰度降低,真实项目可以权衡下。说明效果远比Android 内置的好。
(图片拍摄于-11-16号北京西北旺下班回家的公交站~,留下纪念,说不定哪天就不在北京了。)
libjpeg-turbo在Android环境下的编译
开源libjpeg-turbo源码
这个确实不好编译,有点坑。。。我编译的时候在网上也百度了下,大部分的文章都是几年前的,大部分在linux环境下编译的,提供了一些脚本,但都不是Android平台下的,也不是基于libjpeg-turbo
最新的代码进行,最后尝试都编不过。哭唧唧,只能自己看文档了最后折腾ok。
编译步骤大概如下
下载libjpeg-turbo
源码编写脚本,结合libjpeg-turbo
目录下BUILDING.md
文件,使用Android ndk提供的cmake
自带交叉编译工具链编译跑脚本,最后生成Android下个平台的so和需要的头文件
备注,我用的是最新的ndk 21.1.6352462(最新)编译的,它已经不支持生成armeabi平台的so了,有点奇怪。
下面是完整的编译build.sh
脚本,如果你要编译需要把build.sh
放在与下载后的源码命令同级下执行sh build.sh
。
同时把脚本的CMAKE_PATH
和NDK_PATH
改为自己电脑的路径即可。
build.sh
也放到了这个压缩demo的工程里了。
#编译参考了/p/20902ca448ae?utm_source=oschina-app# lib-nameMY_LIBS_NAME=libjpeg-turboMY_SOURCE_DIR=$(pwd)/libjpeg-turboMY_BUILD_DIR=binaryCMAKE_PATH=/Users/guxiuzhong/Library/Android/sdk/cmake/3.10.2.4988404export PATH=${CMAKE_PATH}/bin:$PATHNDK_PATH=/Users/guxiuzhong/Library/Android/sdk/ndk/21.1.6352462BUILD_PLATFORM=linux-x86_64TOOLCHAIN_VERSION=4.9ANDROID_VERSION=24ANDROID_ARMV5_CFLAGS="-march=armv5te"ANDROID_ARMV7_CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon" # -mfpu=vfpv3-d16 -fexceptions -frttiANDROID_ARMV8_CFLAGS="-march=armv8-a " # -mfloat-abi=softfp -mfpu=neon -fexceptions -frttiANDROID_X86_CFLAGS="-march=i386 -mtune=intel -mssse3 -mfpmath=sse -m32"ANDROID_X86_64_CFLAGS="-march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel"# params($1:arch,$2:arch_abi,$3:host,$4:compiler,$5:cflags,$6:processor)build_bin() {echo "-------------------start build $1-------------------------"ANDROID_ARCH_ABI=$1 # armeabi armeabi-v7a x86 mipsCFALGS="$2"PREFIX=$(pwd)/dist/${MY_LIBS_NAME}/${ANDROID_ARCH_ABI}/# build 中间件BUILD_DIR=./${MY_BUILD_DIR}/${MY_LIBS_NAME}/${ANDROID_ARCH_ABI}echo "path==>$PATH"echo "build_dir==>$BUILD_DIR"echo "ANDROID_ARCH_ABI==>$ANDROID_ARCH_ABI"echo "CFALGS==>$CFALGS"mkdir -p ${BUILD_DIR}cd ${BUILD_DIR}# -DCMAKE_MAKE_PROGRAM=${NDK_PATH}/prebuilt/${BUILD_PLATFORM}/bin/make \# -DCMAKE_ASM_COMPILER=${NDK_PATH}/prebuilt/${BUILD_PLATFORM}/bin/yasm \cmake -G"Unix Makefiles" \-DANDROID_ABI=${ANDROID_ARCH_ABI} \-DANDROID_PLATFORM=android-${ANDROID_VERSION} \-DCMAKE_BUILD_TYPE=Release \-DANDROID_NDK=${NDK_PATH} \-DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \-DCMAKE_POSITION_INDEPENDENT_CODE=1 \-DCMAKE_INSTALL_PREFIX=${PREFIX} \-DANDROID_ARM_NEON=TRUE \-DANDROID_TOOLCHAIN=clang \-DANDROID_STL=c++_static \-DCMAKE_C_FLAGS="${CFALGS} -Os -Wall -pipe -fPIC" \-DCMAKE_CXX_FLAGS="${CFALGS} -Os -Wall -pipe -fPIC" \-DANDROID_CPP_FEATURES=rtti exceptions \-DWITH_JPEG8=1 \${MY_SOURCE_DIR}make cleanmakemake installcd ../../../echo "-------------------$1 build end-------------------------"}# build armeabibuild_bin armeabi "$ANDROID_ARMV5_CFLAGS"#build armeabi-v7abuild_bin armeabi-v7a "$ANDROID_ARMV7_CFLAGS"#build arm64-v8abuild_bin arm64-v8a "$ANDROID_ARMV8_CFLAGS"#build x86build_bin x86 "$ANDROID_X86_CFLAGS"#build x86_64build_bin x86_64 "$ANDROID_X86_64_CFLAGS"
编译结构&编译成功后会生成Android下个平台的so和需要的头文件 如下:
压缩
编译后,把生成的so和头文件拷贝到Android工程中,同时修改CMakeLists.txt
文件,指定头文件,查找so的路径,以及该jni工程生成的so需要链接的so:libjpeg.so
和libturbojpeg.so
怎么压缩呢? 其实很简单的,整体分如下几步。
获取图片Bitmap的像素。取出每个像素的argb通道,alpha通道丢弃,把bitmap的rgb像素转为一维数组进行保存(格式是R,G,B,R,G,B,R,G,B,…)。用libjpeg进行压缩。释放资源
获取Bitmap的像素
这个在上一篇文章有介绍,这里就不多介绍了。
Android-JNI开发系列《九》实战-Bitmap处理实现底片灰度化黑白化暖冷色调等效果
获取每个像素取出ARGB通道
只要拿到了这个就可以对图片进行任何处理了(包含上篇文章对图片的特效处理)。libjpeg-turbo
这个开源库也不例外。
因为我们把它压缩为jepg格式的图片,alpha通道是可以丢弃的。有一个特别注意的点就是:在jni层中,Bitmap像素点的值是ABGR,而不是ARGB,也就是说,高端到低端:A,B,G,R。这个我们在上一篇文章也验证过。 因为libjpeg-turbo
压缩的时候需要的格式是R,G,B,R,G,B,R,G,B,...
也就是一维数组。 也就是把图片的二位像素转为一维数组,很简单,赋值然后指针++处理就行了。部分代码:
int i = 0, j = 0;BYTE r, g, b;//存储RGB所有像素点BYTE *data = (BYTE *) malloc(w * h * 3);// 临时保存指向像素内存的首地址BYTE *tempData = data;uint32_t color;for (i = 0; i < h; i++) {for (j = 0; j < w; j++) {// 取出一个像素 去调了alpha,然后保存到data中,对应指针++color = *((uint32_t *) pixelsColor);// 在jni层中,Bitmap像素点的值是ABGR,而不是ARGB,也就是说,高端到低端:A,B,G,Rb = ((color & 0x00FF0000) >> 16);g = ((color & 0x0000FF00) >> 8);r = ((color & 0x000000FF));// jpeg压缩需要的是rgb// for example, R,G,B,R,G,B,R,G,B,... for 24-bit RGB color.*data = r;*(data + 1) = g;*(data + 2) = b;data += 3;pixelsColor += 4;}}
简单吧, BYTE就类似java的byte,typedef uint8_t BYTE;
别名,无符号8位。
用libjpeg进行压缩
这一步最关键,我们拿到了图片bitmap的原始的像素就可以做处理。怎么使用libjpeg-turbo
这个开源库提供的压缩方法呢,其实你下载后在源码的目录下有一个example.txt
文件,这里有很清晰的使用方法,还有详细的注释。
大概分为7步。
初始化压缩对象。jpeg_create_compress
设置压缩后的数据的输出形式jpeg_stdio_dest
,比如输出到文件设置压缩的参数jpeg_set_defaults
。 这里最重要,也就是我们需要把optimize_coding
设置为true。 因为默认是false。开始压缩jpeg_start_compress
按行循环写入。jpeg_write_scanlines
结束压缩。jpeg_finish_compress
释放压缩对象。jpeg_destroy_compress
代码中有比较详细的注释。按照它提供的example.txt
文件中的示例写就行,压缩方法如下:
int write_JPEG_file(BYTE *data, int w, int h, int quality,const char *outFilename, jboolean optimize) {//jpeg的结构体,保存的比如宽、高、位深、图片格式等信息struct jpeg_compress_struct cinfo;/* Step 1: allocate and initialize JPEG compression object *//* We set up the normal JPEG error routines, then override error_exit. */struct my_error_mgr jem;cinfo.err = jpeg_std_error(&jem.pub);jem.pub.error_exit = my_error_exit;/* Establish the setjmp return context for my_error_exit to use. */if (setjmp(jem.setjmp_buffer)) {/* If we get here, the JPEG code has signaled an error.and return.*/return -1;}jpeg_create_compress(&cinfo);/* Step 2: specify data destination (eg, a file) */FILE *outfile = fopen(outFilename, "wb");if (outfile == nullptr) {LOGE("can't open %s", outFilename);return -1;}jpeg_stdio_dest(&cinfo, outfile);/* Step 3: set parameters for compression */cinfo.image_width = w;/* image width and height, in pixels */cinfo.image_height = h;cinfo.input_components = 3; /* # of color components per pixel */cinfo.in_color_space = JCS_RGB; /* colorspace of input image */cinfo.optimize_coding = optimize;//哈夫曼编码和算术编码,TRUE=arithmetic coding, FALSE=Huffmanif (optimize) {cinfo.arith_code = false;} else {cinfo.arith_code = true;}// 其它参数 全部设置默认参数jpeg_set_defaults(&cinfo);//设置质量jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);/* Step 4: Start compressor */jpeg_start_compress(&cinfo, TRUE);/* Step 5: while (scan lines remain to be written) *//* jpeg_write_scanlines(...); */JSAMPROW row_pointer[1];int row_stride;//一行的RGB数量row_stride = cinfo.image_width * 3; /* JSAMPLEs per row in image_buffer *///一行一行遍历while (cinfo.next_scanline < cinfo.image_height) {//得到一行的首地址row_pointer[0] = &data[cinfo.next_scanline * row_stride];//此方法会将jcs.next_scanline加1jpeg_write_scanlines(&cinfo, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数}/* Step 6: Finish compression */jpeg_finish_compress(&cinfo);/* After finish_compress, we can close the output file. */fclose(outfile);outfile = nullptr;/* Step 7: release JPEG compression object *//* This is an important step since it will release a good deal of memory. */jpeg_destroy_compress(&cinfo);/* And we're done! */return 0;}
我这里是直接同步压缩的,压缩是个耗时的操作(上面的效果图测试大概360毫秒左右),你可以在jni层中开启线程,然后压缩成功/失败通过回调到java层中,当然也可以在java层开启线程,都差不多。这里demo就直接int返回了,成功0,失败-1.
压缩失败的处理,在压缩的步骤1中进行设置,在jpeg_compress_struct
的err
字段,err
字段是一个jpeg_error_mgr
的结构体,该结构体描述压缩失败的信息,比如错误信息,错误码,有几个函数指针,比如error_exit
,emit_message
,output_message
等。如果赋值的话当压缩失败的时候会回调你的方法。
释放资源
最后记得文件该close
的fclose
,内存该free
的free
即可,jni中的也该释放的释放,jpeg的用完调用jpeg_destroy_compress
。避免内存泄漏问题。
最后上源码:
demo是读取sd卡的图片压缩后写到了sd卡里,记得添加读写sd卡的权限。
/ta893115871/JNIBitmapCompress
备注
本文也是为实践jni,学习jpeg压缩。其中编译libjpeg参考了网上的libjpeg-turbo的编译,有一篇不错。
/p/20902ca448ae?utm_source=oschina-app