Android压缩图片和libjpeg库

    • 前言
    • Fjpeg使用
  • Fjpeg
        • 注意
      • 如何使用
      • 如何压缩图片只改变在硬盘的存储大小
      • 如何改变图片分辨率让其Bitmap对象可以加载到内存中
        • 关于重载版本
    • 开始学习之旅
    • 补充知识的结论
    • 修改图片分辨率 防止在Android加载Bitmap的时候oom内存溢出
      • 解决方案1
      • 解决方案2
    • 希望压缩图片方便网络传输
      • 第一种方案利用Bitmapcompress方法压缩
      • 第二种利用libjpeg压缩
    • 在Android50测试两个 图片压缩
    • 在Android60测试两个 图片压缩
    • 解释Android50和60为什么压缩率差别
    • 定长编码
    • 哈夫曼编码
    • 编译libjpeg前言
    • 编译libjpeg步骤
    • 创建Demo项目
    • 撸代JAVA层代码
    • 撸C代码
    • 最终我们看下完整代码
    • 参考文献
    • 编译好的静态库和头文件

前言:

在android开发时我们往往有如对图片如下的需求:
1. 希望压缩图片方便网络传输
2. 修改图片分辨率 防止在Android加载Bitmap的时候oom(内存溢出)

所以我就写了一个工具类Fjpeg封装二次采样和libjpeg进行图片压缩操作。
本文就是讲述我们写Fjpeg的学习过程

Fjpeg使用

源码地址

Fjpeg

封装libjpeg对图片进行压缩,和二次采样等代码封装

  1. 我们知道在Android 6.0以下 系统对图片的编码采用’定长编码’之后才采用‘哈夫曼编码’。
    所以本框架封装libjpeg 进行对图片进行编码 ,方便网络传输等
  2. 我们知道Android加载Bitmap图片时候 如果图片分辨率过大 我们需要对图片进行分辨率裁剪 在进行显示
    所以本框架封装了二次采样代码

注意:

libjpeg压缩不能改变图片分辨率,只能改变存储在硬盘中的大小。也就是说他的目的在于网络传输。
所以如果你想加载大图请使用本框架封装的二次采样代码即可

如何使用?

在你moudle的build.gradle中的dependencies添加以下代码

compile ‘com.fmy:fjpeg:1.0.0@aar

比如:

dependencies {
    ...   
    compile 'com.fmy:fjpeg:1.0.0@aar'
    ...
}

如何压缩图片(只改变在硬盘的存储大小)

        /**
         *  第一个参数:压缩后 文件输出路径,
         *  第二个参数:需要压缩Bitmap对象
         *  第三个参数:压缩质量 1-100  1是最小的
         *  第四个参数:是否启用哈夫曼编码
         *  第五个参数:回调 包含 开始前、错误、结束
         *  第六个参数:是否使用子线程
         */
        ImageUtils.compressQC(new File(Environment.getExternalStorageDirectory(), "测试剥离框架.jpg").getAbsolutePath(), bitmap, 1, true, new NativeCallBack() {
          //开始前回调
            @Override
            public void startCompress() {
                Log.d(TAG, "startCompress() called");
            }

            //错误回调
            @Override
            public void error(int errorNum, String description) {
                Log.d(TAG, "error() called with: errorNum = [" + errorNum + "], description = [" + description + "]");
            }

            //完成结束回调  如果发生错误讲不会回调次方法
            @Override
            public void finish(String filePath) {
                Log.d(TAG, "finish() called with: filePath = [" + filePath + "]");
            }
        }, true);

如何改变图片分辨率(让其Bitmap对象可以加载到内存中)

        //从mipmap中读取。此方法ImageUtils.compressPxSampleSize存在多个重载版本
        //如:从文件 、从资源、从io流、字节数组等
        Bitmap bitmap = ImageUtils.compressPxSampleSize(getResources(), R.mipmap.timg, 1000, 1000);

关于重载版本

Android压缩图片和libjpeg库_第1张图片

开始学习之旅:

Android的Bitmap对象在加载时 内存大小为:

宽的像素*高的像素*位图格式(如ARGB8888)

