前面说过了哈夫曼的原理实际上就是根据加权来重新对可能出现的RGB范围进行定义,将定长的二进制数据编程变长的二进制数据,今天主要介绍哈夫曼算法的实现。直接上代码哈:
下载libjpeg引擎使用的库—基于该引擎来做一定的开发,自己实现编码。
3.1 添加权限
为了让保存好的图片存放到SD卡,这里要提供对SD卡使用权限支持。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
3.2 添加lib依赖
在app下创建jni目录.
添加jpeg文件夹和libjpegbither.so文件。其中jpeg文件夹存放libjpeg引擎主要的代码类。
创建具体的cpp代码实现文件seemygojpgjni.cpp
创建自动化编译脚本makefile文件 Android.mk
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE :=jpegbither LOCAL_SRC_FILES :=libjpegbither.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE :=seemygojpgjni LOCAL_SRC_FILES :=seemygojpgjni.cpp LOCAL_SHARED_LIBRARIES :=jpegbither LOCAL_LDLIBS := -ljnigraphics -llog include $(BUILD_SHARED_LIBRARY)
Application.mk指定生成 arm 平台的二进制处理库。
APP_ABI := armeabi-v7a armeabi #表示 编译目标 ABI(应用二进制接口)
指定生成后的文件存放位置:
android { ... sourceSets { main { jniLibs.srcDirs = ['libs'] } } }
3.3 添加Java代码实现
public class MainActivity extends AppCompatActivity {
public static final int REQUEST_PICK_IMAGE = 10011;
public static final int REQUEST_KITKAT_PICK_IMAGE = 10012;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void pickFromGallery(View v) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*"),
REQUEST_PICK_IMAGE);
} else {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_KITKAT_PICK_IMAGE);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case REQUEST_PICK_IMAGE:
if (data != null) {
Uri uri = data.getData();
compressImage(uri);
} else {
Log.e("520it", "========图片为空======");
}
break;
case REQUEST_KITKAT_PICK_IMAGE:
if (data != null) {
Uri uri = ensureUriPermission(this, data);
compressImage(uri);
} else {
Log.e("520it", "====-----==图片为空======");
}
break;
}
}
}
@SuppressWarnings("ResourceType")
@TargetApi(Build.VERSION_CODES.KITKAT)
public static Uri ensureUriPermission(Context context, Intent intent) {
Uri uri = intent.getData();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
final int takeFlags = intent.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION;
context.getContentResolver().takePersistableUriPermission(uri, takeFlags);
}
return uri;
}
public void compressImage(final Uri uri) {
//Log.e("520it", "====开始====uri==" + uri.getPath());
new Thread(){
@Override
public void run() {
try {
Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
String saveDirPath= Environment.getExternalStorageDirectory()+File.separator+"hfm_img";
File saveDir=new File(saveDirPath);
if (!saveDir.exists())
saveDir.mkdirs();
File saveFile = new File(saveDir, "终极压缩.jpg");
NativeUtil.compressBmpByCpp(bitmap, saveFile.getAbsolutePath());
Log.i("520it","path:"+saveFile.getAbsolutePath());
File saveFile1 = new File(saveDir, "质量压缩.jpg");
NativeUtil.saveBmpByQualityZip(bitmap,saveFile1);
File saveFile2 = new File(saveDir, "尺寸压缩.jpg");
NativeUtil.saveBmpByZipSize(bitmap,saveFile2);
File saveFile3 = new File(saveDir, "采样率压缩.jpg");
NativeUtil.saveBmpBySamplingRate(bitmap,saveFile3);
File saveFile4 = new File(saveDir, "原图.jpg");
NativeUtil.saveBmp(bitmap,saveFile4);
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
public void deleteCacheFile(View v){
String saveDirPath= Environment.getExternalStorageDirectory()+File.separator+"hfm_img";
File saveDir=new File(saveDirPath);
if (saveDir.exists()){
try {
File[] files = saveDir.listFiles();
for (File file:files){
file.delete();
}
}catch (Exception e){
Log.e("520it", e.getLocalizedMessage());
}
}
}
}
对本地JNI代码实现,封装NativeUtil来处理:
public class NativeUtil {
private static int DEFAULT_QUALITY = 95;
/**
* @param bit bitmap对象
* @param fileName 指定保存目录名
* @Description: JNI基本压缩
*/
public static void compressBmpByCpp(Bitmap bit, String fileName) {
compressBitmap(bit, bit.getWidth(), bit.getHeight(), 100, fileName.getBytes(), true);
}
/**
* 调用底层 bitherlibjni.c中的方法
* @param bit 需要处理的图片
* @param w
* @param h
* @param quality 保存的质量
* @param fileNameBytes 文件保存路径
* @param optimize 是否要优化
* @return
* @Description:函数描述
*/
public static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,
boolean optimize);
/**
* 计算缩放比
* @param bitWidth 当前图片宽度
* @param bitHeight 当前图片高度
* @return
* @Description:函数描述
*/
public static int getRatioSize(int bitWidth, int bitHeight) {
// 图片最大分辨率
int imageHeight = 1920;
int imageWidth = 1080;
// 缩放比
int ratio = 1;
// 缩放比,由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
if (bitWidth > bitHeight && bitWidth > imageWidth) {
// 如果图片宽度比高度大,以宽度为基准
ratio = bitWidth / imageHeight;
} else if (bitWidth < bitHeight && bitHeight > imageHeight) {
// 如果图片高度比宽度大,以高度为基准
ratio = bitHeight / imageHeight;
}
// 最小比率为1
if (ratio <= 0)
ratio = 1;
return ratio;
}
/**
* 加载lib下两个so文件
*/
static {
System.loadLibrary("jpegbither");
System.loadLibrary("seemygojpgjni");
}
/**
* 1. 质量压缩
* 设置bitmap options属性,降低图片的质量,像素不会减少
* 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置
* 设置options 属性0-100,来实现压缩
*
* @param bmp
* @param file
*/
public static void saveBmpByQualityZip(Bitmap bmp, File file) {
// 0-100 100为不压缩
int options = 20;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
bmp.compress(Bitmap.CompressFormat.JPEG, options, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 2. 尺寸压缩
* 通过缩放图片像素来减少图片占用内存大小
* @param bmp
* @param file
*/
public static void saveBmpByZipSize(Bitmap bmp, File file) {
// 尺寸压缩倍数,值越大,图片尺寸越小
int ratio = 8;
// 压缩Bitmap到对应尺寸
Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888);
Canvas canvas = new Canvas(result);
Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
canvas.drawBitmap(bmp, null, rect, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 3.设置图片的采样率,降低图片像素
*/
public static void saveBmpBySamplingRate(Bitmap bitmap, File file) {
// 数值越高,图片像素越低
int inSampleSize = 7;
BitmapFactory.Options options = new BitmapFactory.Options();
//采样率
options.inSampleSize = inSampleSize;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
try {
if (file.exists()) {
file.delete();
} else {
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 4.保存原图片
* @param bitmap
* @param file
*/
public static void saveBmp(Bitmap bitmap, File file) {
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
3.4 对C++层代码的实现准备
上节代码中,对哈夫曼底层的实现提供了本地实现,代码如下:
public static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,boolean optimize);
因为这里的使用的是C++代码的实现,所以要先生成头文件:
打开dos命令行,也可以是as自带的Terminal模块
跳转到包名的最顶层,一般我们是到 项目/app/src/main/java文件夹
调用javah -jni 包名.类名
javah -classpath /Users/lean/Library/Android/sdk/platforms/android-26/android.jar:. com.a520it.hfmdemo.NativeUtil
将文件名改为seemygojpgjni.h
为seemygojpgjni.cpp 添加导包
#include "seemygojpgjni.h" #include <jni.h> //统一编译方式 extern "C" { #include "jpeg/jpeglib.h" #include "jpeg/cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ #include "jpeg/jversion.h" /* for version message */ #include "jpeg/android/config.h" }
创建cpp层哈夫曼算法的实现方法:
JNIEXPORT jstring JNICALL Java_com_a520it_hfmdemo_NativeUtil_compressBitmap (JNIEnv *env, jclass thiz, jobject bitmap, jint w, jint h, jint quality, jbyteArray fileNameBytes, jboolean optimize) { }
如果你对C++不是很熟悉,需要查看源码,但是你会发现Ctrl+单击无效。这里需要添加该解决方案的配置:
找到gradle.properties 添加如下代码并重新编译:
android.useDeprecatedNdk=true
3.5 C++层哈夫曼算法实现
导入依赖文件:
#include <string.h> #include <android/bitmap.h> #include <android/log.h> #include <stdio.h> #include <setjmp.h> #include <math.h> #include <stdint.h> #include <time.h>
实现日志打印:
#define LOG_TAG "a520it" #define LOGW(...) __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
具体哈夫曼实现代码:
#define true 1 #define false 0 typedef uint8_t BYTE; char *error; struct my_error_mgr { struct jpeg_error_mgr pub; jmp_buf setjmp_buffer; }; typedef struct my_error_mgr *my_error_ptr; METHODDEF(void) my_error_exit(j_common_ptr cinfo) { my_error_ptr myerr = (my_error_ptr) cinfo->err; (*cinfo->err->output_message)(cinfo); error = (char *) myerr->pub.jpeg_message_table[myerr->pub.msg_code]; LOGE("jpeg_message_table[%d]:%s", myerr->pub.msg_code, myerr->pub.jpeg_message_table[myerr->pub.msg_code]); // LOGE("addon_message_table:%s", myerr->pub.addon_message_table); // LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]); // LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]); longjmp(myerr->setjmp_buffer, 1); } /** * byte数组转C的字符串 */ char *jstrinToCstring(JNIEnv *env, jbyteArray barr) { char *rtn = NULL; jsize alen = env->GetArrayLength(barr); jbyte *ba = env->GetByteArrayElements(barr, 0); if (alen > 0) { rtn = (char *) malloc(alen + 1); memcpy(rtn, ba, alen); rtn[alen] = 0; } env->ReleaseByteArrayElements(barr, ba, 0); return rtn; } int generateJPEG(BYTE *data, int w, int h, int quality, const char *outfilename, jboolean optimize) { //jpeg的结构体,保存的比如宽、高、位深、图片格式等信息,相当于java的类 struct jpeg_compress_struct jcs; //当读完整个文件的时候就会回调my_error_exit这个退出方法。setjmp是一个系统级函数,是一个回调。 struct my_error_mgr jem; jcs.err = jpeg_std_error(&jem.pub); jem.pub.error_exit = my_error_exit; if (setjmp(jem.setjmp_buffer)) { return 0; } //初始化jsc结构体 jpeg_create_compress(&jcs); //打开输出文件 wb:可写byte FILE *f = fopen(outfilename, "wb"); if (f == NULL) { return 0; } //设置结构体的文件路径 jpeg_stdio_dest(&jcs, f); jcs.image_width = w;//设置宽高 jcs.image_height = h; // if (optimize) { // LOGI("optimize==ture"); // } else { // LOGI("optimize==false"); // } //看源码注释,设置哈夫曼编码:/* TRUE=arithmetic coding自适应压缩算法, FALSE=Huffman */ jcs.arith_code = false; int nComponent = 3; /* 颜色的组成 rgb,三个 # of color components in input image */ jcs.input_components = nComponent; //设置结构体的颜色空间为rgb jcs.in_color_space = JCS_RGB; // if (nComponent == 1) // jcs.in_color_space = JCS_GRAYSCALE; // else // jcs.in_color_space = JCS_RGB; //全部设置默认参数/* Default parameter setup for compression */ jpeg_set_defaults(&jcs); //是否采用哈弗曼表数据计算 品质相差5-10倍 jcs.optimize_coding = optimize; //设置质量 jpeg_set_quality(&jcs, quality, true); //开始压缩,(是否写入全部像素) jpeg_start_compress(&jcs, TRUE); JSAMPROW row_pointer[1]; int row_stride; //一行的rgb数量 row_stride = jcs.image_width * nComponent; //一行一行遍历 while (jcs.next_scanline < jcs.image_height) { //得到一行的首地址 row_pointer[0] = &data[jcs.next_scanline * row_stride]; //此方法会将jcs.next_scanline加1 jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数 } jpeg_finish_compress(&jcs);//结束 jpeg_destroy_compress(&jcs);//销毁 回收内存 fclose(f);//关闭文件 return 1; } JNIEXPORT jstring JNICALL Java_com_a520it_hfmdemo_NativeUtil_compressBitmap (JNIEnv *env, jclass thiz, jobject bitmap, jint w, jint h, jint quality, jbyteArray fileNameBytes, jboolean optimize) { //1.将安卓中的bitmap解码,并转化成RGB. //2.为JPEG对象分配内存空间并初始化。 //3.指定压缩数据源 //4.获取文件信息 //5.为压缩设置参数,比如图像的大小 颜色 类型空间 //6.开始压缩 jpeg_start_compress() //7.结束压缩 jpeg_finish_compress() //8.释放资源 //用来保存图片的像素值的二维码数组 BYTE *pixelscolor; //锁定画布 AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelscolor); //解析每一个像素的RGB值到一维数组 BYTE *data = (BYTE *) malloc(w * h * 3); BYTE *tempData = data; BYTE r, g, b; int singlePixelColor; //遍历二维数组每一个像素值的rgb for (int j = 0; j < h; ++j) { for (int i = 0; i < w; ++i) { singlePixelColor = *((int *) pixelscolor); //计算出红绿蓝值 r = (singlePixelColor & 0xFF0000) >> 16; g = (singlePixelColor & 0xFF0000) >> 8; r = singlePixelColor & 0xFF0000; //将取出来的RGB值传递给以为数组 *data = b; *(data + 1) = g; *(data + 2) = r; //一维数组移动三个地址 data += 3; //二维数组移动四个数组 pixelscolor += 4; } } //解锁画布 AndroidBitmap_unlockPixels(env, bitmap); //对一维数组进行哈夫曼算法编码 char *outfileName = jstrinToCstring(env, fileNameBytes); int result = generateJPEG(data, w, h, quality, outfileName, optimize); const char *jresultStr = result == 0 ? "failure" : "success"; return env->NewStringUTF(jresultStr); }
3.5 编译实现
命令行进入jni文件夹:
执行效果如下图:
项目链接: https://pan.baidu.com/s/1Yjn74-H1HgReBTlrx5qGCA 密码: m78n