Bitmap压缩原理解析与Android 7.0之前通过NDK使用libjpeg库高质量压缩图片

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SakuraMashiro/article/details/79182239

一、Bitmap压缩原理

我们平常使用的bitmap.compress() 的内部实际上调用了如下native方法

private static native boolean nativeCompress(long nativeBitmap, int format,
                                            int quality, OutputStream stream,
                                            byte[] tempStorage);

在android源码的\frameworks\base\core\jni\android\graphics\Bitmap.cpp中我们发现nativeCompress这个方法实际对应的C++函数

static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,int format, int quality,object jstream, jbyteArray jstorage) 

在上述方法中,然后是判断了编码的类型(PNG,JPEG,WEBP),然后真正调用编码的是这段

SkImageEncoder* encoder = SkImageEncoder::Create(fm);
        if (NULL != encoder) {
            success = encoder->encodeStream(strm, *bitmap, quality);
            delete encoder;
        }

可以看到这里使用了一个SkImageEncoder的编码器来对我们的图像进行了编码,这个编码器就是Skia引擎的编码器。

什么是Skia引擎?Skia引擎是一个开源的C++二维图形库,目前由Google维护,在chrome浏览器和android系统中应用都很广泛。

Android系统中的skia引擎是阉割的skia版本,对jpeg的处理基于libjpeg开源库,对png的处理则是基于libpng。
早期的Android系统由于cpu吃紧。将libjpeg中的最优哈夫曼编码关闭了。直到7.0才打开。
6.0中SkImageDecoder_libjpeg.cpp源码

     cinfo.input_gamma = 1;

     jpeg_set_defaults(&cinfo);
     jpeg_set_quality(&cinfo, quality, TRUE);

7.0中SkImageDecoder_libjpeg.cpp源码

     cinfo.input_gamma = 1;

     jpeg_set_defaults(&cinfo);

     // Tells libjpeg-turbo to compute optimal Huffman coding tables
     // for the image.  This improves compression at the cost of
     // slower encode performance.
     cinfo.optimize_coding = TRUE;          //开启最优哈夫曼编码
     jpeg_set_quality(&cinfo, quality, TRUE);

开启了最优哈夫曼编码的话图片压缩的质量会明显提高,所以在7.0之前的系统中使用bitmap原始的压缩方式,在压缩比率较高的情况下,图片失真会十分严重。而开启了最优哈夫曼编码的话,不仅图片质量几乎和原图看不出差别,压缩后的图片大小也可以进一步降低。

二、 7.0之前的系统中如何优化图片压缩

我们无法改变android底层skia的压缩机制,也无法操作系统源码中的libjpeg库。这时候就需要使用NDK来调用C++原生库(libjpeg),在应用中通过调用自定义的native方法来压缩图片。

步骤

由于LibJpeg-turbo是用C语言编写的JPEG编解码库。我们直接到官网https://libjpeg-turbo.org/上下载它的源代码 -> 然后在Linux上编译成支持Android CPU架构的库 -> 然后在AndroidStduio中集成-> 最后自己调动libjpeg库进行编码。

三、Linux中编译so库

我用的是Ubuntu 64位虚拟机,直接在终端下输入

wget https://github.com/libjpeg-turbo/libjpeg-turbo/archive/1.5.3.tar.gz

下载libjpeg-turbo,下载完成后解压

tar xvf 1.5.3.tar.gz

如果你要在模拟器上运行,需要编译成x86架构的,除此以外还要安装另外一个工具NASM(armeabi不需要),

wget http://www.nasm.us/pub/nasm/releasebuilds/2.13/nasm-2.13.tar.gz

下载完解压然后编译nasm

tar xvf nasm-2.13.tar.gz
cd nasm-2.13
./configure 
make install

这里我在真机上运行,编译的arme-v7a版本,上述安装nasm的步骤可以省略。

下面开始真正编译我们的libjpeg,实际上在该库的官网上也有很详细的编译步骤https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/BUILDING.md
首先,cd进解压后的libjpeg目录,

cd libjpeg-turbo-1.5.3

然后使用autoconf生成Configure配置脚本

autoreconf -fiv

如果报错,提示你需要安装libtool工具