注意:
这里的宽高是不是你放入文件夹中数据。需要换算
举例:
环境如下:
1. 一张图片宽高为:1000*1000
2. 格式为:ARGB8888(每一个像素点4个字节)
3. 将此图片放入:mipmap-xhdpi(320dpi density:2.0)
4. 设备density:4.0 densityDpi:640
(google规定160dpi density:1 那么 320dpi 的density自然等于2。关于这块基础知识不做详细介绍)
第一步:换算xhdpi和设备dpi
缩放比=设备密度/图文所在文件密度= 4/2 = 2

所以图片大小应为:

图片高 x 图片宽 x 缩放比 x 图片格式内存(图片每一个像素点颜色需要多少个字节保存)

最终: 4/2*1000*1000*8 = 16000000

口说无凭证那么请看如下单元测试:

Android压缩图片和libjpeg库_第2张图片

输出结果:
这里写图片描述

补充知识的结论

所以可以知道一个Bitmap只会和它的 分辨率和图片格式有关。
所以我们的需求:
2. 修改图片分辨率 防止在Android加载Bitmap的时候oom(内存溢出) 只能修改分辨率和格式实现,我们一般从改变的图片分辨率入手,而libjpeg改变不是改变Bitmap内存大小 而是存储大小。

这里举例说明如下:
假设我有一张图片 在硬盘存储的时候大小为:100kb 然后经过libjpeg变成10kb 。但是分辨率没有改变。所以载入内存的时候大小是一样的。但是可以方便我们存储和传输。

修改图片分辨率 防止在Android加载Bitmap的时候oom(内存溢出)

我们来看一下我们如何解决这个需求

解决方案1:

利用canvas修改分辨率:

    /**
     *
     * 2. 尺寸压缩
     通过缩放图片像素来减少图片占用内存大小
     * @param bmp
     * @param file
     */

    public static void compressBitmapToFile(Bitmap bmp, File file){
        // 尺寸压缩倍数,值越大,图片尺寸越小
        int ratio = 8;
        // 压缩Bitmap到对应尺寸
        Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Bitmap.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();
        }
    }

解决方案2:

我们最常见的修改采样率:
代码来自

     /** 
     * @param filePath   要加载的图片路径 
     * @param destWidth  显示图片的控件宽度 
     * @param destHeight 显示图片的控件的高度 
     * @return 
     */  
    public static Bitmap getBitmap(String filePath, int destWidth, int destHeight) {  
        //第一次采样  
        BitmapFactory.Options options = new BitmapFactory.Options();  
        //该属性设置为true只会加载图片的边框进来,并不会加载图片具体的像素点  
        options.inJustDecodeBounds = true;  
        //第一次加载图片,这时只会加载图片的边框进来,并不会加载图片中的像素点  
        BitmapFactory.decodeFile(filePath, options);  
        //获得原图的宽和高  
        int outWidth = options.outWidth;  
        int outHeight = options.outHeight;  
        //定义缩放比例  
        int sampleSize = 1;  
        while (outHeight / sampleSize > destHeight || outWidth / sampleSize > destWidth) {  
            //如果宽高的任意一方的缩放比例没有达到要求,都继续增大缩放比例  
            //sampleSize应该为2的n次幂,如果给sampleSize设置的数字不是2的n次幂,那么系统会就近取值  
            sampleSize *= 2;  
        }  
        /********************************************************************************************/  
        //至此,第一次采样已经结束,我们已经成功的计算出了sampleSize的大小  
        /********************************************************************************************/  
        //二次采样开始  
        //二次采样时我需要将图片加载出来显示,不能只加载图片的框架,因此inJustDecodeBounds属性要设置为false  
        options.inJustDecodeBounds = false;  
        //设置缩放比例  
        options.inSampleSize = sampleSize;  
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;  
        //加载图片并返回  
        return BitmapFactory.decodeFile(filePath, options);  
    }  

采样率inSampleSize 必须是2的倍数.每次采样宽和高分辨率分别除以采样率。所以图片总大小减少采样率平方数。本文主讲libjpeg方面所以跳过详细说明

希望压缩图片方便网络传输

第一种方案利用Bitmap.compress()方法压缩。

 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.timg);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        //第一个参数:压缩的格式
        //第二个参数:压缩自量1-100 1自量最小
        //第三个参数: 输出的字节
        bitmap.compress(Bitmap.CompressFormat.JPEG, 1, baos);
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(new File(Environment.getExternalStorageDirectory(), "测试画布来改变.jpg"));
            fileOutputStream.write(baos.toByteArray());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

第二种利用libjpeg压缩。

