用ndk实现android的图片压缩
最近在看了动脑学院的图片优化之后,感觉通过ndk来实现的压缩,压缩比例还是很好的
我们知道android的图片显示离不开bitmap,而在android开发中避免不了对图片的压缩处理,当然bitmap有自带的压缩方法,调用bitmap的compress方法就可以实现简单的压缩,但是这种压缩出来的图片从清晰度上来说,效果不是很好,原因在于这种压缩的底层处理是由google的skia图片处理引擎来做的,而skia库又是基于jpeg库,我们都知道libjpeg是一个被广泛使用的JPEG压缩/解压缩函数库,应用程序可以每次从JPEG压缩图像中读取一个或多个扫描线(scanline,所谓扫描线,是指由一行像素点构成的一条图像线条),而诸如颜色空间转换、降采样/增采样、颜色量化之类的工作则都由libjpeg去完成的;但是skia在开发的时候阉割来jpeg的一部分功能,使得android原有的压缩方法不尽人意,下面通过代码来实现一个脱离android 底层的skia库,基于ndk libjpeg压缩的图片压缩方案。
导入相关的依赖库
由于要使用到libjpeg相关的函数,所以要导入相关的头文件与so文件, 由于libjpeg也是在android源码里面的,找到android源码中的/external/libjpeg-turbo/jpeglib.h头文件复制到android studio 的cpp目录下面,当然也可以在cpp下面新建一个include目录将头文件放进去也行,然后将jpeglib.h里面依赖的其他头文件都引入进来,接着就是进入libjpeg.so文件,我这里是将so放在来app目录下的libs目录下面
当然这个目录不是必须的,放在其他目录也行;然后用cmake将头文件与目录的路径引入就行了;
cmake的配置
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
include_directories( src/main/cpp/ )
add_library(jpeg-and SHARED IMPORTED)
set_target_properties(jpeg-and
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libjpeg.so)
find_library(
log-lib
log )
#添加jnigraphics,c++代码中使用到了AndroidBitmap,所以需要添加jnigrapics库
find_library(
andbitmap
jnigraphics )
add_library(
native-lib
SHARED
src/main/cpp/native-lib.cpp )
target_link_libraries( # Specifies the target library.
native-lib
${andbitmap}
jpeg-and
${log-lib} )
实现图片压缩
这里我们在java层声明一个native方法
public native void nativeCompress(Bitmap inputBitmap, String absolutePath);
该方法传入原图的bitmap与输出压缩文件的路径,然后在jni层实现压缩代码
#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <android/log.h>
#include <malloc.h>
extern "C" {
#include "jpeglib.h"
}
typedef uint8_t BYTE;
void writeImg(BYTE *data, const char *path, int w, int h) {
//压缩
//-------------初始化----------------
struct jpeg_compress_struct jpeg_struct;
struct jpeg_error_mgr err;
jpeg_create_compress(&jpeg_struct);
jpeg_struct.err = jpeg_std_error(&err);
FILE *file = fopen(path, "wb");
//设置输出路径
jpeg_stdio_dest(&jpeg_struct, file);
//设置宽高
jpeg_struct.image_height = h;
jpeg_struct.image_width = w;
//重要的地方,skia阉割的地方
jpeg_struct.arith_code = FALSE;
jpeg_struct.optimize_coding = true;
jpeg_struct.in_color_space = JCS_RGB;
jpeg_struct.input_components = 3;
//设置压缩质量
jpeg_set_defaults(&jpeg_struct);
jpeg_set_quality(&jpeg_struct, 20, TRUE);
//-------------开始压缩----------------
jpeg_start_compress(&jpeg_struct, TRUE);
//-------------写入数据----------------
//记录一行的首地址
JSAMPROW row_pointer[1];
//一行的rgb数量
int row_stride = jpeg_struct.image_width * 3;
while (jpeg_struct.next_scanline < jpeg_struct.image_height){
row_pointer[0] = &data[jpeg_struct.next_scanline * row_stride];
jpeg_write_scanlines(&jpeg_struct, row_pointer, 1);
}
jpeg_finish_compress(&jpeg_struct);
jpeg_destroy_compress(&jpeg_struct);
fclose(file);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_kangxin_doctor_bitmaphufdemo_MainActivity_nativeCompress(JNIEnv *env, jobject instance,
jobject inputBitmap,
jstring absolutePath_) {
const char *absolutePath = env->GetStringUTFChars(absolutePath_, 0);
//压缩所需要的原材料->bitmap 像素数组 rgb数组
BYTE *pixels;
AndroidBitmapInfo bitmapInfo;
AndroidBitmap_getInfo(env, inputBitmap, &bitmapInfo);
//将bitmap转换成pixels的数组
AndroidBitmap_lockPixels(env, inputBitmap, reinterpret_cast<void **>(&pixels));
int h = bitmapInfo.height;
int w = bitmapInfo.width;
int i = 0;
int j = 0;
int color;
BYTE *data = NULL, *tmpData = NULL;
data = static_cast<BYTE *>(malloc(w * h * 3));
tmpData = data;
BYTE r, g, b;
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
color = *((int *)pixels);
r = ((color & 0x00FF0000) >> 16);
g = ((color & 0x0000FF00) >> 8);
b = (color & 0x000000FF);
*data = b;
*(data + 1) = g;
*(data + 2) = r;
data += 3;
pixels += 4;
}
}
AndroidBitmap_unlockPixels(env, inputBitmap);
//jpeg 压缩
writeImg(tmpData, absolutePath, w, h);
env->ReleaseStringUTFChars(absolutePath_, absolutePath);
}
首先我们通过bitmap函数将bitmap转换成了rgb像素数组,然后通过循环位移计算将每个像素里面的rgb取出来,以bgr的顺序存到了data容器里面,然后调用writeImg函数进行压缩,在这个函数中,一开始就进行了压缩的一些处理话操作,在这里要注意的是 jpeg_struct.arith_code = FALSE;
这个很重要,这个字段在头文件里面是这样描述的boolean arith_code; /* TRUE=arithmetic coding, FALSE=Huffman */
由此可见这个字段应该是设置图片压缩的编码方式,TRUE的话是arithmetic coding编码,FALSE的话是Huffman 而Huffman就是著名的哈夫曼编码,Huffman编码用的最普遍的就是在压缩领域,它的压缩效果相当的好;而android 底层的skia库将这个字段设置成了TRUE 也就是说skia的压缩不是用的哈夫曼编码压缩,所以这也就是android原生压缩出来的图片不尽人意的原因所在。
下面就是我经过测试压缩出来的原图与压缩图的对比
经过对比,原图大小是397kb, 压缩之后的图片大小是58kb