sudo apt-get install libtool

完成后就可以编写我们的shell脚本了,在libjpeg-turbo-1.5.3目录下用vi编辑器新建一个脚本。

vi build.sh

因为我有图形界面,所以我用图形界面的编辑器操作更方便。

gedit build.sh

然后安装官网上编译相应架构的库的提示,
这里写图片描述

将shell脚本,直接复制过来,然后修改相关配置。
比如,修改ndk路径

NDK_PATH=/home/lishuji/android-ndk-r14b

注意你的虚拟机中需要下载了NDK,并且配置好环境变量。

修改最低的android版本

ANDROID_VERSION="14"

此外还要加上下面一句话,设置最终编译出的静态库和动态库存放的目录,我们就在libjpeg-turbo-1.5.3下新建一个android目录来存放。

--prefix=/home/lishuji/libjpeg-turbo-1.5.3/android

最终的build.sh如下:

#!/bin/bash

# Set these variables to suit your needs
#指向ndk目录
NDK_PATH=/home/lishuji/android-ndk-r14b
#查看NDK目录下的 toolchains/x86-4.9/prebuilt
BUILD_PLATFORM="linux-x86_64"
#查看toolchains/x86-xx xx是多少就写多少
TOOLCHAIN_VERSION="4.9"
#查看platforms目录 as默认也是14
ANDROID_VERSION="14"

# It should not be necessary to modify the rest
HOST=arm-linux-androideabi
SYSROOT=${NDK_PATH}/platforms/android-${ANDROID_VERSION}/arch-arm
ANDROID_CFLAGS="-march=armv7-a -mfloat-abi=softfp -fprefetch-loop-arrays \
  -D__ANDROID_API__=${ANDROID_VERSION} --sysroot=${SYSROOT} \
  -isystem ${NDK_PATH}/sysroot/usr/include \
  -isystem ${NDK_PATH}/sysroot/usr/include/${HOST}"

TOOLCHAIN=${NDK_PATH}/toolchains/${HOST}-${TOOLCHAIN_VERSION}/prebuilt/${BUILD_PLATFORM}
export CPP=${TOOLCHAIN}/bin/${HOST}-cpp
export AR=${TOOLCHAIN}/bin/${HOST}-ar
export NM=${TOOLCHAIN}/bin/${HOST}-nm
export CC=${TOOLCHAIN}/bin/${HOST}-gcc
export LD=${TOOLCHAIN}/bin/${HOST}-ld
export RANLIB=${TOOLCHAIN}/bin/${HOST}-ranlib
export OBJDUMP=${TOOLCHAIN}/bin/${HOST}-objdump
export STRIP=${TOOLCHAIN}/bin/${HOST}-strip

./configure --host=${HOST} \
  --prefix=/home/lishuji/libjpeg-turbo-1.5.3/android  \
  CFLAGS="${ANDROID_CFLAGS} -O3 -fPIE" \
  CPPFLAGS="${ANDROID_CFLAGS}" \
  LDFLAGS="${ANDROID_CFLAGS} -pie" --with-simd ${1+"$@"}
make install

注意:编译不同CPU架构下的build.sh文件不同,请参考官网的build.sh脚本。

最终生成的目录如下:

这里写图片描述

其中lib文件夹下就是生成的静态库和动态库,include中包含了我们需要的头文件。

四、AS中集成,编码

我们将上面那些复制到我们的本机上来,然后新建一个AS项目,勾选 Include C++ Support。创建完成后,将include文件夹和lib文件夹下的库复制到我们的cpp目录中。

最终的项目结构如下(这里我只用了一个.a静态库)
这里写图片描述

然后修改CMakeList文件,

#添加静态库
add_library(jpeg STATIC IMPORTED)
#设置静态库地址
set_target_properties(jpeg PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/cpp/libs/libturbojpeg.a)
#包含头文件
include_directories(src/main/cpp/include)

链接

target_link_libraries(
                       native-lib
                       jpeg
                       jnigraphics
                       log )

修改build.gradle,指定abiFilters

externalNativeBuild {
            cmake {
                cppFlags "-frtti -fexceptions"
                abiFilters 'armeabi-v7a'
            }
        }