//第一个参数:文件输出路径
//第二个参数:Bitmap对象
//第三个参数:压缩质量 1-100 1最小 
//第四个参数:是否启用哈夫曼变长编码(使用后更小)
NativeCompress.nativeLibJpegCompress(file.getAbsolutePath(), bitmap, 1, true);

上面的代码是我们这篇文章要写的代码。我们先不用关心内部实现。只需要知道调用这个可以压缩图片

在Android5.0测试两个 图片压缩

Android压缩图片和libjpeg库_第3张图片
我们发现压缩率使用libjpeg是远大于bitmap.compress方法的

在Android6.0测试两个 图片压缩

Android压缩图片和libjpeg库_第4张图片
这时候我们发现两个压缩率是一模一样的。

解释Android5.0和6.0为什么压缩率差别

在Android和IOS中都采用skia图片库进行压缩和解码。
skia在05年被Google收购。
你可以在Android源码中看到\external\目录看到。当然libjpeg源码也在此目录。
在早期Android系统中 由于硬件不像今天晓龙835那样强大,内存多少G之类的。在执行某些图片压缩算法会很吃力,所以当时早期图片压缩的时候采用了 定长编码。而在6.0之后采用了哈夫曼编码

定长编码

假设你图片存储的文本内容为:
abbbbbbcde
我们知道在计算机中存储后:都只是一堆二进制文件而已
那么我们假设用如下方法存储:
用以下二进制代表 上面出现字母

字母 二进制
a 000
b 001
c 010
d 011
e 100

那么上述的文本对应存储的二进制为:
000 001 001 001 001 001 001 010 011 100

然后我们解析文件的时候根据上面的规则就可以翻译出存储后的内容

那么有没有比这个更好的规则呢?
答案是有的 哈夫曼编码

哈夫曼编码

哈夫曼树又称最优二叉树,那么我们一起看看上面例子利用二叉树编码后怎么样。
我们先根据上文的题目:有如下文本‘abbbbbbcde’

构造哈夫曼树。(关于怎么构造哈夫曼树这里不做讲述)
根据出现的次数 字母有如下权重:
a = 1
b = 6
c = 1
d = 1
e =1
所以可以得:
Android压缩图片和libjpeg库_第5张图片

根据上面的编码:

b = 1
a = 000
c = 001
d = 011
e = 010

根据:abbbbbbcde
000 1 1 1 1 1 1 001 011 010

很明显哈夫曼编码在长度上有极大的优势。那么我们开始利用libjpeg完成吧

编译libjpeg前言

libjpeg为c语言编写 所以为了使用libjpeg我们需要把libjpeg编译成'静态库'或者‘动态库’。这里打架可以把 所谓的静态库和动态库当成java中jar包。
这里我们选择在linux下进行交叉编译。

关于交叉编译:我们知道c语言是无法跨平台使用的,我们Android虽然也是linux系统 但是在系统架构上 和我们用来编译的linux 多少是有不同的地方。所以我们要在编译的的时候指定交叉编译两个属性:

  • 1 一个目标系统文件库(可以理解为classPath路径,用c方面的说法就是某个操作系统/usr/目录下面的头文件和实现库):

    一般这个属性名字为->sysroot
    这个目录在NDK文件目录下platforms\android-xx\xxxx
    如:E:\Android\android-ndk-r9d\platforms\android-12\arch-arm\

    举个简单java例子:假设你写了一个Utils工具类给大家使用,类中有这样一个方法用于排序对象数组:传入参数为一个比较器。而工具类不知道某个类的对象谁大谁小,我需要调用则告诉我规则。在这里我们可以类推libjpeg为我们写Utils工具,而比较器的实现就像我们的操作系统,怎么比较是操作系统的事情,你只需要告诉我比较器在哪

  • 2 目标系统的编译软件(可以当成javac那样理解)

编译libjpeg步骤

环境说明:
- linux操作系统
- 已经下载好NDK文件到系统上 ,我用的是ndk-r14

  • 下载libjpeg源码到linux系统上
    下载地址
  • 下载后的源码是压缩包 所以解压
    Android压缩图片和libjpeg库_第6张图片

  • 编写shell脚本 调用configure文件
    configure是一个libjpeg库内置的文件,只需要要简单的传入几个参数既可以生产Makefile
    那么现在编写一个编译出arm平台的共享库吧

