鲁班压缩的压缩质量和压缩大小比原生搞出一个数量级,微信也是使用该技术进行图片压缩。
原生压缩: Bitmap.compress():
主要的处理流程是在native层处理的,bitmap.cpp中的Bitmap_compress方法中通过SkBitmap处理(Sk开头的表示是skia里的相关类,skia是android里的图像渲染引擎),因此bitmap真正的加载图像是在native堆。
Java层 --> Bitmap对象-->skia引擎(native)
skia是基于jpeg的二次封装,而skia不支持哈夫曼压缩(鲁班压缩),jpeg支持
哈夫曼算法:
使用变长编码对元数据(不可再分,例如颜色中的argb)进行编码,其中变长编码是通过一种评估来源符号出现几率的方法得到的,出现几率较高的数据使用较短的编码,反之出现频率低的则使用较长点编码,使得编码之后的数据平均长度大大降低。
如何在android中使用哈夫曼算法压缩案图片:
因为skia是基于jpeg的二次封装,因此使用jni调用android系统源码里jpeg的相关方法来实现
1、先将java层传入点bitmap用像素数组表示,pixels即为获取点数组,里面的每个元素为所表示的颜色,因为是byte,所以4个元素表示一个颜色(颜色是int)
//图片解析成一组像素组 w*h
AndroidBitmapInfo bitmapInfo;
AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
BYTE *pixels;
AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
2、将获得到点数组中点rgb提取出来,存放到一个长度为wh3的字节数组中(如果需要透明度的话就需要乘4)
int h = bitmapInfo.height;
int w = bitmapInfo.width;
BYTE *data,*tmpData;
data= (BYTE *) malloc(w * h * 3);
tmpData = data;
3、通过与运算提取出rgb对应点字节,如提取R对应点字节时,将像素数组与ox00FF0000进行与运算提取,还需将gb的字节排除,一个字节8位,所以需要右移16位。而且使用jpeg时,数组里的排列顺序需要是bgr。
BYTE r, g, b;
int color;
// 遍历每一个像素点 将rgb 提取出来
// jpeg opencv 里面颜色存储的顺序是 b g r
// 数组 w*h*3 【b g r b g r】
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;
}
}
4、引入jpeg的相关头文件之后,创建jpeg相关的bitmap
struct jpeg_compress_struct jpeg_struct;
// 设置错误处理信息
jpeg_error_mgr err;
jpeg_struct.err = jpeg_std_error(&err);
// 给结构体分配内存
jpeg_create_compress(&jpeg_struct);
FILE *file = fopen(path, "wb");
// 设置输出路径
jpeg_stdio_dest(&jpeg_struct, file);
5、初始化相关点属性,其中arith_code这个属性级表示是否需要开启哈夫曼编码,false表示开启
jpeg_struct.image_width = w;
jpeg_struct.image_height = h;
//采取哈夫曼编码 在skia 源码中 jpeg_struct.arith_code=TRUE
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的初始化
6.、开始压缩并写入文件,row_pointer表示每一行的第一个元素
//开始压缩
jpeg_start_compress(&jpeg_struct, TRUE);
JSAMPROW row_pointer[1];
// 一行的rgb int row_stride = w * 3;
while (jpeg_struct.next_scanline < h) {
row_pointer[0] = &data[jpeg_struct.next_scanline * w * 3];
// 这个函数一调用 jpeg_struct.next_scanline ++
jpeg_write_scanlines(&jpeg_struct, row_pointer, 1);
}
jpeg_finish_compress(&jpeg_struct);
jpeg_destroy_compress(&jpeg_struct);
fclose(file);
完整代码:
#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <android/log.h>
#include <malloc.h>
extern "C"
{
#include "jpeglib.h"
}
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOG_TAG "david"
#define true 1
typedef uint8_t BYTE;
void writeImg(BYTE *data, const char *path, int w, int h) {
struct jpeg_compress_struct jpeg_struct;
// 设置错误处理信息
jpeg_error_mgr err;
jpeg_struct.err = jpeg_std_error(&err);
// 给结构体分配内存
jpeg_create_compress(&jpeg_struct);
FILE *file = fopen(path, "wb");
// 设置输出路径
jpeg_stdio_dest(&jpeg_struct, file);
jpeg_struct.image_width = w;
jpeg_struct.image_height = h;
//采取哈夫曼编码 在skia 源码中 jpeg_struct.arith_code=TRUE
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的初始化
jpeg_start_compress(&jpeg_struct, TRUE);
JSAMPROW row_pointer[1];
// 一行的rgb
int row_stride = w * 3;
while (jpeg_struct.next_scanline < h) {
row_pointer[0] = &data[jpeg_struct.next_scanline * w * 3];
// 这个函数一调用 jpeg_struct.next_scanline ++
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_example_hafumanimage_MainActivity_compress(JNIEnv *env, jobject instance,
jobject bitmap, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
AndroidBitmapInfo bitmapInfo;
AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
BYTE *pixels;
AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
int h = bitmapInfo.height;
int w = bitmapInfo.width;
//
BYTE *data,*tmpData;
data= (BYTE *) malloc(w * h * 3);
tmpData = data;
BYTE r, g, b;
int color;
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, bitmap);
writeImg(tmpData, path, w, h);
env->ReleaseStringUTFChars(path_, path);
}
谷歌不采用哈夫曼的原因:
使用定长编码可以直接一个像素一个像素加载,而使用变长编码,则需要先遍历,耗时耗内存,以前手机内存不大,所以没有采用