大家都知道,使用哈夫曼压缩能达到无损压缩,也就是说。保证了原图质量的同时,能够降低图片的大小。这是什么原理呢?首先我们需要了解的是Android系统加载图片使用的是Skia加载库,当然这个库的底层还是是用的jpeg对图片进行压缩处理,但是为了能够适配低端的手机(这里的低端是指以前的硬件配置不高的手机,CPU和内存在手机上都非常吃紧 性能差),由于哈夫曼算法非常吃CPU,被迫用了其他的算法。所以Skia在进行图片处理并没有去使用到哈夫曼压缩。但是解码还是保留了哈夫曼算法,这就导致了图片处理后文件变大了。
一、原理
看一张图,如下:
这里解释一下,一张图片都是有argb组成,这经过哈夫曼压缩之前,可以将图片的a通道拿掉,剩下的 rgb 进行压缩。每一个通道的取值范围都是0~255,也就是说每种颜色都是有256种表现形态,这里以红色 r 为例。假如一张图片红色 r 共有6个等级,每个等级对应的像素点数如上图。那么根据哈夫曼算法会形成一个哈夫曼树。如下图:
哈夫曼树形成的规则,首先从6个等级中找出两个像素点最少的两个(1和10),放在末端,然后将两个像素点相加放在左端,然后再找出原像素点中较少的一个(100),放在右端,并用线将节点和分支连接起来。依次类推,直到找到顶点。其中左边的分支用0标记,右边的分支用1标记。
1个rgb对应3个字节,每一个字节都会形成一个像素表,像素表中存储每个像素等级对应的像素点数。比如根据哈夫曼树,我们如何找到红色的等级1的像素,我们从顶点出发,只需要在表中查找1,就可以找到5000,那么如何找到1000呢?查找01!如何查找500呢?查找001!如何查找100呢?0001?…等等。就是根据分支一直找下去。
优点:对于原始一个像素点占8位,对于5000个像素点,所有那个内存为8*5000 ,当使用哈夫曼压缩后,只要一位(比如1)就可以表示这5000个像素点。并且没有减少图片的像素点数,所以叫做无损压缩。另外,对于颜色值越单一的图片,压缩率越高。颜色值越多,树会越来越长,找到末端的像素点占用的位数会越来越多。所以它的压缩效率一般在20%~90%。
二、实现
1、导库
这里我们需要libjpeg在android环境下的so库,所以需要事先在linux环境下编译好。将编译好的so库放到项目中,如下:
其中x86支持的模拟器,arm64真机。导入需要支持的头文件在libjpeg目录下:
2、链接库
需要在CMakeList.txt文件中配置,如下;
红色框框的内容,就是需要添加的。
3、调用
/**
* @param bitmap 要压缩的图片
* @param path 压缩后存放的文件名称
* @param quality 压缩质量
*/
public native void compressImage(Bitmap bitmap, String path, int quality);
public void compress(View view) {
//sd卡目录下的一张图片
File input = new File(Environment.getExternalStorageDirectory(), "timg750.png");
inputBitmap = BitmapFactory.decodeFile(input.getAbsolutePath());
//开始压缩
compressImage(inputBitmap,Environment.getExternalStorageDirectory() + "/timg751.png",50);
}
我在SD卡目录下有一张timg750.png的图片,压缩后调用jni的compressImage()方法,并且压缩后的文件存为SD卡的timg751.png,压缩质量为50。
4、压缩代码
(1)得到原始图片信息,将原始图片的argb的a通道信息移除,并存入到新的图片信息data(包含r、g、b)中。
extern "C"
JNIEXPORT void JNICALL
Java_com_xinyartech_myhuffman_MainActivity_compressImage(JNIEnv *env, jobject instance,
jobject bitmap, jstring path_,
jint q) {
const char *path = env->GetStringUTFChars(path_, 0);
//从bitmap获取argb数据
AndroidBitmapInfo info;//info=new 对象();
//获取里面的信息
AndroidBitmap_getInfo(env, bitmap, &info);// void method(list)
//得到图片中的像素信息
uint8_t *pixels;//uint8_t char java byte *pixels可以当byte[]
AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
//jpeg argb中去掉他的a ===>rgb
int w = info.width;
int h = info.height;
int color;
//开一块内存用来存入rgb信息
uint8_t* data = (uint8_t *) malloc(w * h * 3);//data中可以存放图片的所有内容
uint8_t* temp = data;
uint8_t r, g, b;//byte
//循环取图片的每一个像素
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
color = *(int *) pixels;//0-3字节 color4 个字节 一个点
//取出rgb
r = (color >> 16) & 0xFF;// #00rrggbb 16 0000rr 8 00rrgg
g = (color >> 8) & 0xFF;
b = color & 0xFF;
//存放,以前的主流格式jpeg bgr
*data = b;
*(data + 1) = g;
*(data + 2) = r;
data += 3;
//指针跳过4个字节
pixels += 4;
}
}
//把得到的新的图片的信息存入一个新文件 中
write_JPEG_file(temp, w, h, q, path);
//释放内存
free(temp);
AndroidBitmap_unlockPixels(env, bitmap);
env->ReleaseStringUTFChars(path_, path);
}
(2)将新的图片信息存入到新的文件中(就是 timg751.png )
void write_JPEG_file(uint8_t *data, int w, int h, jint q, const char *path) {
// 3.1、创建jpeg压缩对象
jpeg_compress_struct jcs;
//错误回调
jpeg_error_mgr error;
jcs.err = jpeg_std_error(&error);
//创建压缩对象
jpeg_create_compress(&jcs);
// 3.2、指定存储文件 write binary
FILE *f = fopen(path, "wb");
jpeg_stdio_dest(&jcs, f);
// 3.3、设置压缩参数
jcs.image_width = w;
jcs.image_height = h;
//bgr
jcs.input_components = 3;
jcs.in_color_space = JCS_RGB;
jpeg_set_defaults(&jcs);
//开启哈夫曼功能
jcs.optimize_coding = true;
//设置压缩质量
jpeg_set_quality(&jcs, q, 1);
// 3.4、开始压缩
jpeg_start_compress(&jcs, 1);
// 3.5、循环写入每一行数据
int row_stride = w * 3;//一行的字节数
JSAMPROW row[1];
while (jcs.next_scanline < jcs.image_height) {
//取一行数据
uint8_t *pixels = data + jcs.next_scanline * row_stride;
row[0]=pixels;
jpeg_write_scanlines(&jcs,row,1);
}
// 3.6、压缩完成
jpeg_finish_compress(&jcs);
// 3.7、释放jpeg对象
fclose(f);
jpeg_destroy_compress(&jcs);
}
这里面的关键代码为 jcs.optimize_coding = true;将开启哈夫曼压缩。
5、压缩结果
压缩前的大小为521kb,压缩后38kb。但是图片完全看不出差异。
三、注意
- 动态库的环境配置地址一定要正确
- app要配置好文件的读写权限
- 我的环境是根据 AS 3.5构建。