#文件名:config.sh 放置到libjpeg解压后的目录。本例在jpeg-6b文件夹内 
NDK=/home/fmy/android-ndk-r14b
PLATFORM=$NDK/platforms/android-15/arch-arm
PREBUILT=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
CC=$PREBUILT/bin/arm-linux-androideabi-gcc
HOST=arm
./configure --enable-shared --prefix=/home/fmy/libjpeg/jpeg-6b/fmy --exec-prefix=/home/fmy/libjpeg/jpeg-6b/fmy --host=$HOST CC="$CC --sysroot=$PLATFORM"

如果你要使用的话只需要修改上面NDK变量的值为你系统NDK文件目录即可。
–prefix属性为你生成动态库目录
-exec-prefix属性为生成头文件目录

注意:关于–enable-shared 这个参数是用与编译生成动态库。但是你需要libtool 如果你不使用的话只能修改Makefile文件 生成静态库

  • 启动shell脚本,目录生产Makefile文件
    打开libjpeg-6b文件夹.然后使用如下命令
sh config.sh
  • 根据情况修改Makefile文件(大多数不需要更改如ffmpeg x264库)
    上面的命令输入完成后 会生产Makefile文件 ,这个文件是用于编译和管理c工程的。因为linux不像windos那样有强的vs这样的工具(当成eclipse就好)。
    因为默认生产Makefile使用libtool编译的c的,而这里我们不使用这个工具。所以我们要进行修改.
vim Makefile

上面的命令会打开linux文本编辑工具,然后按下i键,切换到输入模式修改两处处地方
Android压缩图片和libjpeg库_第7张图片

  • 第一处位于39行
    原本为:
LIBTOOL = ./libtool

修改为

LIBTOOL = 

注意:=号后面需要保留一个空格。也就说别删除等号后面的空格 正确为 LIBTOOL=空格。后面两处修改也需要保留等号后面的空格

  • 第二处位于42和43行:
O = lo
A = la

修改后

O = o
A = a

目的很简单这样修改是为了不使用默认的libtool进行编译

  • 调用make 和make install 编译生产 共享库
make
make install
make distclean

之后在你写的输出目录会得到静态库和头文件

创建Demo项目

  • 打开as创建项目
    记得勾选include C++ support。如果你没有这个选项请更新as到2.2以上版本
    Android压缩图片和libjpeg库_第8张图片

  • 将编译好的静态库文件放入libs目录(不需要放入头文件)
    Android压缩图片和libjpeg库_第9张图片

  • 修改moudle的build.gradle
    Android压缩图片和libjpeg库_第10张图片

  • 放入头文件到cpp文件夹的include文件中Android压缩图片和libjpeg库_第11张图片

  • 配置CMakeLists文件

#设置cmake最小支持版本
cmake_minimum_required(VERSION 3.4.1)

#你要编译c/c++源码
add_library( # 设置生成库文件名称
             native-lib
             # 编译生成共享库 当然你可以指定静态库===>>STATIC
             SHARED
             # 源文件地址
             src/main/cpp/native-lib.cpp
                       )

#设置distribution_DIR变量
#${CMAKE_SOURCE_DIR}为调用系统预先设定好的变量或者方法,这里返回CMakeLists文件地址
set(distribution_DIR   ${CMAKE_SOURCE_DIR}/libs/)


#添加Android的已经有的库文件 这里导入Log是用来在c代码层打印Log
find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )
#同上
find_library( # Sets the name of the path variable.
              android-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              android )




#添加我们编译好的静态库到工程
#libjpeg.a
add_library(libjpeg-lib STATIC IMPORTED)
set_target_properties(libjpeg-lib  PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libjpeg.a)

#添加头文件地址
include_directories(src/main/cpp/include/)
#连接库文件
target_link_libraries(
                       native-lib
                       #这里必须加上 如果你是eclipse方式的Android.mk就不需要添加这个
                       #这里的作用保证AndroidBitmap_getInfo方法可用。你也可以通过find_library方法再添加
                       jnigraphics

                       ${log-lib}
                       ${android-lib}
                      #libjpegbither-lib
                      libjpeg-lib
                        )

撸代JAVA层代码

我们在用libjpeg进行图片编码(压缩)的时候希望有回调。所以定义以下接口。

package org.jpegutil.fjpeg.minterface;
/**
 * Created by FMY on 2017/8/26.
 */

public interface NativeCallBack {