然后新建一个native方法,名叫nativeCompress,传入我们的bitmap,压缩质量(0-100),以及压缩后的图片路径。

public native void nativeCompress(Bitmap bitmap , int q , String path );

最后在native-lib.cpp中实现,主要思路也很明确,首先通过jni的方法从bitmap中获取rgba的数据,然后去除透明度,在C++中开辟一块内存来存放获得的rgb值,接着调用libjpeg中的方法来进行压缩,其中注意让optimize_coding = TRUE即可,具体做法和android源码中很相似,也可查看libjpeg库的examples,在我们编译出来的android/share/doc/libjpeg-turbo/example.c中,最后不要忘了释放相关内存。

native-lib.cpp

#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <malloc.h>
#include <jpeglib.h>

//使用libjpeg库进行压缩
void write_JPEG_file(uint8_t *data, int w, int h, jint q, const char *path) {

    //第一步.创建jpeg压缩对象
    jpeg_compress_struct jcs;
    //错误回调
    jpeg_error_mgr error;
    jcs.err = jpeg_std_error(&error);
    //创建压缩对象
    jpeg_create_compress(&jcs);

    //第二步.指定存储文件
    FILE *f = fopen(path,"wb");
    jpeg_stdio_dest(&jcs,f);

    //第三步.设置压缩参数
    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);

    //第三步.开始压缩
    jpeg_start_compress(&jcs,1);

    //第四步.循环写入每一行数据
    int row_stride = w*3;
    JSAMPROW  row[1];
    //next_scanline 一行数据开头的位置
    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);
    }

    //第五步.压缩完成,释放jpeg对象
    jpeg_finish_compress(&jcs);
    fclose(f);
    jpeg_destroy_compress(&jcs);

}
extern "C"
JNIEXPORT void JNICALL
Java_com_libjpeg_1test2_MainActivity_nativeCompress(JNIEnv *env, jobject instance, jobject bitmap,
                                                    jint q, jstring path_) {
    const char *path = env->GetStringUTFChars(path_, 0);
    //从bitmap获取argb数据
    AndroidBitmapInfo info ;
    //获得bitmap的信息
    AndroidBitmap_getInfo(env,bitmap,&info);

    uint8_t *pixels;
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);

    //argb , 去掉透明度
    int w = info.width;
    int h = info.height;
    int color;
    //开辟一块内存存放rgb值
    uint8_t* data = (uint8_t *) malloc(w * h * 3);
    uint8_t* temp = data;
    uint8_t r, g , b;
    for( int i = 0 ; i < h ; i++ ){
        for( int j = 0 ; j < w ; j ++ ){
            color = *(int*)pixels;
            //操作argb
            //取红色
            r = (color >> 16) & 0xFF;
            g = (color >> 8 ) & 0xFF;
            b = color & 0xFF ;
            //将rgb值放入data数组中,注意libjpeg的顺序是bgr
            *data = b ;
            *(data+1) = g ;
            *(data+2) = r ;
            //指针移动三位
            data += 3 ;
            //指针后移4个字节,指向下一个rgba像素
            pixels += 4;
        }
    }

    write_JPEG_file(temp,w,h,q,path);


    //释放内存
    AndroidBitmap_unlockPixels(env,bitmap);
    free(data);
    env->ReleaseStringUTFChars(path_, path);
}

注意:我压缩的是32位真彩图,也就是带透明度的,如果用24位真彩图会报错,解决方法修改相关获取rgb值的代码即可。

最后我们可以对比自己使用libjpeg库(开启了哈夫曼)和使用bitmap原始的compress方法压缩产生的图片区别。

这里写图片描述

可以看出我们使用native方法压缩的图片比使用bitmap原生方法小了10k,别小看这10k,当图片多了以后就是很大的流量,对性能优化来说,就优化了很多,而且压缩出来的图片质量也高于用bitmap的方法压缩出来的图片。这张图片原本比较清晰,可能看不出来较大差别,但是还是看的出来一些不同。如果你使用原本就比较模糊的图片,使用bitmap压缩的话,最终的效果可能惨不忍睹,

猜你喜欢

转载自blog.csdn.net/SakuraMashiro/article/details/79182239