    void startCompress();

    void error(int errorNum, String description);

    void finish(String filePath);

}

这个时候我们还需要一个类java和c代码层交互。

package org.jpegutil.fjpeg;

import android.graphics.Bitmap;

import org.jpegutil.fjpeg.minterface.NativeCallBack;


/**
 * Created by FMY on 2017/8/25.
 */

public class NativeCompress {

    static {
        //加载我们编译好的动态库 他的全程libnative-lib.so大家在编译完成后再build/intermediates文件夹可以看到
        System.loadLibrary("native-lib");
    }

    /**
     * 采用libjpeg压缩图片 这个方法调用c代码
     *
     * @param outpath          用哈夫曼压缩后文件保存路径
     * @param bitmap           需要压缩的bitmap图片
     * @param CompressionRatio 质量1-100 1表示最低质量
     * @param isUseHoffman     是否使用哈夫曼编码
     */
    public static native void nativeLibJpegCompress(String outpath, Bitmap bitmap, int CompressionRatio, boolean isUseHoffman, NativeCallBack nativeCallBack);

}

到这里所有java层代码都写完了,接下来看我们编写c++代码

撸C++代码

我们创建工程的时候,会自动生成一个c++文件。
这个文件位于cpp/native-lib.cpp。
创建一个和java方法交互的c函数。那么问题来了怎么创建呢?
我们之前写了一个NativeCompress 类,里面nativeLibJpegCompress方法不知道各位还记得吗?我们说这个方法是用来和c交互的。。。

  • 你只需要把鼠标放在这个方法名上。
  • 按下ALT+ENTER(回车)
  • 在弹出的菜单中选择‘creat function XXXXXX’即可在native-lib.cpp中生成一个c函数。
    Android压缩图片和libjpeg库_第12张图片

这时候我们再来看看native-lib.cpp生成了什么。
Android压缩图片和libjpeg库_第13张图片

在这个函数上面写extern “C”(如果不写 c++会在编译的时候改变这个函数名,导致无法被java调用)
Android压缩图片和libjpeg库_第14张图片

第一步.保存回调信息和输出到磁盘路等为全局引用

//定义一个别名 方便存储二进制数据
typedef typedef unsigned char BYTE;
//全局引用
jobject callBack;
JNIEnv *menv;
//防止c++的命名规范导致jni找不到方法
JNIEXPORT void JNICALL
Java_org_jpegutil_fjpeg_NativeCompress_nativeLibJpegCompress(JNIEnv *env, jobject instance,jstring outpath_, jobject bitmap,jint CompressionRatio,jboolean isUseHoffman,jobject nativeCallBack) {
    //文件输出地址
    const char *outpath = env->GetStringUTFChars(outpath_, 0);
    //用于保存bitmap的二进制数据
    BYTE *pixelscolor;

    //保存回调地址为全局引用
    callBack = env->NewGlobalRef(nativeCallBack);
    menv = env;
}

校验传入Bitmap对象

我们这里只压缩像素格式为ARGB8888的,所以不是这个格式的时候我们应该回调错误信息。
那么我们怎么获得Bitmap的一些信息呢?
AndroidBitmapInfo对象是Bitmap的一些配置信息如宽高都可以获取到。
那么AndroidBitmapInfo又怎么获得呢?
我们可以调用AndroidBitmap_getInfo函数获取某个Bitmap对象的配置信息

// 得到bitmap一些信息
    AndroidBitmapInfo info;
    memset(&info, 0, sizeof(info));
    AndroidBitmap_getInfo(env, bitmap, &info);
    int w = info.width;
    int h = info.height;

    jclass nativeCallBackClass = env->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");

    //校验图片合法性
    if (w <= 0 || h <= 0) {
//        LOGE("发生错误:传入的图片宽度或者高度不小于等于0 【width:%d】【height:%d】", w, h);
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "图高度或者宽为0,【高:%d】 【 宽:%d】", h, w);
        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_HEIGHT_WIDTH_ERROR,
                                env->NewStringUTF(description));
        }

        //关闭资源
        freeResource();
        return;
    }

    //校验图片格式
    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
//        LOGE("发生错误:传入的图片不合法");
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");

        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_FOMAT_ERROR,
                                env->NewStringUTF("图片格式错误"));
        }

        //关闭资源
        freeResource();
        return;
    }

获取Bitmap对象像素信息

AndroidBitmap_lockPixels()方法可以锁定Bitmap对象,然后获取对应信息。记得获取完成后调用AndroidBitmap_unlockPixels()解锁对象

  //用于保存bitmap的二进制数据
    BYTE *pixelscolor;
//    LOGE("开始读取数据");
    //锁定bitmap 获取二进制数据
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelscolor);
//
    //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
    BYTE *data;
    BYTE a, r, g, b;
    data = (BYTE *) malloc(w * h * 3);//每一个像素都有四个信息ARGB 并且ARGB8888每一个像素点为64位
    BYTE *tmpdata;
    tmpdata = data;//临时保存data的首地址

    int i, j;
    for (i = 0; i < h; ++i) {
        for (j = 0; j < w; ++j) {
            //读取指针指向数据(这里指向bitmap二进制数据的指针)
            int color = *((int *) pixelscolor);

            //得到透明度
            //a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));

            //保存data
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            // **(data + 3) = *a;

            //地址偏移4个字节
            data += 3;
            pixelscolor += 4;
        }
    }
    //解锁bitmap
    AndroidBitmap_unlockPixels(env, bitmap);

关于以下这一小段代码我相信很多人应该多少有疑惑

          //a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));

&:按位与。讲两个数字直接进行二进制比较,当且仅当两个二进制为1的时候结果为1 这一块
如:
2的二进制为 0010
3的二进制为 0011
那么结果
 0010
& 0011
———————-
 0010

>> x 带符号右移动:讲数字的二进制右移动x位 高位补符号为(正数0负数1)

如:2>>2 解释2向右移动两位
2的二进制为 0010 移动两位 0000
所以结果等于十进制的0

我们解析ARGB8888的图片,而这个格式需要占用4个字节。每个字节存储一个颜色。而4个字节正好不就是int的大小吗?
假设我们图片的第一个像素信息是:0xF0990011
那么透明度 A=F0
红色 R=99
绿色 G=00
蓝色 B=11

  • 那么我们举例说明其中之一如何取到透明度
    0xFF990011
    对象那么一长串的数字我们只需要FF那么我们进行&(按位或)运算 去掉990011这个冗余的数据
    所以 0xF0990011&0xFF000000=0xF0 00 00 00

然后右移24位 0xF0 00 00 00 = 0x 00 00 00 0F
结合来说: a = ((color & 0xFF000000) >> 24);

其他类比。。。。

我们再来回头看看整个方法:


//防止c++的命名规范导致jni找不到方法
JNIEXPORT void JNICALL
Java_org_jpegutil_fjpeg_NativeCompress_nativeLibJpegCompress(JNIEnv *env, jobject instance,
                                                             jstring outpath_, jobject bitmap,
                                                             jint CompressionRatio,
                                                             jboolean isUseHoffman,
                                                             jobject nativeCallBack) {
    //文件输出地址
    const char *outpath = env->GetStringUTFChars(outpath_, 0);


    //保存回调地址为全局引用
    callBack = env->NewGlobalRef(nativeCallBack);
    menv = env;

    // 得到bitmap一些信息
    AndroidBitmapInfo info;
    memset(&info, 0, sizeof(info));
    AndroidBitmap_getInfo(env, bitmap, &info);
    int w = info.width;
    int h = info.height;

    jclass nativeCallBackClass = env->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");

    //校验图片合法性
    if (w <= 0 || h <= 0) {
//        LOGE("发生错误:传入的图片宽度或者高度不小于等于0 【width:%d】【height:%d】", w, h);
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "图高度或者宽为0,【高:%d】 【 宽:%d】", h, w);
        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_HEIGHT_WIDTH_ERROR,
                                env->NewStringUTF(description));
        }

        //关闭资源
        freeResource();
        return;
    }

    //校验图片格式
    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
//        LOGE("发生错误:传入的图片不合法");
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");

        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_FOMAT_ERROR,
                                env->NewStringUTF("图片格式错误"));
        }

        //关闭资源
        freeResource();
        return;
    }
    //用于保存bitmap的二进制数据
    BYTE *pixelscolor;
//    LOGE("开始读取数据");
    //锁定bitmap 获取二进制数据
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelscolor);
//
    //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
    BYTE *data;
    BYTE a, r, g, b;
    data = (BYTE *) malloc(w * h * 3);//每一个像素都有四个信息ARGB 并且ARGB8888每一个像素点为64位
    BYTE *tmpdata;
    tmpdata = data;//临时保存data的首地址

    int i, j;
    for (i = 0; i < h; ++i) {
        for (j = 0; j < w; ++j) {
            //读取指针指向数据(这里指向bitmap二进制数据的指针)
            int color = *((int *) pixelscolor);

            //得到透明度
            //*a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));

            //保存data中
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            // **(data + 3) = *a;

            //地址偏移4个字节
            data += 3;
            pixelscolor += 4;
        }
    }
    //解锁bitmap
    AndroidBitmap_unlockPixels(env, bitmap);
//    LOGE("读取数据完毕");
    //拷贝输出文件地址
    char *outPathBackup = (char *) malloc(sizeof(char) * (strlen(outpath) + 1));
    strcpy(outPathBackup, outpath);
//    LOGE("开始压缩");
    //压缩
    generateJPEG(tmpdata, w, h, CompressionRatio, outPathBackup, isUseHoffman);

    //释放资源
    env->ReleaseStringUTFChars(outpath_, outpath);
}

我们看到最终调用generateJPEG ()函数 完成了全部步骤

贴上

int generateJPEG(BYTE *data, int w, int h, int quality,
                 const char *outfilename, jboolean optimize) {
    //回调java代码
    jclass nativeCallBackClass = menv->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");
    //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;
    //使用longjmp将跳转到这样
    if (setjmp(jem.setjmp_buffer)) {

        //关闭资源
        freeResource();
        return 0;
    }

    //初始化jsc结构体
    jpeg_create_compress(&jcs);
    //打开输出文件 wb:可写byte
    FILE *f = fopen(outfilename, "wb");
    if (f == NULL) {
//        LOGE("打开文件失败");
        jmethodID errorMthodId = menv->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "以二进制打开读写文件路径[%s]失败",outfilename);
        if (callBack!=NULL) {

            menv->CallVoidMethod(callBack, errorMthodId,FILE_ERROR,
                                 menv->NewStringUTF(description));
        }
        //关闭资源
        freeResource();
        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;
    //设置质量 quality是个0~100之间的整数,表示压缩比率
    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);//关闭文件



    jmethodID pID = menv->GetMethodID(nativeCallBackClass, "finish",
                                      "(Ljava/lang/String;)V");
    if (callBack!=NULL) {

        menv->CallVoidMethod(callBack,pID,menv->NewStringUTF(outfilename));
    }
    //关闭资源
    freeResource();
//    LOGE("完成");
    return 1;
}

最终我们看下完整代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "mconst.h"
//告诉编译器以下文件是用c文件
extern "C" {
#include "jpeglib.h"
#include "jerror.h"     /* Common decls for cjpeg/djpeg applications */
#include "jmorecfg.h"   /* for version message */
#include "jconfig.h"
}
#define LOG_TAG "libjpeg"
#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__)
//全局引用
jobject callBack;
JNIEnv *menv;

//关闭资源调用此方法
void freeResource() {

    menv->DeleteGlobalRef(callBack);
    callBack = NULL;
    menv = NULL;
}

//定义一个别名 方便存储二进制数据
typedef typedef unsigned char BYTE;

#define true 1
#define false 0

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]);
    jclass nativeCallBackClass = menv->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");
    jmethodID errorMthodId = menv->GetMethodID(nativeCallBackClass, "error",
                                              "(ILjava/lang/String;)V");
    char description[100];
    sprintf(description, "jpeg_message_table[%d]:%s", myerr->pub.msg_code,
            myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
    if (callBack!=NULL) {

        menv->CallVoidMethod(callBack, errorMthodId, INTERNAL_ERROR,
                             menv->NewStringUTF(description));
    }


    //跳转setjmp 并且返回值为1结束
    longjmp(myerr->setjmp_buffer, 1);



}

int generateJPEG(BYTE *data, int w, int h, int quality,
                 const char *outfilename, jboolean optimize) {
    //回调java代码
    jclass nativeCallBackClass = menv->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");
    //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;
    //使用longjmp将跳转到这样
    if (setjmp(jem.setjmp_buffer)) {

        //关闭资源
        freeResource();
        return 0;
    }

    //初始化jsc结构体
    jpeg_create_compress(&jcs);
    //打开输出文件 wb:可写byte
    FILE *f = fopen(outfilename, "wb");
    if (f == NULL) {
//        LOGE("打开文件失败");
        jmethodID errorMthodId = menv->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "以二进制打开读写文件路径[%s]失败",outfilename);
        if (callBack!=NULL) {

            menv->CallVoidMethod(callBack, errorMthodId,FILE_ERROR,
                                 menv->NewStringUTF(description));
        }
        //关闭资源
        freeResource();
        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;
    //设置质量 quality是个0~100之间的整数,表示压缩比率
    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);//关闭文件



    jmethodID pID = menv->GetMethodID(nativeCallBackClass, "finish",
                                      "(Ljava/lang/String;)V");
    if (callBack!=NULL) {

        menv->CallVoidMethod(callBack,pID,menv->NewStringUTF(outfilename));
    }
    //关闭资源
    freeResource();
//    LOGE("完成");
    return 1;
}

extern "C"



//防止c++的命名规范导致jni找不到方法
JNIEXPORT void JNICALL
Java_org_jpegutil_fjpeg_NativeCompress_nativeLibJpegCompress(JNIEnv *env, jobject instance,
                                                             jstring outpath_, jobject bitmap,
                                                             jint CompressionRatio,
                                                             jboolean isUseHoffman,
                                                             jobject nativeCallBack) {
    //文件输出地址
    const char *outpath = env->GetStringUTFChars(outpath_, 0);


    //保存回调地址为全局引用
    callBack = env->NewGlobalRef(nativeCallBack);
    menv = env;

    // 得到bitmap一些信息
    AndroidBitmapInfo info;
    memset(&info, 0, sizeof(info));
    AndroidBitmap_getInfo(env, bitmap, &info);
    int w = info.width;
    int h = info.height;

    jclass nativeCallBackClass = env->FindClass("org/jpegutil/fjpeg/minterface/NativeCallBack");

    //校验图片合法性
    if (w <= 0 || h <= 0) {
//        LOGE("发生错误:传入的图片宽度或者高度不小于等于0 【width:%d】【height:%d】", w, h);
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");
        char description[100];
        sprintf(description, "图高度或者宽为0,【高:%d】 【 宽:%d】", h, w);
        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_HEIGHT_WIDTH_ERROR,
                                env->NewStringUTF(description));
        }

        //关闭资源
        freeResource();
        return;
    }

    //校验图片格式
    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
//        LOGE("发生错误:传入的图片不合法");
        jmethodID errorMthodId = env->GetMethodID(nativeCallBackClass, "error",
                                                  "(ILjava/lang/String;)V");

        if (callBack!=NULL) {
            env->CallVoidMethod(nativeCallBack, errorMthodId, BITMAP_FOMAT_ERROR,
                                env->NewStringUTF("图片格式错误"));
        }

        //关闭资源
        freeResource();
        return;
    }
    //用于保存bitmap的二进制数据
    BYTE *pixelscolor;
//    LOGE("开始读取数据");
    //锁定bitmap 获取二进制数据
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelscolor);
//
    //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
    BYTE *data;
    BYTE a, r, g, b;
    data = (BYTE *) malloc(w * h * 3);//每一个像素都有四个信息ARGB 并且ARGB8888每一个像素点为64位
    BYTE *tmpdata;
    tmpdata = data;//临时保存data的首地址

    int i, j;
    for (i = 0; i < h; ++i) {
        for (j = 0; j < w; ++j) {
            //读取指针指向数据(这里指向bitmap二进制数据的指针)
            int color = *((int *) pixelscolor);

            //得到透明度
            //*a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));

            //保存data中
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            // **(data + 3) = *a;

            //地址偏移4个字节
            data += 3;
            pixelscolor += 4;
        }
    }
    //解锁bitmap
    AndroidBitmap_unlockPixels(env, bitmap);
//    LOGE("读取数据完毕");
    //拷贝输出文件地址
    char *outPathBackup = (char *) malloc(sizeof(char) * (strlen(outpath) + 1));
    strcpy(outPathBackup, outpath);
//    LOGE("开始压缩");
    //压缩
    generateJPEG(tmpdata, w, h, CompressionRatio, outPathBackup, isUseHoffman);

    //释放资源
    env->ReleaseStringUTFChars(outpath_, outpath);
}

参考文献:

Android开发之高效加载Bitmap

编译好的静态库和头文件

编译好的文件下载地址

你可能感兴趣的:(Android,JNI学习之旅)